diff --git a/Makefile b/Makefile index fb5f22c313..c3511d5dbf 100644 --- a/Makefile +++ b/Makefile @@ -215,6 +215,7 @@ clean-bundle: clean-ui: rm -rf awx/ui/static/ rm -rf awx/ui/node_modules/ + rm -rf awx/ui/coverage/ rm -f $(UI_DEPS_FLAG_FILE) rm -f $(UI_RELEASE_FLAG_FILE) @@ -608,7 +609,7 @@ ui-test-ci: $(UI_DEPS_FLAG_FILE) testjs_ci: echo "Update UI unittests later" #ui-test-ci -jshint: +jshint: $(UI_DEPS_FLAG_FILE) $(NPM_BIN) run --prefix awx/ui jshint ui-test-saucelabs: $(UI_DEPS_FLAG_FILE) diff --git a/awx/__init__.py b/awx/__init__.py index 30256ec453..fe3cf8b31c 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -5,7 +5,7 @@ import os import sys import warnings -__version__ = '3.1.2' +__version__ = '3.2.0' __all__ = ['__version__'] diff --git a/awx/api/filters.py b/awx/api/filters.py index 41bbc3bba8..87097dc308 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -149,6 +149,11 @@ class FieldLookupBackend(BaseFilterBackend): return field.to_python(value) def value_to_python(self, model, lookup, value): + try: + lookup = lookup.encode("ascii") + except UnicodeEncodeError: + raise ValueError("%r is not an allowed field name. Must be ascii encodable." % lookup) + field, new_lookup = self.get_field_from_lookup(model, lookup) # Type names are stored without underscores internally, but are presented and diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 910517a3fd..232ec059f8 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -179,7 +179,8 @@ class Metadata(metadata.SimpleMetadata): # Add version number in which view was added to Tower. added_in_version = '1.2' - for version in ('3.1.0', '3.0.0', '2.4.0', '2.3.0', '2.2.0', '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'): + for version in ('3.2.0', '3.1.0', '3.0.0', '2.4.0', '2.3.0', '2.2.0', + '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'): if getattr(view, 'new_in_%s' % version.replace('.', ''), False): added_in_version = version break diff --git a/awx/api/parsers.py b/awx/api/parsers.py index 8c720201a2..826c67189a 100644 --- a/awx/api/parsers.py +++ b/awx/api/parsers.py @@ -26,6 +26,9 @@ class JSONParser(parsers.JSONParser): try: data = stream.read().decode(encoding) - return json.loads(data, object_pairs_hook=OrderedDict) + obj = json.loads(data, object_pairs_hook=OrderedDict) + if not isinstance(obj, dict): + raise ParseError(_('JSON parse error - not a JSON object')) + return obj except ValueError as exc: raise ParseError(_('JSON parse error - %s') % six.text_type(exc)) diff --git a/awx/api/permissions.py b/awx/api/permissions.py index 8ec26a2cc8..966cf95ea5 100644 --- a/awx/api/permissions.py +++ b/awx/api/permissions.py @@ -16,7 +16,8 @@ from awx.main.utils import get_object_or_400 logger = logging.getLogger('awx.api.permissions') __all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', - 'TaskPermission', 'ProjectUpdatePermission', 'UserPermission',] + 'TaskPermission', 'ProjectUpdatePermission', 'UserPermission', + 'IsSuperUser'] class ModelAccessPermission(permissions.BasePermission): @@ -208,3 +209,10 @@ class UserPermission(ModelAccessPermission): raise PermissionDenied() +class IsSuperUser(permissions.BasePermission): + """ + Allows access only to admin users. + """ + + def has_permission(self, request, view): + return request.user and request.user.is_superuser diff --git a/awx/conf/urls.py b/awx/conf/urls.py index 15505f4c3c..2c4f3ec91d 100644 --- a/awx/conf/urls.py +++ b/awx/conf/urls.py @@ -12,4 +12,5 @@ urlpatterns = patterns( 'awx.conf.views', url(r'^$', 'setting_category_list'), url(r'^(?P[a-z0-9-]+)/$', 'setting_singleton_detail'), + url(r'^logging/test/$', 'setting_logging_test'), ) diff --git a/awx/conf/views.py b/awx/conf/views.py index 99a3daab99..68b399444e 100644 --- a/awx/conf/views.py +++ b/awx/conf/views.py @@ -19,7 +19,9 @@ from rest_framework import status # Tower from awx.api.generics import * # noqa +from awx.api.permissions import IsSuperUser from awx.main.utils import * # noqa +from awx.main.utils.handlers import BaseHTTPSHandler, LoggingConnectivityException from awx.conf.license import get_licensed_features from awx.conf.models import Setting from awx.conf.serializers import SettingCategorySerializer, SettingSingletonSerializer @@ -130,6 +132,32 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView): settings.TOWER_URL_BASE = url +class SettingLoggingTest(GenericAPIView): + + view_name = _('Logging Connectivity Test') + model = Setting + serializer_class = SettingSingletonSerializer + permission_classes = (IsSuperUser,) + filter_backends = [] + new_in_320 = True + + def post(self, request, *args, **kwargs): + defaults = dict() + for key in settings_registry.get_registered_settings(category_slug='logging'): + try: + defaults[key] = settings_registry.get_setting_field(key).get_default() + except serializers.SkipField: + defaults[key] = None + obj = type('Settings', (object,), defaults)() + serializer = self.get_serializer(obj, data=request.data) + serializer.is_valid(raise_exception=True) + try: + BaseHTTPSHandler.perform_test(serializer.validated_data) + except LoggingConnectivityException as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response(status=status.HTTP_200_OK) + + # Create view functions for all of the class-based views to simplify inclusion # in URL patterns and reverse URL lookups, converting CamelCase names to # lowercase_with_underscore (e.g. MyView.as_view() becomes my_view). diff --git a/awx/main/tests/functional/api/test_job_template.py b/awx/main/tests/functional/api/test_job_template.py index ec4286176e..95f0d5d043 100644 --- a/awx/main/tests/functional/api/test_job_template.py +++ b/awx/main/tests/functional/api/test_job_template.py @@ -94,6 +94,23 @@ def test_edit_playbook(patch, job_template_factory, alice): }, alice, expect=403) +@pytest.mark.django_db +@pytest.mark.parametrize('json_body', + ["abc", True, False, "{\"name\": \"test\"}", 100, .5]) +def test_invalid_json_body(patch, job_template_factory, alice, json_body): + objs = job_template_factory('jt', organization='org1') + objs.job_template.admin_role.members.add(alice) + resp = patch( + reverse('api:job_template_detail', args=(objs.job_template.id,)), + json_body, + alice, + expect=400 + ) + assert resp.data['detail'] == ( + u'JSON parse error - not a JSON object' + ) + + @pytest.mark.django_db def test_edit_nonsenstive(patch, job_template_factory, alice): objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred') diff --git a/awx/main/tests/functional/api/test_settings.py b/awx/main/tests/functional/api/test_settings.py index 6322f354e7..77b5294203 100644 --- a/awx/main/tests/functional/api/test_settings.py +++ b/awx/main/tests/functional/api/test_settings.py @@ -2,14 +2,19 @@ # All Rights Reserved. # Python +from collections import OrderedDict import pytest import os +# Mock +import mock + # Django from django.core.urlresolvers import reverse # AWX from awx.conf.models import Setting +from awx.main.utils.handlers import BaseHTTPSHandler, LoggingConnectivityException TEST_GIF_LOGO = 'data:image/gif;base64,R0lGODlhIQAjAPIAAP//////AP8AAMzMAJmZADNmAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAHACwAAAAAIQAjAAADo3i63P4wykmrvTjrzZsxXfR94WMQBFh6RECuixHMLyzPQ13ewZCvow9OpzEAjIBj79cJJmU+FceIVEZ3QRozxBttmyOBwPBtisdX4Bha3oxmS+llFIPHQXQKkiSEXz9PeklHBzx3hYNyEHt4fmmAhHp8Nz45KgV5FgWFOFEGmwWbGqEfniChohmoQZ+oqRiZDZhEgk81I4mwg4EKVbxzrDHBEAkAIfkECQoABwAsAAAAACEAIwAAA6V4utz+MMpJq724GpP15p1kEAQYQmOwnWjgrmxjuMEAx8rsDjZ+fJvdLWQAFAHGWo8FRM54JqIRmYTigDrDMqZTbbbMj0CgjTLHZKvPQH6CTx+a2vKR0XbbOsoZ7SphG057gjl+c0dGgzeGNiaBiSgbBQUHBV08NpOVlkMSk0FKjZuURHiiOJxQnSGfQJuoEKREejK0dFRGjoiQt7iOuLx0rgxYEQkAIfkECQoABwAsAAAAACEAIwAAA7h4utxnxslJDSGR6nrz/owxYB64QUEwlGaVqlB7vrAJscsd3Lhy+wBArGEICo3DUFH4QDqK0GMy51xOgcGlEAfJ+iAFie62chR+jYKaSAuQGOqwJp7jGQRDuol+F/jxZWsyCmoQfwYwgoM5Oyg1i2w0A2WQIW2TPYOIkleQmy+UlYygoaIPnJmapKmqKiusMmSdpjxypnALtrcHioq3ury7hGm3dnVosVpMWFmwREZbddDOSsjVswcJACH5BAkKAAcALAAAAAAhACMAAAOxeLrc/jDKSZUxNS9DCNYV54HURQwfGRlDEFwqdLVuGjOsW9/Odb0wnsUAKBKNwsMFQGwyNUHckVl8bqI4o43lA26PNkv1S9DtNuOeVirw+aTI3qWAQwnud1vhLSnQLS0GeFF+GoVKNF0fh4Z+LDQ6Bn5/MTNmL0mAl2E3j2aclTmRmYCQoKEDiaRDKFhJez6UmbKyQowHtzy1uEl8DLCnEktrQ2PBD1NxSlXKIW5hz6cJACH5BAkKAAcALAAAAAAhACMAAAOkeLrc/jDKSau9OOvNlTFd9H3hYxAEWDJfkK5LGwTq+g0zDR/GgM+10A04Cm56OANgqTRmkDTmSOiLMgFOTM9AnFJHuexzYBAIijZf2SweJ8ttbbXLmd5+wBiJosSCoGF/fXEeS1g8gHl9hxODKkh4gkwVIwUekESIhA4FlgV3PyCWG52WI2oGnR2lnUWpqhqVEF4Xi7QjhpsshpOFvLosrnpoEAkAIfkECQoABwAsAAAAACEAIwAAA6l4utz+MMpJq71YGpPr3t1kEAQXQltQnk8aBCa7bMMLy4wx1G8s072PL6SrGQDI4zBThCU/v50zCVhidIYgNPqxWZkDg0AgxB2K4vEXbBSvr1JtZ3uOext0x7FqovF6OXtfe1UzdjAxhINPM013ChtJER8FBQeVRX8GlpggFZWWfjwblTiigGZnfqRmpUKbljKxDrNMeY2eF4R8jUiSur6/Z8GFV2WBtwwJACH5BAkKAAcALAAAAAAhACMAAAO6eLrcZi3KyQwhkGpq8f6ONWQgaAxB8JTfg6YkO50pzD5xhaurhCsGAKCnEw6NucNDCAkyI8ugdAhFKpnJJdMaeiofBejowUseCr9GYa0j1GyMdVgjBxoEuPSZXWKf7gKBeHtzMms0gHgGfDIVLztmjScvNZEyk28qjT40b5aXlHCbDgOhnzedoqOOlKeopaqrCy56sgtotbYKhYW6e7e9tsHBssO6eSTIm1peV0iuFUZDyU7NJnmcuQsJACH5BAkKAAcALAAAAAAhACMAAAOteLrc/jDKSZsxNS9DCNYV54Hh4H0kdAXBgKaOwbYX/Miza1vrVe8KA2AoJL5gwiQgeZz4GMXlcHl8xozQ3kW3KTajL9zsBJ1+sV2fQfALem+XAlRApxu4ioI1UpC76zJ4fRqDBzI+LFyFhH1iiS59fkgziW07jjRAG5QDeECOLk2Tj6KjnZafW6hAej6Smgevr6yysza2tiCuMasUF2Yov2gZUUQbU8YaaqjLpQkAOw==' TEST_PNG_LOGO = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAjCAYAAAAaLGNkAAAAAXNSR0IB2cksfwAAAdVpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpDb21wcmVzc2lvbj4xPC90aWZmOkNvbXByZXNzaW9uPgogICAgICAgICA8dGlmZjpQaG90b21ldHJpY0ludGVycHJldGF0aW9uPjI8L3RpZmY6UGhvdG9tZXRyaWNJbnRlcnByZXRhdGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cjl0tmoAAAHVSURBVFgJ7VZRsoMgDNTOu5E9U+/Ud6Z6JssGNg2oNKD90xkHCNnNkgTbYbieKwNXBn6bgSXQ4+16xi5UDiqDN3Pecr6+1fM5DHh7n1NEIPjjoRLKzOjG3qQ5dRtEy2LCjh/Gz2wDZE2nZYKkrxdn/kY9XQQkGCGqqDY5IgJFkEKgBCzDNGXhTKEye7boFRH6IPJj5EshiNCSjV4R4eSx7zhmR2tcdIuwmWiMeao7e0JHViZEWUI5aP8a9O+rx74D6sGEiJftiX3YeueIiFXg2KrhpqzjVC3dPZFYJZ7NOwwtNwM8R0UkLfH0sT5qck+OlkMq0BucKr0iWG7gpAQksD9esM1z3Lnf6SHjLh67nnKEGxC/iomWhByTeXOQJGHHcKxwHhHKnt1HIdYtmexkIb/HOURWTSJqn2gKMDG0bDUc/D0iAseovxUBoylmQCug6IVhSv+4DIeKI94jAr4AjiSEgQ25JYB+YWT9BZ94AM8erwgFkRifaArA6U0G5KT0m//z26REZuK9okgrT6VwE1jTHjbVzyNAyRwTEPOtuiex9FVBNZCkruaA4PZqFp1u8Rpww9/6rcK5y0EkAxRiZJt79PWOVYWGRE9pbJhavMengMflGyumk0akMsQnAAAAAElFTkSuQmCC' @@ -183,3 +188,58 @@ def test_ui_settings(get, put, patch, delete, admin, enterprise_license): response = get(url, user=admin, expect=200) assert not response.data['CUSTOM_LOGO'] assert not response.data['CUSTOM_LOGIN_INFO'] + + +@pytest.mark.django_db +def test_logging_aggregrator_connection_test_requires_superuser(get, post, alice): + url = reverse('api:setting_logging_test') + post(url, {}, user=alice, expect=403) + + +@pytest.mark.parametrize('key', [ + 'LOG_AGGREGATOR_TYPE', + 'LOG_AGGREGATOR_HOST', + 'LOG_AGGREGATOR_PORT', +]) +@pytest.mark.django_db +def test_logging_aggregrator_connection_test_bad_request(get, post, admin, key): + url = reverse('api:setting_logging_test') + resp = post(url, {}, user=admin, expect=400) + assert 'This field is required.' in resp.data.get(key, []) + + +@pytest.mark.django_db +def test_logging_aggregrator_connection_test_valid(mocker, get, post, admin): + with mock.patch.object(BaseHTTPSHandler, 'perform_test') as perform_test: + url = reverse('api:setting_logging_test') + post(url, { + 'LOG_AGGREGATOR_TYPE': 'logstash', + 'LOG_AGGREGATOR_HOST': 'localhost', + 'LOG_AGGREGATOR_PORT': 8080, + 'LOG_AGGREGATOR_USERNAME': 'logger', + 'LOG_AGGREGATOR_PASSWORD': 'mcstash' + }, user=admin, expect=200) + perform_test.assert_called_with(OrderedDict([ + ('LOG_AGGREGATOR_HOST', u'localhost'), + ('LOG_AGGREGATOR_PORT', 8080), + ('LOG_AGGREGATOR_TYPE', 'logstash'), + ('LOG_AGGREGATOR_USERNAME', 'logger'), + ('LOG_AGGREGATOR_PASSWORD', 'mcstash'), + ('LOG_AGGREGATOR_LOGGERS', ['awx', 'activity_stream', 'job_events', 'system_tracking']), + ('LOG_AGGREGATOR_INDIVIDUAL_FACTS', False), + ('LOG_AGGREGATOR_ENABLED', False), + ('LOG_AGGREGATOR_TOWER_UUID', '') + ])) + + +@pytest.mark.django_db +def test_logging_aggregrator_connection_test_invalid(mocker, get, post, admin): + with mock.patch.object(BaseHTTPSHandler, 'perform_test') as perform_test: + perform_test.side_effect = LoggingConnectivityException('404: Not Found') + url = reverse('api:setting_logging_test') + resp = post(url, { + 'LOG_AGGREGATOR_TYPE': 'logstash', + 'LOG_AGGREGATOR_HOST': 'localhost', + 'LOG_AGGREGATOR_PORT': 8080 + }, user=admin, expect=500) + assert resp.data == {'error': '404: Not Found'} diff --git a/awx/main/tests/functional/test_python_requirements.py b/awx/main/tests/functional/test_python_requirements.py index 6f16fc2624..88d86cf3f3 100644 --- a/awx/main/tests/functional/test_python_requirements.py +++ b/awx/main/tests/functional/test_python_requirements.py @@ -15,14 +15,21 @@ def test_env_matches_requirements_txt(): return False return True + def skip_line(line): + return ( + line == '' or line.strip().startswith('#') or + line.strip().startswith('git') or line.startswith('-e') or + '## The following requirements were added by pip freeze' in line + ) + base_dir = settings.BASE_DIR requirements_path = os.path.join(base_dir, '../', 'requirements/requirements.txt') reqs_actual = [] xs = freeze.freeze(local_only=True) for x in xs: - if '## The following requirements were added by pip freeze' in x: - break + if skip_line(x): + continue x = x.lower() (pkg_name, pkg_version) = x.split('==') reqs_actual.append([pkg_name, pkg_version]) @@ -33,11 +40,7 @@ def test_env_matches_requirements_txt(): line = line.partition('#')[0] line = line.rstrip().lower() # TODO: process git requiremenst and use egg - if line == '': - continue - if line.strip().startswith('#') or line.strip().startswith('git'): - continue - if line.startswith('-e'): + if skip_line(line): continue ''' diff --git a/awx/main/tests/unit/api/test_filters.py b/awx/main/tests/unit/api/test_filters.py index 6570ada6f7..45eec0df1f 100644 --- a/awx/main/tests/unit/api/test_filters.py +++ b/awx/main/tests/unit/api/test_filters.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import pytest from rest_framework.exceptions import PermissionDenied @@ -24,6 +26,14 @@ def test_valid_in(valid_value): assert 'foo' in value +def test_invalid_field(): + invalid_field = u"ヽヾ" + field_lookup = FieldLookupBackend() + with pytest.raises(ValueError) as excinfo: + field_lookup.value_to_python(WorkflowJobTemplate, invalid_field, 'foo') + assert 'is not an allowed field name. Must be ascii encodable.' in excinfo.value.message + + @pytest.mark.parametrize('lookup_suffix', ['', 'contains', 'startswith', 'in']) @pytest.mark.parametrize('password_field', Credential.PASSWORD_FIELDS) def test_filter_on_password_field(password_field, lookup_suffix): diff --git a/awx/main/tests/unit/utils/test_handlers.py b/awx/main/tests/unit/utils/test_handlers.py index 3de3b2e7b7..0f4dbdff05 100644 --- a/awx/main/tests/unit/utils/test_handlers.py +++ b/awx/main/tests/unit/utils/test_handlers.py @@ -1,13 +1,15 @@ import base64 import json import logging +from uuid import uuid4 from django.conf import LazySettings import pytest import requests from requests_futures.sessions import FuturesSession -from awx.main.utils.handlers import BaseHTTPSHandler as HTTPSHandler, PARAM_NAMES +from awx.main.utils.handlers import (BaseHTTPSHandler as HTTPSHandler, + PARAM_NAMES, LoggingConnectivityException) from awx.main.utils.formatters import LogstashFormatter @@ -25,19 +27,21 @@ def dummy_log_record(): @pytest.fixture() -def ok200_adapter(): - class OK200Adapter(requests.adapters.HTTPAdapter): +def http_adapter(): + class FakeHTTPAdapter(requests.adapters.HTTPAdapter): requests = [] + status = 200 + reason = None def send(self, request, **kwargs): self.requests.append(request) resp = requests.models.Response() - resp.status_code = 200 - resp.raw = '200 OK' + resp.status_code = self.status + resp.reason = self.reason resp.request = request return resp - return OK200Adapter() + return FakeHTTPAdapter() def test_https_logging_handler_requests_sync_implementation(): @@ -73,6 +77,42 @@ def test_https_logging_handler_from_django_settings(param, django_settings_name) assert hasattr(handler, param) and getattr(handler, param) == 'EXAMPLE' +@pytest.mark.parametrize( + 'status, reason, exc', + [(200, '200 OK', None), (404, 'Not Found', LoggingConnectivityException)] +) +def test_https_logging_handler_connectivity_test(http_adapter, status, reason, exc): + http_adapter.status = status + http_adapter.reason = reason + settings = LazySettings() + settings.configure(**{ + 'LOG_AGGREGATOR_HOST': 'example.org', + 'LOG_AGGREGATOR_PORT': 8080, + 'LOG_AGGREGATOR_TYPE': 'logstash', + 'LOG_AGGREGATOR_USERNAME': 'user', + 'LOG_AGGREGATOR_PASSWORD': 'password', + 'LOG_AGGREGATOR_LOGGERS': ['awx', 'activity_stream', 'job_events', 'system_tracking'], + 'CLUSTER_HOST_ID': '', + 'LOG_AGGREGATOR_TOWER_UUID': str(uuid4()) + }) + + class FakeHTTPSHandler(HTTPSHandler): + + def __init__(self, *args, **kwargs): + super(FakeHTTPSHandler, self).__init__(*args, **kwargs) + self.session.mount('http://', http_adapter) + + def emit(self, record): + return super(FakeHTTPSHandler, self).emit(record) + + if exc: + with pytest.raises(exc) as e: + FakeHTTPSHandler.perform_test(settings) + assert str(e).endswith('%s: %s' % (status, reason)) + else: + assert FakeHTTPSHandler.perform_test(settings) is None + + def test_https_logging_handler_logstash_auth_info(): handler = HTTPSHandler(message_type='logstash', username='bob', password='ansible') handler.add_auth_information() @@ -120,19 +160,19 @@ def test_https_logging_handler_skip_log(params, logger_name, expected): ('splunk', False), ('splunk', True), ]) -def test_https_logging_handler_emit(ok200_adapter, dummy_log_record, +def test_https_logging_handler_emit(http_adapter, dummy_log_record, message_type, async): handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True, message_type=message_type, enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'], async=async) handler.setFormatter(LogstashFormatter()) - handler.session.mount('http://', ok200_adapter) + handler.session.mount('http://', http_adapter) async_futures = handler.emit(dummy_log_record) [future.result() for future in async_futures] - assert len(ok200_adapter.requests) == 1 - request = ok200_adapter.requests[0] + assert len(http_adapter.requests) == 1 + request = http_adapter.requests[0] assert request.url == 'http://127.0.0.1/' assert request.method == 'POST' body = json.loads(request.body) @@ -152,7 +192,7 @@ def test_https_logging_handler_emit(ok200_adapter, dummy_log_record, @pytest.mark.parametrize('async', (True, False)) -def test_https_logging_handler_emit_logstash_with_creds(ok200_adapter, +def test_https_logging_handler_emit_logstash_with_creds(http_adapter, dummy_log_record, async): handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True, username='user', password='pass', @@ -160,38 +200,38 @@ def test_https_logging_handler_emit_logstash_with_creds(ok200_adapter, enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'], async=async) handler.setFormatter(LogstashFormatter()) - handler.session.mount('http://', ok200_adapter) + handler.session.mount('http://', http_adapter) async_futures = handler.emit(dummy_log_record) [future.result() for future in async_futures] - assert len(ok200_adapter.requests) == 1 - request = ok200_adapter.requests[0] + assert len(http_adapter.requests) == 1 + request = http_adapter.requests[0] assert request.headers['Authorization'] == 'Basic %s' % base64.b64encode("user:pass") @pytest.mark.parametrize('async', (True, False)) -def test_https_logging_handler_emit_splunk_with_creds(ok200_adapter, +def test_https_logging_handler_emit_splunk_with_creds(http_adapter, dummy_log_record, async): handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True, password='pass', message_type='splunk', enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'], async=async) handler.setFormatter(LogstashFormatter()) - handler.session.mount('http://', ok200_adapter) + handler.session.mount('http://', http_adapter) async_futures = handler.emit(dummy_log_record) [future.result() for future in async_futures] - assert len(ok200_adapter.requests) == 1 - request = ok200_adapter.requests[0] + assert len(http_adapter.requests) == 1 + request = http_adapter.requests[0] assert request.headers['Authorization'] == 'Splunk pass' -def test_https_logging_handler_emit_one_record_per_fact(ok200_adapter): +def test_https_logging_handler_emit_one_record_per_fact(http_adapter): handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True, message_type='logstash', indv_facts=True, enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking']) handler.setFormatter(LogstashFormatter()) - handler.session.mount('http://', ok200_adapter) + handler.session.mount('http://', http_adapter) record = logging.LogRecord( 'awx.analytics.system_tracking', # logger name 20, # loglevel INFO @@ -212,8 +252,8 @@ def test_https_logging_handler_emit_one_record_per_fact(ok200_adapter): async_futures = handler.emit(record) [future.result() for future in async_futures] - assert len(ok200_adapter.requests) == 2 - requests = sorted(ok200_adapter.requests, key=lambda request: json.loads(request.body)['version']) + assert len(http_adapter.requests) == 2 + requests = sorted(http_adapter.requests, key=lambda request: json.loads(request.body)['version']) request = requests[0] assert request.url == 'http://127.0.0.1/' diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index fe2fb87228..a258b8794c 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -5,6 +5,7 @@ import logging import json import requests +from requests.exceptions import RequestException from copy import copy # loggly @@ -40,6 +41,10 @@ def unused_callback(sess, resp): pass +class LoggingConnectivityException(Exception): + pass + + class HTTPSNullHandler(logging.NullHandler): "Placeholder null handler to allow loading without database access" @@ -66,6 +71,31 @@ class BaseHTTPSHandler(logging.Handler): kwargs[param] = getattr(settings, django_setting_name, None) return cls(*args, **kwargs) + @classmethod + def perform_test(cls, settings): + """ + Tests logging connectivity for the current logging settings. + @raises LoggingConnectivityException + """ + handler = cls.from_django_settings(settings, async=True) + handler.enabled_flag = True + handler.setFormatter(LogstashFormatter(settings_module=settings)) + logger = logging.getLogger(__file__) + fn, lno, func = logger.findCaller() + record = logger.makeRecord('awx', 10, fn, lno, + 'Ansible Tower Connection Test', tuple(), + None, func) + futures = handler.emit(record) + for future in futures: + try: + resp = future.result() + if not resp.ok: + raise LoggingConnectivityException( + ': '.join([str(resp.status_code), resp.reason or '']) + ) + except RequestException as e: + raise LoggingConnectivityException(str(e)) + def get_full_message(self, record): if record.exc_info: return '\n'.join(traceback.format_exception(*record.exc_info)) diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index 4cc306cb36..763abab7f2 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -1671,7 +1671,7 @@ tr td button i { .modal-body { min-height: 120px; padding: 20px 0; - + .alert { padding: 10px; margin: 0; @@ -1984,10 +1984,6 @@ tr td button i { width: 73px; } -.JobDetails-status { - margin-bottom: 12px; -} - .red-text { color: @red; } diff --git a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js index 37ffd6a113..7bbb9cdbc4 100644 --- a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js +++ b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js @@ -6,10 +6,10 @@ /* jshint unused: vars */ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateList', 'ProjectList', - 'InventoryList', 'CredentialList', '$compile', 'generateList', 'GetBasePath', 'SelectionInit', + 'InventoryList', 'CredentialList', '$compile', 'generateList', 'GetBasePath', 'OrganizationList', function(addPermissionsTeamsList, addPermissionsUsersList, TemplateList, ProjectList, - InventoryList, CredentialList, $compile, generateList, GetBasePath, SelectionInit, + InventoryList, CredentialList, $compile, generateList, GetBasePath, OrganizationList) { return { restrict: 'E', diff --git a/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js b/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js new file mode 100644 index 0000000000..80ba348b41 --- /dev/null +++ b/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js @@ -0,0 +1,78 @@ +export default + function BuildAnchor($log, $filter) { + // Returns a full resource_name HTML string if link can be derived from supplied context + // returns name of resource if activity stream object doesn't contain enough data to build a UI url + // arguments are: a summary_field object, a resource type, an activity stream object + return function (obj, resource, activity) { + var url = '/#/'; + // try/except pattern asserts that: + // if we encounter a case where a UI url can't or shouldn't be generated, just supply the name of the resource + try { + // catch-all case to avoid generating urls if a resource has been deleted + // if a resource still exists, it'll be serialized in the activity's summary_fields + if (!activity.summary_fields[resource]){ + throw {name : 'ResourceDeleted', message: 'The referenced resource no longer exists'}; + } + switch (resource) { + case 'custom_inventory_script': + url += 'inventory_scripts/' + obj.id + '/'; + break; + case 'group': + if (activity.operation === 'create' || activity.operation === 'delete'){ + // the API formats the changes.inventory field as str 'myInventoryName-PrimaryKey' + var inventory_id = _.last(activity.changes.inventory.split('-')); + url += 'inventories/' + inventory_id + '/manage?group=' + activity.changes.id; + } + else { + url += 'inventories/' + activity.summary_fields.inventory[0].id + '/manage?group=' + (activity.changes.id || activity.changes.object1_pk); + } + break; + case 'host': + url += 'home/hosts/' + obj.id; + break; + case 'job': + url += 'jobs/' + obj.id; + break; + case 'inventory': + url += 'inventories/' + obj.id + '/'; + break; + case 'schedule': + // schedule urls depend on the resource they're associated with + if (activity.summary_fields.job_template){ + url += 'job_templates/' + activity.summary_fields.job_template.id + '/schedules/' + obj.id; + } + else if (activity.summary_fields.project){ + url += 'projects/' + activity.summary_fields.project.id + '/schedules/' + obj.id; + } + else if (activity.summary_fields.system_job_template){ + url += 'management_jobs/' + activity.summary_fields.system_job_template.id + '/schedules/edit/' + obj.id; + } + // urls for inventory sync schedules currently depend on having an inventory id and group id + else { + throw {name : 'NotImplementedError', message : 'activity.summary_fields to build this url not implemented yet'}; + } + break; + case 'notification_template': + url += `notification_templates/${obj.id}`; + break; + case 'role': + throw {name : 'NotImplementedError', message : 'role object management is not consolidated to a single UI view'}; + case 'job_template': + url += `templates/job_template/${obj.id}`; + break; + case 'workflow_job_template': + url += `templates/workflow_job_template/${obj.id}`; + break; + default: + url += resource + 's/' + obj.id + '/'; + } + return ' ' + $filter('sanitize')(obj.name || obj.username) + ' '; + } + catch(err){ + $log.debug(err); + return ' ' + $filter('sanitize')(obj.name || obj.username || '') + ' '; + } + }; + } + + BuildAnchor.$inject = ['$log', '$filter']; diff --git a/awx/ui/client/src/activity-stream/factories/build-description.factory.js b/awx/ui/client/src/activity-stream/factories/build-description.factory.js new file mode 100644 index 0000000000..ecd596dda2 --- /dev/null +++ b/awx/ui/client/src/activity-stream/factories/build-description.factory.js @@ -0,0 +1,126 @@ +export default + function BuildDescription(BuildAnchor, $log, i18n) { + return function (activity) { + + var pastTense = function(operation){ + return (/e$/.test(activity.operation)) ? operation + 'd ' : operation + 'ed '; + }; + // convenience method to see if dis+association operation involves 2 groups + // the group cases are slightly different because groups can be dis+associated into each other + var isGroupRelationship = function(activity){ + return activity.object1 === 'group' && activity.object2 === 'group' && activity.summary_fields.group.length > 1; + }; + + // Activity stream objects will outlive the resources they reference + // in that case, summary_fields will not be available - show generic error text instead + try { + activity.description = pastTense(activity.operation); + switch(activity.object_association){ + // explicit role dis+associations + case 'role': + // object1 field is resource targeted by the dis+association + // object2 field is the resource the role is inherited from + // summary_field.role[0] contains ref info about the role + switch(activity.operation){ + // expected outcome: "disassociated role_name from " + case 'disassociate': + if (isGroupRelationship(activity)){ + activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' from ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); + } + else{ + activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + } + break; + // expected outcome: "associated role_name to " + case 'associate': + if (isGroupRelationship(activity)){ + activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' to ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); + } + else{ + activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + } + break; + } + break; + // inherited role dis+associations (logic identical to case 'role') + case 'parents': + // object1 field is resource targeted by the dis+association + // object2 field is the resource the role is inherited from + // summary_field.role[0] contains ref info about the role + switch(activity.operation){ + // expected outcome: "disassociated role_name from " + case 'disassociate': + if (isGroupRelationship(activity)){ + activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + + 'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); + } + else{ + activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + } + break; + // expected outcome: "associated role_name to " + case 'associate': + if (isGroupRelationship(activity)){ + activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) + + 'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity); + } + else{ + activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + } + break; + } + break; + // CRUD operations / resource on resource dis+associations + default: + switch(activity.operation){ + // expected outcome: "disassociated from " + case 'disassociate' : + if (isGroupRelationship(activity)){ + activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + + 'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); + } + else { + activity.description += activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + + 'from ' + activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + } + break; + // expected outcome "associated to " + case 'associate': + // groups are the only resource that can be associated/disassociated into each other + if (isGroupRelationship(activity)){ + activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) + + 'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity); + } + else { + activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity) + + 'to ' + activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity); + } + break; + case 'delete': + activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity); + break; + // expected outcome: "operation " + case 'update': + activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + break; + case 'create': + activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity); + break; + } + break; + } + } + catch(err){ + $log.debug(err); + activity.description = i18n._('Event summary not available'); + } + }; + } + + BuildDescription.$inject = ['BuildAnchor', '$log', 'i18n']; diff --git a/awx/ui/client/src/activity-stream/factories/show-detail.factory.js b/awx/ui/client/src/activity-stream/factories/show-detail.factory.js new file mode 100644 index 0000000000..fde935e108 --- /dev/null +++ b/awx/ui/client/src/activity-stream/factories/show-detail.factory.js @@ -0,0 +1,39 @@ +export default + function ShowDetail($filter, $rootScope, Rest, Alert, GenerateForm, ProcessErrors, GetBasePath, FormatDate, ActivityDetailForm, Empty, Find) { + return function (params, scope) { + + var activity_id = params.activity_id, + activity = Find({ list: params.scope.activities, key: 'id', val: activity_id }), + element; + + if (activity) { + + // Grab our element out of the dom + element = angular.element(document.getElementById('stream-detail-modal')); + + // Grab the modal's scope so that we can set a few variables + scope = element.scope(); + + scope.changes = activity.changes; + scope.user = ((activity.summary_fields.actor) ? activity.summary_fields.actor.username : 'system') + + ' on ' + $filter('longDate')(activity.timestamp); + scope.operation = activity.description; + scope.header = "Event " + activity.id; + + // Open the modal + $('#stream-detail-modal').modal({ + show: true, + backdrop: 'static', + keyboard: true + }); + + if (!scope.$$phase) { + scope.$digest(); + } + } + + }; + } + + ShowDetail.$inject = ['$filter', '$rootScope', 'Rest', 'Alert', 'GenerateForm', 'ProcessErrors', 'GetBasePath', 'FormatDate', + 'ActivityDetailForm', 'Empty', 'Find']; diff --git a/awx/ui/client/src/activity-stream/factories/stream.factory.js b/awx/ui/client/src/activity-stream/factories/stream.factory.js new file mode 100644 index 0000000000..b3725fc2aa --- /dev/null +++ b/awx/ui/client/src/activity-stream/factories/stream.factory.js @@ -0,0 +1,54 @@ +export default + function Stream($rootScope, $location, $state, Rest, GetBasePath, ProcessErrors, + Wait, StreamList, GenerateList, FormatDate, + BuildDescription, ShowDetail) { + return function (params) { + + var scope = params.scope; + + $rootScope.flashMessage = null; + + // descriptive title describing what AS is showing + scope.streamTitle = (params && params.title) ? params.title : null; + + scope.refreshStream = function () { + $state.go('.', null, {reload: true}); + }; + + scope.showDetail = function (id) { + ShowDetail({ + scope: scope, + activity_id: id + }); + }; + + if(scope.activities && scope.activities.length > 0) { + buildUserAndDescription(); + } + + scope.$watch('activities', function(){ + // Watch for future update to scope.activities (like page change, column sort, search, etc) + buildUserAndDescription(); + }); + + function buildUserAndDescription(){ + scope.activities.forEach(function(activity, i) { + // build activity.user + if (scope.activities[i].summary_fields.actor) { + scope.activities[i].user = "" + + scope.activities[i].summary_fields.actor.username + ""; + } else { + scope.activities[i].user = 'system'; + } + // build description column / action text + BuildDescription(scope.activities[i]); + + }); + } + + }; + } + + Stream.$inject = ['$rootScope', '$location', '$state', 'Rest', 'GetBasePath', + 'ProcessErrors', 'Wait', 'StreamList', 'generateList', 'FormatDate', 'BuildDescription', + 'ShowDetail']; diff --git a/awx/ui/client/src/activity-stream/get-target-title.factory.js b/awx/ui/client/src/activity-stream/get-target-title.factory.js new file mode 100644 index 0000000000..85c6c7a80b --- /dev/null +++ b/awx/ui/client/src/activity-stream/get-target-title.factory.js @@ -0,0 +1,51 @@ +export default + function GetTargetTitle(i18n) { + return function (target) { + + var rtnTitle = i18n._('ALL ACTIVITY'); + + switch(target) { + case 'project': + rtnTitle = i18n._('PROJECTS'); + break; + case 'inventory': + rtnTitle = i18n._('INVENTORIES'); + break; + case 'credential': + rtnTitle = i18n._('CREDENTIALS'); + break; + case 'user': + rtnTitle = i18n._('USERS'); + break; + case 'team': + rtnTitle = i18n._('TEAMS'); + break; + case 'notification_template': + rtnTitle = i18n._('NOTIFICATION TEMPLATES'); + break; + case 'organization': + rtnTitle = i18n._('ORGANIZATIONS'); + break; + case 'job': + rtnTitle = i18n._('JOBS'); + break; + case 'custom_inventory_script': + rtnTitle = i18n._('INVENTORY SCRIPTS'); + break; + case 'schedule': + rtnTitle = i18n._('SCHEDULES'); + break; + case 'host': + rtnTitle = i18n._('HOSTS'); + break; + case 'template': + rtnTitle = i18n._('TEMPLATES'); + break; + } + + return rtnTitle; + + }; + } + +GetTargetTitle.$inject = ['i18n']; diff --git a/awx/ui/client/src/activity-stream/main.js b/awx/ui/client/src/activity-stream/main.js index 381c421c87..3cb93e3003 100644 --- a/awx/ui/client/src/activity-stream/main.js +++ b/awx/ui/client/src/activity-stream/main.js @@ -6,14 +6,24 @@ import activityStreamRoute from './activitystream.route'; import activityStreamController from './activitystream.controller'; - import streamDropdownNav from './streamDropdownNav/stream-dropdown-nav.directive'; - import streamDetailModal from './streamDetailModal/main'; +import BuildAnchor from './factories/build-anchor.factory'; +import BuildDescription from './factories/build-description.factory'; +import ShowDetail from './factories/show-detail.factory'; +import Stream from './factories/stream.factory'; +import GetTargetTitle from './get-target-title.factory'; +import ModelToBasePathKey from './model-to-base-path-key.factory'; export default angular.module('activityStream', [streamDetailModal.name]) .controller('activityStreamController', activityStreamController) .directive('streamDropdownNav', streamDropdownNav) + .factory('BuildAnchor', BuildAnchor) + .factory('BuildDescription', BuildDescription) + .factory('ShowDetail', ShowDetail) + .factory('Stream', Stream) + .factory('GetTargetTitle', GetTargetTitle) + .factory('ModelToBasePathKey', ModelToBasePathKey) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(activityStreamRoute); }]); diff --git a/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js b/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js new file mode 100644 index 0000000000..bc6cfe74b8 --- /dev/null +++ b/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js @@ -0,0 +1,59 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name helpers.function:ApiModel + * @description Helper functions to convert singular/plural versions of our models to the opposite +*/ + +export default + function ModelToBasePathKey() { + return function(model) { + // This function takes in the singular model string and returns the key needed + // to get the base path from $rootScope/local storage. + + var basePathKey; + + switch(model) { + case 'project': + basePathKey = 'projects'; + break; + case 'inventory': + basePathKey = 'inventory'; + break; + case 'job_template': + basePathKey = 'job_templates'; + break; + case 'credential': + basePathKey = 'credentials'; + break; + case 'user': + basePathKey = 'users'; + break; + case 'team': + basePathKey = 'teams'; + break; + case 'notification_template': + basePathKey = 'notification_templates'; + break; + case 'organization': + basePathKey = 'organizations'; + break; + case 'management_job': + basePathKey = 'management_jobs'; + break; + case 'custom_inventory_script': + basePathKey = 'inventory_scripts'; + break; + case 'workflow_job_template': + basePathKey = 'workflow_job_templates'; + break; + } + + return basePathKey; + }; + } diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index c14ced247b..05df2f8cc0 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -37,20 +37,14 @@ if ($basePath) { } // Modules -import './helpers'; +import './forms'; import './lists'; -import './widgets'; -import './filters'; -import { Home } from './controllers/Home'; -import { SocketsController } from './controllers/Sockets'; -import { CredentialsAdd, CredentialsEdit, CredentialsList } from './controllers/Credentials'; import portalMode from './portal-mode/main'; import systemTracking from './system-tracking/main'; import inventories from './inventories/main'; import inventoryScripts from './inventory-scripts/main'; import organizations from './organizations/main'; import managementJobs from './management-jobs/main'; -import jobDetail from './job-detail/main'; import workflowResults from './workflow-results/main'; import jobResults from './job-results/main'; import jobSubmission from './job-submission/main'; @@ -62,30 +56,21 @@ import mainMenu from './main-menu/main'; import breadCrumb from './bread-crumb/main'; import browserData from './browser-data/main'; import configuration from './configuration/main'; -import dashboard from './dashboard/main'; -import moment from './shared/moment/main'; +import home from './home/main'; import login from './login/main'; import activityStream from './activity-stream/main'; import standardOut from './standard-out/main'; import Templates from './templates/main'; import credentials from './credentials/main'; import jobs from './jobs/main'; -import { ProjectsList, ProjectsAdd, ProjectsEdit } from './controllers/Projects'; -import { UsersList, UsersAdd, UsersEdit } from './controllers/Users'; -import { TeamsList, TeamsAdd, TeamsEdit } from './controllers/Teams'; - +import teams from './teams/main'; +import users from './users/main'; +import projects from './projects/main'; import RestServices from './rest/main'; import access from './access/main'; -import './shared/Modal'; -import './shared/prompt-dialog'; -import './shared/directives'; -import './shared/filters'; -import './shared/features/main'; -import config from './shared/config/main'; import './login/authenticationServices/pendo/ng-pendo'; import footer from './footer/main'; import scheduler from './scheduler/main'; -import { N_ } from './i18n'; var tower = angular.module('Tower', [ // how to add CommonJS / AMD third-party dependencies: @@ -97,6 +82,7 @@ var tower = angular.module('Tower', [ require('angular-sanitize'), require('angular-scheduler').name, require('angular-tz-extensions'), + require('angular-md5'), require('lr-infinite-scroll'), require('ng-toast'), 'gettext', @@ -119,12 +105,10 @@ var tower = angular.module('Tower', [ setupMenu.name, mainMenu.name, breadCrumb.name, - dashboard.name, - moment.name, + home.name, login.name, activityStream.name, footer.name, - jobDetail.name, workflowResults.name, jobResults.name, jobSubmission.name, @@ -132,9 +116,11 @@ var tower = angular.module('Tower', [ standardOut.name, Templates.name, portalMode.name, - config.name, credentials.name, jobs.name, + teams.name, + users.name, + projects.name, //'templates', 'Utilities', 'OrganizationFormDefinition', @@ -142,68 +128,44 @@ var tower = angular.module('Tower', [ 'OrganizationListDefinition', 'templates', 'UserListDefinition', - 'UserHelper', 'PromptDialog', 'AWDirectives', 'InventoriesListDefinition', 'InventoryFormDefinition', - 'InventoryHelper', 'InventoryGroupsDefinition', 'InventoryHostsDefinition', - 'HostsHelper', - 'AWFilters', 'HostFormDefinition', 'HostListDefinition', 'GroupFormDefinition', 'GroupListDefinition', - 'GroupsHelper', 'TeamsListDefinition', 'TeamFormDefinition', - 'TeamHelper', 'CredentialsListDefinition', 'CredentialFormDefinition', 'TemplatesListDefinition', 'PortalJobTemplatesListDefinition', 'JobTemplateFormDefinition', - 'JobTemplatesHelper', - 'JobSubmissionHelper', 'ProjectsListDefinition', 'ProjectFormDefinition', 'ProjectStatusDefinition', - 'ProjectsHelper', 'CompletedJobsDefinition', 'AllJobsDefinition', 'JobSummaryDefinition', - 'ParseHelper', - 'ChildrenHelper', - 'ProjectPathHelper', - 'md5Helper', - 'SelectionHelper', 'HostGroupsFormDefinition', - 'StreamWidget', - 'JobsHelper', - 'CredentialsHelper', 'StreamListDefinition', 'ActivityDetailDefinition', - 'VariablesHelper', 'SchedulesListDefinition', 'ScheduledJobsDefinition', //'Timezones', - 'SchedulesHelper', 'JobsListDefinition', 'LogViewerStatusDefinition', 'StandardOutHelper', 'LogViewerOptionsDefinition', - 'JobDetailHelper', 'lrInfiniteScroll', - 'LoadConfigHelper', 'PortalJobsListDefinition', 'features', - 'longDateFilter', 'pendolytics', scheduler.name, - 'ApiModelHelper', - 'ActivityStreamHelper', 'WorkflowFormDefinition', 'InventorySourcesListDefinition', 'WorkflowMakerFormDefinition' @@ -227,10 +189,9 @@ var tower = angular.module('Tower', [ }); }]) .config(['$urlRouterProvider', '$breadcrumbProvider', 'QuerySetProvider', - '$urlMatcherFactoryProvider', 'stateDefinitionsProvider', '$stateProvider', + '$urlMatcherFactoryProvider', function($urlRouterProvider, $breadcrumbProvider, QuerySet, - $urlMatcherFactoryProvider, stateDefinitionsProvider, $stateProvider) { - let stateDefinitions = stateDefinitionsProvider.$get(); + $urlMatcherFactoryProvider) { $urlMatcherFactoryProvider.strictMode(false); $breadcrumbProvider.setOptions({ templateUrl: urlPrefix + 'partials/breadcrumb.html' @@ -266,117 +227,14 @@ var tower = angular.module('Tower', [ // $stateProvider.stateRegistry.onStatesChanged((event, states) =>{ // console.log(event, states) // }) - - - // lazily generate a tree of substates which will replace this node in ui-router's stateRegistry - // see: stateDefinition.factory for usage documentation - $stateProvider.state({ - name: 'projects', - url: '/projects', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'projects', // top-most node in the generated tree (will replace this state definition) - modes: ['add', 'edit'], - list: 'ProjectList', - form: 'ProjectsForm', - controllers: { - list: ProjectsList, // DI strings or objects - add: ProjectsAdd, - edit: ProjectsEdit - }, - data: { - activityStream: true, - activityStreamTarget: 'project', - socket: { - "groups": { - "jobs": ["status_changed"] - } - } - }, - ncyBreadcrumb: { - label: N_('PROJECTS') - } - }) - }); - - $stateProvider.state({ - name: 'credentials', - url: '/credentials', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'credentials', - modes: ['add', 'edit'], - list: 'CredentialList', - form: 'CredentialForm', - controllers: { - list: CredentialsList, - add: CredentialsAdd, - edit: CredentialsEdit - }, - data: { - activityStream: true, - activityStreamTarget: 'credential' - }, - ncyBreadcrumb: { - parent: 'setup', - label: N_('CREDENTIALS') - } - }) - }); - - $stateProvider.state({ - name: 'teams', - url: '/teams', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'teams', - modes: ['add', 'edit'], - list: 'TeamList', - form: 'TeamForm', - controllers: { - list: TeamsList, - add: TeamsAdd, - edit: TeamsEdit - }, - data: { - activityStream: true, - activityStreamTarget: 'team' - }, - ncyBreadcrumb: { - parent: 'setup', - label: N_('TEAMS') - } - }) - }); - - $stateProvider.state({ - name: 'users', - url: '/users', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'users', - modes: ['add', 'edit'], - list: 'UserList', - form: 'UserForm', - controllers: { - list: UsersList, - add: UsersAdd, - edit: UsersEdit - }, - data: { - activityStream: true, - activityStreamTarget: 'user' - }, - ncyBreadcrumb: { - parent: 'setup', - label: N_('USERS') - } - }) - }); } ]) - .run(['$stateExtender', '$q', '$compile', '$cookieStore', '$rootScope', '$log', '$stateParams', + .run(['$stateExtender', '$q', '$compile', '$cookies', '$rootScope', '$log', '$stateParams', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'LoadConfig', 'Store', 'pendoService', 'Prompt', 'Rest', 'Wait', 'ProcessErrors', '$state', 'GetBasePath', 'ConfigService', 'FeaturesService', '$filter', 'SocketService', - function($stateExtender, $q, $compile, $cookieStore, $rootScope, $log, $stateParams, + function($stateExtender, $q, $compile, $cookies, $rootScope, $log, $stateParams, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, LoadConfig, Store, pendoService, Prompt, Rest, Wait, ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService, @@ -392,67 +250,13 @@ var tower = angular.module('Tower', [ $log.debug(`$state.defaultErrorHandler: ${error}`); }); - $stateExtender.addState({ - name: 'dashboard', - url: '/home', - templateUrl: urlPrefix + 'partials/home.html', - controller: Home, - params: { licenseMissing: null }, - data: { - activityStream: true, - refreshButton: true, - socket: { - "groups": { - "jobs": ["status_changed"] - } - }, - }, - ncyBreadcrumb: { - label: N_("DASHBOARD") - }, - resolve: { - graphData: ['$q', 'jobStatusGraphData', '$rootScope', - function($q, jobStatusGraphData, $rootScope) { - return $rootScope.featuresConfigured.promise.then(function() { - return $q.all({ - jobStatus: jobStatusGraphData.get("month", "all"), - }); - }); - } - ] - } - }); + $rootScope.refresh = function() { + $state.go('.', null, {reload: true}); + }; - $stateExtender.addState({ - name: 'userCredentials', - url: '/users/:user_id/credentials', - templateUrl: urlPrefix + 'partials/users.html', - controller: CredentialsList - }); - - $stateExtender.addState({ - name: 'userCredentialAdd', - url: '/users/:user_id/credentials/add', - templateUrl: urlPrefix + 'partials/teams.html', - controller: CredentialsAdd - }); - - $stateExtender.addState({ - name: 'teamUserCredentialEdit', - url: '/teams/:user_id/credentials/:credential_id', - templateUrl: urlPrefix + 'partials/teams.html', - controller: CredentialsEdit - }); - - $stateExtender.addState({ - name: 'sockets', - url: '/sockets', - templateUrl: urlPrefix + 'partials/sockets.html', - controller: SocketsController, - ncyBreadcrumb: { - label: N_('SOCKETS') - } - }); + $rootScope.refreshJobs = function(){ + $state.go('.', null, {reload: true}); + }; function activateTab() { // Make the correct tab active @@ -511,19 +315,19 @@ var tower = angular.module('Tower', [ $rootScope.$on("$stateChangeStart", function (event, next) { // Remove any lingering intervals - // except on jobDetails.* states - var jobDetailStates = [ - 'jobDetail', - 'jobDetail.host-summary', - 'jobDetail.host-event.details', - 'jobDetail.host-event.json', - 'jobDetail.host-events', - 'jobDetail.host-event.stdout' + // except on jobResults.* states + var jobResultStates = [ + 'jobResult', + 'jobResult.host-summary', + 'jobResult.host-event.details', + 'jobResult.host-event.json', + 'jobResult.host-events', + 'jobResult.host-event.stdout' ]; - if ($rootScope.jobDetailInterval && !_.includes(jobDetailStates, next.name) ) { - window.clearInterval($rootScope.jobDetailInterval); + if ($rootScope.jobResultInterval && !_.includes(jobResultStates, next.name) ) { + window.clearInterval($rootScope.jobResultInterval); } - if ($rootScope.jobStdOutInterval && !_.includes(jobDetailStates, next.name) ) { + if ($rootScope.jobStdOutInterval && !_.includes(jobResultStates, next.name) ) { window.clearInterval($rootScope.jobStdOutInterval); } @@ -532,7 +336,7 @@ var tower = angular.module('Tower', [ // capture most recent URL, excluding login/logout $rootScope.lastPath = $location.path(); $rootScope.enteredPath = $location.path(); - $cookieStore.put('lastPath', $location.path()); + $cookies.put('lastPath', $location.path()); } if (Authorization.isUserLoggedIn() === false) { @@ -598,7 +402,7 @@ var tower = angular.module('Tower', [ // User not authenticated, redirect to login page $location.path('/login'); } else { - var lastUser = $cookieStore.get('current_user'), + var lastUser = $cookies.getObject('current_user'), timestammp = Store('sessionTime'); if (lastUser && lastUser.id && timestammp && timestammp[lastUser.id] && timestammp[lastUser.id].loggedIn) { var stime = timestammp[lastUser.id].time, diff --git a/awx/ui/client/src/controllers/Credentials.js b/awx/ui/client/src/controllers/Credentials.js deleted file mode 100644 index 9f049e490b..0000000000 --- a/awx/ui/client/src/controllers/Credentials.js +++ /dev/null @@ -1,630 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Credentials - * @description This controller's for the credentials page - */ - - -export function CredentialsList($scope, $rootScope, $location, $log, - $stateParams, Rest, Alert, CredentialList, Prompt, ClearScope, - ProcessErrors, GetBasePath, Wait, $state, $filter, rbacUiControlService, Dataset, - i18n) { - - ClearScope(); - - var list = CredentialList, - defaultUrl = GetBasePath('credentials'); - - init(); - - function init() { - rbacUiControlService.canAdd('credentials') - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - - // search init - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - $scope.selected = []; - } - - $scope.$on(`${list.iterator}_options`, function(event, data){ - $scope.options = data.data.actions.GET; - optionsRequestDataProcessing(); - }); - - $scope.$watchCollection(`${$scope.list.name}`, function() { - 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] !== 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 && $scope.options && - $scope.options.hasOwnProperty('kind')) { - $scope.options.kind.choices.forEach(function(choice) { - if (choice[0] === item.kind) { - itm.kind_label = choice[1]; - } - }); - } - }); - } - } - - $scope.addCredential = function() { - $state.go('credentials.add'); - }; - - $scope.editCredential = function(id) { - $state.go('credentials.edit', { credential_id: id }); - }; - - $scope.deleteCredential = function(id, name) { - var action = function() { - $('#prompt-modal').modal('hide'); - Wait('start'); - var url = defaultUrl + id + '/'; - Rest.setUrl(url); - Rest.destroy() - .success(function() { - if (parseInt($state.params.credential_id) === id) { - $state.go("^", null, { reload: true }); - } else { - $state.go('.', null, {reload: true}); - } - Wait('stop'); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { - hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status - }); - }); - }; - - Prompt({ - hdr: i18n._('Delete'), - body: '
' + i18n._('Are you sure you want to delete the credential below?') + '
' + $filter('sanitize')(name) + '
', - action: action, - actionText: i18n._('DELETE') - }); - }; -} - -CredentialsList.$inject = ['$scope', '$rootScope', '$location', '$log', - '$stateParams', 'Rest', 'Alert', 'CredentialList', 'Prompt', 'ClearScope', - 'ProcessErrors', 'GetBasePath', 'Wait', '$state', '$filter', 'rbacUiControlService', 'Dataset', 'i18n' -]; - - -export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, - $stateParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, - ClearScope, GetBasePath, GetChoices, Empty, KindChange, BecomeMethodChange, - OwnerChange, FormSave, $state, CreateSelect2, i18n) { - ClearScope(); - - // Inject dynamic view - var form = CredentialForm, - defaultUrl = GetBasePath('credentials'), - url; - - init(); - - function init() { - // Load the list of options for Kind - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'kind', - variable: 'credential_kind_options' - }); - - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'become_method', - variable: 'become_options' - }); - - CreateSelect2({ - element: '#credential_become_method', - multiple: false - }); - - CreateSelect2({ - element: '#credential_kind', - multiple: false - }); - - // apply form definition's default field values - GenerateForm.applyDefaults(form, $scope); - - $scope.keyEntered = false; - $scope.permissionsTooltip = i18n._('Please save before assigning permissions'); - - // determine if the currently logged-in user may share this credential - // previous commentary said: "$rootScope.current_user isn't available because a call to the config endpoint hasn't finished resolving yet" - // I'm 99% sure this state's will never resolve block will be rejected if setup surrounding config endpoint hasn't completed - if ($rootScope.current_user && $rootScope.current_user.is_superuser) { - $scope.canShareCredential = true; - } else { - Rest.setUrl(`/api/v1/users/${$rootScope.current_user.id}/admin_of_organizations`); - Rest.get() - .success(function(data) { - $scope.canShareCredential = (data.count) ? true : false; - }).error(function(data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to find if users is admin of org' + status }); - }); - } - } - - if (!Empty($stateParams.user_id)) { - // Get the username based on incoming route - $scope.owner = 'user'; - $scope.user = $stateParams.user_id; - OwnerChange({ scope: $scope }); - url = GetBasePath('users') + $stateParams.user_id + '/'; - Rest.setUrl(url); - Rest.get() - .success(function(data) { - $scope.user_username = data.username; - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user. GET status: ' + status }); - }); - } else if (!Empty($stateParams.team_id)) { - // Get the username based on incoming route - $scope.owner = 'team'; - $scope.team = $stateParams.team_id; - OwnerChange({ scope: $scope }); - url = GetBasePath('teams') + $stateParams.team_id + '/'; - Rest.setUrl(url); - Rest.get() - .success(function(data) { - $scope.team_name = data.name; - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve team. GET status: ' + status }); - }); - } else { - // default type of owner to a user - $scope.owner = 'user'; - OwnerChange({ scope: $scope }); - } - - $scope.$watch("ssh_key_data", function(val) { - if (val === "" || val === null || val === undefined) { - $scope.keyEntered = false; - $scope.ssh_key_unlock_ask = false; - $scope.ssh_key_unlock = ""; - } else { - $scope.keyEntered = true; - } - }); - - // Handle Kind change - $scope.kindChange = function() { - KindChange({ scope: $scope, form: form, reset: true }); - }; - - $scope.becomeMethodChange = function() { - BecomeMethodChange({ scope: $scope }); - }; - - // Save - $scope.formSave = function() { - if ($scope[form.name + '_form'].$valid) { - FormSave({ scope: $scope, mode: 'add' }); - } - }; - - $scope.formCancel = function() { - $state.go('credentials'); - }; - - // Password change - $scope.clearPWConfirm = function(fld) { - // If password value changes, make sure password_confirm must be re-entered - $scope[fld] = ''; - $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); - }; - - // Respond to 'Ask at runtime?' checkbox - $scope.ask = function(fld, associated) { - if ($scope[fld + '_ask']) { - $scope[fld] = 'ASK'; - $("#" + form.name + "_" + fld + "_input").attr("type", "text"); - $("#" + form.name + "_" + fld + "_show_input_button").html("Hide"); - if (associated !== "undefined") { - $("#" + form.name + "_" + fld + "_input").attr("type", "password"); - $("#" + form.name + "_" + fld + "_show_input_button").html("Show"); - $scope[associated] = ''; - $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); - } - } else { - $scope[fld] = ''; - $("#" + form.name + "_" + fld + "_input").attr("type", "password"); - $("#" + form.name + "_" + fld + "_show_input_button").html("Show"); - if (associated !== "undefined") { - $("#" + form.name + "_" + fld + "_input").attr("type", "text"); - $("#" + form.name + "_" + fld + "_show_input_button").html("Hide"); - $scope[associated] = ''; - $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); - } - } - }; - - // Click clear button - $scope.clear = function(fld, associated) { - $scope[fld] = ''; - $scope[associated] = ''; - $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); - $scope[form.name + '_form'].$setDirty(); - }; - -} - -CredentialsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', - '$log', '$stateParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', - 'ProcessErrors', 'ClearScope', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'BecomeMethodChange', - 'OwnerChange', 'FormSave', '$state', 'CreateSelect2', 'i18n' -]; - -export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, - $stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt, - GetBasePath, GetChoices, KindChange, BecomeMethodChange, Empty, OwnerChange, FormSave, Wait, - $state, CreateSelect2, Authorization, i18n) { - - ClearScope(); - - var defaultUrl = GetBasePath('credentials'), - form = CredentialForm, - base = $location.path().replace(/^\//, '').split('/')[0], - master = {}, - id = $stateParams.credential_id; - - init(); - - function init() { - $scope.id = id; - $scope.$watch('credential_obj.summary_fields.user_capabilities.edit', function(val) { - if (val === false) { - $scope.canAdd = false; - } - }); - - $scope.canShareCredential = false; - Wait('start'); - if (!$rootScope.current_user) { - Authorization.restoreUserInfo(); - } - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'kind', - variable: 'credential_kind_options', - callback: 'choicesReadyCredential' - }); - - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'become_method', - variable: 'become_options' - }); - - if ($rootScope.current_user && $rootScope.current_user.is_superuser) { - $scope.canShareCredential = true; - } else { - Rest.setUrl(`/api/v1/users/${$rootScope.current_user.id}/admin_of_organizations`); - Rest.get() - .success(function(data) { - $scope.canShareCredential = (data.count) ? true : false; - Wait('stop'); - }).error(function(data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to find if users is admin of org' + status }); - }); - } - - $scope.$watch('organization', function(val) { - if (val === undefined) { - $scope.permissionsTooltip = i18n._('Populate the organization field in the form below in order to set permissions.'); - } else { - $scope.permissionsTooltip = ''; - } - }); - - OwnerChange({ scope: $scope }); - $scope.$watch("ssh_key_data", function(val) { - if (val === "" || val === null || val === undefined) { - $scope.keyEntered = false; - $scope.ssh_key_unlock_ask = false; - $scope.ssh_key_unlock = ""; - } else { - $scope.keyEntered = true; - } - }); - } - - function setAskCheckboxes() { - var fld, i; - for (fld in form.fields) { - if (form.fields[fld].type === 'sensitive' && $scope[fld] === 'ASK') { - // turn on 'ask' checkbox for password fields with value of 'ASK' - $("#" + form.name + "_" + fld + "_input").attr("type", "text"); - $("#" + form.name + "_" + fld + "_show_input_button").html("Hide"); - $("#" + fld + "-clear-btn").attr("disabled", "disabled"); - $scope[fld + '_ask'] = true; - } else { - $scope[fld + '_ask'] = false; - $("#" + fld + "-clear-btn").removeAttr("disabled"); - } - master[fld + '_ask'] = $scope[fld + '_ask']; - } - - // Set kind field to the correct option - for (i = 0; i < $scope.credential_kind_options.length; i++) { - if ($scope.kind === $scope.credential_kind_options[i].value) { - $scope.kind = $scope.credential_kind_options[i]; - break; - } - } - } - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('choicesReadyCredential', function() { - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl + ':id/'); - Rest.get({ params: { id: id } }) - .success(function(data) { - if (data && data.summary_fields && - data.summary_fields.organization && - data.summary_fields.organization.id) { - $scope.needsRoleList = true; - } else { - $scope.needsRoleList = false; - } - - $scope.credential_name = data.name; - - var i, fld; - - - for (fld in form.fields) { - if (data[fld] !== null && data[fld] !== undefined) { - $scope[fld] = data[fld]; - master[fld] = $scope[fld]; - } - if (form.fields[fld].type === 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { - $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; - } - } - - if (!Empty($scope.user)) { - $scope.owner = 'user'; - } else { - $scope.owner = 'team'; - } - master.owner = $scope.owner; - - for (i = 0; i < $scope.become_options.length; i++) { - if ($scope.become_options[i].value === data.become_method) { - $scope.become_method = $scope.become_options[i]; - break; - } - } - - if ($scope.become_method && $scope.become_method.value === "") { - $scope.become_method = null; - } - master.become_method = $scope.become_method; - - $scope.$watch('become_method', function(val) { - if (val !== null) { - if (val.value === "") { - $scope.become_username = ""; - $scope.become_password = ""; - } - } - }); - - for (i = 0; i < $scope.credential_kind_options.length; i++) { - if ($scope.credential_kind_options[i].value === data.kind) { - $scope.kind = $scope.credential_kind_options[i]; - break; - } - } - - KindChange({ - scope: $scope, - form: form, - reset: false - }); - - master.kind = $scope.kind; - - CreateSelect2({ - element: '#credential_become_method', - multiple: false - }); - - CreateSelect2({ - element: '#credential_kind', - multiple: false - }); - - switch (data.kind) { - case 'aws': - $scope.access_key = data.username; - $scope.secret_key = data.password; - master.access_key = $scope.access_key; - master.secret_key = $scope.secret_key; - break; - case 'ssh': - $scope.ssh_password = data.password; - master.ssh_password = $scope.ssh_password; - break; - case 'rax': - $scope.api_key = data.password; - master.api_key = $scope.api_key; - break; - case 'gce': - $scope.email_address = data.username; - $scope.project = data.project; - break; - case 'azure': - $scope.subscription = data.username; - break; - } - $scope.credential_obj = data; - - setAskCheckboxes(); - - $scope.$emit('credentialLoaded'); - Wait('stop'); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, form, { - hdr: 'Error!', - msg: 'Failed to retrieve Credential: ' + $stateParams.id + '. GET status: ' + status - }); - }); - }); - - // Save changes to the parent - $scope.formSave = function() { - if ($scope[form.name + '_form'].$valid) { - FormSave({ scope: $scope, mode: 'edit' }); - } - }; - - // Handle Owner change - $scope.ownerChange = function() { - OwnerChange({ scope: $scope }); - }; - - // Handle Kind change - $scope.kindChange = function() { - KindChange({ scope: $scope, form: form, reset: true }); - }; - - $scope.becomeMethodChange = function() { - BecomeMethodChange({ scope: $scope }); - }; - - $scope.formCancel = function() { - $state.transitionTo('credentials'); - }; - - // Related set: Add button - $scope.add = function(set) { - $rootScope.flashMessage = null; - $location.path('/' + base + '/' + $stateParams.id + '/' + set + '/add'); - }; - - // Related set: Edit button - $scope.edit = function(set, id) { - $rootScope.flashMessage = null; - $location.path('/' + base + '/' + $stateParams.id + '/' + set + '/' + id); - }; - - // Related set: Delete button - $scope['delete'] = function(set, itm_id, name, title) { - $rootScope.flashMessage = null; - - var action = function() { - var url = defaultUrl + id + '/' + set + '/'; - Rest.setUrl(url); - Rest.post({ - id: itm_id, - disassociate: 1 - }) - .success(function() { - $('#prompt-modal').modal('hide'); - // @issue: OLD SEARCH - // $scope.search(form.related[set].iterator); - }) - .error(function(data, status) { - $('#prompt-modal').modal('hide'); - ProcessErrors($scope, data, status, null, { - hdr: 'Error!', - msg: 'Call to ' + url + ' failed. POST returned status: ' + status - }); - }); - }; - - Prompt({ - hdr: i18n._('Delete'), - body: '
' + i18n.sprintf(i18n._('Are you sure you want to remove the %s below from %s?'), title, $scope.name) + '
' + name + '
', - action: action, - actionText: i18n._('DELETE') - }); - - }; - - // Password change - $scope.clearPWConfirm = function(fld) { - // If password value changes, make sure password_confirm must be re-entered - $scope[fld] = ''; - $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); - }; - - // Respond to 'Ask at runtime?' checkbox - $scope.ask = function(fld, associated) { - if ($scope[fld + '_ask']) { - $scope[fld] = 'ASK'; - $("#" + form.name + "_" + fld + "_input").attr("type", "text"); - $("#" + form.name + "_" + fld + "_show_input_button").html("Hide"); - if (associated !== "undefined") { - $("#" + form.name + "_" + fld + "_input").attr("type", "password"); - $("#" + form.name + "_" + fld + "_show_input_button").html("Show"); - $scope[associated] = ''; - $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); - } - } else { - $scope[fld] = ''; - $("#" + form.name + "_" + fld + "_input").attr("type", "password"); - $("#" + form.name + "_" + fld + "_show_input_button").html("Show"); - if (associated !== "undefined") { - $("#" + form.name + "_" + fld + "_input").attr("type", "text"); - $("#" + form.name + "_" + fld + "_show_input_button").html("Hide"); - $scope[associated] = ''; - $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); - } - } - }; - - $scope.clear = function(fld, associated) { - $scope[fld] = ''; - $scope[associated] = ''; - $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); - $scope[form.name + '_form'].$setDirty(); - }; - -} - -CredentialsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', - '$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert', - 'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices', - 'KindChange', 'BecomeMethodChange', 'Empty', 'OwnerChange', - 'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n', -]; diff --git a/awx/ui/client/src/controllers/Home.js b/awx/ui/client/src/controllers/Home.js deleted file mode 100644 index f7fdd12cec..0000000000 --- a/awx/ui/client/src/controllers/Home.js +++ /dev/null @@ -1,142 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Home - * @description This controller's for the dashboard -*/ - - -/** - * @ngdoc method - * @name controllers.function:Home#Home - * @methodOf controllers.function:Home - * @description this function loads all the widgets on the dashboard. - * dashboardReady (emit) - this is called when the preliminary parts of the dashboard have been loaded, and loads each of the widgets. Note that the - * Host count graph should only be loaded if the user is a super user - * -*/ - -export function Home($scope, $compile, $stateParams, $rootScope, $location, $log, Wait, - ClearScope, Rest, GetBasePath, ProcessErrors, $window, graphData){ - - ClearScope('home'); - - var dataCount = 0; - - $scope.$on('ws-jobs', function () { - Rest.setUrl(GetBasePath('dashboard')); - Rest.get() - .success(function (data) { - $scope.dashboardData = data; - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard host graph data: ' + status }); - }); - - Rest.setUrl("api/v1/unified_jobs?order_by=-finished&page_size=5&finished__isnull=false&type=workflow_job,job"); - Rest.get() - .success(function (data) { - $scope.dashboardJobsListData = data.results; - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard jobs list: ' + status }); - }); - - Rest.setUrl(GetBasePath("unified_job_templates") + "?order_by=-last_job_run&page_size=5&last_job_run__isnull=false&type=workflow_job_template,job_template"); - Rest.get() - .success(function (data) { - $scope.dashboardJobTemplatesListData = data.results; - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard jobs list: ' + status }); - }); - - }); - - if ($scope.removeDashboardDataLoadComplete) { - $scope.removeDashboardDataLoadComplete(); - } - $scope.removeDashboardDataLoadComplete = $scope.$on('dashboardDataLoadComplete', function () { - dataCount++; - if (dataCount === 3) { - Wait("stop"); - dataCount = 0; - } - }); - - if ($scope.removeDashboardReady) { - $scope.removeDashboardReady(); - } - $scope.removeDashboardReady = $scope.$on('dashboardReady', function (e, data) { - $scope.dashboardCountsData = data; - $scope.graphData = graphData; - $scope.$emit('dashboardDataLoadComplete'); - - var cleanupJobListener = - $rootScope.$on('DataReceived:JobStatusGraph', function(e, data) { - $scope.graphData.jobStatus = data; - }); - - $scope.$on('$destroy', function() { - cleanupJobListener(); - }); - }); - - if ($scope.removeDashboardJobsListReady) { - $scope.removeDashboardJobsListReady(); - } - $scope.removeDashboardJobsListReady = $scope.$on('dashboardJobsListReady', function (e, data) { - $scope.dashboardJobsListData = data; - $scope.$emit('dashboardDataLoadComplete'); - }); - - if ($scope.removeDashboardJobTemplatesListReady) { - $scope.removeDashboardJobTemplatesListReady(); - } - $scope.removeDashboardJobTemplatesListReady = $scope.$on('dashboardJobTemplatesListReady', function (e, data) { - $scope.dashboardJobTemplatesListData = data; - $scope.$emit('dashboardDataLoadComplete'); - }); - - $scope.refresh = function () { - Wait('start'); - Rest.setUrl(GetBasePath('dashboard')); - Rest.get() - .success(function (data) { - $scope.dashboardData = data; - $scope.$emit('dashboardReady', data); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard: ' + status }); - }); - Rest.setUrl("api/v1/unified_jobs?order_by=-finished&page_size=5&finished__isnull=false&type=workflow_job,job"); - Rest.get() - .success(function (data) { - data = data.results; - $scope.$emit('dashboardJobsListReady', data); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard jobs list: ' + status }); - }); - Rest.setUrl(GetBasePath("unified_job_templates") + "?order_by=-last_job_run&page_size=5&last_job_run__isnull=false&type=workflow_job_template,job_template"); - Rest.get() - .success(function (data) { - data = data.results; - $scope.$emit('dashboardJobTemplatesListReady', data); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard job templates list: ' + status }); - }); - }; - - $scope.refresh(); -} - -Home.$inject = ['$scope', '$compile', '$stateParams', '$rootScope', '$location', '$log','Wait', - 'ClearScope', 'Rest', 'GetBasePath', 'ProcessErrors', '$window', 'graphData' -]; diff --git a/awx/ui/client/src/controllers/JobEvents.js b/awx/ui/client/src/controllers/JobEvents.js deleted file mode 100644 index 0ea23a76ce..0000000000 --- a/awx/ui/client/src/controllers/JobEvents.js +++ /dev/null @@ -1,356 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:JobEvent - * @description This controller's for the job event page -*/ - - -export function JobEventsList($sce, $filter, $scope, $rootScope, $location, $log, $stateParams, Rest, Alert, JobEventList, GenerateList, - Prompt, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, LookUpInit, ToggleChildren, - FormatDate, EventView, Wait) { - - ClearScope(); - - var list = JobEventList, - generator = GenerateList; - - // @issue: OLD SEARCH - // var defaultUrl = GetBasePath('jobs') + $stateParams.id + '/job_events/', //?parent__isnull=1'; - // page; - - list.base = $location.path(); - $scope.job_id = $stateParams.id; - $rootScope.flashMessage = null; - $scope.selected = []; - $scope.expand = true; //on load, automatically expand all nodes - - $scope.parentNode = 'parent-event'; // used in ngClass to dynamically set row level class and control - $scope.childNode = 'child-event'; // link color and cursor - - if ($scope.removeSetHostLinks) { - $scope.removeSetHostLinks(); - } - $scope.removeSetHostLinks = $scope.$on('SetHostLinks', function (e, inventory_id) { - for (var i = 0; i < $scope.jobevents.length; i++) { - if ($scope.jobevents[i].summary_fields.host) { - $scope.jobevents[i].hostLink = "/#/inventories/" + inventory_id; - //encodeURI($scope.jobevents[i].summary_fields.host.name); - } - } - }); - - function formatJSON(eventData) { - //turn JSON event data into an html form - - var i, n, rows, fld, txt, - html = '', - found = false; - - if (eventData.res) { - if (typeof eventData.res === 'string') { - n = eventData.res.match(/\n/g); - rows = (n) ? n.length : 1; - rows = (rows > 10) ? 10 : rows; - found = true; - html += "
\n"; - html += "\n"; - html += "\n"; - html += "
\n"; - } else { - for (fld in eventData.res) { - if ((fld === 'msg' || fld === 'stdout' || fld === 'stderr') && - (eventData.res[fld] !== null && eventData.res[fld] !== '')) { - html += "
\n"; - html += "\n"; - n = eventData.res[fld].match(/\n/g); - rows = (n) ? n.length : 1; - rows = (rows > 10) ? 10 : rows; - html += "\n"; - //html += "
" + eventData.res[fld] + "
\n"; - html += "
\n"; - found = true; - } - if (fld === "results" && Array.isArray(eventData.res[fld]) && eventData.res[fld].length > 0) { - txt = ''; - for (i = 0; i < eventData.res[fld].length; i++) { - txt += eventData.res[fld][i]; - } - n = txt.match(/\n/g); - rows = (n) ? n.length : 1; - rows = (rows > 10) ? 10 : rows; - if (txt !== '') { - html += "
\n"; - html += "\n"; - html += "\n"; - //html += "
" + txt + "
\n"; - html += "
\n"; - found = true; - } - } - if (fld === "rc" && eventData.res[fld] !== '') { - - html += "
\n"; - html += "
" + eventData.res[fld] + "
\n"; - //html += "\n"; - html += "
\n"; - found = true; - } - } - } - html = (found) ? "
\n" + html + "
\n" : ''; - } - if (eventData.hosts) { - html = "" + eventData.host + "\n" + html; - } else { - html = (html === '') ? null : html; - } - return html; - } - - if ($scope.removePostRefresh) { - $scope.removePostRefresh(); - } - $scope.removePostRefresh = $scope.$on('PostRefresh', function () { - // Initialize the parent levels - - generator.inject(list, { mode: 'edit', scope: $scope }); - - var set = $scope[list.name], i; - for (i = 0; i < set.length; i++) { - set[i].event_display = set[i].event_display.replace(/^\u00a0*/g, ''); - if (set[i].event_level < 3) { - set[i].ngicon = 'fa fa-minus-square-o node-toggle'; - set[i]['class'] = 'parentNode'; - } else { - set[i].ngicon = 'fa fa-square-o node-no-toggle'; - set[i]['class'] = 'childNode'; - set[i].event_detail = $sce.trustAsHtml(formatJSON(set[i].event_data)); - } - set[i].show = true; - set[i].spaces = set[i].event_level * 24; - if ($scope.jobevents[i].failed) { - $scope.jobevents[i].status = 'error'; - if (i === set.length - 1) { - $scope.jobevents[i].statusBadgeToolTip = "A failure occurred durring one or more playbook tasks."; - } else if (set[i].event_level < 3) { - $scope.jobevents[i].statusBadgeToolTip = "A failure occurred within the children of this event."; - } else { - $scope.jobevents[i].statusBadgeToolTip = "A failure occurred. Click to view details"; - } - } else if ($scope.jobevents[i].changed) { - $scope.jobevents[i].status = 'changed'; - if (i === set.length - 1) { - $scope.jobevents[i].statusBadgeToolTip = "A change was completed durring one or more playbook tasks."; - } else if (set[i].event_level < 3) { - $scope.jobevents[i].statusBadgeToolTip = "A change was completed by one or more children of this event."; - } else { - $scope.jobevents[i].statusBadgeToolTip = "A change was completed. Click to view details"; - } - } else { - $scope.jobevents[i].status = 'success'; - if (i === set.length - 1) { - $scope.jobevents[i].statusBadgeToolTip = "All playbook tasks completed successfully."; - } else if (set[i].event_level < 3) { - $scope.jobevents[i].statusBadgeToolTip = "All the children of this event completed successfully."; - } else { - $scope.jobevents[i].statusBadgeToolTip = "No errors occurred. Click to view details"; - } - } - //cDate = new Date(set[i].created); - //set[i].created = FormatDate(cDate); - set[i].created = $filter('longDate')(set[i].created); - } - - // Need below lookup to get inventory_id, which is not on event record. Plus, good idea to get status and name - // from job in the event that there are no job event records - Rest.setUrl(GetBasePath('jobs') + $scope.job_id); - Rest.get() - .success(function (data) { - $scope.job_status = data.status; - $scope.job_name = data.summary_fields.job_template.name; - $scope.$emit('SetHostLinks', data.inventory); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get job status for job: ' + $scope.job_id + '. GET status: ' + status - }); - }); - }); - - // @issue: OLD SEARCH - // SearchInit({ - // scope: $scope, - // set: 'jobevents', - // list: list, - // url: defaultUrl - // }); - // - // page = ($stateParams.page) ? parseInt($stateParams.page,10) - 1 : null; - // - // PaginateInit({ - // scope: $scope, - // list: list, - // url: defaultUrl, - // page: page - // }); - // - // // Called from Inventories tab, host failed events link: - // if ($stateParams.host) { - // $scope[list.iterator + 'SearchField'] = 'host'; - // $scope[list.iterator + 'SearchValue'] = $stateParams.host; - // $scope[list.iterator + 'SearchFieldLabel'] = list.fields.host.label; - // } - // - // $scope.search(list.iterator, $stateParams.page); - - $scope.toggle = function (id) { - ToggleChildren({ - scope: $scope, - list: list, - id: id - }); - }; - - $scope.viewJobEvent = function (id) { - EventView({ - event_id: id - }); - }; - - $scope.refresh = function () { - // @issue: OLD SEARCH - // $scope.jobSearchSpin = true; - $scope.jobLoading = true; - Wait('start'); - - // @issue: OLD SEARCH - // Refresh({ - // scope: $scope, - // set: 'jobevents', - // iterator: 'jobevent', - // url: $scope.current_url - // }); - }; -} - -JobEventsList.$inject = ['$sce', '$filter', '$scope', '$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'JobEventList', - 'generateList', 'Prompt', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'LookUpInit', 'ToggleChildren', 'FormatDate', 'EventView', 'Wait' -]; - -export function JobEventsEdit($scope, $rootScope, $compile, $location, $log, $stateParams, JobEventsForm, GenerateForm, - Rest, Alert, ProcessErrors, ClearScope, GetBasePath, FormatDate, EventView, Wait) { - - ClearScope(); - - var form = JobEventsForm, - generator = GenerateForm, - defaultUrl = GetBasePath('base') + 'job_events/' + $stateParams.event_id + '/'; - - generator.inject(form, { mode: 'edit', related: true, scope: $scope}); - generator.reset(); - - // Retrieve detail record and prepopulate the form - Wait('start'); - Rest.setUrl(defaultUrl); - Rest.get() - .success(function (data) { - var cDate, fld, n, rows; - $scope.event_display = data.event_display.replace(/^\u00a0*/g, ''); - for (fld in form.fields) { - switch (fld) { - case 'status': - if (data.failed) { - $scope.status = 'error'; - } else if (data.changed) { - $scope.status = 'changed'; - } else { - $scope.status = 'success'; - } - break; - case 'created': - cDate = new Date(data.created); - $scope.created = FormatDate(cDate); - break; - case 'host': - if (data.summary_fields && data.summary_fields.host) { - $scope.host = data.summary_fields.host.name; - } - break; - case 'id': - case 'task': - case 'play': - $scope[fld] = data[fld]; - break; - case 'start': - case 'end': - if (data.event_data && data.event_data.res && data.event_data.res[fld] !== undefined) { - cDate = new Date(data.event_data.res[fld]); - $scope[fld] = FormatDate(cDate); - } - break; - case 'msg': - case 'stdout': - case 'stderr': - case 'delta': - case 'rc': - if (data.event_data && data.event_data.res && data.event_data.res[fld] !== undefined) { - $scope[fld] = data.event_data.res[fld]; - if (form.fields[fld].type === 'textarea') { - n = data.event_data.res[fld].match(/\n/g); - rows = (n) ? n.length : 1; - rows = (rows > 15) ? 5 : rows; - $('textarea[name="' + fld + '"]').attr('rows', rows); - } - } - break; - case 'module_name': - case 'module_args': - if (data.event_data.res && data.event_data.res.invocation) { - $scope[fld] = data.event_data.res.invocation.fld; - } - break; - } - } - Wait('stop'); - }) - .error(function (data) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve host: ' + $stateParams.event_id + - '. GET status: ' + status }); - }); - - $scope.navigateBack = function () { - var url = '/jobs/' + $stateParams.job_id + '/job_events'; - if ($stateParams.page) { - url += '?page=' + $stateParams.page; - } - $location.url(url); - }; - - $scope.rawView = function () { - EventView({ - "event_id": $scope.id - }); - }; - -} - -JobEventsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$stateParams', 'JobEventsForm', 'GenerateForm', - 'Rest', 'Alert', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'FormatDate', 'EventView', 'Wait' -]; diff --git a/awx/ui/client/src/controllers/JobHosts.js b/awx/ui/client/src/controllers/JobHosts.js deleted file mode 100644 index e078bc4df7..0000000000 --- a/awx/ui/client/src/controllers/JobHosts.js +++ /dev/null @@ -1,121 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:JobHosts - * @description This controller's for the job hosts page -*/ - - -export function JobHostSummaryList($scope, $rootScope, $location, $log, $stateParams, Rest, Alert, JobHostList, GenerateList, - Prompt, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, - JobStatusToolTip) { - - ClearScope(); - - var list = JobHostList, - // @issue: OLD SEARCH - // defaultUrl = GetBasePath('jobs') + $stateParams.id + '/job_host_summaries/', - view = GenerateList, - inventory; - - $scope.job_id = $stateParams.id; - $scope.host_id = null; - - // After a refresh, populate any needed summary field values on each row - if ($scope.removePostRefresh) { - $scope.removePostRefresh(); - } - $scope.removePostRefresh = $scope.$on('PostRefresh', function () { - - // Set status, tooltips, badges icons, etc. - $scope.jobhosts.forEach(function(element, i) { - $scope.jobhosts[i].host_name = ($scope.jobhosts[i].summary_fields.host) ? $scope.jobhosts[i].summary_fields.host.name : ''; - $scope.jobhosts[i].status = ($scope.jobhosts[i].failed) ? 'failed' : 'success'; - $scope.jobhosts[i].statusBadgeToolTip = JobStatusToolTip($scope.jobhosts[i].status) + - " Click to view details."; - if ($scope.jobhosts[i].summary_fields.host) { - $scope.jobhosts[i].statusLinkTo = '/#/job_events/' + $scope.jobhosts[i].job + '/?host=' + - encodeURI($scope.jobhosts[i].summary_fields.host.name); - } - else { - $scope.jobhosts[i].statusLinkTo = '/#/job_events/' + $scope.jobhosts[i].job; - } - }); - - for (var i = 0; i < $scope.jobhosts.length; i++) { - $scope.jobhosts[i].hostLinkTo = '/#/inventories/' + inventory + '/?host_name=' + - encodeURI($scope.jobhosts[i].summary_fields.host.name); - } - }); - - if ($scope.removeJobReady) { - $scope.removeJobReady(); - } - $scope.removeJobReady = $scope.$on('JobReady', function() { - view.inject(list, { mode: 'edit', scope: $scope }); - - // @issue: OLD SEARCH - // SearchInit({ - // scope: $scope, - // set: 'jobhosts', - // list: list, - // url: defaultUrl - // }); - // - // PaginateInit({ - // scope: $scope, - // list: list, - // url: defaultUrl - // }); - // - // // Called from Inventories tab, host failed events link: - // if ($stateParams.host_name) { - // $scope[list.iterator + 'SearchField'] = 'host'; - // $scope[list.iterator + 'SearchValue'] = $stateParams.host_name; - // $scope[list.iterator + 'SearchFieldLabel'] = list.fields.host.label; - // } - // $scope.search(list.iterator); - }); - - Rest.setUrl(GetBasePath('jobs') + $scope.job_id); - Rest.get() - .success(function (data) { - inventory = data.inventory; - $scope.job_status = data.status; - $scope.$emit('JobReady'); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get job status for job: ' + $scope.job_id + '. GET status: ' + status - }); - }); - - $scope.showEvents = function (host_name, last_job) { - // When click on !Failed Events link, redirect to latest job/job_events for the host - Rest.setUrl(last_job); - Rest.get() - .success(function (data) { - $location.url('/jobs_events/' + data.id + '/?host=' + encodeURI(host_name)); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to lookup last job: ' + last_job + - '. GET status: ' + status }); - }); - }; - - $scope.refresh = function () { - // @issue: OLD SEARCH - // $scope.search(list.iterator); - }; - -} - -JobHostSummaryList.$inject = ['$scope', '$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'JobHostList', - 'generateList', 'Prompt', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'JobStatusToolTip', 'Wait' -]; diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js deleted file mode 100644 index 555d4e28ca..0000000000 --- a/awx/ui/client/src/controllers/Projects.js +++ /dev/null @@ -1,759 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Projects - * @description This controller's for the projects page - */ - - -export function ProjectsList($scope, $rootScope, $location, $log, $stateParams, - Rest, Alert, ProjectList, Prompt, ReturnToCaller, ClearScope, ProcessErrors, - GetBasePath, ProjectUpdate, Wait, GetChoices, Empty, Find, GetProjectIcon, - GetProjectToolTip, $filter, $state, rbacUiControlService, Dataset, i18n, qs) { - - var list = ProjectList, - defaultUrl = GetBasePath('projects'); - - init(); - - function init() { - $scope.canAdd = false; - - rbacUiControlService.canAdd('projects') - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - - // search init - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - _.forEach($scope[list.name], buildTooltips); - $rootScope.flashMessage = null; - } - - $scope.$on(`${list.iterator}_options`, function(event, data){ - $scope.options = data.data.actions.GET; - optionsRequestDataProcessing(); - }); - - $scope.$watchCollection(`${$scope.list.name}`, function() { - 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] !== undefined) { - $scope[list.name].forEach(function(item, item_idx) { - var itm = $scope[list.name][item_idx]; - - // Set the item type label - if (list.fields.scm_type && $scope.options && - $scope.options.hasOwnProperty('scm_type')) { - $scope.options.scm_type.choices.forEach(function(choice) { - if (choice[0] === item.scm_type) { - itm.type_label = choice[1]; - } - }); - } - - buildTooltips(itm); - - }); - } - } - - function buildTooltips(project) { - project.statusIcon = GetProjectIcon(project.status); - project.statusTip = GetProjectToolTip(project.status); - project.scm_update_tooltip = i18n._("Start an SCM update"); - project.scm_schedule_tooltip = i18n._("Schedule future SCM updates"); - project.scm_type_class = ""; - - if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { - project.statusTip = i18n._('Canceled. Click for details'); - } - - if (project.status === 'running' || project.status === 'updating') { - project.scm_update_tooltip = i18n._("SCM update currently running"); - project.scm_type_class = "btn-disabled"; - } - if (project.scm_type === 'manual') { - project.scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); - project.scm_schedule_tooltip = i18n._('Manual projects do not require a schedule'); - project.scm_type_class = 'btn-disabled'; - project.statusTip = i18n._('Not configured for SCM'); - project.statusIcon = 'none'; - } - } - - $scope.reloadList = function(){ - let path = GetBasePath(list.basePath) || GetBasePath(list.name); - qs.search(path, $state.params[`${list.iterator}_search`]) - .then(function(searchResponse) { - $scope[`${list.iterator}_dataset`] = searchResponse.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - }); - }; - - $scope.$on(`ws-jobs`, function(e, data) { - var project; - $log.debug(data); - if ($scope.projects) { - // Assuming we have a list of projects available - project = Find({ list: $scope.projects, key: 'id', val: data.project_id }); - if (project) { - // And we found the affected project - $log.debug('Received event for project: ' + project.name); - $log.debug('Status changed to: ' + data.status); - if (data.status === 'successful' || data.status === 'failed') { - $scope.reloadList(); - } else { - project.scm_update_tooltip = "SCM update currently running"; - project.scm_type_class = "btn-disabled"; - } - project.status = data.status; - project.statusIcon = GetProjectIcon(data.status); - project.statusTip = GetProjectToolTip(data.status); - } - } - }); - - $scope.addProject = function() { - $state.go('projects.add'); - }; - - $scope.editProject = function(id) { - $state.go('projects.edit', { project_id: id }); - }; - - if ($scope.removeGoToJobDetails) { - $scope.removeGoToJobDetails(); - } - $scope.removeGoToJobDetails = $scope.$on('GoToJobDetails', function(e, data) { - if (data.summary_fields.current_update || data.summary_fields.last_update) { - - Wait('start'); - - // Grab the id from summary_fields - var id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id; - - $state.go('scmUpdateStdout', { id: id }); - - } else { - Alert(i18n._('No Updates Available'), i18n._('There is no SCM update information available for this project. An update has not yet been ' + - ' completed. If you have not already done so, start an update for this project.'), 'alert-info'); - } - }); - - $scope.showSCMStatus = function(id) { - // Refresh the project list - var project = Find({ list: $scope.projects, key: 'id', val: id }); - if (Empty(project.scm_type) || project.scm_type === 'Manual') { - Alert(i18n._('No SCM Configuration'), i18n._('The selected project is not configured for SCM. To configure for SCM, edit the project and provide SCM settings, ' + - 'and then run an update.'), 'alert-info'); - } else { - // Refresh what we have in memory to insure we're accessing the most recent status record - Rest.setUrl(project.url); - Rest.get() - .success(function(data) { - $scope.$emit('GoToJobDetails', data); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n._('Project lookup failed. GET returned: ') + status }); - }); - } - }; - - $scope.deleteProject = function(id, name) { - var action = function() { - $('#prompt-modal').modal('hide'); - Wait('start'); - var url = defaultUrl + id + '/'; - Rest.setUrl(url); - Rest.destroy() - .success(function() { - if (parseInt($state.params.project_id) === id) { - $state.go("^", null, { reload: true }); - } else { - $state.go('.', null, {reload: true}); - } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Call to %s failed. DELETE returned status: '), url) + status }); - }) - .finally(function() { - Wait('stop'); - }); - }; - - Prompt({ - hdr: i18n._('Delete'), - body: '
' + i18n._('Are you sure you want to delete the project below?') + '
' + '
' + $filter('sanitize')(name) + '
', - action: action, - actionText: 'DELETE' - }); - }; - - if ($scope.removeCancelUpdate) { - $scope.removeCancelUpdate(); - } - $scope.removeCancelUpdate = $scope.$on('Cancel_Update', function(e, url) { - // Cancel the project update process - Rest.setUrl(url); - Rest.post() - .success(function () { - Alert(i18n._('SCM Update Cancel'), i18n._('Your request to cancel the update was submitted to the task manager.'), 'alert-info'); - $scope.refresh(); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. POST status: '), url) + status }); - }); - }); - - if ($scope.removeCheckCancel) { - $scope.removeCheckCancel(); - } - $scope.removeCheckCancel = $scope.$on('Check_Cancel', function(e, data) { - // Check that we 'can' cancel the update - var url = data.related.cancel; - Rest.setUrl(url); - Rest.get() - .success(function(data) { - if (data.can_cancel) { - $scope.$emit('Cancel_Update', url); - } else { - Alert(i18n._('Cancel Not Allowed'), '
' + i18n.sprintf(i18n._('Either you do not have access or the SCM update process completed. ' + - 'Click the %sRefresh%s button to view the latest status.'), '', '') + '
', 'alert-info', null, null, null, null, true); - } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. GET status: '), url) + status }); - }); - }); - - $scope.cancelUpdate = function(id, name) { - Rest.setUrl(GetBasePath("projects") + id); - Rest.get() - .success(function(data) { - if (data.related.current_update) { - Rest.setUrl(data.related.current_update); - Rest.get() - .success(function(data) { - $scope.$emit('Check_Cancel', data); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Call to %s failed. GET status: '), data.related.current_update) + status }); - }); - } else { - Alert(i18n._('Update Not Found'), '
' + i18n.sprintf(i18n._('An SCM update does not appear to be running for project: %s. Click the %sRefresh%s ' + - 'button to view the latest status.'), $filter('sanitize')(name), '', '') + '
', 'alert-info',undefined,undefined,undefined,undefined,true); - } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), - msg: i18n._('Call to get project failed. GET status: ') + status }); - }); - }; - - $scope.SCMUpdate = function(project_id, event) { - try { - $(event.target).tooltip('hide'); - } catch (e) { - // ignore - } - $scope.projects.forEach(function(project) { - if (project.id === project_id) { - if (project.scm_type === "Manual" || Empty(project.scm_type)) { - // Do not respond. Button appears greyed out as if it is disabled. Not disabled though, because we need mouse over event - // to work. So user can click, but we just won't do anything. - //Alert('Missing SCM Setup', 'Before running an SCM update, edit the project and provide the SCM access information.', 'alert-info'); - } else if (project.status === 'updating' || project.status === 'running' || project.status === 'pending') { - // Alert('Update in Progress', 'The SCM update process is running. Use the Refresh button to monitor the status.', 'alert-info'); - } else { - ProjectUpdate({ scope: $scope, project_id: project.id }); - } - } - }); - }; - - $scope.editSchedules = function(id) { - var project = Find({ list: $scope.projects, key: 'id', val: id }); - if (!(project.scm_type === "Manual" || Empty(project.scm_type)) && !(project.status === 'updating' || project.status === 'running' || project.status === 'pending')) { - $state.go('projectSchedules', { id: id }); - } - }; -} - -ProjectsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$stateParams', - 'Rest', 'Alert', 'ProjectList', 'Prompt', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'ProjectUpdate', 'Wait', 'GetChoices', 'Empty', 'Find', 'GetProjectIcon', - 'GetProjectToolTip', '$filter', '$state', 'rbacUiControlService', 'Dataset', 'i18n', 'QuerySet' -]; - -export function ProjectsAdd($scope, $rootScope, $compile, $location, $log, - $stateParams, GenerateForm, ProjectsForm, Rest, Alert, ProcessErrors, - GetBasePath, GetProjectPath, GetChoices, Wait, $state, CreateSelect2, i18n) { - - var form = ProjectsForm(), - base = $location.path().replace(/^\//, '').split('/')[0], - defaultUrl = GetBasePath('projects'), - master = {}; - - init(); - - function init() { - Rest.setUrl(GetBasePath('projects')); - Rest.options() - .success(function(data) { - if (!data.actions.POST) { - $state.go("^"); - Alert(i18n._('Permission Error'), i18n._('You do not have permission to add a project.'), 'alert-info'); - } - }); - - // apply form definition's default field values - GenerateForm.applyDefaults(form, $scope); - } - - GetProjectPath({ scope: $scope, master: master }); - - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('choicesReady', function() { - var i; - for (i = 0; i < $scope.scm_type_options.length; i++) { - if ($scope.scm_type_options[i].value === '') { - $scope.scm_type_options[i].value = "manual"; - //$scope.scm_type = $scope.scm_type_options[i]; - break; - } - } - - CreateSelect2({ - element: '#project_scm_type', - multiple: false - }); - - $scope.scmRequired = false; - master.scm_type = $scope.scm_type; - }); - - // Load the list of options for Kind - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'scm_type', - variable: 'scm_type_options', - callback: 'choicesReady' - }); - CreateSelect2({ - element: '#local-path-select', - multiple: false - }); - - // Save - $scope.formSave = function() { - var i, fld, url, data = {}; - data = {}; - for (fld in form.fields) { - if (form.fields[fld].type === 'checkbox_group') { - for (i = 0; i < form.fields[fld].fields.length; i++) { - data[form.fields[fld].fields[i].name] = $scope[form.fields[fld].fields[i].name]; - } - } else { - if (form.fields[fld].type !== 'alertblock') { - data[fld] = $scope[fld]; - } - } - } - - if ($scope.scm_type.value === "manual") { - data.scm_type = ""; - data.local_path = $scope.local_path.value; - } else { - data.scm_type = $scope.scm_type.value; - delete data.local_path; - } - - url = (base === 'teams') ? GetBasePath('teams') + $stateParams.team_id + '/projects/' : defaultUrl; - Wait('start'); - Rest.setUrl(url); - Rest.post(data) - .success(function(data) { - $scope.addedItem = data.id; - $state.go('projects.edit', { project_id: data.id }, { reload: true }); - }) - .error(function(data, status) { - Wait('stop'); - ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), - msg: i18n._('Failed to create new project. POST returned status: ') + status }); - }); - }; - - $scope.scmChange = function() { - // When an scm_type is set, path is not required - if ($scope.scm_type) { - $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; - $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; - $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; - } - - // Dynamically update popover values - if ($scope.scm_type.value) { - switch ($scope.scm_type.value) { - case 'git': - $scope.credentialLabel = "SCM Credential"; - $scope.urlPopover = '

' + - i18n._('Example URLs for GIT SCM include:') + - '

  • https://github.com/ansible/ansible.git
  • ' + - '
  • git@github.com:ansible/ansible.git
  • git://servername.example.com/ansible.git
' + - '

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

' + i18n._('Example URLs for Subversion SCM include:') + '

' + - '
  • https://github.com/ansible/ansible
  • svn://servername.example.com/path
  • ' + - '
  • svn+ssh://servername.example.com/path
'; - break; - case 'hg': - $scope.credentialLabel = "SCM Credential"; - $scope.urlPopover = '

' + i18n._('Example URLs for Mercurial SCM include:') + '

' + - '
  • https://bitbucket.org/username/project
  • ssh://hg@bitbucket.org/username/project
  • ' + - '
  • ssh://server.example.com/path
' + - '

' + i18n.sprintf(i18n._('%sNote:%s Mercurial does not support password authentication for SSH. ' + - 'Do not put the username and key in the URL. ' + - 'If using Bitbucket and SSH, do not supply your Bitbucket username.'), '', ''); - break; - case 'insights': - $scope.pathRequired = false; - $scope.scmRequired = false; - $scope.credentialLabel = "Red Hat Insights"; - break; - default: - $scope.credentialLabel = "SCM Credential"; - $scope.urlPopover = '

' + i18n._('URL popover text'); - } - } - - }; - $scope.formCancel = function() { - $state.go('projects'); - }; -} - -ProjectsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', - '$stateParams', 'GenerateForm', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', - 'GetProjectPath', 'GetChoices', 'Wait', '$state', 'CreateSelect2', 'i18n']; - - -export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, - $stateParams, ProjectsForm, Rest, Alert, ProcessErrors, GenerateForm, - Prompt, ClearScope, GetBasePath, GetProjectPath, Authorization, - GetChoices, Empty, DebugForm, Wait, ProjectUpdate, $state, CreateSelect2, ToggleNotification, i18n) { - - ClearScope('htmlTemplate'); - - var form = ProjectsForm(), - defaultUrl = GetBasePath('projects') + $stateParams.project_id + '/', - master = {}, - id = $stateParams.project_id; - - init(); - - function init() { - $scope.project_local_paths = []; - $scope.base_dir = ''; - } - - $scope.$watch('project_obj.summary_fields.user_capabilities.edit', function(val) { - if (val === false) { - $scope.canAdd = false; - } - }); - - if ($scope.pathsReadyRemove) { - $scope.pathsReadyRemove(); - } - $scope.pathsReadyRemove = $scope.$on('pathsReady', function () { - CreateSelect2({ - element: '#local-path-select', - multiple: false - }); - }); - - // After the project is loaded, retrieve each related set - if ($scope.projectLoadedRemove) { - $scope.projectLoadedRemove(); - } - $scope.projectLoadedRemove = $scope.$on('projectLoaded', function() { - var opts = []; - - if (Authorization.getUserInfo('is_superuser') === true) { - GetProjectPath({ scope: $scope, master: master }); - } else { - opts.push({ - label: $scope.local_path, - value: $scope.local_path - }); - $scope.project_local_paths = opts; - $scope.local_path = $scope.project_local_paths[0]; - $scope.base_dir = i18n._('You do not have access to view this property'); - $scope.$emit('pathsReady'); - } - - $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; - $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; - $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; - Wait('stop'); - - $scope.scmChange(); - }); - - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('choicesReady', function() { - let i; - for (i = 0; i < $scope.scm_type_options.length; i++) { - if ($scope.scm_type_options[i].value === '') { - $scope.scm_type_options[i].value = "manual"; - break; - } - } - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl); - Rest.get({ params: { id: id } }) - .success(function(data) { - var fld, i; - for (fld in form.fields) { - if (form.fields[fld].type === 'checkbox_group') { - for (i = 0; i < form.fields[fld].fields.length; i++) { - $scope[form.fields[fld].fields[i].name] = data[form.fields[fld].fields[i].name]; - master[form.fields[fld].fields[i].name] = data[form.fields[fld].fields[i].name]; - } - } else { - if (data[fld] !== undefined) { - $scope[fld] = data[fld]; - master[fld] = data[fld]; - } - } - if (form.fields[fld].sourceModel && data.summary_fields && - data.summary_fields[form.fields[fld].sourceModel]) { - $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - } - } - - data.scm_type = (Empty(data.scm_type)) ? 'manual' : data.scm_type; - for (i = 0; i < $scope.scm_type_options.length; i++) { - if ($scope.scm_type_options[i].value === data.scm_type) { - $scope.scm_type = $scope.scm_type_options[i]; - break; - } - } - - if ($scope.scm_type.value !== 'manual') { - $scope.pathRequired = false; - $scope.scmRequired = true; - } else { - $scope.pathRequired = true; - $scope.scmRequired = false; - } - - master.scm_type = $scope.scm_type; - CreateSelect2({ - element: '#project_scm_type', - multiple: false - }); - - $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; - $scope.scm_update_tooltip = i18n._("Start an SCM update"); - $scope.scm_type_class = ""; - if (data.status === 'running' || data.status === 'updating') { - $scope.scm_update_tooltip = i18n._("SCM update currently running"); - $scope.scm_type_class = "btn-disabled"; - } - if (Empty(data.scm_type)) { - $scope.scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); - $scope.scm_type_class = "btn-disabled"; - } - - $scope.project_obj = data; - $scope.name = data.name; - $scope.$emit('projectLoaded'); - Wait('stop'); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Failed to retrieve project: %s. GET status: '), id) + status - }); - }); - }); - - // Load the list of options for Kind - Wait('start'); - GetChoices({ - url: GetBasePath('projects'), - scope: $scope, - field: 'scm_type', - variable: 'scm_type_options', - callback: 'choicesReady' - }); - - $scope.toggleNotification = function(event, id, column) { - var notifier = this.notification; - try { - $(event.target).tooltip('hide'); - } catch (e) { - // ignore - } - ToggleNotification({ - scope: $scope, - url: $scope.project_obj.url, - notifier: notifier, - column: column, - callback: 'NotificationRefresh' - }); - }; - - // Save changes to the parent - $scope.formSave = function() { - var fld, i, params; - GenerateForm.clearApiErrors($scope); - Wait('start'); - $rootScope.flashMessage = null; - params = {}; - for (fld in form.fields) { - if (form.fields[fld].type === 'checkbox_group') { - for (i = 0; i < form.fields[fld].fields.length; i++) { - params[form.fields[fld].fields[i].name] = $scope[form.fields[fld].fields[i].name]; - } - } else { - if (form.fields[fld].type !== 'alertblock') { - params[fld] = $scope[fld]; - } - } - } - - if ($scope.scm_type.value === "manual") { - params.scm_type = ""; - params.local_path = $scope.local_path.value; - } else { - params.scm_type = $scope.scm_type.value; - delete params.local_path; - } - - Rest.setUrl(defaultUrl); - Rest.put(params) - .success(function() { - Wait('stop'); - $state.go($state.current, {}, { reload: true }); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Failed to update project: %s. PUT status: '), id) + status }); - }); - }; - - // Related set: Delete button - $scope['delete'] = function(set, itm_id, name, title) { - var action = function() { - var url = GetBasePath('projects') + id + '/' + set + '/'; - $rootScope.flashMessage = null; - Rest.setUrl(url); - Rest.post({ id: itm_id, disassociate: 1 }) - .success(function() { - $('#prompt-modal').modal('hide'); - // @issue: OLD SEARCH - // $scope.search(form.related[set].iterator); - }) - .error(function(data, status) { - $('#prompt-modal').modal('hide'); - ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. POST returned status: '), url) + status }); - }); - }; - - Prompt({ - hdr: i18n._('Delete'), - body: '

' + i18n.sprintf(i18n._('Are you sure you want to remove the %s below from %s?'), title, $scope.name) + '
' + '
' + name + '
', - action: action, - actionText: i18n._('DELETE') - }); - }; - - $scope.scmChange = function() { - if ($scope.scm_type) { - $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; - $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; - $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? i18n._('Revision #') : i18n._('SCM Branch'); - } - - // Dynamically update popover values - if ($scope.scm_type.value) { - switch ($scope.scm_type.value) { - case 'git': - $scope.urlPopover = '

' + i18n._('Example URLs for GIT SCM include:') + '

  • https://github.com/ansible/ansible.git
  • ' + - '
  • git@github.com:ansible/ansible.git
  • git://servername.example.com/ansible.git
' + - '

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

' + i18n._('Example URLs for Subversion SCM include:') + '

' + - '
  • https://github.com/ansible/ansible
  • svn://servername.example.com/path
  • ' + - '
  • svn+ssh://servername.example.com/path
'; - break; - case 'hg': - $scope.urlPopover = '

' + i18n._('Example URLs for Mercurial SCM include:') + '

' + - '
  • https://bitbucket.org/username/project
  • ssh://hg@bitbucket.org/username/project
  • ' + - '
  • ssh://server.example.com/path
' + - '

' + i18n.sprintf(i18n._('%sNote:%s Mercurial does not support password authentication for SSH. ' + - 'Do not put the username and key in the URL. ' + - 'If using Bitbucket and SSH, do not supply your Bitbucket username.'), '', ''); - break; - case 'insights': - $scope.pathRequired = false; - $scope.scmRequired = false; - $scope.credentialLabel = "Red Hat Insights"; - break; - default: - $scope.urlPopover = '

' + i18n._('URL popover text'); - } - } - }; - - $scope.SCMUpdate = function() { - if ($scope.project_obj.scm_type === "Manual" || Empty($scope.project_obj.scm_type)) { - // ignore - } else if ($scope.project_obj.status === 'updating' || $scope.project_obj.status === 'running' || $scope.project_obj.status === 'pending') { - Alert(i18n._('Update in Progress'), i18n._('The SCM update process is running.'), 'alert-info'); - } else { - ProjectUpdate({ scope: $scope, project_id: $scope.project_obj.id }); - } - }; - - $scope.formCancel = function() { - $state.transitionTo('projects'); - }; -} - -ProjectsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', - '$stateParams', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GenerateForm', - 'Prompt', 'ClearScope', 'GetBasePath', 'GetProjectPath', 'Authorization', 'GetChoices', 'Empty', - 'DebugForm', 'Wait', 'ProjectUpdate', '$state', 'CreateSelect2', 'ToggleNotification', 'i18n']; diff --git a/awx/ui/client/src/controllers/Schedules.js b/awx/ui/client/src/controllers/Schedules.js deleted file mode 100644 index edebf66a84..0000000000 --- a/awx/ui/client/src/controllers/Schedules.js +++ /dev/null @@ -1,86 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Schedules - * @description This controller's for schedules -*/ - - -export function ScheduleEditController($scope, $compile, $location, $stateParams, SchedulesList, Rest, ProcessErrors, ReturnToCaller, ClearScope, -GetBasePath, Wait, Find, LoadSchedulesScope, GetChoices) { - - ClearScope(); - - var base, id, url, parentObject; - - // base = $location.path().replace(/^\//, '').split('/')[0]; - - // if ($scope.removePostRefresh) { - // $scope.removePostRefresh(); - // } - // $scope.removePostRefresh = $scope.$on('PostRefresh', function() { - // var list = $scope.schedules; - // list.forEach(function(element, idx) { - // list[idx].play_tip = (element.enabled) ? 'Schedule is Active. Click to temporarily stop.' : 'Schedule is temporarily stopped. Click to activate.'; - // }); - // }); - - // if ($scope.removeParentLoaded) { - // $scope.removeParentLoaded(); - // } - // $scope.removeParentLoaded = $scope.$on('ParentLoaded', function() { - // url += "schedules/"; - // SchedulesList.well = true; - // LoadSchedulesScope({ - // parent_scope: $scope, - // scope: $scope, - // list: SchedulesList, - // id: 'schedule-list-target', - // url: url, - // pageSize: 20 - // }); - // }); - - - if ($scope.removeChoicesReady) { - $scope.removeChocesReady(); - } - $scope.removeChoicesReady = $scope.$on('choicesReady', function() { - // Load the parent object - id = $stateParams.id; - url = GetBasePath(base) + id + '/'; - Rest.setUrl(url); - Rest.get() - .success(function(data) { - parentObject = data; - $scope.$emit('ParentLoaded'); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. GET returned: ' + status }); - }); - }); - - $scope.refreshJobs = function() { - // @issue: OLD SEARCH - // $scope.search(SchedulesList.iterator); - }; - - Wait('start'); - - GetChoices({ - scope: $scope, - url: GetBasePath('unified_jobs'), //'/static/sample/data/types/data.json' - field: 'type', - variable: 'type_choices', - callback: 'choicesReady' - }); -} - -ScheduleEditController.$inject = [ '$scope', '$compile', '$location', '$stateParams', 'SchedulesList', 'Rest', 'ProcessErrors', 'ReturnToCaller', 'ClearScope', - 'GetBasePath', 'Wait', 'Find', 'LoadSchedulesScope', 'GetChoices']; diff --git a/awx/ui/client/src/controllers/Sockets.js b/awx/ui/client/src/controllers/Sockets.js deleted file mode 100644 index 5930b57014..0000000000 --- a/awx/ui/client/src/controllers/Sockets.js +++ /dev/null @@ -1,119 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Sockets - * @description This controller's for controlling websockets - * discuss -*/ - - -export function SocketsController ($scope, $compile, ClearScope, Socket) { - - ClearScope(); - - var test_scope = $scope.$new(), - jobs_scope = $scope.$new(), - job_events_scope = $scope.$new(), - schedules_scope = $scope.$new(), - test_socket = Socket({ scope: test_scope, endpoint: "test" }), - jobs_socket = Socket({ scope: jobs_scope, endpoint: "jobs" }), - schedules_socket = Socket({ scope: schedules_scope, endpoint: "schedules" }), - job_events_socket = Socket({ scope: job_events_scope, endpoint: "job_events" }), - e, html; - - test_scope.messages = []; - jobs_scope.messages = []; - schedules_scope.messages = []; - job_events_scope.messages = []; - - html = "

Socket url: {{ socket_url }}  Status: {{ socket_status }} {{ socket_reason }}
\n" + - "
\n" + - "
Received Messages:
\n" + - "
\n" + - "
\n" + - "
    \n" + - "
  • {{ message }}
  • \n" + - "
\n" + - "
\n"; - - e = angular.element(document.getElementById('test-container')); - e.append(html); - $compile(e)(test_scope); - e = angular.element(document.getElementById('schedules-container')); - e.append(html); - $compile(e)(schedules_scope); - e = angular.element(document.getElementById('jobs-container')); - e.append(html); - $compile(e)(jobs_scope); - - html = "
\n" + - "
\n" + - "
Socket url: {{ socket_url }}  Status: {{ socket_status }} {{ socket_reason }}
\n" + - "
\n" + - "
\n" + - "
\n" + - "
\n" + - "\n" + - "\n" + - "
\n" + - "\n" + - "
\n" + - "
\n" + - "
\n" + - "
" + - "

Subscribed to events for job: {{ jobs_list }}

\n" + - "
Received Messages:
\n" + - "
\n" + - "
\n" + - "
    \n" + - "
  • {{ message }}
  • \n" + - "
\n" + - "
\n"; - - e = angular.element(document.getElementById('job-events-container')); - e.append(html); - $compile(e)(job_events_scope); - - schedules_scope.url = schedules_socket.getUrl(); - test_scope.url = test_socket.getUrl(); - jobs_scope.url = jobs_socket.getUrl(); - job_events_scope.url = job_events_socket.getUrl(); - - test_scope.messages.push('Message Displayed Before Connection'); - - test_socket.on('test', function(data) { - test_scope.messages.push(data); - }); - - schedules_socket.on("schedule_changed", function(data) { - schedules_scope.messages.push(data); - }); - - jobs_socket.on("status_changed", function(data) { - jobs_scope.messages.push(data); - }); - - jobs_socket.on("summary_complete", function(data) { - jobs_scope.messages.push(data); - }); - - job_events_scope.jobs_list = []; - - job_events_scope.subscribeToJobEvent = function() { - job_events_scope.jobs_list.push(job_events_scope.job_id); - job_events_socket.on("job_events-" + job_events_scope.job_id, function(data) { - job_events_scope.messages.push(data); - setTimeout(function() { - $(document).scrollTop($(document).prop("scrollHeight")); - $('#event-message-container').scrollTop($('#event-message-container').prop("scrollHeight")); - }, 300); - }); - }; -} - -SocketsController.$inject = [ '$scope', '$compile', 'ClearScope', 'Socket']; diff --git a/awx/ui/client/src/controllers/Teams.js b/awx/ui/client/src/controllers/Teams.js deleted file mode 100644 index 464a10ee10..0000000000 --- a/awx/ui/client/src/controllers/Teams.js +++ /dev/null @@ -1,247 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Teams - * @description This controller's for teams - */ - -export function TeamsList($scope, $rootScope, $log, $stateParams, - Rest, Alert, TeamList, Prompt, ClearScope, ProcessErrors, - GetBasePath, Wait, $state, $filter, rbacUiControlService, Dataset) { - - ClearScope(); - - var list = TeamList, - defaultUrl = GetBasePath('teams'); - - init(); - - function init() { - $scope.canAdd = false; - - rbacUiControlService.canAdd('teams') - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - // search init - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - _.forEach($scope[list.name], (team) => { - team.organization_name = team.summary_fields.organization.name; - }); - - $scope.selected = []; - } - - $scope.addTeam = function() { - $state.go('teams.add'); - }; - - $scope.editTeam = function(id) { - $state.go('teams.edit', { team_id: id }); - }; - - $scope.deleteTeam = function(id, name) { - - var action = function() { - Wait('start'); - var url = defaultUrl + id + '/'; - Rest.setUrl(url); - Rest.destroy() - .success(function() { - Wait('stop'); - $('#prompt-modal').modal('hide'); - if (parseInt($state.params.team_id) === id) { - $state.go('^', null, { reload: true }); - } else { - $state.go('.', null, { reload: true }); - } - }) - .error(function(data, status) { - Wait('stop'); - $('#prompt-modal').modal('hide'); - ProcessErrors($scope, data, status, null, { - hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status - }); - }); - }; - - Prompt({ - hdr: 'Delete', - body: '
Are you sure you want to delete the team below?
' + $filter('sanitize')(name) + '
', - action: action, - actionText: 'DELETE' - }); - }; -} - - -TeamsList.$inject = ['$scope', '$rootScope', '$log', - '$stateParams', 'Rest', 'Alert', 'TeamList', 'Prompt', 'ClearScope', - 'ProcessErrors', 'GetBasePath', 'Wait', '$state', '$filter', 'rbacUiControlService', 'Dataset' -]; - - -export function TeamsAdd($scope, $rootScope, $stateParams, TeamForm, GenerateForm, Rest, Alert, ProcessErrors, - ClearScope, GetBasePath, Wait, $state) { - ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior - //$scope. - - Rest.setUrl(GetBasePath('teams')); - Rest.options() - .success(function(data) { - if (!data.actions.POST) { - $state.go("^"); - Alert('Permission Error', 'You do not have permission to add a team.', 'alert-info'); - } - }); - - // Inject dynamic view - var defaultUrl = GetBasePath('teams'), - form = TeamForm; - - init(); - - function init() { - // apply form definition's default field values - GenerateForm.applyDefaults(form, $scope); - - $rootScope.flashMessage = null; - } - - // Save - $scope.formSave = function() { - var fld, data; - GenerateForm.clearApiErrors($scope); - Wait('start'); - Rest.setUrl(defaultUrl); - data = {}; - for (fld in form.fields) { - data[fld] = $scope[fld]; - } - Rest.post(data) - .success(function(data) { - Wait('stop'); - $rootScope.flashMessage = "New team successfully created!"; - $rootScope.$broadcast("EditIndicatorChange", "users", data.id); - $state.go('teams.edit', { team_id: data.id }, { reload: true }); - }) - .error(function(data, status) { - Wait('stop'); - ProcessErrors($scope, data, status, form, { - hdr: 'Error!', - msg: 'Failed to add new team. Post returned status: ' + - status - }); - }); - }; - - $scope.formCancel = function() { - $state.go('teams'); - }; -} - -TeamsAdd.$inject = ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'GenerateForm', - 'Rest', 'Alert', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'Wait', '$state' -]; - - -export function TeamsEdit($scope, $rootScope, $stateParams, - TeamForm, Rest, ProcessErrors, ClearScope, GetBasePath, Wait, $state) { - - ClearScope(); - - var form = TeamForm, - id = $stateParams.team_id, - defaultUrl = GetBasePath('teams') + id; - - init(); - - function init() { - $scope.team_id = id; - Rest.setUrl(defaultUrl); - Wait('start'); - Rest.get(defaultUrl).success(function(data) { - setScopeFields(data); - $scope.organization_name = data.summary_fields.organization.name; - - $scope.team_obj = data; - Wait('stop'); - }); - - $scope.$watch('team_obj.summary_fields.user_capabilities.edit', function(val) { - if (val === false) { - $scope.canAdd = false; - } - }); - - - } - - // @issue I think all this really want to do is _.forEach(form.fields, (field) =>{ $scope[field] = data[field]}) - function setScopeFields(data) { - _(data) - .pick(function(value, key) { - return form.fields.hasOwnProperty(key) === true; - }) - .forEach(function(value, key) { - $scope[key] = value; - }) - .value(); - return; - } - - // prepares a data payload for a PUT request to the API - function processNewData(fields) { - var data = {}; - _.forEach(fields, function(value, key) { - if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined) { - data[key] = $scope[key]; - } - }); - return data; - } - - $scope.formCancel = function() { - $state.go('teams', null, { reload: true }); - }; - - $scope.formSave = function() { - $rootScope.flashMessage = null; - if ($scope[form.name + '_form'].$valid) { - var data = processNewData(form.fields); - Rest.setUrl(defaultUrl); - Rest.put(data).success(function() { - $state.go($state.current, null, { reload: true }); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { - hdr: 'Error!', - msg: 'Failed to retrieve user: ' + - $stateParams.id + '. GET status: ' + status - }); - }); - } - }; - - init(); - - $scope.convertApiUrl = function(str) { - if (str) { - return str.replace("api/v1", "#"); - } else { - return null; - } - }; -} - -TeamsEdit.$inject = ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Rest', - 'ProcessErrors', 'ClearScope', 'GetBasePath', 'Wait', '$state' -]; diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js deleted file mode 100644 index a305a3e133..0000000000 --- a/awx/ui/client/src/controllers/Users.js +++ /dev/null @@ -1,357 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Users - * @description This controller's the Users page - */ - -import { N_ } from "../i18n"; - -const user_type_options = [ - { type: 'normal', label: N_('Normal User') }, - { type: 'system_auditor', label: N_('System Auditor') }, - { type: 'system_administrator', label: N_('System Administrator') }, -]; - -function user_type_sync($scope) { - return (type_option) => { - $scope.is_superuser = false; - $scope.is_system_auditor = false; - switch (type_option.type) { - case 'system_administrator': - $scope.is_superuser = true; - break; - case 'system_auditor': - $scope.is_system_auditor = true; - break; - } - }; -} - -export function UsersList($scope, $rootScope, $stateParams, - Rest, Alert, UserList, Prompt, ClearScope, ProcessErrors, GetBasePath, - Wait, $state, $filter, rbacUiControlService, Dataset, i18n) { - - for (var i = 0; i < user_type_options.length; i++) { - user_type_options[i].label = i18n._(user_type_options[i].label); - } - - ClearScope(); - - var list = UserList, - defaultUrl = GetBasePath('users'); - - init(); - - function init() { - $scope.canAdd = false; - - rbacUiControlService.canAdd('users') - .then(function(canAdd) { - $scope.canAdd = canAdd; - }); - - // search init - $scope.list = list; - $scope[`${list.iterator}_dataset`] = Dataset.data; - $scope[list.name] = $scope[`${list.iterator}_dataset`].results; - - - $rootScope.flashMessage = null; - $scope.selected = []; - } - - $scope.addUser = function() { - $state.go('users.add'); - }; - - $scope.editUser = function(id) { - $state.go('users.edit', { user_id: id }); - }; - - $scope.deleteUser = function(id, name) { - - var action = function() { - $('#prompt-modal').modal('hide'); - Wait('start'); - var url = defaultUrl + id + '/'; - Rest.setUrl(url); - Rest.destroy() - .success(function() { - if (parseInt($state.params.user_id) === id) { - $state.go('^', null, { reload: true }); - } else { - $state.go('.', null, { reload: true }); - } - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { - hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Call to %s failed. DELETE returned status: '), url) + status - }); - }); - }; - - Prompt({ - hdr: i18n._('Delete'), - body: '
' + i18n._('Are you sure you want to delete the user below?') + '
' + $filter('sanitize')(name) + '
', - action: action, - actionText: i18n._('DELETE') - }); - }; -} - -UsersList.$inject = ['$scope', '$rootScope', '$stateParams', - 'Rest', 'Alert', 'UserList', 'Prompt', 'ClearScope', 'ProcessErrors', 'GetBasePath', - 'Wait', '$state', '$filter', 'rbacUiControlService', 'Dataset', 'i18n' -]; - - -export function UsersAdd($scope, $rootScope, $stateParams, UserForm, - GenerateForm, Rest, Alert, ProcessErrors, ReturnToCaller, ClearScope, - GetBasePath, ResetForm, Wait, CreateSelect2, $state, $location, i18n) { - - ClearScope(); - - var defaultUrl = GetBasePath('organizations'), - form = UserForm; - - init(); - - function init() { - // apply form definition's default field values - GenerateForm.applyDefaults(form, $scope); - - $scope.ldap_user = false; - $scope.not_ldap_user = !$scope.ldap_user; - $scope.ldap_dn = null; - $scope.socialAuthUser = false; - $scope.external_account = null; - - Rest.setUrl(GetBasePath('users')); - Rest.options() - .success(function(data) { - if (!data.actions.POST) { - $state.go("^"); - Alert(i18n._('Permission Error'), i18n._('You do not have permission to add a user.'), 'alert-info'); - } - }); - - $scope.user_type_options = user_type_options; - $scope.user_type = user_type_options[0]; - $scope.$watch('user_type', user_type_sync($scope)); - CreateSelect2({ - element: '#user_user_type', - multiple: false - }); - } - - // Save - $scope.formSave = function() { - var fld, data = {}; - if ($scope[form.name + '_form'].$valid) { - if ($scope.organization !== undefined && $scope.organization !== null && $scope.organization !== '') { - Rest.setUrl(defaultUrl + $scope.organization + '/users/'); - for (fld in form.fields) { - if (form.fields[fld].realName) { - data[form.fields[fld].realName] = $scope[fld]; - } else { - data[fld] = $scope[fld]; - } - } - data.is_superuser = $scope.is_superuser; - data.is_system_auditor = $scope.is_system_auditor; - Wait('start'); - Rest.post(data) - .success(function(data) { - var base = $location.path().replace(/^\//, '').split('/')[0]; - if (base === 'users') { - $rootScope.flashMessage = i18n._('New user successfully created!'); - $rootScope.$broadcast("EditIndicatorChange", "users", data.id); - $state.go('users.edit', { user_id: data.id }, { reload: true }); - } else { - ReturnToCaller(1); - } - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), msg: i18n._('Failed to add new user. POST returned status: ') + status }); - }); - } else { - $scope.organization_name_api_error = i18n._('A value is required'); - } - } - }; - - $scope.formCancel = function() { - $state.go('users'); - }; - - // Password change - $scope.clearPWConfirm = function(fld) { - // If password value changes, make sure password_confirm must be re-entered - $scope[fld] = ''; - $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); - }; -} - -UsersAdd.$inject = ['$scope', '$rootScope', '$stateParams', 'UserForm', 'GenerateForm', - 'Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'GetBasePath', - 'ResetForm', 'Wait', 'CreateSelect2', '$state', '$location', 'i18n' -]; - -export function UsersEdit($scope, $rootScope, $location, - $stateParams, UserForm, Rest, ProcessErrors, - ClearScope, GetBasePath, ResetForm, Wait, CreateSelect2, $state, i18n) { - - for (var i = 0; i < user_type_options.length; i++) { - user_type_options[i].label = i18n._(user_type_options[i].label); - } - ClearScope(); - - var form = UserForm, - master = {}, - id = $stateParams.user_id, - defaultUrl = GetBasePath('users') + id; - - init(); - - function init() { - $scope.hidePagination = false; - $scope.hideSmartSearch = false; - $scope.user_type_options = user_type_options; - $scope.user_type = user_type_options[0]; - $scope.$watch('user_type', user_type_sync($scope)); - $scope.$watch('is_superuser', hidePermissionsTabSmartSearchAndPaginationIfSuperUser($scope)); - Rest.setUrl(defaultUrl); - Wait('start'); - Rest.get(defaultUrl).success(function(data) { - $scope.user_id = id; - $scope.ldap_user = (data.ldap_dn !== null && data.ldap_dn !== undefined && data.ldap_dn !== '') ? true : false; - $scope.not_ldap_user = !$scope.ldap_user; - master.ldap_user = $scope.ldap_user; - $scope.socialAuthUser = (data.auth.length > 0) ? true : false; - $scope.external_account = data.external_account; - - $scope.user_type = $scope.user_type_options[0]; - $scope.is_system_auditor = false; - $scope.is_superuser = false; - if (data.is_system_auditor) { - $scope.user_type = $scope.user_type_options[1]; - $scope.is_system_auditor = true; - } - if (data.is_superuser) { - $scope.user_type = $scope.user_type_options[2]; - $scope.is_superuser = true; - } - - $scope.user_obj = data; - $scope.name = data.username; - - CreateSelect2({ - element: '#user_user_type', - multiple: false - }); - - $scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) { - if (val === false) { - $scope.canAdd = false; - } - }); - - setScopeFields(data); - Wait('stop'); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { - hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Failed to retrieve user: %s. GET status: '), $stateParams.id) + status - }); - }); - } - - // Organizations and Teams tab pagination is hidden through other mechanism - function hidePermissionsTabSmartSearchAndPaginationIfSuperUser(scope) { - return function(isSuperuserNewValue) { - let shouldHide = isSuperuserNewValue; - if (shouldHide === true) { - scope.hidePagination = true; - scope.hideSmartSearch = true; - } else if (shouldHide === false) { - scope.hidePagination = false; - scope.hideSmartSearch = false; - } - }; - } - - - function setScopeFields(data) { - _(data) - .pick(function(value, key) { - return form.fields.hasOwnProperty(key) === true; - }) - .forEach(function(value, key) { - $scope[key] = value; - }) - .value(); - return; - } - - $scope.convertApiUrl = function(str) { - if (str) { - return str.replace("api/v1", "#"); - } else { - return null; - } - }; - - // prepares a data payload for a PUT request to the API - var processNewData = function(fields) { - var data = {}; - _.forEach(fields, function(value, key) { - if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined) { - data[key] = $scope[key]; - } - }); - data.is_superuser = $scope.is_superuser; - data.is_system_auditor = $scope.is_system_auditor; - return data; - }; - - $scope.formCancel = function() { - $state.go('users', null, { reload: true }); - }; - - $scope.formSave = function() { - $rootScope.flashMessage = null; - if ($scope[form.name + '_form'].$valid) { - Rest.setUrl(defaultUrl + '/'); - var data = processNewData(form.fields); - Rest.put(data).success(function() { - $state.go($state.current, null, { reload: true }); - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { - hdr: i18n._('Error!'), - msg: i18n.sprintf(i18n._('Failed to retrieve user: %s. GET status: '), $stateParams.id) + status - }); - }); - } - }; - - $scope.clearPWConfirm = function(fld) { - // If password value changes, make sure password_confirm must be re-entered - $scope[fld] = ''; - $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); - $rootScope.flashMessage = null; - }; -} - -UsersEdit.$inject = ['$scope', '$rootScope', '$location', - '$stateParams', 'UserForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath', - 'ResetForm', 'Wait', 'CreateSelect2', '$state', 'i18n' -]; diff --git a/awx/ui/client/src/credentials/add/credentials-add.controller.js b/awx/ui/client/src/credentials/add/credentials-add.controller.js new file mode 100644 index 0000000000..88f2a41e57 --- /dev/null +++ b/awx/ui/client/src/credentials/add/credentials-add.controller.js @@ -0,0 +1,177 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$compile', '$location', + '$log', '$stateParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', + 'ProcessErrors', 'ClearScope', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'BecomeMethodChange', + 'OwnerChange', 'CredentialFormSave', '$state', 'CreateSelect2', 'i18n', + function($scope, $rootScope, $compile, $location, $log, + $stateParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, + ClearScope, GetBasePath, GetChoices, Empty, KindChange, BecomeMethodChange, + OwnerChange, CredentialFormSave, $state, CreateSelect2, i18n) { + + ClearScope(); + + // Inject dynamic view + var form = CredentialForm, + defaultUrl = GetBasePath('credentials'), + url; + + init(); + + function init() { + // Load the list of options for Kind + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'kind', + variable: 'credential_kind_options' + }); + + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'become_method', + variable: 'become_options' + }); + + CreateSelect2({ + element: '#credential_become_method', + multiple: false + }); + + CreateSelect2({ + element: '#credential_kind', + multiple: false + }); + + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + + $scope.keyEntered = false; + $scope.permissionsTooltip = i18n._('Please save before assigning permissions'); + + // determine if the currently logged-in user may share this credential + // previous commentary said: "$rootScope.current_user isn't available because a call to the config endpoint hasn't finished resolving yet" + // I'm 99% sure this state's will never resolve block will be rejected if setup surrounding config endpoint hasn't completed + if ($rootScope.current_user && $rootScope.current_user.is_superuser) { + $scope.canShareCredential = true; + } else { + Rest.setUrl(`/api/v1/users/${$rootScope.current_user.id}/admin_of_organizations`); + Rest.get() + .success(function(data) { + $scope.canShareCredential = (data.count) ? true : false; + }).error(function(data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to find if users is admin of org' + status }); + }); + } + } + + if (!Empty($stateParams.user_id)) { + // Get the username based on incoming route + $scope.owner = 'user'; + $scope.user = $stateParams.user_id; + OwnerChange({ scope: $scope }); + url = GetBasePath('users') + $stateParams.user_id + '/'; + Rest.setUrl(url); + Rest.get() + .success(function(data) { + $scope.user_username = data.username; + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user. GET status: ' + status }); + }); + } else if (!Empty($stateParams.team_id)) { + // Get the username based on incoming route + $scope.owner = 'team'; + $scope.team = $stateParams.team_id; + OwnerChange({ scope: $scope }); + url = GetBasePath('teams') + $stateParams.team_id + '/'; + Rest.setUrl(url); + Rest.get() + .success(function(data) { + $scope.team_name = data.name; + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve team. GET status: ' + status }); + }); + } else { + // default type of owner to a user + $scope.owner = 'user'; + OwnerChange({ scope: $scope }); + } + + $scope.$watch("ssh_key_data", function(val) { + if (val === "" || val === null || val === undefined) { + $scope.keyEntered = false; + $scope.ssh_key_unlock_ask = false; + $scope.ssh_key_unlock = ""; + } else { + $scope.keyEntered = true; + } + }); + + // Handle Kind change + $scope.kindChange = function() { + KindChange({ scope: $scope, form: form, reset: true }); + }; + + $scope.becomeMethodChange = function() { + BecomeMethodChange({ scope: $scope }); + }; + + // Save + $scope.formSave = function() { + if ($scope[form.name + '_form'].$valid) { + CredentialFormSave({ scope: $scope, mode: 'add' }); + } + }; + + $scope.formCancel = function() { + $state.go('credentials'); + }; + + // Password change + $scope.clearPWConfirm = function(fld) { + // If password value changes, make sure password_confirm must be re-entered + $scope[fld] = ''; + $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); + }; + + // Respond to 'Ask at runtime?' checkbox + $scope.ask = function(fld, associated) { + if ($scope[fld + '_ask']) { + $scope[fld] = 'ASK'; + $("#" + form.name + "_" + fld + "_input").attr("type", "text"); + $("#" + form.name + "_" + fld + "_show_input_button").html("Hide"); + if (associated !== "undefined") { + $("#" + form.name + "_" + fld + "_input").attr("type", "password"); + $("#" + form.name + "_" + fld + "_show_input_button").html("Show"); + $scope[associated] = ''; + $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); + } + } else { + $scope[fld] = ''; + $("#" + form.name + "_" + fld + "_input").attr("type", "password"); + $("#" + form.name + "_" + fld + "_show_input_button").html("Show"); + if (associated !== "undefined") { + $("#" + form.name + "_" + fld + "_input").attr("type", "text"); + $("#" + form.name + "_" + fld + "_show_input_button").html("Hide"); + $scope[associated] = ''; + $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); + } + } + }; + + // Click clear button + $scope.clear = function(fld, associated) { + $scope[fld] = ''; + $scope[associated] = ''; + $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); + $scope[form.name + '_form'].$setDirty(); + }; + } +]; diff --git a/awx/ui/client/src/credentials/edit/credentials-edit.controller.js b/awx/ui/client/src/credentials/edit/credentials-edit.controller.js new file mode 100644 index 0000000000..9073b80bdb --- /dev/null +++ b/awx/ui/client/src/credentials/edit/credentials-edit.controller.js @@ -0,0 +1,344 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$compile', '$location', + '$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert', + 'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices', + 'KindChange', 'BecomeMethodChange', 'Empty', 'OwnerChange', + 'CredentialFormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n', + function($scope, $rootScope, $compile, $location, $log, + $stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt, + GetBasePath, GetChoices, KindChange, BecomeMethodChange, Empty, OwnerChange, CredentialFormSave, Wait, + $state, CreateSelect2, Authorization, i18n) { + + ClearScope(); + + var defaultUrl = GetBasePath('credentials'), + form = CredentialForm, + base = $location.path().replace(/^\//, '').split('/')[0], + master = {}, + id = $stateParams.credential_id; + + init(); + + function init() { + $scope.id = id; + $scope.$watch('credential_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + + $scope.canShareCredential = false; + Wait('start'); + if (!$rootScope.current_user) { + Authorization.restoreUserInfo(); + } + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'kind', + variable: 'credential_kind_options', + callback: 'choicesReadyCredential' + }); + + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'become_method', + variable: 'become_options' + }); + + if ($rootScope.current_user && $rootScope.current_user.is_superuser) { + $scope.canShareCredential = true; + } else { + Rest.setUrl(`/api/v1/users/${$rootScope.current_user.id}/admin_of_organizations`); + Rest.get() + .success(function(data) { + $scope.canShareCredential = (data.count) ? true : false; + Wait('stop'); + }).error(function(data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to find if users is admin of org' + status }); + }); + } + + $scope.$watch('organization', function(val) { + if (val === undefined) { + $scope.permissionsTooltip = i18n._('Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.'); + } else { + $scope.permissionsTooltip = ''; + } + }); + + setAskCheckboxes(); + OwnerChange({ scope: $scope }); + $scope.$watch("ssh_key_data", function(val) { + if (val === "" || val === null || val === undefined) { + $scope.keyEntered = false; + $scope.ssh_key_unlock_ask = false; + $scope.ssh_key_unlock = ""; + } else { + $scope.keyEntered = true; + } + }); + } + + function setAskCheckboxes() { + var fld, i; + for (fld in form.fields) { + if (form.fields[fld].type === 'sensitive' && $scope[fld] === 'ASK') { + // turn on 'ask' checkbox for password fields with value of 'ASK' + $("#" + form.name + "_" + fld + "_input").attr("type", "text"); + $("#" + form.name + "_" + fld + "_show_input_button").html("Hide"); + $("#" + fld + "-clear-btn").attr("disabled", "disabled"); + $scope[fld + '_ask'] = true; + } else { + $scope[fld + '_ask'] = false; + $("#" + fld + "-clear-btn").removeAttr("disabled"); + } + master[fld + '_ask'] = $scope[fld + '_ask']; + } + + // Set kind field to the correct option + for (i = 0; i < $scope.credential_kind_options.length; i++) { + if ($scope.kind === $scope.credential_kind_options[i].value) { + $scope.kind = $scope.credential_kind_options[i]; + break; + } + } + } + if ($scope.removeChoicesReady) { + $scope.removeChoicesReady(); + } + $scope.removeChoicesReady = $scope.$on('choicesReadyCredential', function() { + // Retrieve detail record and prepopulate the form + Rest.setUrl(defaultUrl + ':id/'); + Rest.get({ params: { id: id } }) + .success(function(data) { + if (data && data.summary_fields && + data.summary_fields.organization && + data.summary_fields.organization.id) { + $scope.needsRoleList = true; + } else { + $scope.needsRoleList = false; + } + + $scope.credential_name = data.name; + + var i, fld; + + + for (fld in form.fields) { + if (data[fld] !== null && data[fld] !== undefined) { + $scope[fld] = data[fld]; + master[fld] = $scope[fld]; + } + if (form.fields[fld].type === 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { + $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; + } + } + + if (!Empty($scope.user)) { + $scope.owner = 'user'; + } else { + $scope.owner = 'team'; + } + master.owner = $scope.owner; + + for (i = 0; i < $scope.become_options.length; i++) { + if ($scope.become_options[i].value === data.become_method) { + $scope.become_method = $scope.become_options[i]; + break; + } + } + + if ($scope.become_method && $scope.become_method.value === "") { + $scope.become_method = null; + } + master.become_method = $scope.become_method; + + $scope.$watch('become_method', function(val) { + if (val !== null) { + if (val.value === "") { + $scope.become_username = ""; + $scope.become_password = ""; + } + } + }); + + for (i = 0; i < $scope.credential_kind_options.length; i++) { + if ($scope.credential_kind_options[i].value === data.kind) { + $scope.kind = $scope.credential_kind_options[i]; + break; + } + } + + KindChange({ + scope: $scope, + form: form, + reset: false + }); + + master.kind = $scope.kind; + + CreateSelect2({ + element: '#credential_become_method', + multiple: false + }); + + CreateSelect2({ + element: '#credential_kind', + multiple: false + }); + + switch (data.kind) { + case 'aws': + $scope.access_key = data.username; + $scope.secret_key = data.password; + master.access_key = $scope.access_key; + master.secret_key = $scope.secret_key; + break; + case 'ssh': + $scope.ssh_password = data.password; + master.ssh_password = $scope.ssh_password; + break; + case 'rax': + $scope.api_key = data.password; + master.api_key = $scope.api_key; + break; + case 'gce': + $scope.email_address = data.username; + $scope.project = data.project; + break; + case 'azure': + $scope.subscription = data.username; + break; + } + $scope.credential_obj = data; + + $scope.$emit('credentialLoaded'); + Wait('stop'); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to retrieve Credential: ' + $stateParams.id + '. GET status: ' + status + }); + }); + }); + + // Save changes to the parent + $scope.formSave = function() { + if ($scope[form.name + '_form'].$valid) { + CredentialFormSave({ scope: $scope, mode: 'edit' }); + } + }; + + // Handle Owner change + $scope.ownerChange = function() { + OwnerChange({ scope: $scope }); + }; + + // Handle Kind change + $scope.kindChange = function() { + KindChange({ scope: $scope, form: form, reset: true }); + }; + + $scope.becomeMethodChange = function() { + BecomeMethodChange({ scope: $scope }); + }; + + $scope.formCancel = function() { + $state.transitionTo('credentials'); + }; + + // Related set: Add button + $scope.add = function(set) { + $rootScope.flashMessage = null; + $location.path('/' + base + '/' + $stateParams.id + '/' + set + '/add'); + }; + + // Related set: Edit button + $scope.edit = function(set, id) { + $rootScope.flashMessage = null; + $location.path('/' + base + '/' + $stateParams.id + '/' + set + '/' + id); + }; + + // Related set: Delete button + $scope['delete'] = function(set, itm_id, name, title) { + $rootScope.flashMessage = null; + + var action = function() { + var url = defaultUrl + id + '/' + set + '/'; + Rest.setUrl(url); + Rest.post({ + id: itm_id, + disassociate: 1 + }) + .success(function() { + $('#prompt-modal').modal('hide'); + }) + .error(function(data, status) { + $('#prompt-modal').modal('hide'); + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Call to ' + url + ' failed. POST returned status: ' + status + }); + }); + }; + + Prompt({ + hdr: i18n._('Delete'), + body: '
' + i18n.sprintf(i18n._('Are you sure you want to remove the %s below from %s?'), title, $scope.name) + '
' + name + '
', + action: action, + actionText: i18n._('DELETE') + }); + + }; + + // Password change + $scope.clearPWConfirm = function(fld) { + // If password value changes, make sure password_confirm must be re-entered + $scope[fld] = ''; + $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); + }; + + // Respond to 'Ask at runtime?' checkbox + $scope.ask = function(fld, associated) { + if ($scope[fld + '_ask']) { + $scope[fld] = 'ASK'; + $("#" + form.name + "_" + fld + "_input").attr("type", "text"); + $("#" + form.name + "_" + fld + "_show_input_button").html("Hide"); + if (associated !== "undefined") { + $("#" + form.name + "_" + fld + "_input").attr("type", "password"); + $("#" + form.name + "_" + fld + "_show_input_button").html("Show"); + $scope[associated] = ''; + $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); + } + } else { + $scope[fld] = ''; + $("#" + form.name + "_" + fld + "_input").attr("type", "password"); + $("#" + form.name + "_" + fld + "_show_input_button").html("Show"); + if (associated !== "undefined") { + $("#" + form.name + "_" + fld + "_input").attr("type", "text"); + $("#" + form.name + "_" + fld + "_show_input_button").html("Hide"); + $scope[associated] = ''; + $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); + } + } + }; + + $scope.clear = function(fld, associated) { + $scope[fld] = ''; + $scope[associated] = ''; + $scope[form.name + '_form'][associated].$setValidity('awpassmatch', true); + $scope[form.name + '_form'].$setDirty(); + }; + } +]; diff --git a/awx/ui/client/src/credentials/factories/become-method-change.factory.js b/awx/ui/client/src/credentials/factories/become-method-change.factory.js new file mode 100644 index 0000000000..0727553528 --- /dev/null +++ b/awx/ui/client/src/credentials/factories/become-method-change.factory.js @@ -0,0 +1,105 @@ +export default + function BecomeMethodChange(Empty, i18n) { + return function(params) { + var scope = params.scope; + + if (!Empty(scope.kind)) { + // Apply kind specific settings + switch (scope.kind.value) { + case 'aws': + scope.aws_required = true; + break; + case 'rax': + scope.rackspace_required = true; + scope.username_required = true; + break; + case 'ssh': + scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' + scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); + scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); + break; + case 'scm': + scope.sshKeyDataLabel = i18n._('SCM Private Key'); + scope.passwordLabel = i18n._('Password'); + break; + case 'gce': + scope.usernameLabel = i18n._('Service Account Email Address'); + scope.sshKeyDataLabel = i18n._('RSA Private Key'); + scope.email_required = true; + scope.key_required = true; + scope.project_required = true; + scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); + scope.projectLabel = i18n._("Project"); + scope.project_required = false; + scope.projectPopOver = "

" + i18n._("The Project ID is the " + + "GCE assigned identification. It is constructed as " + + "two words followed by a three digit number. Such " + + "as: ") + "

adjective-noun-000

"; + break; + case 'azure': + scope.sshKeyDataLabel = i18n._('Management Certificate'); + scope.subscription_required = true; + scope.key_required = true; + scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); + break; + case 'azure_rm': + scope.usernameLabel = i18n._("Username"); + scope.subscription_required = true; + scope.passwordLabel = i18n._('Password'); + scope.azure_rm_required = true; + break; + case 'vmware': + scope.username_required = true; + scope.host_required = true; + scope.password_required = true; + scope.hostLabel = "vCenter Host"; + scope.passwordLabel = i18n._('Password'); + scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); + break; + case 'openstack': + scope.hostLabel = i18n._("Host (Authentication URL)"); + scope.projectLabel = i18n._("Project (Tenant Name)"); + scope.domainLabel = i18n._("Domain Name"); + scope.password_required = true; + scope.project_required = true; + scope.host_required = true; + scope.username_required = true; + scope.projectPopOver = "

" + i18n._("This is the tenant name. " + + " This value is usually the same " + + " as the username.") + "

"; + scope.hostPopOver = "

" + i18n._("The host to authenticate with.") + + "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/"); + break; + case 'satellite6': + scope.username_required = true; + scope.password_required = true; + scope.passwordLabel = i18n._('Password'); + scope.host_required = true; + scope.hostLabel = i18n._("Satellite 6 URL"); + scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" + + "Red Hat Satellite 6 server. %s" + + "For example, %s"), "
", "
", "https://satellite.example.org"); + break; + case 'cloudforms': + scope.username_required = true; + scope.password_required = true; + scope.passwordLabel = i18n._('Password'); + scope.host_required = true; + scope.hostLabel = i18n._("CloudForms URL"); + scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" + + "corresponds to your CloudForm instance. %s" + + "For example, %s"), "
", "
", "https://cloudforms.example.org"); + break; + case 'net': + scope.username_required = true; + scope.password_required = false; + scope.passwordLabel = i18n._('Password'); + scope.sshKeyDataLabel = i18n._('SSH Key'); + break; + } + } + }; + } + +BecomeMethodChange.$inject = + [ 'Empty', 'i18n' ]; diff --git a/awx/ui/client/src/credentials/factories/credential-form-save.factory.js b/awx/ui/client/src/credentials/factories/credential-form-save.factory.js new file mode 100644 index 0000000000..d8efc83ffd --- /dev/null +++ b/awx/ui/client/src/credentials/factories/credential-form-save.factory.js @@ -0,0 +1,105 @@ +export default + function CredentialFormSave($rootScope, $location, Alert, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait, $state, i18n) { + return function(params) { + var scope = params.scope, + mode = params.mode, + form = CredentialForm, + data = {}, fld, url; + + for (fld in form.fields) { + if (fld !== 'access_key' && fld !== 'secret_key' && fld !== 'ssh_username' && + fld !== 'ssh_password') { + if (fld === "organization" && !scope[fld]) { + data.user = $rootScope.current_user.id; + } else if (scope[fld] === null) { + data[fld] = ""; + } else { + data[fld] = scope[fld]; + } + } + } + + data.kind = scope.kind.value; + if (scope.become_method === null || typeof scope.become_method === 'undefined') { + data.become_method = ""; + data.become_username = ""; + data.become_password = ""; + } else { + data.become_method = (scope.become_method.value) ? scope.become_method.value : ""; + } + switch (data.kind) { + case 'ssh': + data.password = scope.ssh_password; + break; + case 'aws': + data.username = scope.access_key; + data.password = scope.secret_key; + break; + case 'rax': + data.password = scope.api_key; + break; + case 'gce': + data.username = scope.email_address; + data.project = scope.project; + break; + case 'azure': + data.username = scope.subscription; + } + + Wait('start'); + if (mode === 'add') { + url = GetBasePath("credentials"); + Rest.setUrl(url); + Rest.post(data) + .success(function (data) { + scope.addedItem = data.id; + + Wait('stop'); + var base = $location.path().replace(/^\//, '').split('/')[0]; + if (base === 'credentials') { + $state.go('credentials.edit', {credential_id: data.id}, {reload: true}); + } + else { + ReturnToCaller(1); + } + }) + .error(function (data, status) { + Wait('stop'); + // TODO: hopefully this conditional error handling will to away in a future version of tower. The reason why we cannot + // simply pass this error to ProcessErrors is because it will actually match the form element 'ssh_key_unlock' and show + // the error there. The ssh_key_unlock field is not shown when the kind of credential is gce/azure and as a result the + // error is never shown. In the future, the API will hopefully either behave or respond differently. + if(status && status === 400 && data && data.ssh_key_unlock && (scope.kind.value === 'gce' || scope.kind.value === 'azure')) { + scope.ssh_key_data_api_error = i18n._("Encrypted credentials are not supported."); + } + else { + ProcessErrors(scope, data, status, form, { + hdr: i18n._('Error!'), + msg: i18n._('Failed to create new Credential. POST status: ') + status + }); + } + }); + } else { + url = GetBasePath('credentials') + scope.id + '/'; + Rest.setUrl(url); + Rest.put(data) + .success(function () { + Wait('stop'); + $state.go($state.current, {}, {reload: true}); + }) + .error(function (data, status) { + Wait('stop'); + ProcessErrors(scope, data, status, form, { + hdr: i18n._('Error!'), + msg: i18n._('Failed to update Credential. PUT status: ') + status + }); + }); + } + }; + } + +CredentialFormSave.$inject = + [ '$rootScope', '$location', 'Alert', 'Rest', + 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', + 'ReturnToCaller', 'Wait', '$state', 'i18n' + ]; diff --git a/awx/ui/client/src/credentials/factories/kind-change.factory.js b/awx/ui/client/src/credentials/factories/kind-change.factory.js new file mode 100644 index 0000000000..e35bedc526 --- /dev/null +++ b/awx/ui/client/src/credentials/factories/kind-change.factory.js @@ -0,0 +1,192 @@ +export default + function KindChange(Empty, i18n) { + return function(params) { + var scope = params.scope, + reset = params.reset, + collapse, id; + + $('.popover').each(function() { + // remove lingering popover

. Seems to be a bug in TB3 RC1 + $(this).remove(); + }); + $('.tooltip').each( function() { + // close any lingering tool tipss + $(this).hide(); + }); + // Put things in a default state + scope.usernameLabel = i18n._('Username'); + scope.aws_required = false; + scope.email_required = false; + scope.rackspace_required = false; + scope.sshKeyDataLabel = i18n._('Private Key'); + scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE) + scope.key_required = false; // JT -- doing the same for key and project + scope.project_required = false; + scope.subscription_required = false; + scope.key_description = i18n.sprintf(i18n._("Paste the contents of the SSH private key file.%s or click to close%s"), "
Esc", "
"); + scope.host_required = false; + scope.password_required = false; + scope.hostLabel = ''; + scope.passwordLabel = i18n._('Password'); + + $('.popover').each(function() { + // remove lingering popover
. Seems to be a bug in TB3 RC1 + $(this).remove(); + }); + $('.tooltip').each( function() { + // close any lingering tool tipss + $(this).hide(); + }); + // Put things in a default state + scope.usernameLabel = i18n._('Username'); + scope.aws_required = false; + scope.email_required = false; + scope.rackspace_required = false; + scope.sshKeyDataLabel = i18n._('Private Key'); + scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE) + scope.key_required = false; // JT -- doing the same for key and project + scope.project_required = false; + scope.domain_required = false; + scope.subscription_required = false; + scope.key_description = i18n._("Paste the contents of the SSH private key file."); + scope.host_required = false; + scope.password_required = false; + scope.hostLabel = ''; + scope.projectLabel = ''; + scope.domainLabel = ''; + scope.project_required = false; + scope.passwordLabel = i18n._('Password (API Key)'); + scope.projectPopOver = "

" + i18n._("The project value") + "

"; + scope.hostPopOver = "

" + i18n._("The host value") + "

"; + scope.ssh_key_data_api_error = ''; + + if (!Empty(scope.kind)) { + // Apply kind specific settings + switch (scope.kind.value) { + case 'aws': + scope.aws_required = true; + break; + case 'rax': + scope.rackspace_required = true; + scope.username_required = true; + break; + case 'ssh': + scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' + scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); + scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); + break; + case 'scm': + scope.sshKeyDataLabel = i18n._('SCM Private Key'); + scope.passwordLabel = i18n._('Password'); + break; + case 'gce': + scope.usernameLabel = i18n._('Service Account Email Address'); + scope.sshKeyDataLabel = i18n._('RSA Private Key'); + scope.email_required = true; + scope.key_required = true; + scope.project_required = true; + scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); + scope.projectLabel = i18n._("Project"); + scope.project_required = false; + scope.projectPopOver = "

" + i18n._("The Project ID is the " + + "GCE assigned identification. It is constructed as " + + "two words followed by a three digit number. Such " + + "as: ") + "

adjective-noun-000

"; + break; + case 'azure': + scope.sshKeyDataLabel = i18n._('Management Certificate'); + scope.subscription_required = true; + scope.key_required = true; + scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); + break; + case 'azure_rm': + scope.usernameLabel = i18n._("Username"); + scope.subscription_required = true; + scope.passwordLabel = i18n._('Password'); + scope.azure_rm_required = true; + break; + case 'vmware': + scope.username_required = true; + scope.host_required = true; + scope.password_required = true; + scope.hostLabel = "vCenter Host"; + scope.passwordLabel = i18n._('Password'); + scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); + break; + case 'openstack': + scope.hostLabel = i18n._("Host (Authentication URL)"); + scope.projectLabel = i18n._("Project (Tenant Name)"); + scope.domainLabel = i18n._("Domain Name"); + scope.password_required = true; + scope.project_required = true; + scope.host_required = true; + scope.username_required = true; + scope.projectPopOver = "

" + i18n._("This is the tenant name. " + + " This value is usually the same " + + " as the username.") + "

"; + scope.hostPopOver = "

" + i18n._("The host to authenticate with.") + + "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/"); + break; + case 'satellite6': + scope.username_required = true; + scope.password_required = true; + scope.passwordLabel = i18n._('Password'); + scope.host_required = true; + scope.hostLabel = i18n._("Satellite 6 URL"); + scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" + + "Red Hat Satellite 6 server. %s" + + "For example, %s"), "
", "
", "https://satellite.example.org"); + break; + case 'cloudforms': + scope.username_required = true; + scope.password_required = true; + scope.passwordLabel = i18n._('Password'); + scope.host_required = true; + scope.hostLabel = i18n._("CloudForms URL"); + scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" + + "corresponds to your CloudForm instance. %s" + + "For example, %s"), "
", "
", "https://cloudforms.example.org"); + break; + case 'net': + scope.username_required = true; + scope.password_required = false; + scope.passwordLabel = i18n._('Password'); + scope.sshKeyDataLabel = i18n._('SSH Key'); + break; + } + } + + // Reset all the field values related to Kind. + if (reset) { + scope.access_key = null; + scope.secret_key = null; + scope.api_key = null; + scope.username = null; + scope.password = null; + scope.password_confirm = null; + scope.ssh_key_data = null; + scope.ssh_key_unlock = null; + scope.ssh_key_unlock_confirm = null; + scope.become_username = null; + scope.become_password = null; + scope.authorize = false; + scope.authorize_password = null; + } + + // Collapse or open help widget based on whether scm value is selected + collapse = $('#credential_kind').parent().find('.panel-collapse').first(); + id = collapse.attr('id'); + if (!Empty(scope.kind) && scope.kind.value !== '') { + if ($('#' + id + '-icon').hasClass('icon-minus')) { + scope.accordionToggle('#' + id); + } + } else { + if ($('#' + id + '-icon').hasClass('icon-plus')) { + scope.accordionToggle('#' + id); + } + } + }; + } + +KindChange.$inject = + [ 'Empty', 'i18n' ]; diff --git a/awx/ui/client/src/credentials/factories/owner-change.factory.js b/awx/ui/client/src/credentials/factories/owner-change.factory.js new file mode 100644 index 0000000000..60b77110bb --- /dev/null +++ b/awx/ui/client/src/credentials/factories/owner-change.factory.js @@ -0,0 +1,18 @@ +export default + function OwnerChange() { + return function(params) { + var scope = params.scope, + owner = scope.owner; + if (owner === 'team') { + scope.team_required = true; + scope.user_required = false; + scope.user = null; + scope.user_username = null; + } else { + scope.team_required = false; + scope.user_required = true; + scope.team = null; + scope.team_name = null; + } + }; + } diff --git a/awx/ui/client/src/credentials/list/credentials-list.controller.js b/awx/ui/client/src/credentials/list/credentials-list.controller.js new file mode 100644 index 0000000000..6f420f2cd9 --- /dev/null +++ b/awx/ui/client/src/credentials/list/credentials-list.controller.js @@ -0,0 +1,104 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$location', '$log', + '$stateParams', 'Rest', 'Alert', 'CredentialList', 'Prompt', 'ClearScope', + 'ProcessErrors', 'GetBasePath', 'Wait', '$state', '$filter', 'rbacUiControlService', 'Dataset', 'i18n', + function($scope, $rootScope, $location, $log, + $stateParams, Rest, Alert, CredentialList, Prompt, ClearScope, + ProcessErrors, GetBasePath, Wait, $state, $filter, rbacUiControlService, Dataset, + i18n) { + + ClearScope(); + + var list = CredentialList, + defaultUrl = GetBasePath('credentials'); + + init(); + + function init() { + rbacUiControlService.canAdd('credentials') + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + $scope.selected = []; + } + + $scope.$on(`${list.iterator}_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + $scope.$watchCollection(`${$scope.list.name}`, function() { + 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] !== 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 && $scope.options && + $scope.options.hasOwnProperty('kind')) { + $scope.options.kind.choices.forEach(function(choice) { + if (choice[0] === item.kind) { + itm.kind_label = choice[1]; + } + }); + } + }); + } + } + + $scope.addCredential = function() { + $state.go('credentials.add'); + }; + + $scope.editCredential = function(id) { + $state.go('credentials.edit', { credential_id: id }); + }; + + $scope.deleteCredential = function(id, name) { + var action = function() { + $('#prompt-modal').modal('hide'); + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.destroy() + .success(function() { + if (parseInt($state.params.credential_id) === id) { + $state.go("^", null, { reload: true }); + } else { + $state.go('.', null, {reload: true}); + } + Wait('stop'); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status + }); + }); + }; + + Prompt({ + hdr: i18n._('Delete'), + body: '

' + i18n._('Are you sure you want to delete the credential below?') + '
' + $filter('sanitize')(name) + '
', + action: action, + actionText: i18n._('DELETE') + }); + }; + } +]; diff --git a/awx/ui/client/src/credentials/main.js b/awx/ui/client/src/credentials/main.js index faee3f5093..e4bb2f6f2d 100644 --- a/awx/ui/client/src/credentials/main.js +++ b/awx/ui/client/src/credentials/main.js @@ -5,7 +5,53 @@ *************************************************/ import ownerList from './ownerList.directive'; +import CredentialsList from './list/credentials-list.controller'; +import CredentialsAdd from './add/credentials-add.controller'; +import CredentialsEdit from './edit/credentials-edit.controller'; +import BecomeMethodChange from './factories/become-method-change.factory'; +import CredentialFormSave from './factories/credential-form-save.factory'; +import KindChange from './factories/kind-change.factory'; +import OwnerChange from './factories/owner-change.factory'; +import { N_ } from '../i18n'; export default angular.module('credentials', []) - .directive('ownerList', ownerList); + .directive('ownerList', ownerList) + .factory('BecomeMethodChange', BecomeMethodChange) + .factory('CredentialFormSave', CredentialFormSave) + .factory('KindChange', KindChange) + .factory('OwnerChange', OwnerChange) + .controller('CredentialsList', CredentialsList) + .controller('CredentialsAdd', CredentialsAdd) + .controller('CredentialsEdit', CredentialsEdit) + .config(['$stateProvider', 'stateDefinitionsProvider', + function($stateProvider, stateDefinitionsProvider) { + let stateDefinitions = stateDefinitionsProvider.$get(); + + // lazily generate a tree of substates which will replace this node in ui-router's stateRegistry + // see: stateDefinition.factory for usage documentation + $stateProvider.state({ + name: 'credentials', + url: '/credentials', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'credentials', + modes: ['add', 'edit'], + list: 'CredentialList', + form: 'CredentialForm', + controllers: { + list: CredentialsList, + add: CredentialsAdd, + edit: CredentialsEdit + }, + data: { + activityStream: true, + activityStreamTarget: 'credential' + }, + ncyBreadcrumb: { + parent: 'setup', + label: N_('CREDENTIALS') + } + }) + }); + } + ]); diff --git a/awx/ui/client/src/filters.js b/awx/ui/client/src/filters.js deleted file mode 100644 index e6549bb779..0000000000 --- a/awx/ui/client/src/filters.js +++ /dev/null @@ -1,14 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import sanitizeFilter from './shared/xss-sanitizer.filter'; -import capitalizeFilter from './shared/capitalize.filter'; -import longDateFilter from './shared/long-date.filter'; -export { - sanitizeFilter, - capitalizeFilter, - longDateFilter -}; diff --git a/awx/ui/client/src/helpers.js b/awx/ui/client/src/helpers.js deleted file mode 100644 index b22938443d..0000000000 --- a/awx/ui/client/src/helpers.js +++ /dev/null @@ -1,60 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import './forms'; -import './lists'; - -import Children from "./helpers/Children"; -import Credentials from "./helpers/Credentials"; -import Events from "./helpers/Events"; -import Groups from "./helpers/Groups"; -import Hosts from "./helpers/Hosts"; -import JobDetail from "./helpers/JobDetail"; -import JobSubmission from "./helpers/JobSubmission"; -import JobTemplates from "./helpers/JobTemplates"; -import Jobs from "./helpers/Jobs"; -import LoadConfig from "./helpers/LoadConfig"; -import Parse from "./helpers/Parse"; -import ProjectPath from "./helpers/ProjectPath"; -import Projects from "./helpers/Projects"; -import Schedules from "./helpers/Schedules"; -import Selection from "./helpers/Selection"; -import Users from "./helpers/Users"; -import Variables from "./helpers/Variables"; -import ApiDefaults from "./helpers/api-defaults"; -import inventory from "./helpers/inventory"; -import MD5 from "./helpers/md5"; -import Teams from "./helpers/teams"; -import AdhocHelper from "./helpers/Adhoc"; -import ApiModelHelper from "./helpers/ApiModel"; -import ActivityStreamHelper from "./helpers/ActivityStream"; - -export - { Children, - Credentials, - Events, - Groups, - Hosts, - JobDetail, - JobSubmission, - JobTemplates, - Jobs, - LoadConfig, - Parse, - ProjectPath, - Projects, - Schedules, - Selection, - Users, - Variables, - ApiDefaults, - inventory, - MD5, - Teams, - AdhocHelper, - ApiModelHelper, - ActivityStreamHelper - }; diff --git a/awx/ui/client/src/helpers/ActivityStream.js b/awx/ui/client/src/helpers/ActivityStream.js deleted file mode 100644 index 02bd972eee..0000000000 --- a/awx/ui/client/src/helpers/ActivityStream.js +++ /dev/null @@ -1,64 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:ActivityStream - * @description Helper functions for the activity stream -*/ - -export default - angular.module('ActivityStreamHelper', ['Utilities']) - .factory('GetTargetTitle', ['i18n', - function (i18n) { - return function (target) { - - var rtnTitle = i18n._('ALL ACTIVITY'); - - switch(target) { - case 'project': - rtnTitle = i18n._('PROJECTS'); - break; - case 'inventory': - rtnTitle = i18n._('INVENTORIES'); - break; - case 'credential': - rtnTitle = i18n._('CREDENTIALS'); - break; - case 'user': - rtnTitle = i18n._('USERS'); - break; - case 'team': - rtnTitle = i18n._('TEAMS'); - break; - case 'notification_template': - rtnTitle = i18n._('NOTIFICATION TEMPLATES'); - break; - case 'organization': - rtnTitle = i18n._('ORGANIZATIONS'); - break; - case 'job': - rtnTitle = i18n._('JOBS'); - break; - case 'custom_inventory_script': - rtnTitle = i18n._('INVENTORY SCRIPTS'); - break; - case 'schedule': - rtnTitle = i18n._('SCHEDULES'); - break; - case 'host': - rtnTitle = i18n._('HOSTS'); - break; - case 'template': - rtnTitle = i18n._('TEMPLATES'); - break; - } - - return rtnTitle; - - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Adhoc.js b/awx/ui/client/src/helpers/Adhoc.js deleted file mode 100644 index d29e591ce5..0000000000 --- a/awx/ui/client/src/helpers/Adhoc.js +++ /dev/null @@ -1,172 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name helpers.function:Adhoc - * @description These routines are shared by adhoc command related controllers. - * The content here is very similar to the JobSubmission helper, and in fact, - * certain services are pulled from that helper. This leads to an important - * point: if you need to create functionality that is shared between the command - * and playbook run process, put that code in the JobSubmission helper and make - * it into a reusable step (by specifying a callback parameter in the factory). - * For a good example of this, please see how the AdhocLaunch factory in this - * file utilizes the CheckPasswords factory from the JobSubmission helper. - * - * #AdhocRelaunch Step 1: preparing the GET to ad_hoc_commands/n/relaunch - * The adhoc relaunch process is called from the JobSubmission helper. It is a - * separate process from the initial adhoc run becuase of the way the API - * endpoints work. For AdhocRelaunch, we have access to the original run and - * we can pull the related relaunch URL by knowing the original Adhoc runs ID. - * - * #AdhocRelaunch Step 2: If we got passwords back, add them - * The relaunch URL gives us back the passwords we need to prompt for (if any). - * We'll go to step 3 if there are passwords, and step 4 if not. - * - * #AdhocRelaunch Step 3: PromptForPasswords and the CreateLaunchDialog - * - * #AdhocRelaunch Step 5: StartAdhocRun - * - * #AdhocRelaunch Step 6: LaunchJob and navigate to the standard out page. - - * **If you are - * TODO: once the API endpoint is figured out for running an adhoc command - * from the form is figured out, the rest work should probably be excised from - * the controller and moved into here. See the todo statements in the - * controller for more information about this. - */ - -export default - angular.module('AdhocHelper', ['RestServices', 'Utilities', - 'CredentialFormDefinition', 'CredentialsListDefinition', - 'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog', - 'FormGenerator', 'JobVarsPromptFormDefinition']) - - /** - * @ngdoc method - * @name helpers.function:JobSubmission#AdhocRun - * @methodOf helpers.function:JobSubmission - * @description The adhoc Run function is run when the user clicks the relaunch button - * - */ - // Submit request to run an adhoc comamand - .factory('AdhocRun', ['$location','$stateParams', 'LaunchJob', - 'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors', - 'Wait', 'Empty', 'CreateLaunchDialog', '$state', - function ($location, $stateParams, LaunchJob, PromptForPasswords, - Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, CreateLaunchDialog, $state) { - return function (params) { - var id = params.project_id, - scope = params.scope.$new(), - new_job_id, - html, - url; - - // this is used to cancel a running adhoc command from - // the jobs page - if (scope.removeCancelJob) { - scope.removeCancelJob(); - } - scope.removeCancelJob = scope.$on('CancelJob', function() { - // Delete the job - Wait('start'); - Rest.setUrl(GetBasePath('ad_hoc_commands') + new_job_id + '/'); - Rest.destroy() - .success(function() { - Wait('stop'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, - null, { hdr: 'Error!', - msg: 'Call to ' + url + - ' failed. DELETE returned status: ' + - status }); - }); - }); - - if (scope.removeStartAdhocRun) { - scope.removeStartAdhocRun(); - } - - scope.removeStartAdhocRun = scope.$on('StartAdhocRun', function() { - var password, - postData={}; - for (password in scope.passwords) { - postData[scope.passwords[password]] = scope[ - scope.passwords[password] - ]; - } - // Re-launch the adhoc job - Rest.setUrl(url); - Rest.post(postData) - .success(function (data) { - Wait('stop'); - if($location.path().replace(/^\//, '').split('/')[0] !== 'jobs') { - $state.go('adHocJobStdout', {id: data.id}); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, { - hdr: 'Error!', - msg: 'Failed to launch adhoc command. POST ' + - 'returned status: ' + status }); - }); - }); - - // start routine only if passwords need to be prompted - if (scope.removeCreateLaunchDialog) { - scope.removeCreateLaunchDialog(); - } - scope.removeCreateLaunchDialog = scope.$on('CreateLaunchDialog', - function(e, html, url) { - CreateLaunchDialog({ - scope: scope, - html: html, - url: url, - callback: 'StartAdhocRun' - }); - }); - - if (scope.removePromptForPasswords) { - scope.removePromptForPasswords(); - } - scope.removePromptForPasswords = scope.$on('PromptForPasswords', - function(e, passwords_needed_to_start,html, url) { - PromptForPasswords({ - scope: scope, - passwords: passwords_needed_to_start, - callback: 'CreateLaunchDialog', - html: html, - url: url - }); - }); // end password prompting routine - - // start the adhoc relaunch routine - Wait('start'); - url = GetBasePath('ad_hoc_commands') + id + '/relaunch/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - new_job_id = data.id; - - scope.passwords_needed_to_start = data.passwords_needed_to_start; - if (!Empty(data.passwords_needed_to_start) && - data.passwords_needed_to_start.length > 0) { - // go through the password prompt routine before - // starting the adhoc run - scope.$emit('PromptForPasswords', data.passwords_needed_to_start, html, url); - } - else { - // no prompting of passwords needed - scope.$emit('StartAdhocRun'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get job template details. GET returned status: ' + status }); - }); - }; - }]); diff --git a/awx/ui/client/src/helpers/ApiModel.js b/awx/ui/client/src/helpers/ApiModel.js deleted file mode 100644 index 20bed707f2..0000000000 --- a/awx/ui/client/src/helpers/ApiModel.js +++ /dev/null @@ -1,63 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:ApiModel - * @description Helper functions to convert singular/plural versions of our models to the opposite -*/ - -export default - angular.module('ApiModelHelper', ['Utilities']) - .factory('ModelToBasePathKey', [ - function () { - return function (model) { - // This function takes in the singular model string and returns the key needed - // to get the base path from $rootScope/local storage. - - var basePathKey; - - switch(model) { - case 'project': - basePathKey = 'projects'; - break; - case 'inventory': - basePathKey = 'inventory'; - break; - case 'job_template': - basePathKey = 'job_templates'; - break; - case 'credential': - basePathKey = 'credentials'; - break; - case 'user': - basePathKey = 'users'; - break; - case 'team': - basePathKey = 'teams'; - break; - case 'notification_template': - basePathKey = 'notification_templates'; - break; - case 'organization': - basePathKey = 'organizations'; - break; - case 'management_job': - basePathKey = 'management_jobs'; - break; - case 'custom_inventory_script': - basePathKey = 'inventory_scripts'; - break; - case 'workflow_job_template': - basePathKey = 'workflow_job_templates'; - break; - } - - return basePathKey; - - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Children.js b/awx/ui/client/src/helpers/Children.js deleted file mode 100644 index 7c8246dd62..0000000000 --- a/awx/ui/client/src/helpers/Children.js +++ /dev/null @@ -1,111 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Children - * @descriptionUsed in job_events to expand/collapse children by setting the - * 'show' attribute of each job_event in the set of job_events. - * See the filter in job_events.js list. -*/ - -export default - angular.module('ChildrenHelper', ['RestServices', 'Utilities']) - .factory('ToggleChildren', ['$location', 'Store', function ($location, Store) { - return function (params) { - - var scope = params.scope, - list = params.list, - id = params.id, - set = scope[list.name], - clicked, - //base = $location.path().replace(/^\//, '').split('/')[0], - path = $location.path(), - local_child_store; - - function updateExpand(key, expand) { - var found = false; - local_child_store.every(function(child, i) { - if (child.key === key) { - local_child_store[i].expand = expand; - found = true; - return false; - } - return true; - }); - if (!found) { - local_child_store.push({ key: key, expand: expand }); - } - } - - function updateShow(key, show) { - var found = false; - local_child_store.every(function(child, i) { - if (child.key === key) { - local_child_store[i].show = show; - found = true; - return false; - } - return true; - }); - if (!found) { - local_child_store.push({ key: key, show: show }); - } - } - - function expand(node) { - var i, has_children = false; - for (i = node + 1; i < set.length; i++) { - if (set[i].parent === set[node].id) { - updateShow(set[i].key, true); - set[i].show = true; - } - } - set[node].ngicon = (has_children) ? 'fa fa-minus-square-o node-toggle' : 'fa fa-minus-square-o node-toggle'; - } - - function collapse(node) { - var i, has_children = false; - for (i = node + 1; i < set.length; i++) { - if (set[i].parent === set[node].id) { - set[i].show = false; - has_children = true; - updateShow(set[i].key, false); - if (set[i].related.children) { - collapse(i); - } - } - } - set[node].ngicon = (has_children) ? 'fa fa-plus-square-o node-toggle' : 'fa fa-square-o node-toggle'; - } - - local_child_store = Store(path + '_children'); - if (!local_child_store) { - local_child_store = []; - } - - // Scan the array list and find the clicked element - set.every(function(row, i) { - if (row.id === id) { - clicked = i; - return false; - } - return true; - }); - - // Expand or collapse children based on clicked element's icon - if (/plus-square-o/.test(set[clicked].ngicon)) { - // Expand: lookup and display children - expand(clicked); - updateExpand(set[clicked].key, true); - } else if (/minus-square-o/.test(set[clicked].ngicon)) { - collapse(clicked); - updateExpand(set[clicked].key, false); - } - Store(path + '_children', local_child_store); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Credentials.js b/awx/ui/client/src/helpers/Credentials.js deleted file mode 100644 index 007756d17c..0000000000 --- a/awx/ui/client/src/helpers/Credentials.js +++ /dev/null @@ -1,441 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name helpers.function:Credentials - * @description Functions shared amongst Credential related controllers - */ - -export default -angular.module('CredentialsHelper', ['Utilities']) - -.factory('KindChange', ['Empty', 'i18n', - function (Empty, i18n) { - return function (params) { - var scope = params.scope, - reset = params.reset, - collapse, id; - - $('.popover').each(function() { - // remove lingering popover
. Seems to be a bug in TB3 RC1 - $(this).remove(); - }); - $('.tooltip').each( function() { - // close any lingering tool tipss - $(this).hide(); - }); - // Put things in a default state - scope.usernameLabel = i18n._('Username'); - scope.aws_required = false; - scope.email_required = false; - scope.rackspace_required = false; - scope.sshKeyDataLabel = i18n._('Private Key'); - scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE) - scope.key_required = false; // JT -- doing the same for key and project - scope.project_required = false; - scope.subscription_required = false; - scope.key_description = i18n.sprintf(i18n._("Paste the contents of the SSH private key file.%s or click to close%s"), "
Esc", "
"); - scope.host_required = false; - scope.password_required = false; - scope.hostLabel = ''; - scope.passwordLabel = i18n._('Password'); - - $('.popover').each(function() { - // remove lingering popover
. Seems to be a bug in TB3 RC1 - $(this).remove(); - }); - $('.tooltip').each( function() { - // close any lingering tool tipss - $(this).hide(); - }); - // Put things in a default state - scope.usernameLabel = i18n._('Username'); - scope.aws_required = false; - scope.email_required = false; - scope.rackspace_required = false; - scope.sshKeyDataLabel = i18n._('Private Key'); - scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE) - scope.key_required = false; // JT -- doing the same for key and project - scope.project_required = false; - scope.domain_required = false; - scope.subscription_required = false; - scope.key_description = i18n._("Paste the contents of the SSH private key file."); - scope.host_required = false; - scope.password_required = false; - scope.hostLabel = ''; - scope.projectLabel = ''; - scope.domainLabel = ''; - scope.project_required = false; - scope.passwordLabel = i18n._('Password (API Key)'); - scope.projectPopOver = "

" + i18n._("The project value") + "

"; - scope.hostPopOver = "

" + i18n._("The host value") + "

"; - scope.ssh_key_data_api_error = ''; - - if (!Empty(scope.kind)) { - // Apply kind specific settings - switch (scope.kind.value) { - case 'aws': - scope.aws_required = true; - break; - case 'rax': - scope.rackspace_required = true; - scope.username_required = true; - break; - case 'ssh': - scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' - scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); - scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); - break; - case 'scm': - scope.sshKeyDataLabel = i18n._('SCM Private Key'); - scope.passwordLabel = i18n._('Password'); - break; - case 'gce': - scope.usernameLabel = i18n._('Service Account Email Address'); - scope.sshKeyDataLabel = i18n._('RSA Private Key'); - scope.email_required = true; - scope.key_required = true; - scope.project_required = true; - scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); - scope.projectLabel = i18n._("Project"); - scope.project_required = false; - scope.projectPopOver = "

" + i18n._("The Project ID is the " + - "GCE assigned identification. It is constructed as " + - "two words followed by a three digit number. Such " + - "as: ") + "

adjective-noun-000

"; - break; - case 'azure': - scope.sshKeyDataLabel = i18n._('Management Certificate'); - scope.subscription_required = true; - scope.key_required = true; - scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); - break; - case 'azure_rm': - scope.usernameLabel = i18n._("Username"); - scope.subscription_required = true; - scope.passwordLabel = i18n._('Password'); - scope.azure_rm_required = true; - break; - case 'vmware': - scope.username_required = true; - scope.host_required = true; - scope.password_required = true; - scope.hostLabel = "vCenter Host"; - scope.passwordLabel = i18n._('Password'); - scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); - break; - case 'openstack': - scope.hostLabel = i18n._("Host (Authentication URL)"); - scope.projectLabel = i18n._("Project (Tenant Name)"); - scope.domainLabel = i18n._("Domain Name"); - scope.password_required = true; - scope.project_required = true; - scope.host_required = true; - scope.username_required = true; - scope.projectPopOver = "

" + i18n._("This is the tenant name. " + - " This value is usually the same " + - " as the username.") + "

"; - scope.hostPopOver = "

" + i18n._("The host to authenticate with.") + - "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/"); - break; - case 'satellite6': - scope.username_required = true; - scope.password_required = true; - scope.passwordLabel = i18n._('Password'); - scope.host_required = true; - scope.hostLabel = i18n._("Satellite 6 URL"); - scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" + - "Red Hat Satellite 6 server. %s" + - "For example, %s"), "
", "
", "https://satellite.example.org"); - break; - case 'cloudforms': - scope.username_required = true; - scope.password_required = true; - scope.passwordLabel = i18n._('Password'); - scope.host_required = true; - scope.hostLabel = i18n._("CloudForms URL"); - scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" + - "corresponds to your CloudForm instance. %s" + - "For example, %s"), "
", "
", "https://cloudforms.example.org"); - break; - case 'net': - scope.username_required = true; - scope.password_required = false; - scope.passwordLabel = i18n._('Password'); - scope.sshKeyDataLabel = i18n._('SSH Key'); - break; - } - } - - // Reset all the field values related to Kind. - if (reset) { - scope.access_key = null; - scope.secret_key = null; - scope.api_key = null; - scope.username = null; - scope.password = null; - scope.password_confirm = null; - scope.ssh_key_data = null; - scope.ssh_key_unlock = null; - scope.ssh_key_unlock_confirm = null; - scope.become_username = null; - scope.become_password = null; - scope.authorize = false; - scope.authorize_password = null; - } - - // Collapse or open help widget based on whether scm value is selected - collapse = $('#credential_kind').parent().find('.panel-collapse').first(); - id = collapse.attr('id'); - if (!Empty(scope.kind) && scope.kind.value !== '') { - if ($('#' + id + '-icon').hasClass('icon-minus')) { - scope.accordionToggle('#' + id); - } - } else { - if ($('#' + id + '-icon').hasClass('icon-plus')) { - scope.accordionToggle('#' + id); - } - } - - }; - } -]) - -.factory('BecomeMethodChange', ['Empty', 'i18n', - function (Empty, i18n) { - return function (params) { - console.log('become method has changed'); - var scope = params.scope; - - if (!Empty(scope.kind)) { - // Apply kind specific settings - switch (scope.kind.value) { - case 'aws': - scope.aws_required = true; - break; - case 'rax': - scope.rackspace_required = true; - scope.username_required = true; - break; - case 'ssh': - scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' - scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); - scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); - break; - case 'scm': - scope.sshKeyDataLabel = i18n._('SCM Private Key'); - scope.passwordLabel = i18n._('Password'); - break; - case 'gce': - scope.usernameLabel = i18n._('Service Account Email Address'); - scope.sshKeyDataLabel = i18n._('RSA Private Key'); - scope.email_required = true; - scope.key_required = true; - scope.project_required = true; - scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); - scope.projectLabel = i18n._("Project"); - scope.project_required = false; - scope.projectPopOver = "

" + i18n._("The Project ID is the " + - "GCE assigned identification. It is constructed as " + - "two words followed by a three digit number. Such " + - "as: ") + "

adjective-noun-000

"; - break; - case 'azure': - scope.sshKeyDataLabel = i18n._('Management Certificate'); - scope.subscription_required = true; - scope.key_required = true; - scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); - break; - case 'azure_rm': - scope.usernameLabel = i18n._("Username"); - scope.subscription_required = true; - scope.passwordLabel = i18n._('Password'); - scope.azure_rm_required = true; - break; - case 'vmware': - scope.username_required = true; - scope.host_required = true; - scope.password_required = true; - scope.hostLabel = "vCenter Host"; - scope.passwordLabel = i18n._('Password'); - scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); - break; - case 'openstack': - scope.hostLabel = i18n._("Host (Authentication URL)"); - scope.projectLabel = i18n._("Project (Tenant Name)"); - scope.domainLabel = i18n._("Domain Name"); - scope.password_required = true; - scope.project_required = true; - scope.host_required = true; - scope.username_required = true; - scope.projectPopOver = "

" + i18n._("This is the tenant name. " + - " This value is usually the same " + - " as the username.") + "

"; - scope.hostPopOver = "

" + i18n._("The host to authenticate with.") + - "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/"); - break; - case 'satellite6': - scope.username_required = true; - scope.password_required = true; - scope.passwordLabel = i18n._('Password'); - scope.host_required = true; - scope.hostLabel = i18n._("Satellite 6 URL"); - scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" + - "Red Hat Satellite 6 server. %s" + - "For example, %s"), "
", "
", "https://satellite.example.org"); - break; - case 'cloudforms': - scope.username_required = true; - scope.password_required = true; - scope.passwordLabel = i18n._('Password'); - scope.host_required = true; - scope.hostLabel = i18n._("CloudForms URL"); - scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" + - "corresponds to your CloudForm instance. %s" + - "For example, %s"), "
", "
", "https://cloudforms.example.org"); - break; - case 'net': - scope.username_required = true; - scope.password_required = false; - scope.passwordLabel = i18n._('Password'); - scope.sshKeyDataLabel = i18n._('SSH Key'); - break; - } - } - }; - } -]) - - -.factory('OwnerChange', [ - function () { - return function (params) { - var scope = params.scope, - owner = scope.owner; - if (owner === 'team') { - scope.team_required = true; - scope.user_required = false; - scope.user = null; - scope.user_username = null; - } else { - scope.team_required = false; - scope.user_required = true; - scope.team = null; - scope.team_name = null; - } - }; -} -]) - -.factory('FormSave', ['$rootScope', '$location', 'Alert', 'Rest', 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', 'ReturnToCaller', 'Wait', '$state', 'i18n', - function ($rootScope, $location, Alert, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait, $state, i18n) { - return function (params) { - var scope = params.scope, - mode = params.mode, - form = CredentialForm, - data = {}, fld, url; - - for (fld in form.fields) { - if (fld !== 'access_key' && fld !== 'secret_key' && fld !== 'ssh_username' && - fld !== 'ssh_password') { - if (fld === "organization" && !scope[fld]) { - data.user = $rootScope.current_user.id; - } else if (scope[fld] === null) { - data[fld] = ""; - } else { - data[fld] = scope[fld]; - } - } - } - - data.kind = scope.kind.value; - if (scope.become_method === null || typeof scope.become_method === 'undefined') { - data.become_method = ""; - data.become_username = ""; - data.become_password = ""; - } else { - data.become_method = (scope.become_method.value) ? scope.become_method.value : ""; - } - switch (data.kind) { - case 'ssh': - data.password = scope.ssh_password; - break; - case 'aws': - data.username = scope.access_key; - data.password = scope.secret_key; - break; - case 'rax': - data.password = scope.api_key; - break; - case 'gce': - data.username = scope.email_address; - data.project = scope.project; - break; - case 'azure': - data.username = scope.subscription; - } - - Wait('start'); - if (mode === 'add') { - url = GetBasePath("credentials"); - Rest.setUrl(url); - Rest.post(data) - .success(function (data) { - scope.addedItem = data.id; - - // @issue: OLD SEARCH - // Refresh({ - // scope: scope, - // set: 'credentials', - // iterator: 'credential', - // url: url - // }); - - Wait('stop'); - var base = $location.path().replace(/^\//, '').split('/')[0]; - if (base === 'credentials') { - $state.go('credentials.edit', {credential_id: data.id}, {reload: true}); - } - else { - ReturnToCaller(1); - } - }) - .error(function (data, status) { - Wait('stop'); - // TODO: hopefully this conditional error handling will to away in a future version of tower. The reason why we cannot - // simply pass this error to ProcessErrors is because it will actually match the form element 'ssh_key_unlock' and show - // the error there. The ssh_key_unlock field is not shown when the kind of credential is gce/azure and as a result the - // error is never shown. In the future, the API will hopefully either behave or respond differently. - if(status && status === 400 && data && data.ssh_key_unlock && (scope.kind.value === 'gce' || scope.kind.value === 'azure')) { - scope.ssh_key_data_api_error = i18n._("Encrypted credentials are not supported."); - } - else { - ProcessErrors(scope, data, status, form, { - hdr: i18n._('Error!'), - msg: i18n._('Failed to create new Credential. POST status: ') + status - }); - } - }); - } else { - url = GetBasePath('credentials') + scope.id + '/'; - Rest.setUrl(url); - Rest.put(data) - .success(function () { - Wait('stop'); - $state.go($state.current, {}, {reload: true}); - }) - .error(function (data, status) { - Wait('stop'); - ProcessErrors(scope, data, status, form, { - hdr: i18n._('Error!'), - msg: i18n._('Failed to update Credential. PUT status: ') + status - }); - }); - } - }; - } -]); diff --git a/awx/ui/client/src/helpers/Events.js b/awx/ui/client/src/helpers/Events.js deleted file mode 100644 index be58e72661..0000000000 --- a/awx/ui/client/src/helpers/Events.js +++ /dev/null @@ -1,237 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Events - * @description EventView - show the job_events form in a modal dialog -*/ - -export default - angular.module('EventsHelper', ['RestServices', 'Utilities', 'JobEventDataDefinition', 'JobEventsFormDefinition']) - - .factory('EventView', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'JobEventDataForm', 'Empty', 'JobEventsForm', - function ($rootScope, $location, $log, $stateParams, Rest, Alert, GenerateForm, Prompt, ProcessErrors, GetBasePath, - FormatDate, JobEventDataForm, Empty, JobEventsForm) { - return function (params) { - - var event_id = params.event_id, - generator = GenerateForm, - form = angular.copy(JobEventsForm), - scope, - defaultUrl = GetBasePath('base') + 'job_events/' + event_id + '/'; - - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl); - Rest.get() - .success(function (data) { - var i, n, fld, rows, txt, cDate; - - // If event_data is not available, remove fields that depend on it - if ($.isEmptyObject(data.event_data) || !data.event_data.res || typeof data.event_data.res === 'string') { - for (fld in form.fields) { - switch (fld) { - case 'start': - case 'end': - case 'delta': - case 'msg': - case 'stdout': - case 'stderr': - case 'msg': - case 'results': - case 'module_name': - case 'module_args': - case 'rc': - delete form.fields[fld]; - break; - } - } - } - - if ($.isEmptyObject(data.event_data) || !data.event_data.res || typeof data.event_data.res !== 'string') { - delete form.fields.traceback; - } - - // Remove remaining form fields that do not have a corresponding data value - for (fld in form.fields) { - switch (fld) { - case 'start': - case 'end': - case 'delta': - case 'msg': - case 'stdout': - case 'stderr': - case 'msg': - case 'rc': - if (data.event_data && data.event_data.res && Empty(data.event_data.res[fld])) { - delete form.fields[fld]; - } else { - if (form.fields[fld].type === 'textarea') { - n = data.event_data.res[fld].match(/\n/g); - rows = (n) ? n.length : 1; - rows = (rows > 10) ? 10 : rows; - rows = (rows < 3) ? 3 : rows; - form.fields[fld].rows = rows; - } - } - break; - case 'results': - if (data.event_data && data.event_data.res && data.event_data.res[fld] === undefined) { - // not defined - delete form.fields[fld]; - } else if (!Array.isArray(data.event_data.res[fld]) || data.event_data.res[fld].length === 0) { - // defined, but empty - delete form.fields[fld]; - } else { - // defined and not empty, so attempt to size the textarea field - txt = ''; - for (i = 0; i < data.event_data.res[fld].length; i++) { - txt += data.event_data.res[fld][i]; - } - if (txt === '') { - // there's an array, but the actual text is empty - delete form.fields[fld]; - } else { - n = txt.match(/\n/g); - rows = (n) ? n.length : 1; - rows = (rows > 10) ? 10 : rows; - rows = (rows < 3) ? 3 : rows; - form.fields[fld].rows = rows; - } - } - break; - case 'module_name': - case 'module_args': - if (data.event_data && data.event_data.res) { - if (data.event_data.res.invocation === undefined || - data.event_data.res.invocation[fld] === undefined) { - delete form.fields[fld]; - } - } - break; - } - } - - // load the form - scope = generator.inject(form, { - mode: 'edit', - modal: true, - related: false - }); - generator.reset(); - scope.formModalAction = function () { - $('#form-modal').modal("hide"); - }; - scope.formModalActionLabel = 'OK'; - scope.formModalCancelShow = false; - scope.formModalInfo = 'View JSON'; - $('#form-modal .btn-success').removeClass('btn-success').addClass('btn-none'); - $('#form-modal').addClass('skinny-modal'); - scope.formModalHeader = data.event_display.replace(/^\u00a0*/g, ''); - - // Respond to View JSON button - scope.formModalInfoAction = function () { - var generator = GenerateForm, - scope = generator.inject(JobEventDataForm, { - mode: 'edit', - modal: true, - related: false, - modal_selector: '#form-modal2', - modal_body_id: 'form-modal2-body', - modal_title_id: 'formModal2Header' - }); - generator.reset(); - scope.formModal2Header = data.event_display.replace(/^\u00a0*/g, ''); - scope.event_data = JSON.stringify(data.event_data, null, '\t'); - scope.formModal2ActionLabel = 'OK'; - scope.formModal2CancelShow = false; - scope.formModal2Info = false; - scope.formModalInfo = 'View JSON'; - scope.formModal2Action = function () { - $('#form-modal2').modal("hide"); - }; - $('#form-modal2 .btn-success').removeClass('btn-success').addClass('btn-none'); - }; - - if (typeof data.event_data.res === 'string') { - scope.traceback = data.event_data.res; - } - - for (fld in form.fields) { - switch (fld) { - case 'status': - if (data.failed) { - scope.status = 'error'; - } else if (data.changed) { - scope.status = 'changed'; - } else { - scope.status = 'success'; - } - break; - case 'created': - cDate = new Date(data.created); - scope.created = FormatDate(cDate); - break; - case 'host': - if (data.summary_fields && data.summary_fields.host) { - scope.host = data.summary_fields.host.name; - } - break; - case 'id': - case 'task': - case 'play': - scope[fld] = data[fld]; - break; - case 'start': - case 'end': - if (data.event_data && data.event_data.res && !Empty(data.event_data.res[fld])) { - scope[fld] = data.event_data.res[fld]; - } - - break; - case 'results': - if (Array.isArray(data.event_data.res[fld]) && data.event_data.res[fld].length > 0) { - txt = ''; - for (i = 0; i < data.event_data.res[fld].length; i++) { - txt += data.event_data.res[fld][i]; - } - if (txt !== '') { - scope[fld] = txt; - } - } - break; - case 'msg': - case 'stdout': - case 'stderr': - case 'delta': - case 'rc': - if (data.event_data && data.event_data.res && data.event_data.res[fld] !== undefined) { - scope[fld] = data.event_data.res[fld]; - } - break; - case 'module_name': - case 'module_args': - if (data.event_data.res && data.event_data.res.invocation) { - scope[fld] = data.event_data.res.invocation[fld]; - } - break; - } - } - - if (!scope.$$phase) { - scope.$digest(); - } - - }) - .error(function (data, status) { - $('#form-modal').modal("hide"); - ProcessErrors(scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to retrieve event: ' + event_id + '. GET status: ' + status }); - }); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Groups.js b/awx/ui/client/src/helpers/Groups.js deleted file mode 100644 index 8e7dcfc7ad..0000000000 --- a/awx/ui/client/src/helpers/Groups.js +++ /dev/null @@ -1,1047 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -'use strict'; - -/** - * @ngdoc function - * @name helpers.function:Groups - * @description inventory tree widget add/edit/delete -*/ - -import listGenerator from '../shared/list-generator/main'; - -export default -angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name, 'GroupListDefinition', listGenerator.name, 'GroupsHelper', 'InventoryHelper', 'SelectionHelper', - 'JobSubmissionHelper', 'PromptDialog', 'CredentialsListDefinition', - 'InventoryStatusDefinition', 'VariablesHelper', 'SchedulesListDefinition', 'StandardOutHelper', - 'SchedulesHelper' -]) - -/** - * - * Lookup options for group source and build an array of drop-down choices - * - */ -.factory('GetSourceTypeOptions', ['Rest', 'ProcessErrors', 'GetBasePath', - function (Rest, ProcessErrors, GetBasePath) { - return function (params) { - var scope = params.scope, - variable = params.variable; - - if (scope[variable] === undefined) { - scope[variable] = []; - Rest.setUrl(GetBasePath('inventory_sources')); - Rest.options() - .success(function (data) { - var i, choices = data.actions.GET.source.choices; - for (i = 0; i < choices.length; i++) { - if (choices[i][0] !== 'file') { - scope[variable].push({ - label: choices[i][1], - value: choices[i][0] - }); - } - } - scope.cloudCredentialRequired = false; - scope.$emit('sourceTypeOptionsReady'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status - }); - }); - } - }; - } -]) - -/** - * - * TODO: Document - * - */ -.factory('ViewUpdateStatus', ['$state', 'Rest', 'ProcessErrors', 'GetBasePath', 'Alert', 'Wait', 'Empty', 'Find', - function ($state, Rest, ProcessErrors, GetBasePath, Alert, Wait, Empty, Find) { - return function (params) { - - var scope = params.scope, - group_id = params.group_id, - group = Find({ list: scope.groups, key: 'id', val: group_id }); - - if (scope.removeSourceReady) { - scope.removeSourceReady(); - } - scope.removeSourceReady = scope.$on('SourceReady', function(e, source) { - - // Get the ID from the correct summary field - var update_id = (source.summary_fields.current_update) ? source.summary_fields.current_update.id : source.summary_fields.last_update.id; - - $state.go('inventorySyncStdout', {id: update_id}); - - }); - - if (group) { - if (Empty(group.source)) { - // do nothing - } else if (Empty(group.status) || group.status === "never updated") { - Alert('No Status Available', '

An inventory sync has not been performed for the selected group. Start the process by ' + - 'clicking the button.
', 'alert-info', null, null, null, null, true); - } else { - Wait('start'); - Rest.setUrl(group.related.inventory_source); - Rest.get() - .success(function (data) { - scope.$emit('SourceReady', data); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + - ' GET returned status: ' + status }); - }); - } - } - - }; - } -]) - -/** - * - * TODO: Document - * - */ -.factory('GetHostsStatusMsg', [ - function () { - return function (params) { - - var active_failures = params.active_failures, - total_hosts = params.total_hosts, - tip, failures, html_class; - - // Return values for use on host status indicator - - if (active_failures > 0) { - tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; - html_class = 'error'; - failures = true; - } else { - failures = false; - if (total_hosts === 0) { - // no hosts - tip = "Contains 0 hosts."; - html_class = 'none'; - } else { - // many hosts with 0 failures - tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; - html_class = 'success'; - } - } - - return { - tooltip: tip, - failures: failures, - 'class': html_class - }; - }; -} -]) - -/** - * - * TODO: Document - * - */ -.factory('GetSyncStatusMsg', [ 'Empty', - function (Empty) { - return function (params) { - - var status = params.status, - source = params.source, - has_inventory_sources = params.has_inventory_sources, - launch_class = '', - launch_tip = 'Start sync process', - schedule_tip = 'Schedule future inventory syncs', - stat, stat_class, status_tip; - - stat = status; - stat_class = stat; - - switch (status) { - case 'never updated': - stat = 'never'; - stat_class = 'na'; - status_tip = 'Sync not performed. Click to start it now.'; - break; - case 'none': - case 'ok': - case '': - launch_class = 'btn-disabled'; - stat = 'n/a'; - stat_class = 'na'; - status_tip = 'Cloud source not configured. Click to update.'; - launch_tip = 'Cloud source not configured.'; - break; - case 'canceled': - status_tip = 'Sync canceled. Click to view log.'; - break; - case 'failed': - status_tip = 'Sync failed. Click to view log.'; - break; - case 'successful': - status_tip = 'Sync completed. Click to view log.'; - break; - case 'pending': - status_tip = 'Sync pending.'; - launch_class = "btn-disabled"; - launch_tip = "Sync pending"; - break; - case 'updating': - case 'running': - launch_class = "btn-disabled"; - launch_tip = "Sync running"; - status_tip = "Sync running. Click to view log."; - break; - } - - if (has_inventory_sources && Empty(source)) { - // parent has a source, therefore this group should not have a source - launch_class = "btn-disabled"; - status_tip = 'Managed by an external cloud source.'; - launch_tip = 'Can only be updated by running a sync on the parent group.'; - } - - if (has_inventory_sources === false && Empty(source)) { - launch_class = 'btn-disabled'; - status_tip = 'Cloud source not configured. Click to update.'; - launch_tip = 'Cloud source not configured.'; - } - - return { - "class": stat_class, - "tooltip": status_tip, - "status": stat, - "launch_class": launch_class, - "launch_tip": launch_tip, - "schedule_tip": schedule_tip - }; - }; - } -]) - -/** - * - * Cancel a pending or running inventory sync - * - */ -.factory('GroupsCancelUpdate', ['Empty', 'Rest', 'ProcessErrors', 'Alert', 'Wait', 'Find', - function (Empty, Rest, ProcessErrors, Alert, Wait, Find) { - return function (params) { - - var scope = params.scope, - id = params.id, - group = params.group; - - if (scope.removeCancelUpdate) { - scope.removeCancelUpdate(); - } - scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { - // Cancel the update process - Rest.setUrl(url); - Rest.post() - .success(function () { - Wait('stop'); - //Alert('Inventory Sync Cancelled', 'Request to cancel the sync process was submitted to the task manger. ' + - // 'Click the button to monitor the status.', 'alert-info'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. POST status: ' + status - }); - }); - }); - - if (scope.removeCheckCancel) { - scope.removeCheckCancel(); - } - scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { - // Check that we have access to cancelling an update - var url = (current_update) ? current_update : last_update; - url += 'cancel/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - if (data.can_cancel) { - scope.$emit('CancelUpdate', url); - //} else { - // Wait('stop'); - // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + - // 'the latest status.', 'alert-info'); - } - else { - Wait('stop'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. GET status: ' + status - }); - }); - }); - - // Cancel the update process - if (Empty(group)) { - group = Find({ list: scope.groups, key: 'id', val: id }); - scope.selected_group_id = group.id; - } - - if (group && (group.status === 'running' || group.status === 'pending')) { - // We found the group, and there is a running update - Wait('start'); - Rest.setUrl(group.related.inventory_source); - Rest.get() - .success(function (data) { - scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status - }); - }); - } - }; - } -]) - - -/** - * - * Deprecated factory that used to support /#/home/groups/ - * - */ -.factory('GroupsEdit', ['$filter', '$rootScope', '$location', '$log', '$stateParams', '$compile', 'Rest', 'Alert', 'GroupForm', 'GenerateForm', - 'Prompt', 'ProcessErrors', 'GetBasePath', 'SetNodeName', 'ParseTypeChange', 'GetSourceTypeOptions', 'InventoryUpdate', - 'Empty', 'Wait', 'GetChoices', 'UpdateGroup', 'SourceChange', 'Find', - 'ParseVariableString', 'ToJSON', 'GroupsScheduleListInit', 'SetSchedulesInnerDialogSize', 'CreateSelect2', - function ($filter, $rootScope, $location, $log, $stateParams, $compile, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors, - GetBasePath, SetNodeName, ParseTypeChange, GetSourceTypeOptions, InventoryUpdate, Empty, Wait, - GetChoices, UpdateGroup, SourceChange, Find, ParseVariableString, ToJSON, GroupsScheduleListInit, - SetSchedulesInnerDialogSize, CreateSelect2) { - return function (params) { - - var parent_scope = params.scope, - group_id = params.group_id, - mode = params.mode, // 'add' or 'edit' - inventory_id = params.inventory_id, - generator = GenerateForm, - group_created = false, - defaultUrl, - master = {}, - choicesReady, - modal_scope = parent_scope.$new(), - properties_scope = parent_scope.$new(), - sources_scope = parent_scope.$new(), - elem, x, y, ww, wh, maxrows, - group, - schedules_url = ''; - - if (mode === 'edit') { - defaultUrl = GetBasePath('groups') + group_id + '/'; - } - else { - defaultUrl = (group_id !== null) ? GetBasePath('groups') + group_id + '/children/' : - GetBasePath('inventory') + inventory_id + '/groups/'; - } - - $('#properties-tab').empty(); - $('#sources-tab').empty(); - $('#schedules-list').empty(); - $('#schedules-form').empty(); - $('#schedules-detail').empty(); - - elem = document.getElementById('group-modal-dialog'); - $compile(elem)(modal_scope); - - var form_scope = - generator.inject(GroupForm, { mode: mode, id: 'properties-tab', related: false, scope: properties_scope }); - var source_form_scope = - generator.inject(GroupForm, { mode: mode, id: 'sources-tab', related: false, scope: sources_scope }); - - //generator.reset(); - - GetSourceTypeOptions({ scope: sources_scope, variable: 'source_type_options' }); - sources_scope.source = GroupForm.fields.source['default']; - sources_scope.sourcePathRequired = false; - sources_scope[GroupForm.fields.source_vars.parseTypeName] = 'yaml'; - sources_scope.update_cache_timeout = 0; - properties_scope.parseType = 'yaml'; - - function waitStop() { Wait('stop'); } - - // Attempt to create the largest textarea field that will fit on the window. Minimum - // height is 6 rows, so on short windows you will see vertical scrolling - function textareaResize(textareaID) { - var textArea, formHeight, model, windowHeight, offset, rows; - textArea = $('#' + textareaID); - if (properties_scope.codeMirror) { - model = textArea.attr('ng-model'); - properties_scope[model] = properties_scope.codeMirror.getValue(); - properties_scope.codeMirror.destroy(); - } - textArea.attr('rows', 1); - formHeight = $('#group_form').height(); - windowHeight = $('#group-modal-dialog').height() - 20; //leave a margin of 20px - offset = Math.floor(windowHeight - formHeight); - rows = Math.floor(offset / 24); - rows = (rows < 6) ? 6 : rows; - textArea.attr('rows', rows); - while(rows > 6 && $('#group_form').height() > $('#group-modal-dialog').height()) { - rows--; - textArea.attr('rows', rows); - } - ParseTypeChange({ scope: properties_scope, field_id: textareaID, onReady: waitStop }); - } - - function initSourceChange() { - parent_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value!=="manual") ? true : false; - SourceChange({ scope: sources_scope, form: GroupForm }); - } - - // Set modal dimensions based on viewport width - ww = $(document).width(); - wh = $('body').height(); - if (ww > 1199) { - // desktop - x = 675; - y = (800 > wh) ? wh - 15 : 800; - maxrows = 18; - } else if (ww <= 1199 && ww >= 768) { - x = 550; - y = (770 > wh) ? wh - 15 : 770; - maxrows = 12; - } else { - x = (ww - 20); - y = (770 > wh) ? wh - 15 : 770; - maxrows = 10; - } - - // Create the modal - $('#group-modal-dialog').dialog({ - buttons: { - 'Save': function () { - modal_scope.saveGroup(); - }, - 'Cancel': function() { - modal_scope.cancelModal(); - } - }, - modal: true, - width: x, - height: y, - autoOpen: false, - minWidth: 440, - title: (mode === 'edit') ? 'Edit Group' : 'Add Group', - closeOnEscape: false, - create: function () { - $('.ui-dialog[aria-describedby="group-modal-dialog"]').find('.ui-dialog-titlebar button').empty().attr({'class': 'close'}).text('x'); - $('.ui-dialog[aria-describedby="group-modal-dialog"]').find('.ui-dialog-buttonset button').each(function () { - var c, h, i, l; - l = $(this).text(); - if (l === 'Cancel') { - h = "fa-times"; - c = "btn btn-default"; - i = "group-close-button"; - $(this).attr({ - 'class': c, - 'id': i - }).html(" Cancel"); - } else if (l === 'Save') { - h = "fa-check"; - c = "btn btn-primary"; - i = "group-save-button"; - $(this).attr({ - 'class': c, - 'id': i - }).html(" Save"); - } - }); - }, - resizeStop: function () { - // for some reason, after resizing dialog the form and fields (the content) doesn't expand to 100% - var dialog = $('.ui-dialog[aria-describedby="group-modal-dialog"]'), - titleHeight = dialog.find('.ui-dialog-titlebar').outerHeight(), - buttonHeight = dialog.find('.ui-dialog-buttonpane').outerHeight(), - content = dialog.find('#group-modal-dialog'), - w; - content.width(dialog.width() - 28); - content.css({ height: (dialog.height() - titleHeight - buttonHeight - 10) }); - - if ($('#group_tabs .active a').text() === 'Properties') { - textareaResize('group_variables', properties_scope); - } - else if ($('#group_tabs .active a').text() === 'Schedule') { - w = $('#group_tabs').width() - 18; - $('#schedules-overlay').width(w); - $('#schedules-form-container').width(w); - SetSchedulesInnerDialogSize(); - } - }, - close: function () { - // Destroy on close - $('.tooltip').each(function () { - // Remove any lingering tooltip
elements - $(this).remove(); - }); - $('.popover').each(function () { - // remove lingering popover
elements - $(this).remove(); - }); - if (properties_scope.codeMirror) { - properties_scope.codeMirror.destroy(); - } - if (sources_scope.codeMirror) { - sources_scope.codeMirror.destroy(); - } - $('#properties-tab').empty(); - $('#sources-tab').empty(); - $('#schedules-list').empty(); - $('#schedules-form').empty(); - $('#schedules-detail').empty(); - $('#group-modal-dialog').hide(); - $('#group-modal-dialog').dialog('destroy'); - modal_scope.cancelModal(); - }, - open: function () { - function updateButtonStatus(isValid) { - $('.ui-dialog[aria-describedby="group-modal-dialog"]').find('.btn-primary').prop('disabled', !isValid); - } - form_scope.$watch('group_form.$valid', updateButtonStatus); - source_form_scope.$watch('source_form.$valid', updateButtonStatus); - $('#group_name').focus(); - Wait('stop'); - } - }); - - $('#group_tabs a[data-toggle="tab"]').on('show.bs.tab', function (e) { - if ($(e.target).text() === 'Properties') { - Wait('start'); - setTimeout(function(){ textareaResize('group_variables'); }, 300); - } - else if ($(e.target).text() === 'Source') { - if (sources_scope.source && (sources_scope.source.value === 'ec2')) { - Wait('start'); - ParseTypeChange({ scope: sources_scope, variable: 'source_vars', parse_variable: GroupForm.fields.source_vars.parseTypeName, - field_id: 'source_source_vars', onReady: waitStop }); - } else if (sources_scope.source && (sources_scope.source.value === 'vmware' || - sources_scope.source.value === 'openstack')) { - Wait('start'); - ParseTypeChange({ scope: sources_scope, variable: 'inventory_variables', parse_variable: GroupForm.fields.inventory_variables.parseTypeName, - field_id: 'source_inventory_variables', onReady: waitStop }); - } - else if (sources_scope.source && (sources_scope.source.value === 'custom')) { - Wait('start'); - ParseTypeChange({ scope: sources_scope, variable: 'extra_vars', parse_variable: GroupForm.fields.extra_vars.parseTypeName, - field_id: 'source_extra_vars', onReady: waitStop }); - } - } - else if ($(e.target).text() === 'Schedule') { - $('#schedules-overlay').hide(); - } - }); - - if (modal_scope.groupVariablesLoadedRemove) { - modal_scope.groupVariablesLoadedRemove(); - } - modal_scope.groupVariablesLoadedRemove = modal_scope.$on('groupVariablesLoaded', function () { - if (mode === 'edit' && - group.has_inventory_sources && - Empty(group.summary_fields.inventory_source.source) && - sources_scope.source && - sources_scope.source.value !== 'manual') { - modal_scope.showSourceTab = false; - } else { - modal_scope.showSourceTab = true; - } - modal_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value!=='manual') ? true : false; - if (mode === 'edit' && modal_scope.showSourceTab) { - // the use has access to the source tab, so they may create a schedule - GroupsScheduleListInit({ scope: modal_scope, url: schedules_url }); - } - $('#group_tabs a:first').tab('show'); - Wait('start'); - $('#group-modal-dialog').dialog('open'); - setTimeout(function() { textareaResize('group_variables', properties_scope); }, 300); - }); - - // JT -- this gets called after the properties & properties variables are loaded, and is emitted from (groupLoaded) - if (modal_scope.removeLoadSourceData) { - modal_scope.removeLoadSourceData(); - } - modal_scope.removeLoadSourceData = modal_scope.$on('LoadSourceData', function () { - if (sources_scope.source_url) { - // get source data - Rest.setUrl(sources_scope.source_url); - Rest.get() - .success(function (data) { - var fld, i, j, flag, found, set, opts, list, form; - form = GroupForm; - for (fld in form.fields) { - if (fld === 'checkbox_group') { - for (i = 0; i < form.fields[fld].fields.length; i++) { - flag = form.fields[fld].fields[i]; - if (data[flag.name] !== undefined) { - sources_scope[flag.name] = data[flag.name]; - master[flag.name] = sources_scope[flag.name]; - } - } - } - if (fld === 'source') { - found = false; - data.source = (data.source === "" ) ? "manual" : data.source; - for (i = 0; i < sources_scope.source_type_options.length; i++) { - if (sources_scope.source_type_options[i].value === data.source) { - sources_scope.source = sources_scope.source_type_options[i]; - found = true; - } - } - if (!found || sources_scope.source.value === "manual") { - sources_scope.groupUpdateHide = true; - } else { - sources_scope.groupUpdateHide = false; - } - master.source = sources_scope.source; - } else if (fld === 'source_vars') { - // Parse source_vars, converting to YAML. - sources_scope.source_vars = ParseVariableString(data.source_vars); - master.source_vars = sources_scope.variables; - } - else if(fld === "inventory_script"){ - // the API stores it as 'source_script', we call it inventory_script - data.summary_fields.inventory_script = data.summary_fields.source_script; - sources_scope.inventory_script = data.source_script; - master.inventory_script = sources_scope.inventory_script; - } else if (fld === "source_regions") { - if (data[fld] === "") { - sources_scope[fld] = data[fld]; - master[fld] = sources_scope[fld]; - } else { - sources_scope[fld] = data[fld].split(","); - master[fld] = sources_scope[fld]; - } - } else if (data[fld] !== undefined) { - sources_scope[fld] = data[fld]; - master[fld] = sources_scope[fld]; - } - - if (form.fields[fld].sourceModel && data.summary_fields && - data.summary_fields[form.fields[fld].sourceModel]) { - sources_scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - } - } - - initSourceChange(); - - if (data.source_regions) { - if (data.source === 'ec2' || - data.source === 'rax' || - data.source === 'gce' || - data.source === 'azure') { - if (data.source === 'ec2') { - set = sources_scope.ec2_regions; - } else if (data.source === 'rax') { - set = sources_scope.rax_regions; - } else if (data.source === 'gce') { - set = sources_scope.gce_regions; - } else if (data.source === 'azure') { - set = sources_scope.azure_regions; - } - opts = []; - list = data.source_regions.split(','); - for (i = 0; i < list.length; i++) { - for (j = 0; j < set.length; j++) { - if (list[i] === set[j].value) { - opts.push({ - id: set[j].value, - text: set[j].label - }); - } - } - } - master.source_regions = opts; - CreateSelect2({ - element: "#source_source_regions", - opts: opts - }); - - } - } else { - // If empty, default to all - master.source_regions = [{ - id: 'all', - text: 'All' - }]; - } - if (data.group_by && data.source === 'ec2') { - set = sources_scope.ec2_group_by; - opts = []; - list = data.group_by.split(','); - for (i = 0; i < list.length; i++) { - for (j = 0; j < set.length; j++) { - if (list[i] === set[j].value) { - opts.push({ - id: set[j].value, - text: set[j].label - }); - } - } - } - master.group_by = opts; - CreateSelect2({ - element: "#source_group_by", - opts: opts - }); - } - sources_scope.group_update_url = data.related.update; - modal_scope.$emit('groupVariablesLoaded'); // JT-- "groupVariablesLoaded" is where the schedule info is loaded, so I make a call after the sources_scope.source has been loaded - //Wait('stop'); - }) - .error(function (data, status) { - sources_scope.source = ""; - ProcessErrors(modal_scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve inventory source. GET status: ' + status }); - }); - } - else { - modal_scope.$emit('groupVariablesLoaded'); // JT-- "groupVariablesLoaded" is where the schedule info is loaded, so I make a call after the sources_scope.source has been loaded - } - }); - - if (sources_scope.removeScopeSourceTypeOptionsReady) { - sources_scope.removeScopeSourceTypeOptionsReady(); - } - sources_scope.removeScopeSourceTypeOptionsReady = sources_scope.$on('sourceTypeOptionsReady', function() { - if (mode === 'add') { - sources_scope.source = Find({ - list: sources_scope.source_type_options, - key: 'value', - val: '' - }); - modal_scope.showSchedulesTab = false; - } - }); - - if (modal_scope.removeChoicesComplete) { - modal_scope.removeChoicesComplete(); - } - modal_scope.removeChoicesComplete = modal_scope.$on('choicesCompleteGroup', function () { - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl); - Rest.get() - .success(function (data) { - group = data; - for (var fld in GroupForm.fields) { - if (data[fld]) { - properties_scope[fld] = data[fld]; - master[fld] = properties_scope[fld]; - } - } - schedules_url = data.related.inventory_source + 'schedules/'; - properties_scope.variable_url = data.related.variable_data; - sources_scope.source_url = data.related.inventory_source; - modal_scope.$emit('LoadSourceData'); - }) - .error(function (data, status) { - ProcessErrors(modal_scope, data, status, { hdr: 'Error!', - msg: 'Failed to retrieve group: ' + defaultUrl + '. GET status: ' + status }); - }); - }); - - choicesReady = 0; - - if (sources_scope.removeChoicesReady) { - sources_scope.removeChoicesReady(); - } - sources_scope.removeChoicesReady = sources_scope.$on('choicesReadyGroup', function () { - CreateSelect2({ - element: '#source_source', - multiple: false - }); - choicesReady++; - if (choicesReady === 2) { - if (mode === 'edit') { - modal_scope.$emit('choicesCompleteGroup'); - } - else { - properties_scope.variables = "---"; - master.variables = properties_scope.variables; - modal_scope.$emit('groupVariablesLoaded'); - } - } - }); - - // Load options for source regions - GetChoices({ - scope: sources_scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'rax_regions', - choice_name: 'rax_region_choices', - callback: 'choicesReadyGroup' - }); - - GetChoices({ - scope: sources_scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'ec2_regions', - choice_name: 'ec2_region_choices', - callback: 'choicesReadyGroup' - }); - - GetChoices({ - scope: sources_scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'gce_regions', - choice_name: 'gce_region_choices', - callback: 'choicesReadyGroup' - }); - - GetChoices({ - scope: sources_scope, - url: GetBasePath('inventory_sources'), - field: 'source_regions', - variable: 'azure_regions', - choice_name: 'azure_region_choices', - callback: 'choicesReadyGroup' - }); - - // Load options for group_by - GetChoices({ - scope: sources_scope, - url: GetBasePath('inventory_sources'), - field: 'group_by', - variable: 'ec2_group_by', - choice_name: 'ec2_group_by_choices', - callback: 'choicesReadyGroup' - }); - - Wait('start'); - - if (parent_scope.removeAddTreeRefreshed) { - parent_scope.removeAddTreeRefreshed(); - } - parent_scope.removeAddTreeRefreshed = parent_scope.$on('GroupTreeRefreshed', function() { - // Clean up - Wait('stop'); - - // @issue: OLD SEARCH - // if (modal_scope.searchCleanUp) { - // modal_scope.searchCleanup(); - // } - - try { - $('#group-modal-dialog').dialog('close'); - } - catch(e) { - // ignore - } - }); - - if (modal_scope.removeSaveComplete) { - modal_scope.removeSaveComplete(); - } - modal_scope.removeSaveComplete = modal_scope.$on('SaveComplete', function (e, error) { - if (!error) { - modal_scope.cancelModal(); - } - }); - - if (modal_scope.removeFormSaveSuccess) { - modal_scope.removeFormSaveSuccess(); - } - modal_scope.removeFormSaveSuccess = modal_scope.$on('formSaveSuccess', function () { - - // Source data gets stored separately from the group. Validate and store Source - // related fields, then call SaveComplete to wrap things up. - - var parseError = false, - regions, r, i, - group_by, - data = { - group: group_id, - source: ((sources_scope.source && sources_scope.source.value!=='manual') ? sources_scope.source.value : ''), - source_path: sources_scope.source_path, - credential: sources_scope.credential, - overwrite: sources_scope.overwrite, - overwrite_vars: sources_scope.overwrite_vars, - source_script: sources_scope.inventory_script, - update_on_launch: sources_scope.update_on_launch, - update_cache_timeout: (sources_scope.update_cache_timeout || 0) - }; - - // Create a string out of selected list of regions - if(sources_scope.source_regions){ - regions = $('#source_source_regions').select2("data"); - r = []; - for (i = 0; i < regions.length; i++) { - r.push(regions[i].id); - } - data.source_regions = r.join(); - } - - if (sources_scope.source && (sources_scope.source.value === 'ec2')) { - data.instance_filters = sources_scope.instance_filters; - // Create a string out of selected list of regions - group_by = $('#source_group_by').select2("data"); - r = []; - for (i = 0; i < group_by.length; i++) { - r.push(group_by[i].id); - } - data.group_by = r.join(); - } - - if (sources_scope.source && (sources_scope.source.value === 'ec2' )) { - // for ec2, validate variable data - data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.source_vars, true); - } - - if (sources_scope.source && (sources_scope.source.value === 'custom')) { - data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.extra_vars, true); - } - - if (sources_scope.source && (sources_scope.source.value === 'vmware' || - sources_scope.source.value === 'openstack')) { - data.source_vars = ToJSON(sources_scope.envParseType, sources_scope.inventory_variables, true); - } - - // the API doesn't expect the credential to be passed with a custom inv script - if(sources_scope.source && sources_scope.source.value === 'custom'){ - delete(data.credential); - } - - if (!parseError) { - Rest.setUrl(sources_scope.source_url); - Rest.put(data) - .success(function () { - modal_scope.$emit('SaveComplete', false); - }) - .error(function (data, status) { - $('#group_tabs a:eq(1)').tab('show'); - ProcessErrors(sources_scope, data, status, GroupForm, { hdr: 'Error!', - msg: 'Failed to update group inventory source. PUT status: ' + status }); - }); - } - }); - - // Cancel - modal_scope.cancelModal = function () { - try { - $('#group-modal-dialog').dialog('close'); - } - catch(e) { - //ignore - } - - // @issue: OLD SEARCH - // if (modal_scope.searchCleanup) { - // modal_scope.searchCleanup(); - // } - // if (parent_scope.restoreSearch) { - // parent_scope.restoreSearch(); - // } - // else { - // Wait('stop'); - // } - - Wait('stop'); - }; - - // Save - modal_scope.saveGroup = function () { - Wait('start'); - var fld, data, json_data; - - try { - - json_data = ToJSON(properties_scope.parseType, properties_scope.variables, true); - - data = {}; - for (fld in GroupForm.fields) { - data[fld] = properties_scope[fld]; - } - - data.inventory = inventory_id; - - Rest.setUrl(defaultUrl); - if (mode === 'edit' || (mode === 'add' && group_created)) { - Rest.put(data) - .success(function () { - modal_scope.$emit('formSaveSuccess'); - }) - .error(function (data, status) { - $('#group_tabs a:eq(0)').tab('show'); - ProcessErrors(properties_scope, data, status, GroupForm, { hdr: 'Error!', - msg: 'Failed to update group: ' + group_id + '. PUT status: ' + status - }); - }); - } - else { - Rest.post(data) - .success(function (data) { - group_created = true; - group_id = data.id; - sources_scope.source_url = data.related.inventory_source; - modal_scope.$emit('formSaveSuccess'); - }) - .error(function (data, status) { - $('#group_tabs a:eq(0)').tab('show'); - ProcessErrors(properties_scope, data, status, GroupForm, { hdr: 'Error!', - msg: 'Failed to create group: ' + group_id + '. POST status: ' + status - }); - }); - } - } - catch(e) { - // ignore. ToJSON will have already alerted the user - } - }; - - // Start the update process - modal_scope.updateGroup = function () { - if (sources_scope.source === "manual" || sources_scope.source === null) { - Alert('Missing Configuration', 'The selected group is not configured for updates. You must first edit the group, provide Source settings, ' + - 'and then run an update.', 'alert-info'); - } else if (sources_scope.status === 'updating') { - Alert('Update in Progress', '
The inventory update process is currently running for group ' + - $filter('sanitize')(sources_scope.summary_fields.group.name) + '. Use the Refresh button to monitor the status.
', 'alert-info', null, null, null, null, true); - } else { - InventoryUpdate({ - scope: parent_scope, - group_id: group_id, - url: properties_scope.group_update_url, - group_name: properties_scope.name, - group_source: sources_scope.source.value - }); - } - }; - - // Change the lookup and regions when the source changes - sources_scope.sourceChange = function () { - sources_scope.credential_name = ""; - sources_scope.credential = ""; - if (sources_scope.credential_name_api_error) { - delete sources_scope.credential_name_api_error; - } - initSourceChange(); - }; - - }; - } -]); diff --git a/awx/ui/client/src/helpers/Hosts.js b/awx/ui/client/src/helpers/Hosts.js deleted file mode 100644 index 2cb4aa36a5..0000000000 --- a/awx/ui/client/src/helpers/Hosts.js +++ /dev/null @@ -1,465 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - -/* jshint loopfunc: true */ -/** - * @ngdoc function - * @name helpers.function:Hosts - * @description Routines that handle host add/edit/delete on the Inventory detail page. - */ - -'use strict'; - -import listGenerator from '../shared/list-generator/main'; - -export default -angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name, 'HostListDefinition', - listGenerator.name, 'HostsHelper', - 'InventoryHelper', 'InventoryFormDefinition', 'SelectionHelper', - 'HostGroupsFormDefinition', 'VariablesHelper', 'ModalDialog', 'StandardOutHelper', - 'GroupListDefinition' -]) - -.factory('SetEnabledMsg', [ function() { - return function(host) { - if (host.has_inventory_sources) { - // Inventory sync managed, so not clickable - host.enabledToolTip = (host.enabled) ? 'Host is available' : 'Host is not available'; - } - else { - // Clickable - host.enabledToolTip = (host.enabled) ? 'Host is available. Click to toggle.' : 'Host is not available. Click to toggle.'; - } - }; -}]) - -.factory('SetHostStatus', ['SetEnabledMsg', function(SetEnabledMsg) { - return function(host) { - // Set status related fields on a host object - host.activeFailuresLink = '/#/hosts/' + host.id + '/job_host_summaries/?inventory=' + host.inventory + - '&host_name=' + encodeURI(host.name); - if (host.has_active_failures === true) { - host.badgeToolTip = 'Most recent job failed. Click to view jobs.'; - host.active_failures = 'failed'; - } - else if (host.has_active_failures === false && host.last_job === null) { - host.has_active_failures = 'none'; - host.badgeToolTip = "No job data available."; - host.active_failures = 'n/a'; - } - else if (host.has_active_failures === false && host.last_job !== null) { - host.badgeToolTip = "Most recent job successful. Click to view jobs."; - host.active_failures = 'success'; - } - - host.enabled_flag = host.enabled; - SetEnabledMsg(host); - - }; -}]) - -.factory('SetStatus', ['$filter', 'SetEnabledMsg', 'Empty', function($filter, SetEnabledMsg, Empty) { - return function(params) { - - var scope = params.scope, - host = params.host, - i, html, title; - - function ellipsis(a) { - if (a.length > 25) { - return a.substr(0,25) + '...'; - } - return a; - } - - function noRecentJobs() { - title = 'No job data'; - html = "

No recent job data available for this host.

\n"; - } - - function setMsg(host) { - var j, job, jobs; - - if (host.has_active_failures === true || (host.has_active_failures === false && host.last_job !== null)) { - if (host.has_active_failures === true) { - host.badgeToolTip = 'Most recent job failed. Click to view jobs.'; - host.active_failures = 'error'; - } - else { - host.badgeToolTip = "Most recent job successful. Click to view jobs."; - host.active_failures = 'successful'; - } - if (host.summary_fields.recent_jobs.length > 0) { - // build html table of job status info - jobs = host.summary_fields.recent_jobs.sort( - function(a,b) { - // reverse numerical order - return -1 * (a - b); - }); - title = "Recent Jobs"; - html = "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - for (j=0; j < jobs.length; j++) { - job = jobs[j]; - html += "\n"; - - // SmartStatus-tooltips are named --success whereas icon-job uses successful - var iconStatus = (job.status === 'successful') ? 'success' : 'failed'; - - html += "\n"; - - html += "\n"; - - html += "\n"; - - html += "\n"; - } - html += "\n"; - html += "
StatusFinishedName
" + ($filter('longDate')(job.finished)).replace(/ /,'
') + "
" + ellipsis(job.name) + "
\n"; - } - else { - noRecentJobs(); - } - } - else if (host.has_active_failures === false && host.last_job === null) { - host.badgeToolTip = "No job data available."; - host.active_failures = 'none'; - noRecentJobs(); - } - host.job_status_html = html; - host.job_status_title = title; - } - - if (!Empty(host)) { - // update single host - setMsg(host); - SetEnabledMsg(host); - } - else { - // update all hosts - for (i=0; i < scope.hosts.length; i++) { - setMsg(scope.hosts[i]); - SetEnabledMsg(scope.hosts[i]); - } - } - - }; -}]) - -.factory('HostsReload', [ '$stateParams', 'Empty', 'InventoryHosts', 'GetBasePath', 'Wait', - 'SetHostStatus', 'SetStatus', 'ApplyEllipsis', - function($stateParams, Empty, InventoryHosts, GetBasePath, Wait, SetHostStatus, SetStatus, - ApplyEllipsis) { - return function(params) { - - var scope = params.scope, - parent_scope = params.parent_scope; - - // @issue: OLD SEARCH - // var list = InventoryHosts, - // group_id = params.group_id, - // inventory_id = params.inventory_id; - // pageSize = (params.pageSize) ? params.pageSize : 20, - // - // url = ( !Empty(group_id) ) ? GetBasePath('groups') + group_id + '/all_hosts/' : - // GetBasePath('inventory') + inventory_id + '/hosts/'; - - // @issue: OLD SEARCH - // scope.search_place_holder='Search ' + scope.selected_group_name; - - if (scope.removeHostsReloadPostRefresh) { - scope.removeHostsReloadPostRefresh(); - } - scope.removeHostsReloadPostRefresh = scope.$on('PostRefresh', function(e, set) { - if (set === 'hosts') { - for (var i=0; i < scope.hosts.length; i++) { - //Set tooltip for host enabled flag - scope.hosts[i].enabled_flag = scope.hosts[i].enabled; - } - SetStatus({ scope: scope }); - setTimeout(function() { ApplyEllipsis('#hosts_table .host-name a'); }, 2500); - Wait('stop'); - if (parent_scope) { - parent_scope.$emit('HostReloadComplete'); - } - } - }); - - // @issue: OLD SEARCH - // SearchInit({ scope: scope, set: 'hosts', list: list, url: url }); - // PaginateInit({ scope: scope, list: list, url: url, pageSize: pageSize }); - // - // if ($stateParams.host_name) { - // scope[list.iterator + 'InputDisable'] = false; - // scope[list.iterator + 'SearchValue'] = $stateParams.host_name; - // scope[list.iterator + 'SearchField'] = 'name'; - // scope[list.iterator + 'SearchFieldLabel'] = list.fields.name.label; - // scope[list.iterator + 'SearchSelectValue'] = null; - // } - // - // if (scope.show_failures) { - // scope[list.iterator + 'InputDisable'] = true; - // scope[list.iterator + 'SearchValue'] = 'true'; - // scope[list.iterator + 'SearchField'] = 'has_active_failures'; - // scope[list.iterator + 'SearchFieldLabel'] = list.fields.has_active_failures.label; - // scope[list.iterator + 'SearchSelectValue'] = { value: 1 }; - // } - // scope.search(list.iterator, null, true); - }; - }]) - -.factory('HostsCopy', ['$compile', 'Rest', 'ProcessErrors', 'CreateDialog', 'GetBasePath', 'Wait', 'generateList', 'GroupList', -function($compile, Rest, ProcessErrors, CreateDialog, GetBasePath, Wait, GenerateList, GroupList) { -return function(params) { - - var host_id = params.host_id, - group_scope = params.group_scope, - parent_scope = params.host_scope, - parent_group = group_scope.selected_group_id, - scope = parent_scope.$new(), - buttonSet, url, host; - - buttonSet = [{ - label: "Cancel", - onClick: function() { - scope.cancel(); - }, - icon: "fa-times", - "class": "btn btn-default", - "id": "host-copy-cancel-button" - },{ - label: "OK", - onClick: function() { - scope.performCopy(); - }, - icon: "fa-check", - "class": "btn btn-primary", - "id": "host-copy-ok-button" - }]; - - if (scope.removeHostCopyPostRefresh) { - scope.removeHostCopyPostRefresh(); - } - scope.removeHostCopyPostRefresh = scope.$on('PostRefresh', function() { - scope.copy_groups.forEach(function(row, i) { - scope.copy_groups[i].checked = '0'; - }); - Wait('stop'); - $('#host-copy-dialog').dialog('open'); - $('#host-copy-ok-button').attr('disabled','disabled'); - - // prevent backspace from navigation when not in input or textarea field - $(document).on("keydown", function (e) { - if (e.which === 8 && !$(e.target).is('input[type="text"], textarea')) { - e.preventDefault(); - } - }); - }); - - if (scope.removeHostCopyDialogReady) { - scope.removeHostCopyDialogReady(); - } - scope.removeCopyDialogReady = scope.$on('HostCopyDialogReady', function() { - // @issue: OLD SEARCH - // var url = GetBasePath('inventory') + group_scope.inventory.id + '/groups/'; - - GenerateList.inject(GroupList, { - mode: 'lookup', - id: 'copy-host-select-container', - scope: scope - //, - //instructions: instructions - }); - - // @issue: OLD SEARCH - // SearchInit({ - // scope: scope, - // set: GroupList.name, - // list: GroupList, - // url: url - // }); - // PaginateInit({ - // scope: scope, - // list: GroupList, - // url: url, - // mode: 'lookup' - // }); - // scope.search(GroupList.iterator, null, true, false); - }); - - if (scope.removeShowDialog) { - scope.removeShowDialog(); - } - scope.removeShowDialog = scope.$on('ShowDialog', function() { - var d; - scope.name = host.name; - scope.copy_choice = "copy"; - d = angular.element(document.getElementById('host-copy-dialog')); - $compile(d)(scope); - CreateDialog({ - id: 'host-copy-dialog', - scope: scope, - buttons: buttonSet, - width: 650, - height: 650, - minWidth: 600, - title: 'Copy or Move Host', - callback: 'HostCopyDialogReady', - onClose: function() { - scope.cancel(); - } - }); - }); - - Wait('start'); - - url = GetBasePath('hosts') + host_id + '/'; - Rest.setUrl(url); - Rest.get() - .success(function(data) { - host = data; - scope.$emit('ShowDialog'); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. GET returned: ' + status }); - }); - - - scope.cancel = function() { - $(document).off("keydown"); - try { - $('#host-copy-dialog').dialog('close'); - } - catch(e) { - // ignore - } - - // @issue: OLD SEARCH - // scope.searchCleanup(); - - // @issue: OLD SEARCH - // group_scope.restoreSearch(); // Restore all parent search stuff and refresh hosts and groups lists - scope.$destroy(); - }; - - scope['toggle_' + GroupList.iterator] = function (id) { - var count = 0, - list = GroupList; - scope[list.name].forEach( function(row, i) { - if (row.id === id) { - if (row.checked) { - scope[list.name][i].success_class = 'success'; - } - else { - scope[list.name][i].success_class = ''; - } - } else { - scope[list.name][i].checked = 0; - scope[list.name][i].success_class = ''; - } - }); - // Check if any rows are checked - scope[list.name].forEach(function(row) { - if (row.checked) { - count++; - } - }); - if (count === 0) { - $('#host-copy-ok-button').attr('disabled','disabled'); - } - else { - $('#host-copy-ok-button').removeAttr('disabled'); - } - }; - - scope.performCopy = function() { - var list = GroupList, - target, - url; - - Wait('start'); - - if (scope.use_root_group) { - target = null; - } - else { - scope[list.name].every(function(row) { - if (row.checked === 1) { - target = row; - return false; - } - return true; - }); - } - - if (scope.copy_choice === 'move') { - // Respond to move - - // disassociate the host from the original parent - if (scope.removeHostRemove) { - scope.removeHostRemove(); - } - scope.removeHostRemove = scope.$on('RemoveHost', function () { - if (parent_group > 0) { - // Only remove a host from a parent when the parent is a group and not the inventory root - url = GetBasePath('groups') + parent_group + '/hosts/'; - Rest.setUrl(url); - Rest.post({ id: host.id, disassociate: 1 }) - .success(function () { - scope.cancel(); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to remove ' + host.name + ' from group ' + parent_group + '. POST returned: ' + status }); - }); - } else { - scope.cancel(); - } - }); - - // add the new host to the target - url = GetBasePath('groups') + target.id + '/hosts/'; - Rest.setUrl(url); - Rest.post(host) - .success(function () { - scope.$emit('RemoveHost'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to add ' + host.name + ' to ' + target.name + '. POST returned: ' + status }); - }); - } - else { - // Respond to copy by adding the new host to the target - url = GetBasePath('groups') + target.id + '/hosts/'; - Rest.setUrl(url); - Rest.post(host) - .success(function () { - scope.cancel(); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to add ' + host.name + ' to ' + target.name + '. POST returned: ' + status - }); - }); - } - }; - - -}; -}]); diff --git a/awx/ui/client/src/helpers/JobDetail.js b/awx/ui/client/src/helpers/JobDetail.js deleted file mode 100644 index 0cda454f2e..0000000000 --- a/awx/ui/client/src/helpers/JobDetail.js +++ /dev/null @@ -1,1286 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:JobDetail - * @description helper moduler for jobdetails controller - # Playbook events will be structured to form the following hierarchy: - # - playbook_on_start (once for each playbook file) - # - playbook_on_vars_prompt (for each play, but before play starts, we - # currently don't handle responding to these prompts) - # - playbook_on_play_start (once for each play) - # - playbook_on_import_for_host - # - playbook_on_not_import_for_host - # - playbook_on_no_hosts_matched - # - playbook_on_no_hosts_remaining - # - playbook_on_setup - # - runner_on* - # - playbook_on_task_start (once for each task within a play) - # - runner_on_failed - # - runner_on_ok - # - runner_on_error - # - runner_on_skipped - # - runner_on_unreachable - # - runner_on_no_hosts - # - runner_on_async_poll - # - runner_on_async_ok - # - runner_on_async_failed - # - runner_on_file_diff - # - playbook_on_notify (once for each notification from the play) - # - playbook_on_stats - -*/ - - -export default - angular.module('JobDetailHelper', ['Utilities', 'RestServices', 'ModalDialog']) - - .factory('DigestEvent', ['$rootScope', '$log', 'UpdatePlayStatus', 'UpdateHostStatus', 'AddHostResult', - 'GetElapsed', 'UpdateTaskStatus', 'JobIsFinished', 'AddNewTask', 'AddNewPlay', - function($rootScope, $log, UpdatePlayStatus, UpdateHostStatus, AddHostResult, GetElapsed, - UpdateTaskStatus, JobIsFinished, AddNewTask, AddNewPlay) { - return function(params) { - - var scope = params.scope, - event = params.event, - msg; - - $log.debug('processing event: ' + event.id); - $log.debug(event); - - function getMsg(event) { - var msg = ''; - if (event.event_data && event.event_data.res) { - if (typeof event.event_data.res === 'object') { - msg = event.event_data.res.msg; - } else { - msg = event.event_data.res; - } - } - return msg; - } - - switch (event.event) { - case 'playbook_on_start': - if (!JobIsFinished(scope)) { - scope.job_status.started = event.created; - scope.job_status.status = 'running'; - } - break; - - case 'playbook_on_play_start': - AddNewPlay({ scope: scope, event: event }); - break; - - case 'playbook_on_setup': - AddNewTask({ scope: scope, event: event }); - break; - - case 'playbook_on_task_start': - AddNewTask({ scope: scope, event: event }); - break; - - case 'runner_on_ok': - case 'runner_on_async_ok': - msg = getMsg(event); - UpdateHostStatus({ - scope: scope, - name: event.host_name, - host_id: event.host, - task_id: event.parent, - status: ( (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful' ), - id: event.id, - created: event.created, - modified: event.modified, - message: msg, - counter: event.counter, - item: (event.event_data && event.event_data.res) ? event.event_data.res.item : '' - }); - break; - - case 'playbook_on_no_hosts_matched': - UpdatePlayStatus({ - scope: scope, - play_id: event.parent, - failed: false, - changed: false, - modified: event.modified, - no_hosts: true - }); - break; - - case 'runner_on_unreachable': - msg = getMsg(event); - UpdateHostStatus({ - scope: scope, - name: event.host_name, - host_id: event.host, - task_id: event.parent, - status: 'unreachable', - id: event.id, - created: event.created, - modified: event.modified, - message: msg, - counter: event.counter, - item: (event.event_data && event.event_data.res) ? event.event_data.res.item : '' - }); - break; - - case 'runner_on_error': - case 'runner_on_async_failed': - msg = getMsg(event); - UpdateHostStatus({ - scope: scope, - name: event.host_name, - host_id: event.host, - task_id: event.parent, - status: 'failed', - id: event.id, - created: event.created, - modified: event.modified, - message: msg, - counter: event.counter, - item: (event.event_data && event.event_data.res) ? event.event_data.res.item : '' - }); - break; - - case 'runner_on_no_hosts': - UpdateTaskStatus({ - scope: scope, - failed: event.failed, - changed: event.changed, - task_id: event.parent, - modified: event.modified, - no_hosts: true - }); - break; - - case 'runner_on_skipped': - msg = getMsg(event); - UpdateHostStatus({ - scope: scope, - name: event.host_name, - host_id: event.host, - task_id: event.parent, - status: 'skipped', - id: event.id, - created: event.created, - modified: event.modified, - message: msg, - counter: event.counter, - item: (event.event_data && event.event_data.res) ? event.event_data.res.item : '' - }); - } - }; - }]) - - .factory('JobIsFinished', [ function() { - return function(scope) { - return (scope.job_status.status === 'failed' || scope.job_status.status === 'canceled' || - scope.job_status.status === 'error' || scope.job_status.status === 'successful'); - }; - }]) - - .factory('GetElapsed', [function() { - return function(params) { - var start = params.start, - end = params.end, - dt1, dt2, sec, hours, min; - dt1 = new Date(start); - dt2 = new Date(end); - if ( dt2.getTime() !== dt1.getTime() ) { - sec = Math.floor( (dt2.getTime() - dt1.getTime()) / 1000 ); - hours = Math.floor(sec / 3600); - sec = sec - (hours * 3600); - if (('' + hours).length < 2) { - hours = ('00' + hours).substr(-2, 2); - } - min = Math.floor(sec / 60); - sec = sec - (min * 60); - min = ('00' + min).substr(-2,2); - sec = ('00' + sec).substr(-2,2); - return hours + ':' + min + ':' + sec; - } - else { - return '00:00:00'; - } - }; - }]) - - .factory('SetActivePlay', [ function() { - return function(params) { - //find the most recent task in the list of 'active' tasks - - var scope = params.scope, - activeList = [], - newActivePlay, - key; - - for (key in scope.jobData.plays) { - if (scope.jobData.plays[key].taskCount > 0) { - activeList.push(key); - } - } - - if (activeList.length > 0) { - newActivePlay = scope.jobData.plays[activeList[activeList.length - 1]].id; - if (newActivePlay && scope.activePlay && newActivePlay !== scope.activePlay) { - scope.jobData.plays[scope.activePlay].tasks = {}; - scope.jobData.plays[scope.activePlay].playActiveClass = ''; - scope.activeTask = null; - } - if (newActivePlay) { - scope.activePlay = newActivePlay; - scope.jobData.plays[scope.activePlay].playActiveClass = 'JobDetail-tableRow--selected'; - } - } - }; - }]) - - .factory('SetActiveTask', [ function() { - return function(params) { - //find the most recent task in the list of 'active' tasks - var scope = params.scope, - key, - newActiveTask, - activeList = []; - - for (key in scope.jobData.plays[scope.activePlay].tasks) { - if (scope.jobData.plays[scope.activePlay].tasks[key].reportedHosts > 0 || scope.jobData.plays[scope.activePlay].tasks[key].status === 'no-matching-hosts') { - activeList.push(key); - } - } - - if (activeList.length > 0) { - newActiveTask = scope.jobData.plays[scope.activePlay].tasks[activeList[activeList.length - 1]].id; - if (newActiveTask && scope.activeTask && newActiveTask !== scope.activeTask) { - if (scope.activeTask && scope.jobData.plays[scope.activePlay].tasks[scope.activeTask] !== undefined) { - scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = ''; - scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].hostResults = {}; - } - } - if (newActiveTask) { - scope.activeTask = newActiveTask; - scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'JobDetail-tableRow--selected'; - } - } - }; - }]) - - .factory('AddNewPlay', ['SetActivePlay', function(SetActivePlay) { - return function(params) { - var scope = params.scope, - event = params.event, - status, status_text; - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - - scope.jobData.plays[event.id] = { - id: event.id, - name: event.play, - created: event.created, - status: status, - status_text: status_text, - elapsed: '00:00:00', - hostCount: 0, - taskCount: 0, - fistTask: null, - unreachableCount: 0, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - tasks: {} - }; - - SetActivePlay({ scope: scope }); - }; - }]) - - .factory('AddNewTask', ['UpdatePlayStatus', 'SetActivePlay', 'SetActiveTask', function(UpdatePlayStatus, SetActivePlay, SetActiveTask) { - return function(params) { - var scope = params.scope, - event = params.event, - status, status_text; - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - - scope.jobData.plays[event.parent].tasks[event.id] = { - id: event.id, - play_id: event.parent, - name: (event.task) ? event.task : event.event_display, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - created: event.created, - modified: event.modified, - hostCount: (scope.jobData.plays[event.parent]) ? scope.jobData.plays[event.parent].hostCount : 0, - reportedHosts: 0, - successfulCount: 0, - failedCount: 0, - changedCount: 0, - skippedCount: 0, - unreachableCount: 0, - successfulStyle: { display: 'none'}, - failedStyle: { display: 'none' }, - changedStyle: { display: 'none' }, - skippedStyle: { display: 'none' }, - unreachableStyle: { display: 'none' }, - hostResults: {} - }; - - if (scope.jobData.plays[event.parent].firstTask === undefined || scope.jobData.plays[event.parent].firstTask === null) { - scope.jobData.plays[event.parent].firstTask = event.id; - } - scope.jobData.plays[event.parent].taskCount++; - - SetActivePlay({ scope: scope }); - - SetActiveTask({ scope: scope }); - - UpdatePlayStatus({ - scope: scope, - play_id: event.parent, - failed: event.failed, - changed: event.changed, - modified: event.modified - }); - }; - }]) - - .factory('UpdateJobStatus', ['GetElapsed', 'Empty', 'JobIsFinished', 'longDateFilter', function(GetElapsed, Empty, JobIsFinished, longDateFilter) { - return function(params) { - var scope = params.scope, - failed = params.failed, - modified = params.modified, - started = params.started, - finished = params.finished; - - if (failed && scope.job_status.status !== 'failed' && scope.job_status.status !== 'error' && - scope.job_status.status !== 'canceled') { - scope.job_status.status = 'failed'; - } - if (JobIsFinished(scope) && !Empty(modified)) { - scope.job_status.finished = longDateFilter(modified); - } - if (!Empty(started) && Empty(scope.job_status.started)) { - scope.job_status.started = longDateFilter(modified); - } - if (!Empty(scope.job_status.finished) && !Empty(scope.job_status.started)) { - scope.job_status.elapsed = GetElapsed({ - start: started, - end: finished - }); - } - }; - }]) - - // Update the status of a play - .factory('UpdatePlayStatus', ['GetElapsed', 'UpdateJobStatus', function(GetElapsed, UpdateJobStatus) { - return function(params) { - var scope = params.scope, - failed = params.failed, - changed = params.changed, - id = params.play_id, - modified = params.modified, - no_hosts = params.no_hosts, - play; - - if (scope.jobData.plays[id] !== undefined) { - play = scope.jobData.plays[id]; - if (failed) { - play.status = 'failed'; - play.status_text = 'Failed'; - } - else if (play.status !== 'changed' && play.status !== 'failed') { - // once the status becomes 'changed' or 'failed' don't modify it - if (no_hosts) { - play.status = 'no-matching-hosts'; - play.status_text = 'No matching hosts'; - } else { - play.status = (changed) ? 'changed' : (failed) ? 'failed' : 'successful'; - play.status_text = (changed) ? 'Changed' : (failed) ? 'Failed' : 'OK'; - } - } - play.taskCount = (play.taskCount > 0) ? play.taskCount : 1; // set to a minimum of 1 to force drawing - play.status_tip = "Event ID: " + play.id + "
Status: " + play.status_text; - play.finished = modified; - play.elapsed = GetElapsed({ - start: play.created, - end: modified - }); - //play.status_text = (status_text) ? status_text : play.status; - } - - UpdateJobStatus({ - scope: scope, - failed: null, - modified: modified - }); - }; - }]) - - .factory('UpdateTaskStatus', ['UpdatePlayStatus', 'GetElapsed', function(UpdatePlayStatus, GetElapsed) { - return function(params) { - var scope = params.scope, - failed = params.failed, - changed = params.changed, - id = params.task_id, - modified = params.modified, - no_hosts = params.no_hosts, - play, task; - - // find the task in our hierarchy - for (play in scope.jobData.plays) { - if (scope.jobData.plays[play].tasks[id]) { - task = scope.jobData.plays[play].tasks[id]; - } - } - - if (task) { - if (no_hosts){ - task.status = 'no-matching-hosts'; - task.status_text = 'No matching hosts'; - } - else if (failed) { - task.status = 'failed'; - task.status_text = 'Failed'; - } - else if (task.status !== 'changed' && task.status !== 'failed') { - // once the status becomes 'changed' or 'failed' don't modify it - task.status = (failed) ? 'failed' : (changed) ? 'changed' : 'successful'; - task.status_text = (failed) ? 'Failed' : (changed) ? 'Changed' : 'OK'; - } - task.status_tip = "Event ID: " + task.id + "
Status: " + task.status_text; - task.finished = params.modified; - task.elapsed = GetElapsed({ - start: task.created, - end: modified - }); - - UpdatePlayStatus({ - scope: scope, - failed: failed, - changed: changed, - play_id: task.play_id, - modified: modified, - no_hosts: no_hosts - }); - } - }; - }]) - - // Each time a runner event is received update host summary totals and the parent task - .factory('UpdateHostStatus', ['UpdateTaskStatus', 'AddHostResult', function(UpdateTaskStatus, AddHostResult) { - return function(params) { - var scope = params.scope, - status = params.status, // successful, changed, unreachable, failed, skipped - name = params.name, - event_id = params.id, - host_id = params.host_id, - task_id = params.task_id, - modified = params.modified, - created = params.created, - msg = params.message, - item = params.item, - counter = params.counter; - - if (scope.jobData.hostSummaries[host_id] !== undefined) { - scope.jobData.hostSummaries[host_id].ok += (status === 'successful') ? 1 : 0; - scope.jobData.hostSummaries[host_id].changed += (status === 'changed') ? 1 : 0; - scope.jobData.hostSummaries[host_id].unreachable += (status === 'unreachable') ? 1 : 0; - scope.jobData.hostSummaries[host_id].failed += (status === 'failed') ? 1 : 0; - if (status === 'failed' || status === 'unreachable') { - scope.jobData.hostSummaries[host_id].status = 'failed'; - } - } - else { - scope.jobData.hostSummaries[host_id] = { - id: host_id, - name: name, - ok: (status === 'successful') ? 1 : 0, - changed: (status === 'changed') ? 1 : 0, - unreachable: (status === 'unreachable') ? 1 : 0, - failed: (status === 'failed') ? 1 : 0, - status: (status === 'failed' || status === 'unreachable') ? 'failed' : 'successful' - }; - } - UpdateTaskStatus({ - scope: scope, - task_id: task_id, - failed: ((status === 'failed' || status === 'unreachable') ? true :false), - changed: ((status === 'changed') ? true : false), - modified: modified - }); - - AddHostResult({ - scope: scope, - task_id: task_id, - host_id: host_id, - event_id: event_id, - status: status, - name: name, - created: created, - counter: counter, - message: msg, - item: item - }); - }; - }]) - - // Add a new host result - .factory('AddHostResult', ['SetTaskStyles', 'SetActivePlay', 'SetActiveTask', function(SetTaskStyles, SetActivePlay, SetActiveTask) { - return function(params) { - var scope = params.scope, - task_id = params.task_id, - host_id = params.host_id, - event_id = params.event_id, - status = params.status, - created = params.created, - counter = params.counter, - name = params.name, - msg = params.message, - item = params.item, - status_text = '', - task, play, play_id; - - switch(status) { - case "successful": - status_text = 'OK'; - break; - case "changed": - status_text = "Changed"; - break; - case "failed": - status_text = "Failed"; - break; - case "unreachable": - status_text = "Unreachable"; - break; - case "skipped": - status_text = "Skipped"; - } - - if (typeof item === "object") { - item = JSON.stringify(item); - } - - for (play in scope.jobData.plays) { - for (task in scope.jobData.plays[play].tasks) { - if (parseInt(task,10) === parseInt(task_id,10)) { - play_id = parseInt(play,10); - } - } - } - - if (play_id) { - scope.jobData.plays[play_id].tasks[task_id].hostResults[event_id] = { - id: event_id, - status: status, - status_text: status_text, - host_id: host_id, - task_id: task_id, - name: name, - created: created, - counter: counter, - msg: msg, - item: item - }; - - // increment the unreachable count on the play - if (status === 'unreachable') { - scope.jobData.plays[play_id].unreachableCount++; - } - - // update the task status bar - task = scope.jobData.plays[play_id].tasks[task_id]; - - if (task_id === scope.jobData.plays[play_id].firstTask) { - scope.jobData.plays[play_id].hostCount++; - task.hostCount++; - } - - task.reportedHosts += 1; - task.failedCount += (status === 'failed') ? 1 : 0; - task.changedCount += (status === 'changed') ? 1 : 0; - task.successfulCount += (status === 'successful') ? 1 : 0; - task.skippedCount += (status === 'skipped') ? 1 : 0; - task.unreachableCount += (status === 'unreachable') ? 1 : 0; - - SetTaskStyles({ - task: task - }); - - SetActivePlay({ scope: scope }); - - SetActiveTask({ scope: scope }); - } - }; - }]) - - .factory('SetTaskStyles', [ function() { - return function(params) { - var task = params.task, - diff; - - task.missingCount = task.hostCount - (task.failedCount + task.changedCount + task.skippedCount + task.successfulCount + - task.unreachableCount); - if(task.missingCount<0){ - task.hostCount = (task.failedCount + task.changedCount + task.skippedCount + task.successfulCount + - task.unreachableCount); - } - task.missingPct = (task.hostCount > 0) ? Math.ceil((100 * (task.missingCount / task.hostCount))) : 0; - task.failedPct = (task.hostCount > 0) ? Math.ceil((100 * (task.failedCount / task.hostCount))) : 0; - task.changedPct = (task.hostCount > 0) ? Math.ceil((100 * (task.changedCount / task.hostCount))) : 0; - task.skippedPct = (task.hostCount > 0) ? Math.ceil((100 * (task.skippedCount / task.hostCount))) : 0; - task.successfulPct = (task.hostCount > 0) ? Math.ceil((100 * (task.successfulCount / task.hostCount))) : 0; - task.unreachablePct = (task.hostCount > 0) ? Math.ceil((100 * (task.unreachableCount / task.hostCount))) : 0; - - // cap % at 100 - task.missingPct = (task.missingPct > 100) ? 100 : task.missingPct; - task.failedPct = (task.failedPct > 100) ? 100 : task.failedPct; - task.changedPct = (task.changedPct > 100) ? 100 : task.changedPct; - task.skippedPct = (task.skippedPct > 100) ? 100 : task.skippedPct; - task.successfulPct = ( task.successfulPct > 100) ? 100 : task.successfulPct; - task.unreachablePct = (task.unreachablePct > 100) ? 100 : task.unreachablePct; - - diff = (task.failedPct + task.changedPct + task.skippedPct + task.successfulPct + task.unreachablePct + task.missingPct) - 100; - if (diff > 0) { - if (task.failedPct > diff) { - task.failedPct = task.failedPct - diff; - } - else if (task.changedPct > diff) { - task.changedPct = task.changedPct - diff; - } - else if (task.skippedPct > diff) { - task.skippedPct = task.skippedPct - diff; - } - else if (task.successfulPct > diff) { - task.successfulPct = task.successfulPct - diff; - } - else if (task.unreachablePct > diff) { - task.unreachablePct = task.unreachablePct - diff; - } - else if (task.missingPct > diff) { - task.missingPct = task.missingPct - diff; - } - } - task.successfulStyle = (task.successfulPct > 0) ? { 'display': 'inline-block' }: { 'display': 'none' }; - task.changedStyle = (task.changedPct > 0) ? { 'display': 'inline-block'} : { 'display': 'none' }; - task.skippedStyle = (task.skippedPct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' }; - task.failedStyle = (task.failedPct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' }; - task.unreachableStyle = (task.unreachablePct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' }; - task.missingStyle = (task.missingPct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' }; - }; - }]) - - .factory('LoadPlays', ['Rest', 'ProcessErrors', 'GetElapsed', 'SelectPlay', 'JobIsFinished', - function(Rest, ProcessErrors, GetElapsed, SelectPlay, JobIsFinished) { - return function(params) { - var scope = params.scope, - callback = params.callback, - url; - - scope.plays = []; - - // @issue: OLD SEARCH - factory needs refactoring! will be completely rehauled for job details 3.1 update - // url = scope.job.url + 'job_plays/?page_size=' + scope.playsMaxRows + '&order=id'; - // url += (scope.search_play_name) ? '&play__icontains=' + encodeURIComponent(scope.search_play_name) : ''; - // url += (scope.search_play_status === 'failed') ? '&failed=true' : ''; - - scope.playsLoading = true; - Rest.setUrl(url); - Rest.get() - .success(function(data) { - scope.next_plays = data.next; - scope.plays = []; - data.results.forEach(function(event, idx) { - var status, status_text, start, end, elapsed; - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - start = event.started; - - if (idx < data.results.length - 1) { - // end date = starting date of the next event - end = data.results[idx + 1].started; - } - else if (JobIsFinished(scope)) { - // this is the last play and the job already finished - end = scope.job_status.finished; - } - if (end) { - elapsed = GetElapsed({ - start: start, - end: end - }); - } - else { - elapsed = '00:00:00'; - } - - scope.plays.push({ - id: event.id, - name: event.play, - created: start, - finished: end, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - elapsed: elapsed, - hostCount: 0, - fistTask: null, - playActiveClass: '', - unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, - }); - }); - - // set the active task - SelectPlay({ - scope: scope, - id: (scope.plays.length > 0) ? scope.plays[0].id : null, - callback: callback - }); - scope.playsLoading = false; - }) - .error(function(data) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - }; - }]) - - // Call when the selected Play needs to change - .factory('SelectPlay', ['LoadTasks', function(LoadTasks) { - return function(params) { - var scope = params.scope, - id = params.id, - callback = params.callback; - - scope.selectedPlay = id; - scope.plays.forEach(function(play, idx) { - if (play.id === scope.selectedPlay) { - scope.plays[idx].playActiveClass = 'JobDetail-tableRow--selected'; - } - else { - scope.plays[idx].playActiveClass = ''; - } - }); - - LoadTasks({ - scope: scope, - callback: callback, - clear: true - }); - - }; - }]) - - .factory('LoadTasks', ['Rest', 'ProcessErrors', 'GetElapsed', 'SelectTask', 'SetTaskStyles', function(Rest, ProcessErrors, GetElapsed, SelectTask, SetTaskStyles) { - return function(params) { - var scope = params.scope, - callback = params.callback, - url, play; - - scope.tasks = []; - if (scope.selectedPlay) { - url = scope.job.url + 'job_tasks/?event_id=' + scope.selectedPlay; - - // @issue: OLD SEARCH - // url += (scope.search_task_name) ? '&task__icontains=' + encodeURIComponent(scope.search_task_name) : ''; - // url += (scope.search_task_status === 'failed') ? '&failed=true' : ''; - // url += '&page_size=' + scope.tasksMaxRows + '&order=id'; - - scope.plays.every(function(p, idx) { - if (p.id === scope.selectedPlay) { - play = scope.plays[idx]; - return false; - } - return true; - }); - - scope.tasksLoading = true; - - Rest.setUrl(url); - Rest.get() - .success(function(data) { - scope.next_tasks = data.next; - scope.tasks = []; - data.results.forEach(function(event, idx) { - var end, elapsed, status, status_text; - - if (play.firstTask === undefined || play.firstTask === null) { - play.firstTask = event.id; - play.hostCount = (event.host_count) ? event.host_count : 0; - } - - if (idx < data.results.length - 1) { - // end date = starting date of the next event - end = data.results[idx + 1].created; - } - else { - // no next event (task), get the end time of the play - scope.plays.every(function(play) { - if (play.id === scope.selectedPlay) { - end = play.finished; - return false; - } - return true; - }); - } - - if (end) { - elapsed = GetElapsed({ - start: event.created, - end: end - }); - } - else { - elapsed = '00:00:00'; - } - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - - scope.tasks.push({ - id: event.id, - play_id: scope.selectedPlay, - name: event.name, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - created: event.created, - modified: event.modified, - finished: end, - elapsed: elapsed, - hostCount: (event.host_count) ? event.host_count : 0, - reportedHosts: (event.reported_hosts) ? event.reported_hosts : 0, - successfulCount: (event.successful_count) ? event.successful_count : 0, - failedCount: (event.failed_count) ? event.failed_count : 0, - changedCount: (event.changed_count) ? event.changed_count : 0, - skippedCount: (event.skipped_count) ? event.skipped_count : 0, - unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, - taskActiveClass: '' - }); - - if (play.firstTask !== event.id) { - // this is not the first task - scope.tasks[scope.tasks.length - 1].hostCount = play.hostCount; - } - - SetTaskStyles({ - task: scope.tasks[scope.tasks.length - 1] - }); - }); - - // set the active task - SelectTask({ - scope: scope, - id: (scope.tasks.length > 0) ? scope.tasks[0].id : null, - callback: callback - }); - - scope.tasksLoading = false; - - }) - .error(function(data) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - } - else { - SelectTask({ - scope: scope, - id: null, - callback: callback - }); - } - }; - }]) - - // Call when the selected task needs to change - .factory('SelectTask', ['JobDetailService', function(JobDetailService) { - return function(params) { - var scope = params.scope, - id = params.id; - - scope.selectedTask = id; - scope.tasks.forEach(function(task, idx) { - if (task.id === scope.selectedTask) { - scope.tasks[idx].taskActiveClass = 'JobDetail-tableRow--selected'; - } - else { - scope.tasks[idx].taskActiveClass = ''; - } - }); - if (scope.selectedTask !== null){ - params = { - parent: scope.selectedTask, - event__startswith: 'runner', - page_size: scope.hostResultsMaxRows, - order: 'host_name,counter', - }; - - // @issue: OLD SEARCH - // if (scope.search_host_status === 'failed'){ - // params.failed = true; - // } - - JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){ - scope.hostResults = JobDetailService.processHostEvents(res.results); - scope.hostResultsLoading = false; - }); - } - else{ - scope.hostResults = []; - scope.hostResultsLoading = false; - } - }; - }]) - - .factory('DrawGraph', ['DonutChart', function(DonutChart) { - return function(params) { - var count = params.count, - graph_data = []; - // Ready the data - if (count.ok.length > 0) { - graph_data.push({ - label: 'OK', - value: count.ok.length, - color: '#5CB85C' - }); - } - if (count.changed.length > 0) { - graph_data.push({ - label: 'CHANGED', - value: count.changed.length, - color: '#FF9900' - }); - } - if (count.unreachable.length > 0) { - graph_data.push({ - label: 'UNREACHABLE', - value: count.unreachable.length, - color: '#FF0000' - }); - } - if (count.failures.length > 0) { - graph_data.push({ - label: 'FAILED', - value: count.failures.length, - color: '#D9534F' - }); - } - DonutChart({ - data: graph_data - }); - }; - }]) - - .factory('DonutChart', [function() { - return function(params) { - var dataset = params.data, - element = $("#graph-section"), - colors, total,job_detail_chart; - - colors = _.map(dataset, function(d){ - return d.color; - }); - total = d3.sum(dataset.map(function(d) { - return d.value; - })); - job_detail_chart = nv.models.pieChart() - .margin({bottom: 15}) - .x(function(d) { - return d.label +': '+ Math.floor((d.value/total)*100) + "%"; - }) - .y(function(d) { return d.value; }) - .showLabels(false) - .showLegend(true) - .growOnHover(false) - .labelThreshold(0.01) - .tooltipContent(function(x, y) { - return '

'+x+'

'+ '

' + Math.floor(y.replace(',','')) + ' HOSTS ' + '

'; - }) - .color(colors); - job_detail_chart.legend.rightAlign(false); - job_detail_chart.legend.margin({top: 5, right: 450, left:0, bottom: 0}); - d3.select(element.find('svg')[0]) - .datum(dataset) - .transition().duration(350) - .call(job_detail_chart) - .style({ - "font-family": 'Open Sans', - "font-style": "normal", - "font-weight":400, - "src": "url(/static/assets/OpenSans-Regular.ttf)", - "width": 600, - "height": 300, - "color": '#848992' - }); - d3.select(element.find(".nv-noData")[0]) - .style({ - "text-anchor": 'start' - }); - return job_detail_chart; - }; - }]) - - - .factory('DrawPlays', [function() { - return function(params) { - var scope = params.scope, - idx = 0, - result = [], - newKeys = [], - //plays = JSON.parse(JSON.stringify(scope.jobData.plays)), - plays = scope.jobData.plays, - filteredListX = [], - filteredListB = [], - key, - keys; - - function listSort(a,b) { - if (parseInt(a,10) < parseInt(b,10)) { - return -1; - } - if (parseInt(a,10) > parseInt(b,10)) { - return 1; - } - return 0; - } - - // Only draw plays that are in the 'active' list - for (key in plays) { - if (plays[key].taskCount > 0) { - filteredListX[key] = plays[key]; - } - } - - // @issue: OLD SEARCH - // if (scope.search_play_name) { - // for (key in plays) { - // if (filteredListX[key].name.indexOf(scope.search_play_name) > 0) { - // filteredListA[key] = filteredListX[key]; - // } - // } - // } - // else { - // filteredListA = filteredListX; - // } - - // @issue: OLD SEARCH - // if (scope.search_play_status === 'failed') { - // for (key in filteredListA) { - // if (filteredListA[key].status === 'failed') { - // filteredListB[key] = plays[key]; - // } - // } - // } - // else { - // filteredListB = filteredListA; - // } - - keys = Object.keys(filteredListB); - keys.sort(function(a,b) { return listSort(a,b); }).reverse(); - for (idx=0; idx < scope.playsMaxRows && idx < keys.length; idx++) { - newKeys.push(keys[idx]); - } - newKeys.sort(function(a,b) { return listSort(a,b); }); - idx = 0; - while (idx < newKeys.length) { - result.push(filteredListB[newKeys[idx]]); - idx++; - } - setTimeout( function() { - scope.$apply( function() { - scope.plays = result; - scope.selectedPlay = scope.activePlay; - if (scope.liveEventProcessing) { - $('#plays-table-detail').scrollTop($('#plays-table-detail').prop("scrollHeight")); - } - }); - }); - }; - }]) - - .factory('DrawTasks', [ function() { - return function(params) { - var scope = params.scope, - result = [], - filteredListX = [], - filteredListB = [], - idx, key, keys, newKeys, tasks, t; - - function listSort(a,b) { - if (parseInt(a,10) < parseInt(b,10)) { - return -1; - } - if (parseInt(a,10) > parseInt(b,10)) { - return 1; - } - return 0; - } - - if (scope.activePlay && scope.jobData.plays[scope.activePlay]) { - - //tasks = JSON.parse(JSON.stringify(scope.jobData.plays[scope.activePlay].tasks)); - tasks = scope.jobData.plays[scope.activePlay].tasks; - - // Only draw tasks that are in the 'active' list - for (key in tasks) { - t = tasks[key]; - if (t.reportedHosts > 0 || t.hostCount > 0 || t.successfulCount >0 || t.failedCount > 0 || - t.changedCount > 0 || t.skippedCount > 0 || t.unreachableCount > 0) { - filteredListX[key] = tasks[key]; - } - } - - // @issue: OLD SEARCH - // if (scope.search_task_name) { - // for (key in filteredListX) { - // if (filteredListX[key].name.indexOf(scope.search_task_name) > 0) { - // filteredListA[key] = filteredListX[key]; - // } - // } - // } - // else { - // filteredListA = filteredListX; - // } - - // @issue: OLD SEARCH - // if (scope.search_task_status === 'failed') { - // for (key in filteredListA) { - // if (filteredListA[key].status === 'failed') { - // filteredListB[key] = tasks[key]; - // } - // } - // } - // else { - // filteredListB = filteredListA; - // } - - keys = Object.keys(filteredListB); - keys.sort(function(a,b) { return listSort(a,b); }).reverse(); - newKeys = []; - for (idx=0; result.length < scope.tasksMaxRows && idx < keys.length; idx++) { - newKeys.push(keys[idx]); - } - newKeys.sort(function(a,b) { return listSort(a,b); }); - idx = 0; - while (idx < newKeys.length) { - result.push(filteredListB[newKeys[idx]]); - idx++; - } - } - - setTimeout( function() { - scope.$apply( function() { - scope.tasks = result; - scope.selectedTask = scope.activeTask; - if (scope.liveEventProcessing) { - $('#tasks-table-detail').scrollTop($('#tasks-table-detail').prop("scrollHeight")); - } - }); - }); - - }; - }]) - - .factory('DrawHostResults', [ function() { - return function(params) { - var scope = params.scope, - result = [], - filteredListB = [], - idx = 0, - hostResults, - keys; - - if (scope.activePlay && scope.activeTask && scope.jobData.plays[scope.activePlay] && - scope.jobData.plays[scope.activePlay].tasks[scope.activeTask]) { - - //hostResults = JSON.parse(JSON.stringify(scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].hostResults)); - hostResults = scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].hostResults; - - // @issue: OLD SEARCH - // if (scope.search_host_name) { - // for (key in hostResults) { - // if (hostResults[key].name.indexOf(scope.search_host_name) > 0) { - // filteredListA[key] = hostResults[key]; - // } - // } - // } - // else { - // filteredListA = hostResults; - // } - - // @issue: OLD SEARCH - // if (scope.search_host_status === 'failed' || scope.search_host_status === 'unreachable') { - // for (key in filteredListA) { - // if (filteredListA[key].status === 'failed') { - // filteredListB[key] = filteredListA[key]; - // } - // } - // } - // else { - // filteredListB = filteredListA; - // } - - keys = Object.keys(filteredListB); - keys.sort(function compare(a, b) { - if (filteredListB[a].name === filteredListB[b].name) { - if (filteredListB[a].counter < filteredListB[b].counter) { - return -1; - } - if (filteredListB[a].counter >filteredListB[b].counter) { - return 1; - } - } else { - if (filteredListB[a].name < filteredListB[b].name) { - return -1; - } - if (filteredListB[a].name > filteredListB[b].name) { - return 1; - } - } - // a must be equal to b - return 0; - }); - while (idx < keys.length && result.length < scope.hostResultsMaxRows) { - result.push(filteredListB[keys[idx]]); - idx++; - } - } - - setTimeout( function() { - scope.$apply( function() { - scope.hostResults = result; - if (scope.liveEventProcessing) { - $('#hosts-table-detail').scrollTop($('#hosts-table-detail').prop("scrollHeight")); - } - }); - }); - - }; - }]) - - .factory('UpdateDOM', ['DrawPlays', 'DrawTasks', 'DrawHostResults', - function(DrawPlays, DrawTasks, DrawHostResults) { - return function(params) { - var scope = params.scope; - if (!scope.pauseLiveEvents) { - DrawPlays({ scope: scope }); - DrawTasks({ scope: scope }); - DrawHostResults({ scope: scope }); - } - - setTimeout(function() { - scope.playsLoading = false; - scope.tasksLoading = false; - scope.hostResultsLoading = false; - },100); - }; - }]); diff --git a/awx/ui/client/src/helpers/JobSubmission.js b/awx/ui/client/src/helpers/JobSubmission.js deleted file mode 100644 index c4fd62434d..0000000000 --- a/awx/ui/client/src/helpers/JobSubmission.js +++ /dev/null @@ -1,337 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -'use strict'; - -export default -angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'CredentialFormDefinition', 'CredentialsListDefinition', -'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog', 'FormGenerator', 'JobVarsPromptFormDefinition']) - -.factory('CreateLaunchDialog', ['$compile', 'CreateDialog', 'Wait', 'ParseTypeChange', -function($compile, CreateDialog, Wait, ParseTypeChange) { - return function(params) { - var buttons, - scope = params.scope, - html = params.html, - // job_launch_data = {}, - callback = params.callback || 'PlaybookLaunchFinished', - // url = params.url, - e; - - // html+='
job_launch_form.$valid = {{job_launch_form.$valid}}
'; - html+=''; - $('#password-modal').empty().html(html); - $('#password-modal').find('#job_extra_vars').before(scope.helpContainer); - e = angular.element(document.getElementById('password-modal')); - $compile(e)(scope); - - if(scope.prompt_for_vars===true){ - ParseTypeChange({ scope: scope, field_id: 'job_extra_vars' , variable: "extra_vars"}); - } - - buttons = [{ - label: "Cancel", - onClick: function() { - $('#password-modal').dialog('close'); - // scope.$emit('CancelJob'); - // scope.$destroy(); - }, - icon: "fa-times", - "class": "btn btn-default", - "id": "password-cancel-button" - },{ - label: "Launch", - onClick: function() { - scope.$emit(callback); - $('#password-modal').dialog('close'); - }, - icon: "fa-check", - "class": "btn btn-primary", - "id": "password-accept-button" - }]; - - CreateDialog({ - id: 'password-modal', - scope: scope, - buttons: buttons, - width: 620, - height: 700, //(scope.passwords.length > 1) ? 700 : 500, - minWidth: 500, - title: 'Launch Configuration', - callback: 'DialogReady', - onOpen: function(){ - Wait('stop'); - } - }); - - if (scope.removeDialogReady) { - scope.removeDialogReady(); - } - scope.removeDialogReady = scope.$on('DialogReady', function() { - $('#password-modal').dialog('open'); - $('#password-accept-button').attr('ng-disabled', 'job_launch_form.$invalid' ); - e = angular.element(document.getElementById('password-accept-button')); - $compile(e)(scope); - }); - }; - - }]) - - .factory('PromptForPasswords', ['CredentialForm', - function(CredentialForm) { - return function(params) { - var scope = params.scope, - callback = params.callback || 'PasswordsAccepted', - url = params.url, - form = CredentialForm, - fld, field, - html=params.html || ""; - - scope.passwords = params.passwords; - - html += "
Launching this job requires the passwords listed below. Enter and confirm each password before continuing.
\n"; - - scope.passwords.forEach(function(password) { - // Prompt for password - field = form.fields[password]; - fld = password; - scope[fld] = ''; - html += "
\n"; - html += "\n"; - html += "Please enter a password.
\n"; - html += "
\n"; - html += "
\n"; - - // Add the related confirm field - if (field.associated) { - fld = field.associated; - field = form.fields[field.associated]; - scope[fld] = ''; - html += "
\n"; - html += "\n"; - html += "Please confirm the password.\n"; - html += (field.awPassMatch) ? "This value does not match the password you entered previously. Please confirm that password.
\n" : ""; - html += "
\n"; - html += "
\n"; - } - }); - - scope.$emit(callback, html, url); - - // Password change - scope.clearPWConfirm = function (fld) { - // If password value changes, make sure password_confirm must be re-entered - scope[fld] = ''; - scope.job_launch_form[fld].$setValidity('awpassmatch', false); - scope.checkStatus(); - }; - - scope.checkStatus = function() { - if (!scope.job_launch_form.$invalid) { - $('#password-accept-button').removeAttr('disabled'); - } - else { - $('#password-accept-button').attr({ "disabled": "disabled" }); - } - }; - }; - }]) - - .factory('CheckPasswords', ['Rest', 'GetBasePath', 'ProcessErrors', 'Empty', - function(Rest, GetBasePath, ProcessErrors, Empty) { - return function(params) { - var scope = params.scope, - callback = params.callback, - credential = params.credential; - - var passwords = []; - if (!Empty(credential)) { - Rest.setUrl(GetBasePath('credentials')+credential); - Rest.get() - .success(function (data) { - if(data.kind === "ssh"){ - if(data.password === "ASK" ){ - passwords.push("ssh_password"); - } - if(data.ssh_key_unlock === "ASK"){ - passwords.push("ssh_key_unlock"); - } - if(data.become_password === "ASK"){ - passwords.push("become_password"); - } - if(data.vault_password === "ASK"){ - passwords.push("vault_password"); - } - } - scope.$emit(callback, passwords); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get job template details. GET returned status: ' + status }); - }); - } - - }; - }]) - - // Submit SCM Update request - .factory('ProjectUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait', - function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) { - return function (params) { - var scope = params.scope, - project_id = params.project_id, - url = GetBasePath('projects') + project_id + '/update/', - project; - - if (scope.removeUpdateSubmitted) { - scope.removeUpdateSubmitted(); - } - scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function() { - // Refresh the project list after update request submitted - Wait('stop'); - if (/\d$/.test($location.path())) { - //Request submitted from projects/N page. Navigate back to the list so user can see status - $location.path('/projects'); - } - if (scope.socketStatus === 'error') { - Alert('Update Started', '
The request to start the SCM update process was submitted. ' + - 'To monitor the update status, refresh the page by clicking the button.
', 'alert-info', null, null, null, null, true); - if (scope.refresh) { - scope.refresh(); - } - } - }); - - if (scope.removePromptForPasswords) { - scope.removePromptForPasswords(); - } - scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { - PromptForPasswords({ scope: scope, passwords: project.passwords_needed_to_update, callback: 'StartTheUpdate' }); - }); - - if (scope.removeStartTheUpdate) { - scope.removeStartTheUpdate(); - } - scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { - LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); - }); - - // Check to see if we have permission to perform the update and if any passwords are needed - Wait('start'); - Rest.setUrl(url); - Rest.get() - .success(function (data) { - project = data; - if (project.can_update) { - if (project.passwords_needed_to_updated) { - Wait('stop'); - scope.$emit('PromptForPasswords'); - } - else { - scope.$emit('StartTheUpdate', {}); - } - } - else { - Alert('Permission Denied', 'You do not have access to update this project. Please contact your system administrator.', - 'alert-danger'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to lookup project ' + url + ' GET returned: ' + status }); - }); - }; - } - ]) - - // Submit Inventory Update request - .factory('InventoryUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait', - function (PromptForPasswords, LaunchJob, Rest, GetBasePath, ProcessErrors, Alert, Wait) { - return function (params) { - - var scope = params.scope, - url = params.url, - inventory_source; - - if (scope.removeUpdateSubmitted) { - scope.removeUpdateSubmitted(); - } - scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function () { - Wait('stop'); - if (scope.socketStatus === 'error') { - Alert('Sync Started', '
The request to start the inventory sync process was submitted. ' + - 'To monitor the status refresh the page by clicking the button.
', 'alert-info', null, null, null, null, true); - if (scope.refreshGroups) { - // inventory detail page - scope.refreshGroups(); - } - else if (scope.refresh) { - scope.refresh(); - } - } - }); - - if (scope.removePromptForPasswords) { - scope.removePromptForPasswords(); - } - scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { - PromptForPasswords({ scope: scope, passwords: inventory_source.passwords_needed_to_update, callback: 'StartTheUpdate' }); - }); - - if (scope.removeStartTheUpdate) { - scope.removeStartTheUpdate(); - } - scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { - LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); - }); - - // Check to see if we have permission to perform the update and if any passwords are needed - Wait('start'); - Rest.setUrl(url); - Rest.get() - .success(function (data) { - inventory_source = data; - if (data.can_update) { - if (data.passwords_needed_to_update) { - Wait('stop'); - scope.$emit('PromptForPasswords'); - } - else { - scope.$emit('StartTheUpdate', {}); - } - } else { - Wait('stop'); - Alert('Permission Denied', 'You do not have access to run the inventory sync. Please contact your system administrator.', - 'alert-danger'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get inventory source ' + url + ' GET returned: ' + status }); - }); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/JobTemplates.js b/awx/ui/client/src/helpers/JobTemplates.js deleted file mode 100644 index 35d80e78f6..0000000000 --- a/awx/ui/client/src/helpers/JobTemplates.js +++ /dev/null @@ -1,173 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name helpers.function:JobTemplatesHelper - * @description Routines shared by job related controllers - */ - -export default -angular.module('JobTemplatesHelper', ['Utilities']) - -/* - * Add bits to $scope for handling callback url help - * - */ - -.factory('CallbackHelpInit', ['$location', 'GetBasePath', 'Rest', 'JobTemplateForm', 'GenerateForm', '$stateParams', 'ProcessErrors', 'ParseTypeChange', - 'ParseVariableString', 'Empty', 'InventoryList', 'CredentialList','ProjectList', 'Wait', - function($location, GetBasePath, Rest, JobTemplateForm, GenerateForm, $stateParams, ProcessErrors, ParseTypeChange, - ParseVariableString, Empty, InventoryList, CredentialList, ProjectList, Wait) { - return function(params) { - - var scope = params.scope, - defaultUrl = GetBasePath('job_templates'), - // generator = GenerateForm, - form = JobTemplateForm(), - // loadingFinishedCount = 0, - // base = $location.path().replace(/^\//, '').split('/')[0], - master = {}, - id = $stateParams.job_template_id; - // checkSCMStatus, getPlaybooks, callback, - // choicesCount = 0; - - CredentialList = _.cloneDeep(CredentialList); - - // The form uses awPopOverWatch directive to 'watch' scope.callback_help for changes. Each time the - // popover is activated, a function checks the value of scope.callback_help before constructing the content. - scope.setCallbackHelp = function() { - scope.callback_help = "

With a provisioning callback URL and a host config key a host can contact Tower and request a configuration update using this job " + - "template. The request from the host must be a POST. Here is an example using curl:

\n" + - "
curl --data \"host_config_key=" + scope.example_config_key + "\" " +
-                                  scope.callback_server_path + GetBasePath('job_templates') + scope.example_template_id + "/callback/
\n" + - "

Note the requesting host must be defined in the inventory associated with the job template. If Tower fails to " + - "locate the host, the request will be denied.

" + - "

Successful requests create an entry on the Jobs page, where results and history can be viewed.

"; - }; - - // The md5 helper emits NewMD5Generated whenever a new key is available - if (scope.removeNewMD5Generated) { - scope.removeNewMD5Generated(); - } - scope.removeNewMD5Generated = scope.$on('NewMD5Generated', function() { - scope.configKeyChange(); - }); - - // Fired when user enters a key value - scope.configKeyChange = function() { - scope.example_config_key = scope.host_config_key; - scope.setCallbackHelp(); - }; - - // Set initial values and construct help text - scope.callback_server_path = $location.protocol() + '://' + $location.host() + (($location.port()) ? ':' + $location.port() : ''); - scope.example_config_key = '5a8ec154832b780b9bdef1061764ae5a'; - scope.example_template_id = 'N'; - scope.setCallbackHelp(); - - // this fills the job template form both on copy of the job template - // and on edit - scope.fillJobTemplate = function(){ - // id = id || $rootScope.copy.id; - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl + id); - Rest.get() - .success(function (data) { - scope.job_template_obj = data; - scope.name = data.name; - var fld, i; - for (fld in form.fields) { - if (fld !== 'variables' && fld !== 'survey' && data[fld] !== null && data[fld] !== undefined) { - if (form.fields[fld].type === 'select') { - if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) { - for (i = 0; i < scope[fld + '_options'].length; i++) { - if (data[fld] === scope[fld + '_options'][i].value) { - scope[fld] = scope[fld + '_options'][i]; - } - } - } else { - scope[fld] = data[fld]; - } - } else { - scope[fld] = data[fld]; - if(!Empty(data.summary_fields.survey)) { - scope.survey_exists = true; - } - } - master[fld] = scope[fld]; - } - if (fld === 'variables') { - // Parse extra_vars, converting to YAML. - scope.variables = ParseVariableString(data.extra_vars); - master.variables = scope.variables; - } - if (form.fields[fld].type === 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { - scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; - } - if (form.fields[fld].type === 'checkbox_group') { - for(var j=0; j" + i18n._("Submit the request to cancel?") + "
"; - var deleteBody = "
" + i18n._("Are you sure you want to delete the job below?") + "
#" + id + " " + $filter('sanitize')(job.name) + "
"; - Prompt({ - hdr: hdr, - body: (action_label === 'cancel' || job.status === 'new') ? cancelBody : deleteBody, - action: action, - actionText: (action_label === 'cancel' || job.status === 'new') ? "OK" : "DELETE" - }); - }); - - if (action_label === 'cancel') { - Rest.setUrl(url); - Rest.get() - .success(function(data) { - if (data.can_cancel) { - scope.$emit('CancelJob'); - } - else { - scope.$emit('CancelNotAllowed'); - } - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + - ' failed. GET returned: ' + status }); - }); - } - else { - scope.$emit('CancelJob'); - } - - }; - }]) - - .factory('RelaunchInventory', ['Find', 'Wait', 'Rest', 'InventoryUpdate', 'ProcessErrors', 'GetBasePath', - function(Find, Wait, Rest, InventoryUpdate, ProcessErrors, GetBasePath) { - return function(params) { - var scope = params.scope, - id = params.id, - url = GetBasePath('inventory_sources') + id + '/'; - Wait('start'); - Rest.setUrl(url); - Rest.get() - .success(function (data) { - InventoryUpdate({ - scope: scope, - url: data.related.update, - group_name: data.summary_fields.group.name, - group_source: data.source, - tree_id: null, - group_id: data.group - }); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + - url + ' GET returned: ' + status }); - }); - }; - }]) - - .factory('RelaunchPlaybook', ['InitiatePlaybookRun', function(InitiatePlaybookRun) { - return function(params) { - var scope = params.scope, - id = params.id, - job_type = params.job_type; - InitiatePlaybookRun({ scope: scope, id: id, relaunch: true, job_type: job_type }); - }; - }]) - - .factory('RelaunchSCM', ['ProjectUpdate', function(ProjectUpdate) { - return function(params) { - var scope = params.scope, - id = params.id; - ProjectUpdate({ scope: scope, project_id: id }); - }; - }]) - - .factory('RelaunchAdhoc', ['AdhocRun', function(AdhocRun) { - return function(params) { - var scope = params.scope, - id = params.id; - AdhocRun({ scope: scope, project_id: id, relaunch: true }); - }; - }]); diff --git a/awx/ui/client/src/helpers/Parse.js b/awx/ui/client/src/helpers/Parse.js deleted file mode 100644 index a2715f6efd..0000000000 --- a/awx/ui/client/src/helpers/Parse.js +++ /dev/null @@ -1,108 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Parse - * @description - * Show the CodeMirror variable editor and allow - * toggle between JSON and YAML - * - */ - -import 'codemirror/lib/codemirror.js'; -import 'codemirror/mode/javascript/javascript.js'; -import 'codemirror/mode/yaml/yaml.js'; -import 'codemirror/addon/lint/lint.js'; -import 'angular-codemirror/lib/yaml-lint.js'; -import 'codemirror/addon/edit/closebrackets.js'; -import 'codemirror/addon/edit/matchbrackets.js'; -import 'codemirror/addon/selection/active-line.js'; - - -export default - angular.module('ParseHelper', ['Utilities', 'AngularCodeMirrorModule']) - .factory('ParseTypeChange', ['Alert', 'AngularCodeMirror', - function (Alert, AngularCodeMirror) { - return function (params) { - - var scope = params.scope, - field_id = params.field_id, - fld = (params.variable) ? params.variable : 'variables', - pfld = (params.parse_variable) ? params.parse_variable : 'parseType', - onReady = params.onReady, - onChange = params.onChange, - readOnly = params.readOnly; - - function removeField(fld) { - //set our model to the last change in CodeMirror and then destroy CodeMirror - scope[fld] = scope[fld + 'codeMirror'].getValue(); - $('#cm-' + fld + '-container > .CodeMirror').empty().remove(); - } - - function createField(onChange, onReady, fld) { - //hide the textarea and show a fresh CodeMirror with the current mode (json or yaml) - - scope[fld + 'codeMirror'] = AngularCodeMirror(readOnly); - scope[fld + 'codeMirror'].addModes(global.$AnsibleConfig.variable_edit_modes); - scope[fld + 'codeMirror'].showTextArea({ - scope: scope, - model: fld, - element: field_id, - lineNumbers: true, - mode: scope[pfld], - onReady: onReady, - onChange: onChange - }); - } - - // Hide the textarea and show a CodeMirror editor - createField(onChange, onReady, fld); - - - // Toggle displayed variable string between JSON and YAML - scope.parseTypeChange = function(model, fld) { - var json_obj; - if (scope[model] === 'json') { - // converting yaml to json - try { - removeField(fld); - json_obj = jsyaml.load(scope[fld]); - if ($.isEmptyObject(json_obj)) { - scope[fld] = "{}"; - } - else { - scope[fld] = JSON.stringify(json_obj, null, " "); - } - createField(onReady, onChange, fld); - } - catch (e) { - Alert('Parse Error', 'Failed to parse valid YAML. ' + e.message); - setTimeout( function() { scope.$apply( function() { scope[model] = 'yaml'; createField(onReady, onChange, fld); }); }, 500); - } - } - else { - // convert json to yaml - try { - removeField(fld); - json_obj = JSON.parse(scope[fld]); - if ($.isEmptyObject(json_obj)) { - scope[fld] = '---'; - } - else { - scope[fld] = jsyaml.safeDump(json_obj); - } - createField(onReady, onChange, fld); - } - catch (e) { - Alert('Parse Error', 'Failed to parse valid JSON. ' + e.message); - setTimeout( function() { scope.$apply( function() { scope[model] = 'json'; createField(onReady, onChange, fld); }); }, 500 ); - } - } - }; - }; - } - ]); diff --git a/awx/ui/client/src/helpers/ProjectPath.js b/awx/ui/client/src/helpers/ProjectPath.js deleted file mode 100644 index 83753be443..0000000000 --- a/awx/ui/client/src/helpers/ProjectPath.js +++ /dev/null @@ -1,91 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:ProjectPath - * @description - * Use GetProjectPath({ scope: , master: }) to - * load scope.project_local_paths (array of options for drop-down) and - * scope.base_dir (readonly field). - * - */ - -export default - angular.module('ProjectPathHelper', ['RestServices', 'Utilities']) - .factory('GetProjectPath', ['Alert', 'Rest', 'GetBasePath', 'ProcessErrors', - function (Alert, Rest, GetBasePath, ProcessErrors) { - return function (params) { - - var scope = params.scope, - master = params.master; - - function arraySort(data) { - //Sort nodes by name - var i, j, names = [], - newData = []; - for (i = 0; i < data.length; i++) { - names.push(data[i].value); - } - names.sort(); - for (j = 0; j < names.length; j++) { - for (i = 0; i < data.length; i++) { - if (data[i].value === names[j]) { - newData.push(data[i]); - } - } - } - return newData; - } - - scope.showMissingPlaybooksAlert = false; - - Rest.setUrl(GetBasePath('config')); - Rest.get() - .success(function (data) { - var opts = [], i; - if (data.project_local_paths) { - for (i = 0; i < data.project_local_paths.length; i++) { - opts.push({ - label: data.project_local_paths[i], - value: data.project_local_paths[i] - }); - } - } - if (scope.local_path) { - // List only includes paths not assigned to projects, so add the - // path assigned to the current project. - opts.push({ - label: scope.local_path, - value: scope.local_path - }); - } - scope.project_local_paths = arraySort(opts); - if (scope.local_path) { - for (i = 0; scope.project_local_paths.length; i++) { - if (scope.project_local_paths[i].value === scope.local_path) { - scope.local_path = scope.project_local_paths[i]; - break; - } - } - } - scope.base_dir = data.project_base_dir; - master.local_path = scope.local_path; - master.base_dir = scope.base_dir; // Keep in master object so that it doesn't get - // wiped out on form reset. - if (opts.length === 0) { - // trigger display of alert block when scm_type == manual - scope.showMissingPlaybooksAlert = true; - } - scope.$emit('pathsReady'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to access API config. GET status: ' + status }); - }); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Projects.js b/awx/ui/client/src/helpers/Projects.js deleted file mode 100644 index 613dac0c86..0000000000 --- a/awx/ui/client/src/helpers/Projects.js +++ /dev/null @@ -1,84 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Projects - * @description - * Use GetProjectPath({ scope: , master: }) to - * load scope.project_local_paths (array of options for drop-down) and - * scope.base_dir (readonly field). - * - */ - - -export default - angular.module('ProjectsHelper', ['RestServices', 'Utilities', 'ProjectStatusDefinition', 'ProjectFormDefinition']) - - .factory('GetProjectIcon', [ function() { - return function(status) { - var result = ''; - switch (status) { - case 'n/a': - case 'ok': - case 'never updated': - result = 'none'; - break; - case 'pending': - case 'waiting': - case 'new': - result = 'none'; - break; - case 'updating': - case 'running': - result = 'running'; - break; - case 'successful': - result = 'success'; - break; - case 'failed': - case 'missing': - case 'canceled': - result = 'error'; - } - return result; - }; - }]) - - .factory('GetProjectToolTip', ['i18n', function(i18n) { - return function(status) { - var result = ''; - switch (status) { - case 'n/a': - case 'ok': - case 'never updated': - result = i18n._('No SCM updates have run for this project'); - break; - case 'pending': - case 'waiting': - case 'new': - result = i18n._('Queued. Click for details'); - break; - case 'updating': - case 'running': - result = i18n._('Running! Click for details'); - break; - case 'successful': - result = i18n._('Success! Click for details'); - break; - case 'failed': - result = i18n._('Failed. Click for details'); - break; - case 'missing': - result = i18n._('Missing. Click for details'); - break; - case 'canceled': - result = i18n._('Canceled. Click for details'); - break; - } - return result; - }; - }]); diff --git a/awx/ui/client/src/helpers/Schedules.js b/awx/ui/client/src/helpers/Schedules.js deleted file mode 100644 index 90c1589e5b..0000000000 --- a/awx/ui/client/src/helpers/Schedules.js +++ /dev/null @@ -1,506 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Schedules - * @description - * Schedules Helper - * - * Display the scheduler widget in a dialog - * - */ - -import listGenerator from '../shared/list-generator/main'; - -export default - angular.module('SchedulesHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', listGenerator.name, 'ModalDialog', - 'GeneratorHelpers']) - - .factory('EditSchedule', ['SchedulerInit', '$rootScope', 'Wait', 'Rest', - 'ProcessErrors', 'GetBasePath', 'SchedulePost', '$state', - function(SchedulerInit, $rootScope, Wait, Rest, ProcessErrors, - GetBasePath, SchedulePost, $state) { - return function(params) { - var scope = params.scope, - id = params.id, - callback = params.callback, - schedule, scheduler, - url = GetBasePath('schedules') + id + '/'; - - delete scope.isFactCleanup; - delete scope.cleanupJob; - - function setGranularity(){ - var a,b, prompt_for_days, - keep_unit, - granularity, - granularity_keep_unit; - - if(scope.cleanupJob){ - scope.schedulerPurgeDays = Number(schedule.extra_data.days); - // scope.scheduler_form.schedulerPurgeDays.$setViewValue( Number(schedule.extra_data.days)); - } - else if(scope.isFactCleanup){ - scope.keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - scope.granularity_keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - // the API returns something like 20w or 1y - a = schedule.extra_data.older_than; // "20y" - b = schedule.extra_data.granularity; // "1w" - prompt_for_days = Number(_.initial(a,1).join('')); // 20 - keep_unit = _.last(a); // "y" - granularity = Number(_.initial(b,1).join('')); // 1 - granularity_keep_unit = _.last(b); // "w" - - scope.keep_amount = prompt_for_days; - scope.granularity_keep_amount = granularity; - scope.keep_unit = _.find(scope.keep_unit_choices, function(i){ - return i.value === keep_unit; - }); - scope.granularity_keep_unit =_.find(scope.granularity_keep_unit_choices, function(i){ - return i.value === granularity_keep_unit; - }); - } - } - - if (scope.removeScheduleFound) { - scope.removeScheduleFound(); - } - scope.removeScheduleFound = scope.$on('ScheduleFound', function() { - $('#form-container').empty(); - scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); - scheduler.inject('form-container', false); - scheduler.injectDetail('occurrences', false); - - if (!/DTSTART/.test(schedule.rrule)) { - schedule.rrule += ";DTSTART=" + schedule.dtstart.replace(/\.\d+Z$/,'Z'); - } - schedule.rrule = schedule.rrule.replace(/ RRULE:/,';'); - schedule.rrule = schedule.rrule.replace(/DTSTART:/,'DTSTART='); - scope.$on("htmlDetailReady", function() { - scheduler.setRRule(schedule.rrule); - scheduler.setName(schedule.name); - $rootScope.$broadcast("ScheduleFormCreated", scope); - }); - scope.showRRuleDetail = false; - - scheduler.setRRule(schedule.rrule); - scheduler.setName(schedule.name); - if(scope.isFactCleanup || scope.cleanupJob){ - setGranularity(); - } - }); - - - if (scope.removeScheduleSaved) { - scope.removeScheduleSaved(); - } - scope.removeScheduleSaved = scope.$on('ScheduleSaved', function(e, data) { - Wait('stop'); - if (callback) { - scope.$emit(callback, data); - } - $state.go("^"); - }); - scope.saveSchedule = function() { - schedule.extra_data = scope.extraVars; - SchedulePost({ - scope: scope, - url: url, - scheduler: scheduler, - callback: 'ScheduleSaved', - mode: 'edit', - schedule: schedule - }); - }; - - Wait('start'); - - // Get the existing record - Rest.setUrl(url); - Rest.get() - .success(function(data) { - schedule = data; - try { - schedule.extra_data = JSON.parse(schedule.extra_data); - } catch(e) { - // do nothing - } - scope.extraVars = data.extra_data === '' ? '---' : '---\n' + jsyaml.safeDump(data.extra_data); - - if(schedule.extra_data.hasOwnProperty('granularity')){ - scope.isFactCleanup = true; - } - if (schedule.extra_data.hasOwnProperty('days')){ - scope.cleanupJob = true; - } - - scope.schedule_obj = data; - - scope.$emit('ScheduleFound'); - }) - .error(function(data,status){ - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve schedule ' + id + ' GET returned: ' + status }); - }); - }; - }]) - - .factory('AddSchedule', ['$location', '$rootScope', '$stateParams', - 'SchedulerInit', 'Wait', 'GetBasePath', 'Empty', 'SchedulePost', '$state', 'Rest', 'ProcessErrors', - function($location, $rootScope, $stateParams, SchedulerInit, - Wait, GetBasePath, Empty, SchedulePost, $state, Rest, - ProcessErrors) { - return function(params) { - var scope = params.scope, - callback= params.callback, - base = params.base || $location.path().replace(/^\//, '').split('/')[0], - url = params.url || null, - scheduler, - job_type; - - job_type = scope.parentObject.job_type; - if (!Empty($stateParams.id) && base !== 'system_job_templates' && base !== 'inventories' && !url) { - url = GetBasePath(base) + $stateParams.id + '/schedules/'; - } - else if(base === "inventories"){ - if (!params.url){ - url = GetBasePath('groups') + $stateParams.id + '/'; - Rest.setUrl(url); - Rest.get(). - then(function (data) { - url = data.data.related.inventory_source + 'schedules/'; - }).catch(function (response) { - ProcessErrors(null, response.data, response.status, null, { - hdr: 'Error!', - msg: 'Failed to get inventory group info. GET returned status: ' + - response.status - }); - }); - } - else { - url = params.url; - } - } - else if (base === 'system_job_templates') { - url = GetBasePath(base) + $stateParams.id + '/schedules/'; - if(job_type === "cleanup_facts"){ - scope.isFactCleanup = true; - scope.keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - scope.granularity_keep_unit_choices = [{ - "label" : "Days", - "value" : "d" - }, - { - "label": "Weeks", - "value" : "w" - }, - { - "label" : "Years", - "value" : "y" - }]; - scope.prompt_for_days_facts_form.keep_amount.$setViewValue(30); - scope.prompt_for_days_facts_form.granularity_keep_amount.$setViewValue(1); - scope.keep_unit = scope.keep_unit_choices[0]; - scope.granularity_keep_unit = scope.granularity_keep_unit_choices[1]; - } - else { - scope.cleanupJob = true; - } - } - - Wait('start'); - $('#form-container').empty(); - scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); - if(scope.schedulerUTCTime) { - // The UTC time is already set - scope.processSchedulerEndDt(); - } - else { - // We need to wait for it to be set by angular-scheduler because the following function depends - // on it - var schedulerUTCTimeWatcher = scope.$watch('schedulerUTCTime', function(newVal) { - if(newVal) { - // Remove the watcher - schedulerUTCTimeWatcher(); - scope.processSchedulerEndDt(); - } - }); - } - scheduler.inject('form-container', false); - scheduler.injectDetail('occurrences', false); - scheduler.clear(); - scope.$on("htmlDetailReady", function() { - $rootScope.$broadcast("ScheduleFormCreated", scope); - }); - scope.showRRuleDetail = false; - - if (scope.removeScheduleSaved) { - scope.removeScheduleSaved(); - } - scope.removeScheduleSaved = scope.$on('ScheduleSaved', function(e, data) { - Wait('stop'); - if (callback) { - scope.$emit(callback, data); - } - $state.go("^", null, {reload: true}); - }); - scope.saveSchedule = function() { - SchedulePost({ - scope: scope, - url: url, - scheduler: scheduler, - callback: 'ScheduleSaved', - mode: 'add' - }); - }; - - $('#scheduler-tabs li a').on('shown.bs.tab', function(e) { - if ($(e.target).text() === 'Details') { - if (!scheduler.isValid()) { - $('#scheduler-tabs a:first').tab('show'); - } - } - }); - }; - }]) - - .factory('SchedulePost', ['Rest', 'ProcessErrors', 'RRuleToAPI', 'Wait', - function(Rest, ProcessErrors, RRuleToAPI, Wait) { - return function(params) { - var scope = params.scope, - url = params.url, - scheduler = params.scheduler, - mode = params.mode, - schedule = (params.schedule) ? params.schedule : {}, - callback = params.callback, - newSchedule, rrule, extra_vars; - if (scheduler.isValid()) { - Wait('start'); - newSchedule = scheduler.getValue(); - rrule = scheduler.getRRule(); - schedule.name = newSchedule.name; - schedule.rrule = RRuleToAPI(rrule.toString()); - schedule.description = (/error/.test(rrule.toText())) ? '' : rrule.toText(); - - if (scope.isFactCleanup) { - extra_vars = { - "older_than": scope.scheduler_form.keep_amount.$viewValue + scope.scheduler_form.keep_unit.$viewValue.value, - "granularity": scope.scheduler_form.granularity_keep_amount.$viewValue + scope.scheduler_form.granularity_keep_unit.$viewValue.value - }; - schedule.extra_data = JSON.stringify(extra_vars); - } else if (scope.cleanupJob) { - extra_vars = { - "days" : scope.scheduler_form.schedulerPurgeDays.$viewValue - }; - schedule.extra_data = JSON.stringify(extra_vars); - } - else if(scope.extraVars){ - schedule.extra_data = scope.parseType === 'yaml' ? - (scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars; - } - Rest.setUrl(url); - if (mode === 'add') { - Rest.post(schedule) - .success(function(){ - if (callback) { - scope.$emit(callback); - } - else { - Wait('stop'); - } - }) - .error(function(data, status){ - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'POST to ' + url + ' returned: ' + status }); - }); - } - else { - Rest.put(schedule) - .success(function(){ - if (callback) { - scope.$emit(callback, schedule); - } - else { - Wait('stop'); - } - }) - .error(function(data, status){ - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'POST to ' + url + ' returned: ' + status }); - }); - } - } - else { - return false; - } - }; - }]) - - /** - * Flip a schedule's enable flag - * - * ToggleSchedule({ - * scope: scope, - * id: schedule.id to update - * callback: scope.$emit label to call when update completes - * }); - * - */ - .factory('ToggleSchedule', ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest', '$state', - function(Wait, GetBasePath, ProcessErrors, Rest, $state) { - return function(params) { - var scope = params.scope, - id = params.id, - url = GetBasePath('schedules') + id +'/'; - - // Perform the update - if (scope.removeScheduleFound) { - scope.removeScheduleFound(); - } - scope.removeScheduleFound = scope.$on('ScheduleFound', function(e, data) { - data.enabled = (data.enabled) ? false : true; - Rest.put(data) - .success( function() { - Wait('stop'); - $state.go('.', null, {reload: true}); - }) - .error( function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to update schedule ' + id + ' PUT returned: ' + status }); - }); - }); - - Wait('start'); - - // Get the schedule - Rest.setUrl(url); - Rest.get() - .success(function(data) { - scope.$emit('ScheduleFound', data); - }) - .error(function(data,status){ - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve schedule ' + id + ' GET returned: ' + status }); - }); - }; - }]) - - /** - * Delete a schedule. Prompts user to confirm delete - * - * DeleteSchedule({ - * scope: $scope containing list of schedules - * id: id of schedule to delete - * callback: $scope.$emit label to call when delete is completed - * }) - * - */ - .factory('DeleteSchedule', ['GetBasePath','Rest', 'Wait', '$state', - 'ProcessErrors', 'Prompt', 'Find', '$location', '$filter', - function(GetBasePath, Rest, Wait, $state, ProcessErrors, Prompt, Find, - $location, $filter) { - return function(params) { - - var scope = params.scope, - id = params.id, - callback = params.callback, - action, schedule, list, url, hdr; - - if (scope.schedules) { - list = scope.schedules; - } - else if (scope.scheduled_jobs) { - list = scope.scheduled_jobs; - } - - url = GetBasePath('schedules') + id + '/'; - schedule = Find({list: list, key: 'id', val: id }); - hdr = 'Delete Schedule'; - - action = function () { - Wait('start'); - Rest.setUrl(url); - Rest.destroy() - .success(function () { - $('#prompt-modal').modal('hide'); - scope.$emit(callback, id); - if (new RegExp('/' + id + '$').test($location.$$url)) { - $location.url($location.url().replace(/[/][0-9]+$/, "")); // go to list view - } - else{ - $state.go('.', null, {reload: true}); - } - }) - .error(function (data, status) { - try { - $('#prompt-modal').modal('hide'); - } - catch(e) { - // ignore - } - ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + - ' failed. DELETE returned: ' + status }); - }); - }; - - Prompt({ - hdr: hdr, - body: '
Are you sure you want to delete the schedule below?
' + $filter('sanitize')(schedule.name) + '
', - action: action, - actionText: 'DELETE', - backdrop: false - }); - - }; - }]) - - /** - * Convert rrule string to an API agreeable format - * - */ - .factory('RRuleToAPI', [ function() { - return function(rrule) { - var response; - response = rrule.replace(/(^.*(?=DTSTART))(DTSTART=.*?;)(.*$)/, function(str, p1, p2, p3) { - return p2.replace(/\;/,'').replace(/=/,':') + ' ' + 'RRULE:' + p1 + p3; - }); - return response; - }; - }]); diff --git a/awx/ui/client/src/helpers/Selection.js b/awx/ui/client/src/helpers/Selection.js deleted file mode 100644 index 3fa4fa826b..0000000000 --- a/awx/ui/client/src/helpers/Selection.js +++ /dev/null @@ -1,177 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Selection - * @description - * SelectionHelper - * Used in list controllers where the list might also be used as a selection list. - * - * SelectionInit( { - * scope: , - * list: - * }) - */ - - -export default - angular.module('SelectionHelper', ['Utilities', 'RestServices']) - - .factory('SelectionInit', ['Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'Wait', - function (Rest, Alert, ProcessErrors, ReturnToCaller, Wait) { - return function (params) { - - var scope = params.scope, - list = params.list, - target_url = params.url, - returnToCaller = params.returnToCaller, - selected; - - if (params.selected !== undefined) { - selected = params.selected; - } else { - selected = []; //array of selected row IDs - } - - scope.formModalActionDisabled = true; - scope.disableSelectBtn = true; - - // toggle row selection - scope['toggle_' + list.iterator] = function (id, ischeckbox) { - var i, j, found; - for (i = 0; i < scope[list.name].length; i++) { - if (scope[list.name][i].id === id) { - var control = scope[list.name][i]; - if (ischeckbox && control.checked) { - scope[list.name][i].success_class = 'success'; - // add selected object to the array - found = false; - for (j = 0; j < selected.length; j++) { - if (selected[j].id === id) { - found = true; - break; - } - } - if (!found) { - selected.push(scope[list.name][i]); - } - } else if (ischeckbox) { - scope[list.name][i].success_class = ''; - - // remove selected object from the array - for (j = 0; j < selected.length; j++) { - if (selected[j].id === id) { - selected.splice(j, 1); - break; - } - } - } - } - } - if (selected.length > 0) { - scope.formModalActionDisabled = false; - scope.disableSelectBtn = false; - } else { - scope.formModalActionDisabled = true; - scope.disableSelectBtn = true; - } - }; - - // Add the selections - scope.finishSelection = function () { - Rest.setUrl(target_url); - - var queue = [], j; - - scope.formModalActionDisabled = true; - scope.disableSelectBtn = true; - - Wait('start'); - - function finished() { - selected = []; - if (returnToCaller !== undefined) { - ReturnToCaller(returnToCaller); - } else { - $('#form-modal').modal('hide'); - scope.$emit('modalClosed'); - } - } - - function postIt(data) { - Rest.post(data) - .success(function (data, status) { - queue.push({ result: 'success', data: data, status: status }); - scope.$emit('callFinished'); - }) - .error(function (data, status, headers) { - queue.push({ result: 'error', data: data, status: status, headers: headers }); - scope.$emit('callFinished'); - }); - } - - if (scope.callFinishedRemove) { - scope.callFinishedRemove(); - } - scope.callFinishedRemove = scope.$on('callFinished', function () { - // We call the API for each selected item. We need to hang out until all the api - // calls are finished. - var i, errors=0; - if (queue.length === selected.length) { - Wait('stop'); - for (i = 0; i < queue.length; i++) { - if (queue[i].result === 'error') { - ProcessErrors(scope, queue[i].data, queue[i].status, null, { hdr: 'POST Failure', - msg: 'Failed to add ' + list.iterator + '. POST returned status: ' + queue[i].status }); - errors++; - } - } - if (errors === 0) { - finished(); - } - } - }); - - if (selected.length > 0) { - for (j = 0; j < selected.length; j++) { - postIt(selected[j]); - } - } else { - finished(); - } - }; - - scope.formModalAction = scope.finishSelection; - - // Initialize our data set after a refresh (page change or search) - if (scope.SelectPostRefreshRemove) { - scope.SelectPostRefreshRemove(); - } - scope.SelectPostRefreshRemove = scope.$on('PostRefresh', function () { - var i, j, found; - if (scope[list.name]) { - for (i = 0; i < scope[list.name].length; i++) { - found = false; - for (j = 0; j < selected.length; j++) { - if (selected[j].id === scope[list.name][i].id) { - found = true; - break; - } - } - if (found) { - scope[list.name][i].checked = '1'; - scope[list.name][i].success_class = 'success'; - } else { - scope[list.name][i].checked = '0'; - scope[list.name][i].success_class = ''; - } - } - } - }); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Users.js b/awx/ui/client/src/helpers/Users.js deleted file mode 100644 index 7f09b486c3..0000000000 --- a/awx/ui/client/src/helpers/Users.js +++ /dev/null @@ -1,46 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Users - * @description - * UserHelper - * Routines shared amongst the user controllers - * - */ - - -export default - angular.module('UserHelper', ['UserFormDefinition']) - .factory('ResetForm', ['UserForm', - function (UserForm) { - return function () { - // Restore form to default conditions. Run before applying LDAP configuration. - // LDAP may manage some or all of these fields in which case the user cannot - // make changes to their values in AWX. - - UserForm.fields.first_name.readonly = false; - UserForm.fields.first_name.editRequired = true; - UserForm.fields.last_name.readonly = false; - UserForm.fields.last_name.editRequired = true; - UserForm.fields.email.readonly = false; - UserForm.fields.email.editRequired = true; - UserForm.fields.organization.awRequiredWhen = { - reqExpression: "orgrequired", - init: true - }; - UserForm.fields.organization.readonly = false; - UserForm.fields.username.awRequiredWhen = { - reqExpression: "not_ldap_user", - init: true - }; - UserForm.fields.username.readonly = false; - UserForm.fields.password.editRequired = false; - UserForm.fields.password.addRrequired = true; - }; - } - ]); diff --git a/awx/ui/client/src/helpers/Variables.js b/awx/ui/client/src/helpers/Variables.js deleted file mode 100644 index 8364c4c15b..0000000000 --- a/awx/ui/client/src/helpers/Variables.js +++ /dev/null @@ -1,180 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Variables - * @description - * VariablesHelper - * - * - */ - -export default -angular.module('VariablesHelper', ['Utilities']) - - /** - variables: string containing YAML or JSON | a JSON object. - - If JSON string, convert to JSON object and run through jsyaml.safeDump() to create a YAML document. If YAML, - will attempt to load via jsyaml.safeLoad() and return a YAML document using jsyaml.safeDump(). In all cases - a YAML document is returned. - **/ - .factory('ParseVariableString', ['$log', 'ProcessErrors', 'SortVariables', function ($log, ProcessErrors, SortVariables) { - return function (variables) { - var result = "---", json_obj; - if (typeof variables === 'string') { - if (variables === "{}" || variables === "null" || variables === "" || variables === "\"\"") { - // String is empty, return --- - } else { - try { - json_obj = JSON.parse(variables); - json_obj = SortVariables(json_obj); - result = jsyaml.safeDump(json_obj); - - } - catch (e) { - $log.debug('Attempt to parse extra_vars as JSON failed. Check that the variables parse as yaml. Set the raw string as the result.'); - try { - // do safeLoad, which well error if not valid yaml - json_obj = jsyaml.safeLoad(variables); - // but just send the variables - result = variables; - } - catch(e2) { - ProcessErrors(null, variables, e2.message, null, { hdr: 'Error!', - msg: 'Attempts to parse variables as JSON and YAML failed. Last attempt returned: ' + e2.message }); - } - } - } - } - else { - if ($.isEmptyObject(variables) || variables === null) { - // Empty object, return --- - } - else { - // convert object to yaml - try { - json_obj = SortVariables(variables); - result = jsyaml.safeDump(json_obj); - // result = variables; - } - catch(e3) { - ProcessErrors(null, variables, e3.message, null, { hdr: 'Error!', - msg: 'Attempt to convert JSON object to YAML document failed: ' + e3.message }); - } - } - } - return result; - }; - }]) - - /** - parseType: 'json' | 'yaml' - variables: string containing JSON or YAML - stringify: optional, boolean - - Parse the given string according to the parseType to a JSON object. If stringify true, - stringify the object and return the string. Otherwise, return the JSON object. - - **/ - .factory('ToJSON', ['$log', 'ProcessErrors', function($log, ProcessErrors) { - return function(parseType, variables, stringify, reviver) { - var json_data, - result, - tmp; - // bracketVar, - // key, - // lines, i, newVars = []; - if (parseType === 'json') { - try { - // perform a check to see if the user cleared the field completly - if(variables.trim() === "" || variables.trim() === "{" || variables.trim() === "}" ){ - variables = "{}"; - } - //parse a JSON string - if (reviver) { - json_data = JSON.parse(variables, reviver); - } - else { - json_data = JSON.parse(variables); - } - } - catch(e) { - json_data = {}; - $log.error('Failed to parse JSON string. Parser returned: ' + e.message); - ProcessErrors(null, variables, e.message, null, { hdr: 'Error!', - msg: 'Failed to parse JSON string. Parser returned: ' + e.message }); - throw 'Parse error. Failed to parse variables.'; - } - } else { - try { - if(variables.trim() === "" || variables.trim() === "-" || variables.trim() === "--"){ - variables = '---'; - } - json_data = jsyaml.safeLoad(variables); - if(json_data!==null){ - // unparsing just to make sure no weird characters are included. - tmp = jsyaml.dump(json_data); - if(tmp.indexOf('[object Object]')!==-1){ - throw "Failed to parse YAML string. Parser returned' + key + ' : ' +value + '.' "; - } - } - } - catch(e) { - json_data = undefined; // {}; - // $log.error('Failed to parse YAML string. Parser returned undefined'); - ProcessErrors(null, variables, e.message, null, { hdr: 'Error!', - msg: 'Failed to parse YAML string. Parser returned undefined'}); - } - } - // Make sure our JSON is actually an object - if (typeof json_data !== 'object') { - ProcessErrors(null, variables, null, null, { hdr: 'Error!', - msg: 'Failed to parse variables. Attempted to parse ' + parseType + '. Parser did not return an object.' }); - // setTimeout( function() { - throw 'Parse error. Failed to parse variables.'; - // }, 1000); - } - result = json_data; - if (stringify) { - if(json_data === undefined){ - result = undefined; - } - else if ($.isEmptyObject(json_data)) { - result = ""; - } else { - // utilize the parsing to get here - // but send the raw variable string - result = variables; - } - } - return result; - }; - }]) - - .factory('SortVariables', [ function() { - return function(variableObj) { - var newObj; - function sortIt(objToSort) { - var i, - keys = Object.keys(objToSort), - newObj = {}; - keys = keys.sort(); - for (i=0; i < keys.length; i++) { - if (typeof objToSort[keys[i]] === 'object' && objToSort[keys[i]] !== null && !Array.isArray(objToSort[keys[i]])) { - newObj[keys[i]] = sortIt(objToSort[keys[i]]); - } - else { - newObj[keys[i]] = objToSort[keys[i]]; - } - } - return newObj; - } - newObj = sortIt(variableObj); - return newObj; - }; - }]); diff --git a/awx/ui/client/src/helpers/api-defaults.js b/awx/ui/client/src/helpers/api-defaults.js deleted file mode 100644 index bf9308b0da..0000000000 --- a/awx/ui/client/src/helpers/api-defaults.js +++ /dev/null @@ -1,94 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:api-defaults - * @description this could use more discussion -*/ - -export default - angular.module('APIDefaults', ['RestServices', 'Utilities']) - .factory('GetAPIDefaults', ['Alert', 'Rest', '$rootScope', - function (Alert, Rest, $rootScope) { - return function (key) { - - //Reload a related collection on pagination or search change - - var result = {}, cnt = 0, url; - - function lookup(key) { - var id, result = {}; - for (id in $rootScope.apiDefaults) { - if (id === key || id.iterator === key) { - result[id] = $rootScope.apiDefaults[id]; - break; - } - } - return result; - } - - function wait() { - if ($.isEmptyObject(result) && cnt < 5) { - cnt++; - setTimeout(1000, wait()); - } else if (result.status === 'success') { - return lookup(key); - } - } - - if ($rootScope.apiDefaults === null || $rootScope.apiDefaults === undefined) { - url = '/api/v1/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - var id, defaults = data; - for (id in defaults) { - switch (id) { - case 'organizations': - defaults[id].iterator = 'organization'; - break; - case 'jobs': - defaults[id].iterator = 'job'; - break; - case 'users': - defaults[id].iterator = 'user'; - break; - case 'teams': - defaults[id].iterator = 'team'; - break; - case 'hosts': - defaults[id].iterator = 'host'; - break; - case 'groups': - defaults[id].iterator = 'group'; - break; - case 'projects': - defaults[id].iterator = 'project'; - break; - case 'inventories': - defaults[id].iterator = 'inventory'; - break; - } - } - $rootScope.apiDefaults = defaults; - result = { - status: 'success' - }; - }) - .error(function (data, status) { - result = { - status: 'error', - msg: 'Call to ' + url + ' failed. GET returned status: ' + status - }; - }); - return wait(); - } else { - return lookup(key); - } - }; - } - ]); diff --git a/awx/ui/client/src/helpers/inventory.js b/awx/ui/client/src/helpers/inventory.js deleted file mode 100644 index 1310899fa0..0000000000 --- a/awx/ui/client/src/helpers/inventory.js +++ /dev/null @@ -1,84 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:Inventory - * @description InventoryHelper - * Routines for building the tree. Everything related to the tree is here except - * for the menu piece. The routine for building the menu is in InventoriesEdit controller - * (controllers/Inventories.js) -*/ - -import listGenerator from '../shared/list-generator/main'; - -export default - angular.module('InventoryHelper', ['RestServices', 'Utilities', 'OrganizationListDefinition', listGenerator.name, - 'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper', 'VariablesHelper', - ]) - - .factory('SaveInventory', ['InventoryForm', 'Rest', 'Alert', 'ProcessErrors', 'OrganizationList', - 'GetBasePath', 'ParseTypeChange', 'Wait', 'ToJSON', - function (InventoryForm, Rest, Alert, ProcessErrors, OrganizationList, GetBasePath, ParseTypeChange, Wait, - ToJSON) { - return function (params) { - - // Save inventory property modifications - - var scope = params.scope, - form = InventoryForm, - defaultUrl = GetBasePath('inventory'), - fld, json_data, data; - - Wait('start'); - - // Make sure we have valid variable data - json_data = ToJSON(scope.parseType, scope.variables); - - data = {}; - for (fld in form.fields) { - if (fld !== 'variables') { - if (form.fields[fld].realName) { - data[form.fields[fld].realName] = scope[fld]; - } else { - data[fld] = scope[fld]; - } - } - } - - if (scope.removeUpdateInventoryVariables) { - scope.removeUpdateInventoryVariables(); - } - scope.removeUpdateInventoryVariables = scope.$on('UpdateInventoryVariables', function(e, data) { - Rest.setUrl(data.related.variable_data); - Rest.put(json_data) - .success(function () { - Wait('stop'); - scope.$emit('InventorySaved'); - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to update inventory varaibles. PUT returned status: ' + status - }); - }); - }); - - Rest.setUrl(defaultUrl + scope.inventory_id + '/'); - Rest.put(data) - .success(function (data) { - if (scope.variables) { - scope.$emit('UpdateInventoryVariables', data); - } else { - scope.$emit('InventorySaved'); - } - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to update inventory. PUT returned status: ' + status }); - }); - }; - } - ]); diff --git a/awx/ui/client/src/helpers/md5.js b/awx/ui/client/src/helpers/md5.js deleted file mode 100644 index 26590be8fc..0000000000 --- a/awx/ui/client/src/helpers/md5.js +++ /dev/null @@ -1,46 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:md5 - * @description - * Run md5Setup({ scope: , master:, check_field:, default_val: }) - * to initialize md5 fields (checkbox and text field). - * discussion - */ - - -export default - angular.module('md5Helper', ['RestServices', 'Utilities', require('angular-md5')]) - .factory('md5Setup', ['md5', function (md5) { - return function (params) { - - var scope = params.scope, - master = params.master, - check_field = params.check_field, - default_val = params.default_val; - - scope[check_field] = default_val; - master[check_field] = default_val; - - scope.genMD5 = function (fld) { - var now = new Date(); - scope[fld] = md5.createHash('AnsibleWorks' + now.getTime()); - scope.$emit('NewMD5Generated'); - }; - - scope.toggleCallback = function (fld) { - if (scope.allow_callbacks === false) { - scope[fld] = ''; - } - }; - - scope.selectAll = function (fld) { - $('input[name="' + fld + '"]').focus().select(); - }; - }; - }]); diff --git a/awx/ui/client/src/helpers/teams.js b/awx/ui/client/src/helpers/teams.js deleted file mode 100644 index b89d2a1283..0000000000 --- a/awx/ui/client/src/helpers/teams.js +++ /dev/null @@ -1,76 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name helpers.function:teams - * @description - * TeamHelper - * Routines shared amongst the team controllers - */ - -import listGenerator from '../shared/list-generator/main'; - -export default - angular.module('TeamHelper', ['RestServices', 'Utilities', 'OrganizationListDefinition', listGenerator.name - ]) - .factory('SetTeamListeners', ['Alert', 'Rest', - function (Alert, Rest) { - return function (params) { - - var scope = params.scope, - set = params.set; - - // Listeners to perform lookups after main inventory list loads - - scope.$on('TeamResultFound', function (e, results, lookup_results) { - var i, j, key, property; - if (lookup_results.length === results.length) { - key = 'organization'; - property = 'organization_name'; - for (i = 0; i < results.length; i++) { - for (j = 0; j < lookup_results.length; j++) { - if (results[i][key] === lookup_results[j].id) { - results[i][property] = lookup_results[j].value; - } - } - } - - // @issue: OLD SEARCH - // scope[iterator + 'SearchSpin'] = false; - - scope[set] = results; - } - }); - - scope.$on('TeamRefreshFinished', function (e, results) { - // Loop through the result set (sent to us by the search helper) and - // lookup the id and name of each organization. After each lookup - // completes, call resultFound. - - var i, lookup_results = [], url; - - function getOrganization(url) { - Rest.setUrl(url); - Rest.get() - .success(function (data) { - lookup_results.push({ id: data.id, value: data.name }); - scope.$emit('TeamResultFound', results, lookup_results); - }) - .error(function () { - lookup_results.push({ id: 'error' }); - scope.$emit('TeamResultFound', results, lookup_results); - }); - } - - for (i = 0; i < results.length; i++) { - url = '/api/v1/organizations/' + results[i].organization + '/'; - getOrganization(url); - } - }); - }; - } - ]); diff --git a/awx/ui/client/src/dashboard/counts/dashboard-counts.block.less b/awx/ui/client/src/home/dashboard/counts/dashboard-counts.block.less similarity index 97% rename from awx/ui/client/src/dashboard/counts/dashboard-counts.block.less rename to awx/ui/client/src/home/dashboard/counts/dashboard-counts.block.less index d9a85ed444..8d85f93bf5 100644 --- a/awx/ui/client/src/dashboard/counts/dashboard-counts.block.less +++ b/awx/ui/client/src/home/dashboard/counts/dashboard-counts.block.less @@ -1,4 +1,4 @@ -@import "../../shared/branding/colors.default.less"; +@import "../../../shared/branding/colors.default.less"; /** @define DashboardCounts */ diff --git a/awx/ui/client/src/dashboard/counts/dashboard-counts.directive.js b/awx/ui/client/src/home/dashboard/counts/dashboard-counts.directive.js similarity index 97% rename from awx/ui/client/src/dashboard/counts/dashboard-counts.directive.js rename to awx/ui/client/src/home/dashboard/counts/dashboard-counts.directive.js index 8d26b9d1c6..bc91c9c8ba 100644 --- a/awx/ui/client/src/dashboard/counts/dashboard-counts.directive.js +++ b/awx/ui/client/src/home/dashboard/counts/dashboard-counts.directive.js @@ -9,7 +9,7 @@ export default data: '=' }, replace: false, - templateUrl: templateUrl('dashboard/counts/dashboard-counts'), + templateUrl: templateUrl('home/dashboard/counts/dashboard-counts'), link: function(scope, element, attrs) { scope.$watch("data", function(data) { if (data && data.hosts) { diff --git a/awx/ui/client/src/dashboard/counts/dashboard-counts.partial.html b/awx/ui/client/src/home/dashboard/counts/dashboard-counts.partial.html similarity index 100% rename from awx/ui/client/src/dashboard/counts/dashboard-counts.partial.html rename to awx/ui/client/src/home/dashboard/counts/dashboard-counts.partial.html diff --git a/awx/ui/client/src/dashboard/counts/main.js b/awx/ui/client/src/home/dashboard/counts/main.js similarity index 100% rename from awx/ui/client/src/dashboard/counts/main.js rename to awx/ui/client/src/home/dashboard/counts/main.js diff --git a/awx/ui/client/src/dashboard/dashboard.block.less b/awx/ui/client/src/home/dashboard/dashboard.block.less similarity index 93% rename from awx/ui/client/src/dashboard/dashboard.block.less rename to awx/ui/client/src/home/dashboard/dashboard.block.less index 40bcd84002..f1b6446fa6 100644 --- a/awx/ui/client/src/dashboard/dashboard.block.less +++ b/awx/ui/client/src/home/dashboard/dashboard.block.less @@ -1,5 +1,5 @@ /** @define Dashboard */ -@import "../shared/branding/colors.default.less"; +@import "../../shared/branding/colors.default.less"; .Dashboard { display: flex; diff --git a/awx/ui/client/src/dashboard/dashboard.directive.js b/awx/ui/client/src/home/dashboard/dashboard.directive.js similarity index 80% rename from awx/ui/client/src/dashboard/dashboard.directive.js rename to awx/ui/client/src/home/dashboard/dashboard.directive.js index 243a423a67..b7bf23d019 100644 --- a/awx/ui/client/src/dashboard/dashboard.directive.js +++ b/awx/ui/client/src/home/dashboard/dashboard.directive.js @@ -5,7 +5,7 @@ export default return { restrict: 'E', scope: true, - templateUrl: templateUrl('dashboard/dashboard'), + templateUrl: templateUrl('home/dashboard/dashboard'), link: function(scope, element, attrs) { } }; diff --git a/awx/ui/client/src/dashboard/dashboard.partial.html b/awx/ui/client/src/home/dashboard/dashboard.partial.html similarity index 100% rename from awx/ui/client/src/dashboard/dashboard.partial.html rename to awx/ui/client/src/home/dashboard/dashboard.partial.html diff --git a/awx/ui/client/src/dashboard/graphs/dashboard-graphs.block.less b/awx/ui/client/src/home/dashboard/graphs/dashboard-graphs.block.less similarity index 98% rename from awx/ui/client/src/dashboard/graphs/dashboard-graphs.block.less rename to awx/ui/client/src/home/dashboard/graphs/dashboard-graphs.block.less index 09cdbf7b72..5de5162761 100644 --- a/awx/ui/client/src/dashboard/graphs/dashboard-graphs.block.less +++ b/awx/ui/client/src/home/dashboard/graphs/dashboard-graphs.block.less @@ -1,6 +1,6 @@ /** @define DashboardGraphs */ -@import "../../shared/branding/colors.default.less"; +@import "../../../shared/branding/colors.default.less"; .DashboardGraphs { margin-top: 20px; diff --git a/awx/ui/client/src/dashboard/graphs/dashboard-graphs.directive.js b/awx/ui/client/src/home/dashboard/graphs/dashboard-graphs.directive.js similarity index 86% rename from awx/ui/client/src/dashboard/graphs/dashboard-graphs.directive.js rename to awx/ui/client/src/home/dashboard/graphs/dashboard-graphs.directive.js index 64fb391738..80a0d4e181 100644 --- a/awx/ui/client/src/dashboard/graphs/dashboard-graphs.directive.js +++ b/awx/ui/client/src/home/dashboard/graphs/dashboard-graphs.directive.js @@ -4,7 +4,7 @@ export default ['templateUrl', return { restrict: 'E', scope: true, - templateUrl: templateUrl('dashboard/graphs/dashboard-graphs'), + templateUrl: templateUrl('home/dashboard/graphs/dashboard-graphs'), link: function(scope, element, attrs) { function clearStatus() { diff --git a/awx/ui/client/src/dashboard/graphs/dashboard-graphs.partial.html b/awx/ui/client/src/home/dashboard/graphs/dashboard-graphs.partial.html similarity index 100% rename from awx/ui/client/src/dashboard/graphs/dashboard-graphs.partial.html rename to awx/ui/client/src/home/dashboard/graphs/dashboard-graphs.partial.html diff --git a/awx/ui/client/src/dashboard/graphs/graph-helpers/adjust-graph-size.service.js b/awx/ui/client/src/home/dashboard/graphs/graph-helpers/adjust-graph-size.service.js similarity index 100% rename from awx/ui/client/src/dashboard/graphs/graph-helpers/adjust-graph-size.service.js rename to awx/ui/client/src/home/dashboard/graphs/graph-helpers/adjust-graph-size.service.js diff --git a/awx/ui/client/src/dashboard/graphs/graph-helpers/auto-size.directive.js b/awx/ui/client/src/home/dashboard/graphs/graph-helpers/auto-size.directive.js similarity index 100% rename from awx/ui/client/src/dashboard/graphs/graph-helpers/auto-size.directive.js rename to awx/ui/client/src/home/dashboard/graphs/graph-helpers/auto-size.directive.js diff --git a/awx/ui/client/src/dashboard/graphs/graph-helpers/main.js b/awx/ui/client/src/home/dashboard/graphs/graph-helpers/main.js similarity index 100% rename from awx/ui/client/src/dashboard/graphs/graph-helpers/main.js rename to awx/ui/client/src/home/dashboard/graphs/graph-helpers/main.js diff --git a/awx/ui/client/src/dashboard/graphs/job-status/job-status-graph.directive.js b/awx/ui/client/src/home/dashboard/graphs/job-status/job-status-graph.directive.js similarity index 98% rename from awx/ui/client/src/dashboard/graphs/job-status/job-status-graph.directive.js rename to awx/ui/client/src/home/dashboard/graphs/job-status/job-status-graph.directive.js index 8202fbc21d..d6a289b953 100644 --- a/awx/ui/client/src/dashboard/graphs/job-status/job-status-graph.directive.js +++ b/awx/ui/client/src/home/dashboard/graphs/job-status/job-status-graph.directive.js @@ -23,7 +23,7 @@ function JobStatusGraph($rootScope, $compile , $location, $window, Wait, adjustG scope: { data: '=' }, - templateUrl: templateUrl('dashboard/graphs/job-status/job_status_graph'), + templateUrl: templateUrl('home/dashboard/graphs/job-status/job_status_graph'), link: link }; diff --git a/awx/ui/client/src/dashboard/graphs/job-status/job-status-graph.service.js b/awx/ui/client/src/home/dashboard/graphs/job-status/job-status-graph.service.js similarity index 100% rename from awx/ui/client/src/dashboard/graphs/job-status/job-status-graph.service.js rename to awx/ui/client/src/home/dashboard/graphs/job-status/job-status-graph.service.js diff --git a/awx/ui/client/src/dashboard/graphs/job-status/job_status_graph.partial.html b/awx/ui/client/src/home/dashboard/graphs/job-status/job_status_graph.partial.html similarity index 100% rename from awx/ui/client/src/dashboard/graphs/job-status/job_status_graph.partial.html rename to awx/ui/client/src/home/dashboard/graphs/job-status/job_status_graph.partial.html diff --git a/awx/ui/client/src/dashboard/graphs/job-status/main.js b/awx/ui/client/src/home/dashboard/graphs/job-status/main.js similarity index 86% rename from awx/ui/client/src/dashboard/graphs/job-status/main.js rename to awx/ui/client/src/home/dashboard/graphs/job-status/main.js index 66a6086723..927bc475df 100644 --- a/awx/ui/client/src/dashboard/graphs/job-status/main.js +++ b/awx/ui/client/src/home/dashboard/graphs/job-status/main.js @@ -1,7 +1,7 @@ import JobStatusGraphDirective from './job-status-graph.directive'; import JobStatusGraphService from './job-status-graph.service'; import DashboardGraphHelpers from '../graph-helpers/main'; -import templateUrl from '../../../shared/template-url/main'; +import templateUrl from '../../../../shared/template-url/main'; export default angular.module('JobStatusGraph', [DashboardGraphHelpers.name, templateUrl.name]) .directive('jobStatusGraph', JobStatusGraphDirective) diff --git a/awx/ui/client/src/dashboard/graphs/main.js b/awx/ui/client/src/home/dashboard/graphs/main.js similarity index 100% rename from awx/ui/client/src/dashboard/graphs/main.js rename to awx/ui/client/src/home/dashboard/graphs/main.js diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-edit.controller.js b/awx/ui/client/src/home/dashboard/hosts/dashboard-hosts-edit.controller.js similarity index 100% rename from awx/ui/client/src/dashboard/hosts/dashboard-hosts-edit.controller.js rename to awx/ui/client/src/home/dashboard/hosts/dashboard-hosts-edit.controller.js diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.controller.js b/awx/ui/client/src/home/dashboard/hosts/dashboard-hosts-list.controller.js similarity index 100% rename from awx/ui/client/src/dashboard/hosts/dashboard-hosts-list.controller.js rename to awx/ui/client/src/home/dashboard/hosts/dashboard-hosts-list.controller.js diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.form.js b/awx/ui/client/src/home/dashboard/hosts/dashboard-hosts.form.js similarity index 100% rename from awx/ui/client/src/dashboard/hosts/dashboard-hosts.form.js rename to awx/ui/client/src/home/dashboard/hosts/dashboard-hosts.form.js diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.list.js b/awx/ui/client/src/home/dashboard/hosts/dashboard-hosts.list.js similarity index 100% rename from awx/ui/client/src/dashboard/hosts/dashboard-hosts.list.js rename to awx/ui/client/src/home/dashboard/hosts/dashboard-hosts.list.js diff --git a/awx/ui/client/src/dashboard/hosts/dashboard-hosts.service.js b/awx/ui/client/src/home/dashboard/hosts/dashboard-hosts.service.js similarity index 100% rename from awx/ui/client/src/dashboard/hosts/dashboard-hosts.service.js rename to awx/ui/client/src/home/dashboard/hosts/dashboard-hosts.service.js diff --git a/awx/ui/client/src/dashboard/hosts/main.js b/awx/ui/client/src/home/dashboard/hosts/main.js similarity index 98% rename from awx/ui/client/src/dashboard/hosts/main.js rename to awx/ui/client/src/home/dashboard/hosts/main.js index 84d04fa8ad..e68efc7300 100644 --- a/awx/ui/client/src/dashboard/hosts/main.js +++ b/awx/ui/client/src/home/dashboard/hosts/main.js @@ -9,7 +9,7 @@ import form from './dashboard-hosts.form'; import listController from './dashboard-hosts-list.controller'; import editController from './dashboard-hosts-edit.controller'; import service from './dashboard-hosts.service'; -import { N_ } from '../../i18n'; +import { N_ } from '../../../i18n'; export default angular.module('dashboardHosts', []) diff --git a/awx/ui/client/src/dashboard/lists/dashboard-list.block.less b/awx/ui/client/src/home/dashboard/lists/dashboard-list.block.less similarity index 98% rename from awx/ui/client/src/dashboard/lists/dashboard-list.block.less rename to awx/ui/client/src/home/dashboard/lists/dashboard-list.block.less index 16b5262e46..a33f7b3ec2 100644 --- a/awx/ui/client/src/dashboard/lists/dashboard-list.block.less +++ b/awx/ui/client/src/home/dashboard/lists/dashboard-list.block.less @@ -1,6 +1,6 @@ /** @define DashboardList */ -@import "../../shared/branding/colors.default.less"; +@import "../../../shared/branding/colors.default.less"; .DashboardList { flex: 1; diff --git a/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.directive.js b/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js similarity index 97% rename from awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.directive.js rename to awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js index 5d4a8a11fd..cfed3878f7 100644 --- a/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.directive.js +++ b/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.directive.js @@ -11,7 +11,7 @@ export default scope: { data: '=' }, - templateUrl: templateUrl('dashboard/lists/job-templates/job-templates-list') + templateUrl: templateUrl('home/dashboard/lists/job-templates/job-templates-list') }; function link(scope, element, attr) { diff --git a/awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.partial.html b/awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.partial.html similarity index 100% rename from awx/ui/client/src/dashboard/lists/job-templates/job-templates-list.partial.html rename to awx/ui/client/src/home/dashboard/lists/job-templates/job-templates-list.partial.html diff --git a/awx/ui/client/src/dashboard/lists/job-templates/main.js b/awx/ui/client/src/home/dashboard/lists/job-templates/main.js similarity index 56% rename from awx/ui/client/src/dashboard/lists/job-templates/main.js rename to awx/ui/client/src/home/dashboard/lists/job-templates/main.js index 90c7f1deb5..9825630182 100644 --- a/awx/ui/client/src/dashboard/lists/job-templates/main.js +++ b/awx/ui/client/src/home/dashboard/lists/job-templates/main.js @@ -1,6 +1,5 @@ import JobTemplatesListDirective from './job-templates-list.directive'; -import systemStatus from '../../../smart-status/main'; -import jobSubmissionHelper from '../../../helpers/JobSubmission'; +import systemStatus from '../../../../smart-status/main'; -export default angular.module('JobTemplatesList', [systemStatus.name, jobSubmissionHelper.name]) +export default angular.module('JobTemplatesList', [systemStatus.name]) .directive('jobTemplatesList', JobTemplatesListDirective); diff --git a/awx/ui/client/src/dashboard/lists/jobs/jobs-list.directive.js b/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js similarity index 94% rename from awx/ui/client/src/dashboard/lists/jobs/jobs-list.directive.js rename to awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js index dd9b1b578f..be3d78c8bb 100644 --- a/awx/ui/client/src/dashboard/lists/jobs/jobs-list.directive.js +++ b/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js @@ -10,7 +10,7 @@ export default scope: { data: '=' }, - templateUrl: templateUrl('dashboard/lists/jobs/jobs-list') + templateUrl: templateUrl('home/dashboard/lists/jobs/jobs-list') }; function link(scope, element, attr) { diff --git a/awx/ui/client/src/dashboard/lists/jobs/jobs-list.partial.html b/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.partial.html similarity index 100% rename from awx/ui/client/src/dashboard/lists/jobs/jobs-list.partial.html rename to awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.partial.html diff --git a/awx/ui/client/src/dashboard/lists/jobs/main.js b/awx/ui/client/src/home/dashboard/lists/jobs/main.js similarity index 100% rename from awx/ui/client/src/dashboard/lists/jobs/main.js rename to awx/ui/client/src/home/dashboard/lists/jobs/main.js diff --git a/awx/ui/client/src/dashboard/lists/main.js b/awx/ui/client/src/home/dashboard/lists/main.js similarity index 100% rename from awx/ui/client/src/dashboard/lists/main.js rename to awx/ui/client/src/home/dashboard/lists/main.js diff --git a/awx/ui/client/src/dashboard/main.js b/awx/ui/client/src/home/dashboard/main.js similarity index 100% rename from awx/ui/client/src/dashboard/main.js rename to awx/ui/client/src/home/dashboard/main.js diff --git a/awx/ui/client/src/home/home.controller.js b/awx/ui/client/src/home/home.controller.js new file mode 100644 index 0000000000..80d4da45f1 --- /dev/null +++ b/awx/ui/client/src/home/home.controller.js @@ -0,0 +1,125 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$compile', '$stateParams', '$rootScope', '$location', '$log','Wait', + 'ClearScope', 'Rest', 'GetBasePath', 'ProcessErrors', '$window', 'graphData', + function($scope, $compile, $stateParams, $rootScope, $location, $log, Wait, + ClearScope, Rest, GetBasePath, ProcessErrors, $window, graphData) { + + ClearScope('home'); + + var dataCount = 0; + + $scope.$on('ws-jobs', function () { + Rest.setUrl(GetBasePath('dashboard')); + Rest.get() + .success(function (data) { + $scope.dashboardData = data; + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard host graph data: ' + status }); + }); + + Rest.setUrl("api/v1/unified_jobs?order_by=-finished&page_size=5&finished__isnull=false&type=workflow_job,job"); + Rest.get() + .success(function (data) { + $scope.dashboardJobsListData = data.results; + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard jobs list: ' + status }); + }); + + Rest.setUrl(GetBasePath("unified_job_templates") + "?order_by=-last_job_run&page_size=5&last_job_run__isnull=false&type=workflow_job_template,job_template"); + Rest.get() + .success(function (data) { + $scope.dashboardJobTemplatesListData = data.results; + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard jobs list: ' + status }); + }); + + }); + + if ($scope.removeDashboardDataLoadComplete) { + $scope.removeDashboardDataLoadComplete(); + } + $scope.removeDashboardDataLoadComplete = $scope.$on('dashboardDataLoadComplete', function () { + dataCount++; + if (dataCount === 3) { + Wait("stop"); + dataCount = 0; + } + }); + + if ($scope.removeDashboardReady) { + $scope.removeDashboardReady(); + } + $scope.removeDashboardReady = $scope.$on('dashboardReady', function (e, data) { + $scope.dashboardCountsData = data; + $scope.graphData = graphData; + $scope.$emit('dashboardDataLoadComplete'); + + var cleanupJobListener = + $rootScope.$on('DataReceived:JobStatusGraph', function(e, data) { + $scope.graphData.jobStatus = data; + }); + + $scope.$on('$destroy', function() { + cleanupJobListener(); + }); + }); + + if ($scope.removeDashboardJobsListReady) { + $scope.removeDashboardJobsListReady(); + } + $scope.removeDashboardJobsListReady = $scope.$on('dashboardJobsListReady', function (e, data) { + $scope.dashboardJobsListData = data; + $scope.$emit('dashboardDataLoadComplete'); + }); + + if ($scope.removeDashboardJobTemplatesListReady) { + $scope.removeDashboardJobTemplatesListReady(); + } + $scope.removeDashboardJobTemplatesListReady = $scope.$on('dashboardJobTemplatesListReady', function (e, data) { + $scope.dashboardJobTemplatesListData = data; + $scope.$emit('dashboardDataLoadComplete'); + }); + + $scope.refresh = function () { + Wait('start'); + Rest.setUrl(GetBasePath('dashboard')); + Rest.get() + .success(function (data) { + $scope.dashboardData = data; + $scope.$emit('dashboardReady', data); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard: ' + status }); + }); + Rest.setUrl("api/v1/unified_jobs?order_by=-finished&page_size=5&finished__isnull=false&type=workflow_job,job"); + Rest.get() + .success(function (data) { + data = data.results; + $scope.$emit('dashboardJobsListReady', data); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard jobs list: ' + status }); + }); + Rest.setUrl(GetBasePath("unified_job_templates") + "?order_by=-last_job_run&page_size=5&last_job_run__isnull=false&type=workflow_job_template,job_template"); + Rest.get() + .success(function (data) { + data = data.results; + $scope.$emit('dashboardJobTemplatesListReady', data); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get dashboard job templates list: ' + status }); + }); + }; + + $scope.refresh(); + + } +]; diff --git a/awx/ui/client/src/partials/home.html b/awx/ui/client/src/home/home.partial.html similarity index 99% rename from awx/ui/client/src/partials/home.html rename to awx/ui/client/src/home/home.partial.html index 4069ec9c7b..6a45b63334 100644 --- a/awx/ui/client/src/partials/home.html +++ b/awx/ui/client/src/home/home.partial.html @@ -1,4 +1,3 @@ -
diff --git a/awx/ui/client/src/home/home.route.js b/awx/ui/client/src/home/home.route.js new file mode 100644 index 0000000000..6c06e510e1 --- /dev/null +++ b/awx/ui/client/src/home/home.route.js @@ -0,0 +1,46 @@ +import {templateUrl} from '../shared/template-url/template-url.factory'; +import controller from './home.controller'; +import { N_ } from '../i18n'; + +export default { + name: 'dashboard', + url: '/home', + templateUrl: templateUrl('home/home'), + controller: controller, + params: { licenseMissing: null }, + data: { + activityStream: true, + refreshButton: true, + socket: { + "groups": { + "jobs": ["status_changed"] + } + }, + }, + ncyBreadcrumb: { + label: N_("DASHBOARD") + }, + resolve: { + graphData: ['$q', 'jobStatusGraphData', '$rootScope', + function($q, jobStatusGraphData, $rootScope) { + return $rootScope.featuresConfigured.promise.then(function() { + return $q.all({ + jobStatus: jobStatusGraphData.get("month", "all"), + }); + }); + } + ] + } + // name: 'setup.about', + // route: '/about', + // controller: controller, + // ncyBreadcrumb: { + // label: N_("ABOUT") + // }, + // onExit: function(){ + // // hacky way to handle user browsing away via URL bar + // $('.modal-backdrop').remove(); + // $('body').removeClass('modal-open'); + // }, + // templateUrl: templateUrl('about/about') +}; diff --git a/awx/ui/client/src/home/main.js b/awx/ui/client/src/home/main.js new file mode 100644 index 0000000000..cc4c6ebb19 --- /dev/null +++ b/awx/ui/client/src/home/main.js @@ -0,0 +1,12 @@ +import dashboard from './dashboard/main'; +import HomeController from './home.controller'; +import route from './home.route'; + +export default + angular.module('home', [ + dashboard.name + ]) + .controller('HomeController', HomeController) + .run(['$stateExtender', function($stateExtender){ + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories/list/inventory-list.controller.js index 6b8a75f0cc..59f36574ff 100644 --- a/awx/ui/client/src/inventories/list/inventory-list.controller.js +++ b/awx/ui/client/src/inventories/list/inventory-list.controller.js @@ -11,7 +11,7 @@ */ function InventoriesList($scope, $rootScope, $location, $log, - $stateParams, $compile, $filter, sanitizeFilter, Rest, Alert, InventoryList, Prompt, + $stateParams, $compile, $filter, Rest, Alert, InventoryList, Prompt, ClearScope, ProcessErrors, GetBasePath, Wait, Find, Empty, $state, rbacUiControlService, Dataset) { let list = InventoryList, @@ -304,6 +304,6 @@ function InventoriesList($scope, $rootScope, $location, $log, } export default ['$scope', '$rootScope', '$location', '$log', - '$stateParams', '$compile', '$filter', 'sanitizeFilter', 'Rest', 'Alert', 'InventoryList', + '$stateParams', '$compile', '$filter', 'Rest', 'Alert', 'InventoryList', 'Prompt', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', 'rbacUiControlService', 'Dataset', InventoriesList ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js new file mode 100644 index 0000000000..19a846c414 --- /dev/null +++ b/awx/ui/client/src/inventories/manage/groups/factories/get-hosts-status-msg.factory.js @@ -0,0 +1,33 @@ +export default + function GetHostsStatusMsg() { + return function(params) { + var active_failures = params.active_failures, + total_hosts = params.total_hosts, + tip, failures, html_class; + + // Return values for use on host status indicator + + if (active_failures > 0) { + tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; + html_class = 'error'; + failures = true; + } else { + failures = false; + if (total_hosts === 0) { + // no hosts + tip = "Contains 0 hosts."; + html_class = 'none'; + } else { + // many hosts with 0 failures + tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; + html_class = 'success'; + } + } + + return { + tooltip: tip, + failures: failures, + 'class': html_class + }; + }; + } diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js new file mode 100644 index 0000000000..befef8a499 --- /dev/null +++ b/awx/ui/client/src/inventories/manage/groups/factories/get-source-type-options.factory.js @@ -0,0 +1,37 @@ +export default + function GetSourceTypeOptions(Rest, ProcessErrors, GetBasePath) { + return function(params) { + var scope = params.scope, + variable = params.variable; + + if (scope[variable] === undefined) { + scope[variable] = []; + Rest.setUrl(GetBasePath('inventory_sources')); + Rest.options() + .success(function (data) { + var i, choices = data.actions.GET.source.choices; + for (i = 0; i < choices.length; i++) { + if (choices[i][0] !== 'file') { + scope[variable].push({ + label: choices[i][1], + value: choices[i][0] + }); + } + } + scope.cloudCredentialRequired = false; + scope.$emit('sourceTypeOptionsReady'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status + }); + }); + } + }; + } + +GetSourceTypeOptions.$inject = + [ 'Rest', + 'ProcessErrors', + 'GetBasePath' + ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js new file mode 100644 index 0000000000..2541abcc27 --- /dev/null +++ b/awx/ui/client/src/inventories/manage/groups/factories/get-sync-status-msg.factory.js @@ -0,0 +1,77 @@ +export default + function GetSyncStatusMsg(Empty) { + return function(params) { + var status = params.status, + source = params.source, + has_inventory_sources = params.has_inventory_sources, + launch_class = '', + launch_tip = 'Start sync process', + schedule_tip = 'Schedule future inventory syncs', + stat, stat_class, status_tip; + + stat = status; + stat_class = stat; + + switch (status) { + case 'never updated': + stat = 'never'; + stat_class = 'na'; + status_tip = 'Sync not performed. Click to start it now.'; + break; + case 'none': + case 'ok': + case '': + launch_class = 'btn-disabled'; + stat = 'n/a'; + stat_class = 'na'; + status_tip = 'Cloud source not configured. Click to update.'; + launch_tip = 'Cloud source not configured.'; + break; + case 'canceled': + status_tip = 'Sync canceled. Click to view log.'; + break; + case 'failed': + status_tip = 'Sync failed. Click to view log.'; + break; + case 'successful': + status_tip = 'Sync completed. Click to view log.'; + break; + case 'pending': + status_tip = 'Sync pending.'; + launch_class = "btn-disabled"; + launch_tip = "Sync pending"; + break; + case 'updating': + case 'running': + launch_class = "btn-disabled"; + launch_tip = "Sync running"; + status_tip = "Sync running. Click to view log."; + break; + } + + if (has_inventory_sources && Empty(source)) { + // parent has a source, therefore this group should not have a source + launch_class = "btn-disabled"; + status_tip = 'Managed by an external cloud source.'; + launch_tip = 'Can only be updated by running a sync on the parent group.'; + } + + if (has_inventory_sources === false && Empty(source)) { + launch_class = 'btn-disabled'; + status_tip = 'Cloud source not configured. Click to update.'; + launch_tip = 'Cloud source not configured.'; + } + + return { + "class": stat_class, + "tooltip": status_tip, + "status": stat, + "launch_class": launch_class, + "launch_tip": launch_tip, + "schedule_tip": schedule_tip + }; + }; + } + +GetSyncStatusMsg.$inject = + [ 'Empty' ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/groups-cancel-update.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/groups-cancel-update.factory.js new file mode 100644 index 0000000000..1447d0aa1c --- /dev/null +++ b/awx/ui/client/src/inventories/manage/groups/factories/groups-cancel-update.factory.js @@ -0,0 +1,81 @@ +export default + function GroupsCancelUpdate(Empty, Rest, ProcessErrors, Alert, Wait, Find) { + return function(params) { + var scope = params.scope, + id = params.id, + group = params.group; + + if (scope.removeCancelUpdate) { + scope.removeCancelUpdate(); + } + scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { + // Cancel the update process + Rest.setUrl(url); + Rest.post() + .success(function () { + Wait('stop'); + //Alert('Inventory Sync Cancelled', 'Request to cancel the sync process was submitted to the task manger. ' + + // 'Click the button to monitor the status.', 'alert-info'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. POST status: ' + status + }); + }); + }); + + if (scope.removeCheckCancel) { + scope.removeCheckCancel(); + } + scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { + // Check that we have access to cancelling an update + var url = (current_update) ? current_update : last_update; + url += 'cancel/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + if (data.can_cancel) { + scope.$emit('CancelUpdate', url); + //} else { + // Wait('stop'); + // Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + + // 'the latest status.', 'alert-info'); + } + else { + Wait('stop'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. GET status: ' + status + }); + }); + }); + + // Cancel the update process + if (Empty(group)) { + group = Find({ list: scope.groups, key: 'id', val: id }); + scope.selected_group_id = group.id; + } + + if (group && (group.status === 'running' || group.status === 'pending')) { + // We found the group, and there is a running update + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status + }); + }); + } + }; + } + +GroupsCancelUpdate.$inject = + [ 'Empty', 'Rest', 'ProcessErrors', + 'Alert', 'Wait', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js b/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js new file mode 100644 index 0000000000..da186e77aa --- /dev/null +++ b/awx/ui/client/src/inventories/manage/groups/factories/view-update-status.factory.js @@ -0,0 +1,46 @@ +export default + function ViewUpdateStatus($state, Rest, ProcessErrors, GetBasePath, Alert, Wait, Empty, Find) { + return function(params) { + var scope = params.scope, + group_id = params.group_id, + group = Find({ list: scope.groups, key: 'id', val: group_id }); + + if (scope.removeSourceReady) { + scope.removeSourceReady(); + } + scope.removeSourceReady = scope.$on('SourceReady', function(e, source) { + + // Get the ID from the correct summary field + var update_id = (source.summary_fields.current_update) ? source.summary_fields.current_update.id : source.summary_fields.last_update.id; + + $state.go('inventorySyncStdout', {id: update_id}); + + }); + + if (group) { + if (Empty(group.source)) { + // do nothing + } else if (Empty(group.status) || group.status === "never updated") { + Alert('No Status Available', '
An inventory sync has not been performed for the selected group. Start the process by ' + + 'clicking the button.
', 'alert-info', null, null, null, null, true); + } else { + Wait('start'); + Rest.setUrl(group.related.inventory_source); + Rest.get() + .success(function (data) { + scope.$emit('SourceReady', data); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + + ' GET returned status: ' + status }); + }); + } + } + }; + } + +ViewUpdateStatus.$inject = + [ '$state', 'Rest', 'ProcessErrors', + 'GetBasePath', 'Alert', 'Wait', 'Empty', 'Find' + ]; diff --git a/awx/ui/client/src/inventories/manage/groups/main.js b/awx/ui/client/src/inventories/manage/groups/main.js index 35e7b80c59..e1c2c16ddb 100644 --- a/awx/ui/client/src/inventories/manage/groups/main.js +++ b/awx/ui/client/src/inventories/manage/groups/main.js @@ -6,8 +6,18 @@ import GroupAddController from './groups-add.controller'; import GroupEditController from './groups-edit.controller'; +import GetHostsStatusMsg from './factories/get-hosts-status-msg.factory'; +import GetSourceTypeOptions from './factories/get-source-type-options.factory'; +import GetSyncStatusMsg from './factories/get-sync-status-msg.factory'; +import GroupsCancelUpdate from './factories/groups-cancel-update.factory'; +import ViewUpdateStatus from './factories/view-update-status.factory'; export default angular.module('manageGroups', []) + .factory('GetHostsStatusMsg', GetHostsStatusMsg) + .factory('GetSourceTypeOptions', GetSourceTypeOptions) + .factory('GetSyncStatusMsg', GetSyncStatusMsg) + .factory('GroupsCancelUpdate', GroupsCancelUpdate) + .factory('ViewUpdateStatus', ViewUpdateStatus) .controller('GroupAddController', GroupAddController) .controller('GroupEditController', GroupEditController); diff --git a/awx/ui/client/src/inventories/manage/hosts/factories/set-enabled-msg.factory.js b/awx/ui/client/src/inventories/manage/hosts/factories/set-enabled-msg.factory.js new file mode 100644 index 0000000000..7c60174611 --- /dev/null +++ b/awx/ui/client/src/inventories/manage/hosts/factories/set-enabled-msg.factory.js @@ -0,0 +1,13 @@ +export default + function SetEnabledMsg() { + return function(host) { + if (host.has_inventory_sources) { + // Inventory sync managed, so not clickable + host.enabledToolTip = (host.enabled) ? 'Host is available' : 'Host is not available'; + } + else { + // Clickable + host.enabledToolTip = (host.enabled) ? 'Host is available. Click to toggle.' : 'Host is not available. Click to toggle.'; + } + }; + } diff --git a/awx/ui/client/src/inventories/manage/hosts/factories/set-status.factory.js b/awx/ui/client/src/inventories/manage/hosts/factories/set-status.factory.js new file mode 100644 index 0000000000..c03468ee1f --- /dev/null +++ b/awx/ui/client/src/inventories/manage/hosts/factories/set-status.factory.js @@ -0,0 +1,102 @@ +export default + function SetStatus($filter, SetEnabledMsg, Empty) { + return function(params) { + var scope = params.scope, + host = params.host, + i, html, title; + + function ellipsis(a) { + if (a.length > 25) { + return a.substr(0,25) + '...'; + } + return a; + } + + function noRecentJobs() { + title = 'No job data'; + html = "

No recent job data available for this host.

\n"; + } + + function setMsg(host) { + var j, job, jobs; + + if (host.has_active_failures === true || (host.has_active_failures === false && host.last_job !== null)) { + if (host.has_active_failures === true) { + host.badgeToolTip = 'Most recent job failed. Click to view jobs.'; + host.active_failures = 'error'; + } + else { + host.badgeToolTip = "Most recent job successful. Click to view jobs."; + host.active_failures = 'successful'; + } + if (host.summary_fields.recent_jobs.length > 0) { + // build html table of job status info + jobs = host.summary_fields.recent_jobs.sort( + function(a,b) { + // reverse numerical order + return -1 * (a - b); + }); + title = "Recent Jobs"; + html = "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + html += "\n"; + for (j=0; j < jobs.length; j++) { + job = jobs[j]; + html += "\n"; + + // SmartStatus-tooltips are named --success whereas icon-job uses successful + var iconStatus = (job.status === 'successful') ? 'success' : 'failed'; + + html += "\n"; + + html += "\n"; + + html += "\n"; + + html += "\n"; + } + html += "\n"; + html += "
StatusFinishedName
" + ($filter('longDate')(job.finished)).replace(/ /,'
') + "
" + ellipsis(job.name) + "
\n"; + } + else { + noRecentJobs(); + } + } + else if (host.has_active_failures === false && host.last_job === null) { + host.badgeToolTip = "No job data available."; + host.active_failures = 'none'; + noRecentJobs(); + } + host.job_status_html = html; + host.job_status_title = title; + } + + if (!Empty(host)) { + // update single host + setMsg(host); + SetEnabledMsg(host); + } + else { + // update all hosts + for (i=0; i < scope.hosts.length; i++) { + setMsg(scope.hosts[i]); + SetEnabledMsg(scope.hosts[i]); + } + } + }; + } + +SetStatus.$inject = + [ '$filter', + 'SetEnabledMsg', + 'Empty' + ]; diff --git a/awx/ui/client/src/inventories/manage/hosts/main.js b/awx/ui/client/src/inventories/manage/hosts/main.js index 6dd1f334a7..1b22bd3d35 100644 --- a/awx/ui/client/src/inventories/manage/hosts/main.js +++ b/awx/ui/client/src/inventories/manage/hosts/main.js @@ -6,8 +6,12 @@ import HostsAddController from './hosts-add.controller'; import HostsEditController from './hosts-edit.controller'; +import SetStatus from './factories/set-status.factory'; +import SetEnabledMsg from './factories/set-enabled-msg.factory'; export default angular.module('manageHosts', []) + .factory('SetStatus', SetStatus) + .factory('SetEnabledMsg', SetEnabledMsg) .controller('HostsAddController', HostsAddController) .controller('HostEditController', HostsEditController); diff --git a/awx/ui/client/src/job-detail/host-event/host-event-codemirror.partial.html b/awx/ui/client/src/job-detail/host-event/host-event-codemirror.partial.html deleted file mode 100644 index 7c744d2169..0000000000 --- a/awx/ui/client/src/job-detail/host-event/host-event-codemirror.partial.html +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/awx/ui/client/src/job-detail/host-event/host-event-details.partial.html b/awx/ui/client/src/job-detail/host-event/host-event-details.partial.html deleted file mode 100644 index c287788f19..0000000000 --- a/awx/ui/client/src/job-detail/host-event/host-event-details.partial.html +++ /dev/null @@ -1,45 +0,0 @@ -
-
EVENT
- -
- STATUS - - - - - {{processEventStatus(event).status || "No result found"}} - -
-
- ID - {{event.id || "No result found"}} -
-
- CREATED - {{(event.created | longDate) || "No result found"}} -
-
- PLAY - {{event.play || "No result found"}} -
-
- TASK - {{event.task || "No result found"}} -
-
- MODULE - {{event.event_data.res.invocation.module_name || "No result found"}} -
-
-
-
RESULTS
- -
- {{key}} - {{value}} -
-
diff --git a/awx/ui/client/src/job-detail/host-event/host-event-modal.partial.html b/awx/ui/client/src/job-detail/host-event/host-event-modal.partial.html deleted file mode 100644 index db236894e8..0000000000 --- a/awx/ui/client/src/job-detail/host-event/host-event-modal.partial.html +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/awx/ui/client/src/job-detail/host-event/host-event-timing.partial.html b/awx/ui/client/src/job-detail/host-event/host-event-timing.partial.html deleted file mode 100644 index 06171bd1c5..0000000000 --- a/awx/ui/client/src/job-detail/host-event/host-event-timing.partial.html +++ /dev/null @@ -1 +0,0 @@ -
timing
\ No newline at end of file diff --git a/awx/ui/client/src/job-detail/host-event/host-event.block.less b/awx/ui/client/src/job-detail/host-event/host-event.block.less deleted file mode 100644 index 9b31b74e87..0000000000 --- a/awx/ui/client/src/job-detail/host-event/host-event.block.less +++ /dev/null @@ -1,150 +0,0 @@ -// @import "./client/src/shared/branding/colors.less"; -// @import "./client/src/shared/branding/colors.default.less"; -// @import "./client/src/shared/layouts/one-plus-two.less"; -// -// .noselect { -// -webkit-touch-callout: none; /* iOS Safari */ -// -webkit-user-select: none; /* Chrome/Safari/Opera */ -// -khtml-user-select: none; /* Konqueror */ -// -moz-user-select: none; /* Firefox */ -// -ms-user-select: none; /* Internet Explorer/Edge */ -// user-select: none; /* Non-prefixed version, currently -// not supported by any browser */ -// } -// -// @media screen and (min-width: 768px){ -// .HostEvent .modal-dialog{ -// width: 700px; -// } -// } -// .HostEvent .CodeMirror{ -// overflow-x: hidden; -// } -// .HostEvent-controls button.HostEvent-close{ -// color: #FFFFFF; -// text-transform: uppercase; -// padding-left: 15px; -// padding-right: 15px; -// background-color: @default-link; -// border-color: @default-link; -// &:hover{ -// background-color: @default-link-hov; -// border-color: @default-link-hov; -// } -// } -// .HostEvent-body{ -// margin-bottom: 10px; -// } -// .HostEvent-tab { -// color: @btn-txt; -// background-color: @btn-bg; -// font-size: 12px; -// border: 1px solid @btn-bord; -// height: 30px; -// border-radius: 5px; -// margin-right: 20px; -// padding-left: 10px; -// padding-right: 10px; -// padding-bottom: 5px; -// padding-top: 5px; -// transition: background-color 0.2s; -// text-transform: uppercase; -// text-align: center; -// white-space: nowrap; -// .noselect; -// } -// .HostEvent-tab:hover { -// color: @btn-txt; -// background-color: @btn-bg-hov; -// cursor: pointer; -// } -// .HostEvent-tab--selected{ -// color: @btn-txt-sel!important; -// background-color: @default-icon!important; -// border-color: @default-icon!important; -// } -// .HostEvent-view--container{ -// width: 100%; -// display: flex; -// flex-direction: row; -// flex-wrap: nowrap; -// justify-content: space-between; -// } -// .HostEvent .modal-footer{ -// border: 0; -// margin-top: 0px; -// padding-top: 5px; -// } -// .HostEvent-controls{ -// float: right; -// button { -// margin-left: 10px; -// } -// } -// .HostEvent-status--ok{ -// color: @green; -// } -// .HostEvent-status--unreachable{ -// color: @unreachable; -// } -// .HostEvent-status--changed{ -// color: @changed; -// } -// .HostEvent-status--failed{ -// color: @default-err; -// } -// .HostEvent-status--skipped{ -// color: @skipped; -// } -// .HostEvent-title{ -// color: @default-interface-txt; -// font-weight: 600; -// margin-bottom: 8px; -// } -// // .HostEvent .modal-body{ -// // max-height: 500px; -// // overflow-y: auto; -// // padding: 20px; -// // } -// .HostEvent-nav{ -// padding-top: 12px; -// padding-bottom: 12px; -// } -// .HostEvent-field{ -// margin-bottom: 8px; -// flex: 0 1 12em; -// } -// .HostEvent-field--label{ -// text-transform: uppercase; -// flex: 0 1 80px; -// max-width: 80px; -// font-size: 12px; -// word-wrap: break-word; -// } -// .HostEvent-field{ -// .OnePlusTwo-left--detailsRow; -// } -// .HostEvent-field--content{ -// word-wrap: break-word; -// max-width: 13em; -// flex: 0 1 13em; -// } -// .HostEvent-details--left, .HostEvent-details--right{ -// flex: 1 1 47%; -// } -// .HostEvent-details--left{ -// margin-right: 40px; -// } -// .HostEvent-details--right{ -// .HostEvent-field--label{ -// flex: 0 1 25em; -// } -// .HostEvent-field--content{ -// max-width: 15em; -// flex: 0 1 15em; -// align-self: flex-end; -// } -// } -// .HostEvent-button:disabled { -// pointer-events: all!important; -// } diff --git a/awx/ui/client/src/job-detail/host-event/host-event.controller.js b/awx/ui/client/src/job-detail/host-event/host-event.controller.js deleted file mode 100644 index f86452b005..0000000000 --- a/awx/ui/client/src/job-detail/host-event/host-event.controller.js +++ /dev/null @@ -1,110 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - export default - ['$stateParams', '$scope', '$state', 'Wait', 'JobDetailService', 'hostEvent', 'hostResults', - function($stateParams, $scope, $state, Wait, JobDetailService, hostEvent, hostResults){ - - $scope.processEventStatus = JobDetailService.processEventStatus; - $scope.hostResults = []; - // Avoid rendering objects in the details fieldset - // ng-if="processResults(value)" via host-event-details.partial.html - $scope.processResults = function(value){ - if (typeof value === 'object'){return false;} - else {return true;} - }; - $scope.isStdOut = function(){ - if ($state.current.name === 'jobDetails.host-event.stdout' || $state.current.name === 'jobDetaisl.histe-event.stderr'){ - return 'StandardOut-preContainer StandardOut-preContent'; - } - }; - /*ignore jslint start*/ - var initCodeMirror = function(el, data, mode){ - var container = document.getElementById(el); - var editor = CodeMirror.fromTextArea(container, { // jshint ignore:line - lineNumbers: true, - mode: mode - }); - editor.setSize("100%", 300); - editor.getDoc().setValue(data); - }; - /*ignore jslint end*/ - $scope.isActiveState = function(name){ - return $state.current.name === name; - }; - - $scope.getActiveHostIndex = function(){ - var result = $scope.hostResults.filter(function( obj ) { - return obj.id === $scope.event.id; - }); - return $scope.hostResults.indexOf(result[0]); - }; - - $scope.showPrev = function(){ - return $scope.getActiveHostIndex() !== 0; - }; - - $scope.showNext = function(){ - return $scope.getActiveHostIndex() < $scope.hostResults.indexOf($scope.hostResults[$scope.hostResults.length - 1]); - }; - - $scope.goNext = function(){ - var index = $scope.getActiveHostIndex() + 1; - var id = $scope.hostResults[index].id; - $state.go('jobDetail.host-event.details', {eventId: id}); - }; - - $scope.goPrev = function(){ - var index = $scope.getActiveHostIndex() - 1; - var id = $scope.hostResults[index].id; - $state.go('jobDetail.host-event.details', {eventId: id}); - }; - - var init = function(){ - $scope.event = _.cloneDeep(hostEvent); - $scope.hostResults = hostResults; - $scope.json = JobDetailService.processJson(hostEvent); - - // grab standard out & standard error if present, and remove from the results displayed in the details panel - if (hostEvent.event_data.res.stdout){ - $scope.stdout = hostEvent.event_data.res.stdout; - delete $scope.event.event_data.res.stdout; - } - if (hostEvent.event_data.res.stderr){ - $scope.stderr = hostEvent.event_data.res.stderr; - delete $scope.event.event_data.res.stderr; - } - // instantiate Codemirror - // try/catch pattern prevents the abstract-state controller from complaining about element being null - if ($state.current.name === 'jobDetail.host-event.json'){ - try{ - initCodeMirror('HostEvent-codemirror', JSON.stringify($scope.json, null, 4), {name: "javascript", json: true}); - } - catch(err){ - // element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController - } - } - else if ($state.current.name === 'jobDetail.host-event.stdout'){ - try{ - initCodeMirror('HostEvent-codemirror', $scope.stdout, 'shell'); - } - catch(err){ - // element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController - } - } - else if ($state.current.name === 'jobDetail.host-event.stderr'){ - try{ - initCodeMirror('HostEvent-codemirror', $scope.stderr, 'shell'); - } - catch(err){ - // element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController - } - } - $('#HostEvent').modal('show'); - }; - init(); - }]; diff --git a/awx/ui/client/src/job-detail/host-event/host-event.route.js b/awx/ui/client/src/job-detail/host-event/host-event.route.js deleted file mode 100644 index 86e499c2b0..0000000000 --- a/awx/ui/client/src/job-detail/host-event/host-event.route.js +++ /dev/null @@ -1,65 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import { templateUrl } from '../../shared/template-url/template-url.factory'; - -var hostEventModal = { - name: 'jobDetail.host-event', - url: '/task/:taskId/host-event/:eventId', - controller: 'HostEventController', - templateUrl: templateUrl('job-detail/host-event/host-event-modal'), - 'abstract': true, - resolve: { - hostEvent: ['JobDetailService', '$stateParams', function(JobDetailService, $stateParams) { - return JobDetailService.getRelatedJobEvents($stateParams.id, { - id: $stateParams.eventId - }).then(function(res) { - return res.data.results[0]; }); - }], - hostResults: ['JobDetailService', '$stateParams', function(JobDetailService, $stateParams) { - return JobDetailService.getJobEventChildren($stateParams.taskUuid).then(res => res.data.results); - }] - }, - onExit: function() { - // close the modal - // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" - $('#HostEvent').modal('hide'); - // hacky way to handle user browsing away via URL bar - $('.modal-backdrop').remove(); - $('body').removeClass('modal-open'); - } -}; - -var hostEventDetails = { - name: 'jobDetail.host-event.details', - url: '/details', - controller: 'HostEventController', - templateUrl: templateUrl('job-detail/host-event/host-event-details'), -}; - -var hostEventJson = { - name: 'jobDetail.host-event.json', - url: '/json', - controller: 'HostEventController', - templateUrl: templateUrl('job-detail/host-event/host-event-codemirror') -}; - -var hostEventStdout = { - name: 'jobDetail.host-event.stdout', - url: '/stdout', - controller: 'HostEventController', - templateUrl: templateUrl('job-detail/host-event/host-event-codemirror') -}; - -var hostEventStderr = { - name: 'jobDetail.host-event.stderr', - url: '/stderr', - controller: 'HostEventController', - templateUrl: templateUrl('job-detail/host-event/host-event-codemirror') -}; - - -export { hostEventDetails, hostEventJson, hostEventModal, hostEventStdout, hostEventStderr }; diff --git a/awx/ui/client/src/job-detail/host-event/main.js b/awx/ui/client/src/job-detail/host-event/main.js deleted file mode 100644 index 4379427ff8..0000000000 --- a/awx/ui/client/src/job-detail/host-event/main.js +++ /dev/null @@ -1,21 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - import {hostEventModal, hostEventDetails, - hostEventJson, hostEventStdout, hostEventStderr} from './host-event.route'; - import controller from './host-event.controller'; - - export default - angular.module('jobDetail.hostEvent', []) - .controller('HostEventController', controller) - - .run(['$stateExtender', function($stateExtender){ - $stateExtender.addState(hostEventModal); - $stateExtender.addState(hostEventDetails); - $stateExtender.addState(hostEventJson); - $stateExtender.addState(hostEventStdout); - $stateExtender.addState(hostEventStderr); - }]); diff --git a/awx/ui/client/src/job-detail/host-events/host-events.block.less b/awx/ui/client/src/job-detail/host-events/host-events.block.less deleted file mode 100644 index c309dfeab6..0000000000 --- a/awx/ui/client/src/job-detail/host-events/host-events.block.less +++ /dev/null @@ -1,99 +0,0 @@ -@import "./client/src/shared/branding/colors.less"; -@import "./client/src/shared/branding/colors.default.less"; - -.HostEvents .CodeMirror{ - border: none; -} -.HostEvents .modal-footer{ - border: 0; - margin-top: 0px; - padding: 0px 20px 20px 20px; -} -button.HostEvents-close{ - width: 70px; - color: #FFFFFF!important; - text-transform: uppercase; - padding-left: 15px; - padding-right: 15px; - background-color: @default-link; - border-color: @default-link; - &:hover{ - background-color: @default-link-hov; - border-color: @default-link-hov; - } -} -.HostEvents-status--ok{ - color: @green; -} -.HostEvents-status--unreachable{ - color: @unreachable; -} -.HostEvents-status--changed{ - color: @changed; -} -.HostEvents-status--failed{ - color: @default-err; -} -.HostEvents-status--skipped{ - color: @skipped; -} - -// @issue: OLD SEARCH -// .HostEvents-search--form{ -// max-width: 420px; -// display: inline-block; -// } - -.HostEvents-filter--form{ - padding-top: 15px; - padding-bottom: 15px; - float: right; - display: inline-block; -} -.HostEvents .modal-body{ - padding: 20px; -} -.HostEvents .select2-container{ - text-transform: capitalize; - max-width: 220px; - float: right; -} -.HostEvents-form--container{ - padding-top: 15px; - padding-bottom: 15px; -} -.HostEvents-title{ - text-transform: uppercase; - color: @default-interface-txt; - font-weight: 600; -} -.HostEvents-status i { - padding-right: 10px; -} -.HostEvents-table--header { - height: 30px; - font-size: 14px; - font-weight: normal; - text-transform: uppercase; - color: @default-interface-txt; - background-color: @default-list-header-bg; - padding-left: 15px; - padding-right: 15px; - border-bottom-width: 0px; -} -.HostEvents-table--header:first-of-type{ - border-top-left-radius: 5px; -} -.HostEvents-table--header:last-of-type{ - border-top-right-radius: 5px; -} -.HostEvents-table--row{ - color: @default-data-txt; - border: 0 !important; -} -.HostEvents-table--row:nth-child(odd){ - background: @default-tertiary-bg; -} -.HostEvents-table--cell{ - border: 0 !important; -} diff --git a/awx/ui/client/src/job-detail/host-events/host-events.controller.js b/awx/ui/client/src/job-detail/host-events/host-events.controller.js deleted file mode 100644 index 87b1bc00a0..0000000000 --- a/awx/ui/client/src/job-detail/host-events/host-events.controller.js +++ /dev/null @@ -1,96 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - export default - ['$stateParams', '$scope', '$rootScope', '$state', 'Wait', - 'JobDetailService', 'CreateSelect2', 'hosts', - function($stateParams, $scope, $rootScope, $state, Wait, - JobDetailService, CreateSelect2, hosts){ - - // pagination not implemented yet, but it'll depend on this - $scope.page_size = $stateParams.page_size; - - $scope.processEventStatus = JobDetailService.processEventStatus; - $scope.activeFilter = $stateParams.filter || null; - - // @issue: OLD SEARCH - // $scope.search = function(){ - // Wait('start'); - // //http://docs.ansible.com/ansible-tower/latest/html/towerapi/intro.html#filtering - // // SELECT WHERE host_name LIKE str OR WHERE play LIKE str OR WHERE task LIKE str AND host_name NOT "" - // // selecting non-empty host_name fields prevents us from displaying non-runner events, like playbook_on_task_start - // var params = { - // host_name: $scope.hostName, - // }; - // if ($scope.searchStr && $scope.searchStr !== ''){ - // params.or__play__icontains = encodeURIComponent($scope.searchStr); - // params.or__task__icontains = encodeURIComponent($scope.searchStr); - // } - // - // switch($scope.activeFilter){ - // case 'skipped': - // params.event = 'runner_on_skipped'; - // break; - // case 'unreachable': - // params.event = 'runner_on_unreachable'; - // break; - // case 'ok': - // params.event = 'runner_on_ok'; - // params.changed = 'false'; - // break; - // case 'failed': - // params.event = 'runner_on_failed'; - // break; - // case 'changed': - // params.event = 'runner_on_ok'; - // params.changed = true; - // break; - // default: - // break; - // } - // JobDetailService.getRelatedJobEvents($stateParams.id, params) - // .success(function(res){ - // $scope.results = res.results; - // Wait('stop'); - // }); - // }; - - $scope.filters = ['all', 'changed', 'failed', 'ok', 'unreachable', 'skipped']; - - // watch select2 for changes - $('.HostEvents-select').on("select2:select", function () { - $scope.activeFilter = $('.HostEvents-select').val(); - - // @issue: OLD SEARCH - // $scope.search(); - }); - - var init = function(){ - $scope.hostName = $stateParams.hostName; - // create filter dropdown - CreateSelect2({ - element: '.HostEvents-select', - multiple: false - }); - // process the filter if one was passed - if ($stateParams.filter){ - $scope.activeFilter = $stateParams.filter; - - // @issue: OLD SEARCH - // $scope.search(); - - $('#HostEvents').modal('show'); - } - else{ - $scope.results = hosts.data.results; - $('#HostEvents').modal('show'); - } - }; - - - init(); - - }]; diff --git a/awx/ui/client/src/job-detail/host-events/host-events.partial.html b/awx/ui/client/src/job-detail/host-events/host-events.partial.html deleted file mode 100644 index c0b598a23a..0000000000 --- a/awx/ui/client/src/job-detail/host-events/host-events.partial.html +++ /dev/null @@ -1,63 +0,0 @@ - diff --git a/awx/ui/client/src/job-detail/host-events/host-events.route.js b/awx/ui/client/src/job-detail/host-events/host-events.route.js deleted file mode 100644 index 835d17b2b9..0000000000 --- a/awx/ui/client/src/job-detail/host-events/host-events.route.js +++ /dev/null @@ -1,32 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import {templateUrl} from '../../shared/template-url/template-url.factory'; - -export default { - name: 'jobDetail.host-events', - url: '/host-events/{hostName:any}?:filter', - controller: 'HostEventsController', - params: { - page_size: 10 - }, - templateUrl: templateUrl('job-detail/host-events/host-events'), - onExit: function(){ - // close the modal - // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" - $('#HostEvents').modal('hide'); - // hacky way to handle user browsing away via URL bar - $('.modal-backdrop').remove(); - $('body').removeClass('modal-open'); - }, - resolve: { - hosts: ['JobDetailService','$stateParams', function(JobDetailService, $stateParams) { - return JobDetailService.getRelatedJobEvents($stateParams.id, { - host_name: $stateParams.hostName - }).success(function(res){ return res.results[0];}); - }] - } -}; diff --git a/awx/ui/client/src/job-detail/host-events/main.js b/awx/ui/client/src/job-detail/host-events/main.js deleted file mode 100644 index 766dd92ca4..0000000000 --- a/awx/ui/client/src/job-detail/host-events/main.js +++ /dev/null @@ -1,15 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import route from './host-events.route'; -import controller from './host-events.controller'; - -export default - angular.module('jobDetail.hostEvents', []) - .controller('HostEventsController', controller) - .run(['$stateExtender', function($stateExtender){ - $stateExtender.addState(route); - }]); diff --git a/awx/ui/client/src/job-detail/host-summary/host-summary.block.less b/awx/ui/client/src/job-detail/host-summary/host-summary.block.less deleted file mode 100644 index 00af0d30ea..0000000000 --- a/awx/ui/client/src/job-detail/host-summary/host-summary.block.less +++ /dev/null @@ -1,17 +0,0 @@ -@import '../../shared/branding/colors.default.less'; -.HostSummary-graph--successful{ - text-anchor: start !important; -} -.HostSummary-graph--failed{ - text-anchor: end !important; -} -.HostSummary-graph--changed{ - text-anchor: start !important; -} -.HostSummary-loading{ - border: none; -} -.HostSummary-loading{ - padding-left: 0px !important; - color: @default-interface-txt; -} diff --git a/awx/ui/client/src/job-detail/host-summary/host-summary.controller.js b/awx/ui/client/src/job-detail/host-summary/host-summary.controller.js deleted file mode 100644 index 2e3716a945..0000000000 --- a/awx/ui/client/src/job-detail/host-summary/host-summary.controller.js +++ /dev/null @@ -1,154 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - export default - ['$scope', '$rootScope', '$stateParams', 'Wait', 'JobDetailService', 'DrawGraph', function($scope, $rootScope, $stateParams, Wait, JobDetailService, DrawGraph){ - - var page_size = 200; - $scope.loading = $scope.hosts.length > 0 ? false : true; - $scope.filter = 'all'; - - var buildGraph = function(hosts){ - // status waterfall: unreachable > failed > changed > ok > skipped - var count; - count = { - ok : _.filter(hosts, function(o){ - return o.failures === 0 && o.changed === 0 && o.ok > 0; - }), - skipped : _.filter(hosts, function(o){ - return o.skipped > 0; - }), - unreachable : _.filter(hosts, function(o){ - return o.dark > 0; - }), - failures : _.filter(hosts, function(o){ - return o.failed === true; - }), - changed : _.filter(hosts, function(o){ - return o.changed > 0; - }) - }; - return count; - }; - var init = function(){ - Wait('start'); - JobDetailService.getJobHostSummaries($stateParams.id, {page_size: page_size, order_by: 'host_name'}) - .success(function(res){ - $scope.hosts = res.results; - $scope.next = res.next; - $scope.count = buildGraph(res.results); - Wait('stop'); - DrawGraph({count: $scope.count, resize:true}); - }); - JobDetailService.getJob({id: $stateParams.id}) - .success(function(res){ - $scope.status = res.results[0].status; - }); - }; - if ($rootScope.removeJobSummaryComplete) { - $rootScope.removeJobSummaryComplete(); - } - // emitted by the API in the same function used to persist host summary data - // JobEvent.update_host_summary_from_stats() from /awx/main.models.jobs.py - $scope.$on('ws-jobs-summary', function(e, data) { - // discard socket msgs we don't care about in this context - if (parseInt($stateParams.id) === data.unified_job_id){ - init(); - } - }); - - $scope.$on('ws-jobs', function(e, data) { - if (parseInt($stateParams.id) === data.unified_job_id){ - $scope.status = data.status; - } - }); - - - $scope.buildTooltip = function(n, status){ - var grammar = function(n, status){ - var dict = { - 0: 'No host events were ', - 1: ' host event was ', - 2: ' host events were ' - }; - if (n >= 2){ - return n + dict[2] + status; - } - else{ - return n !== 0 ? n + dict[n] + status : dict[n] + status; - } - }; - return grammar(n, status); - }; - $scope.getNextPage = function(){ - if ($scope.next){ - JobDetailService.getNextPage($scope.next).success(function(res){ - res.results.forEach(function(key, index){ - $scope.hosts.push(res.results[index]); - }); - $scope.hosts.push(res.results); - $scope.next = res.next; - }); - } - }; - - // @issue: OLD SEARCH - // $scope.search = function(){ - // if($scope.searchTerm && $scope.searchTerm !== '') { - // $scope.searchActive = true; - // Wait('start'); - // JobDetailService.getJobHostSummaries($stateParams.id, { - // page_size: page_size, - // host_name__icontains: encodeURIComponent($scope.searchTerm), - // }).success(function(res){ - // $scope.hosts = res.results; - // $scope.next = res.next; - // Wait('stop'); - // }); - // } - // }; - - // @issue: OLD SEARCH - // $scope.clearSearch = function(){ - // $scope.searchActive = false; - // $scope.searchTerm = null; - // init(); - // }; - - $scope.setFilter = function(filter){ - $scope.filter = filter; - var getAll = function(){ - Wait('start'); - JobDetailService.getJobHostSummaries($stateParams.id, { - page_size: page_size, - order_by: 'host_name' - }).success(function(res){ - Wait('stop'); - $scope.hosts = res.results; - $scope.next = res.next; - }); - }; - var getFailed = function(){ - Wait('start'); - JobDetailService.getJobHostSummaries($stateParams.id, { - page_size: page_size, - failed: true, - order_by: 'host_name' - }).success(function(res){ - Wait('stop'); - $scope.hosts = res.results; - $scope.next = res.next; - }); - }; - $scope.get = filter === 'all' ? getAll() : getFailed(); - }; - - init(); - // calling the init routine twice will size the d3 chart correctly - no idea why - // instantiating the graph inside a setTimeout() SHOULD have the same effect, but it doesn't - // instantiating the graph further down the promise chain e.g. .then() or .finally() also does not work - init(); - }]; diff --git a/awx/ui/client/src/job-detail/host-summary/host-summary.partial.html b/awx/ui/client/src/job-detail/host-summary/host-summary.partial.html deleted file mode 100644 index 5452990e76..0000000000 --- a/awx/ui/client/src/job-detail/host-summary/host-summary.partial.html +++ /dev/null @@ -1,72 +0,0 @@ - -
-
4 Please select a host below to view a summary of all associated tasks.
-
-
-
-
- - - -
-
-
-
-
- - -
-
-
- -
- - - - - - - -
HostsCompleted Tasks
-
- -
- - - - - - - - - - - - - - - - -
- {{ host.host_name }} - - {{ host.ok - host.changed }} - {{ host.changed }} - {{ host.skipped }} - {{ host.dark }} - {{ host.failures }} -
Initiating job run.
Job is running. Summary will be available on completion.
No matching hosts
-
- -
- -
- -
-
Host Status Summary
-
- -
diff --git a/awx/ui/client/src/job-detail/host-summary/host-summary.route.js b/awx/ui/client/src/job-detail/host-summary/host-summary.route.js deleted file mode 100644 index a2de70e5d4..0000000000 --- a/awx/ui/client/src/job-detail/host-summary/host-summary.route.js +++ /dev/null @@ -1,21 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import {templateUrl} from '../../shared/template-url/template-url.factory'; - -export default { - name: 'jobDetail.host-summary', - url: '/event-summary', - views:{ - 'host-summary': { - controller: 'HostSummaryController', - templateUrl: templateUrl('job-detail/host-summary/host-summary'), - } - }, - ncyBreadcrumb: { - skip: true // Never display this state in breadcrumb. - } -}; diff --git a/awx/ui/client/src/job-detail/host-summary/main.js b/awx/ui/client/src/job-detail/host-summary/main.js deleted file mode 100644 index fad85ffaf3..0000000000 --- a/awx/ui/client/src/job-detail/host-summary/main.js +++ /dev/null @@ -1,15 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import route from './host-summary.route'; -import controller from './host-summary.controller'; - -export default - angular.module('jobDetail.hostSummary', []) - .controller('HostSummaryController', controller) - .run(['$stateExtender', function($stateExtender){ - $stateExtender.addState(route); - }]); \ No newline at end of file diff --git a/awx/ui/client/src/job-detail/job-detail.block.less b/awx/ui/client/src/job-detail/job-detail.block.less deleted file mode 100644 index db69b8bdc0..0000000000 --- a/awx/ui/client/src/job-detail/job-detail.block.less +++ /dev/null @@ -1,239 +0,0 @@ -/** @define SetupItem */ - -@import '../shared/branding/colors.less'; -@import '../shared/branding/colors.default.less'; -@import '../shared/layouts/one-plus-two.less'; - -@breakpoint-md: 1200px; -@breakpoint-sm: 623px; - -.JobDetail-tasks.section{ - margin-top:40px; -} -.JobDetail-instructions{ - color: @default-interface-txt; - margin: 10px 0 10px 0; - - .badge { - background-color: @default-list-header-bg; - color: @default-interface-txt; - padding: 5px 7px; - } -} -.JobDetail{ - .OnePlusTwo-container(100%, @breakpoint-md); - - &.fullscreen { - .JobDetail-rightSide { - max-width: 100%; - } - } -} - -.JobDetail-leftSide{ - .OnePlusTwo-left--panel(100%, @breakpoint-md); -} - -.JobDetail-rightSide{ - .OnePlusTwo-right--panel(100%, @breakpoint-md); - @media (max-width: @breakpoint-md - 1px) { - padding-right: 15px; - } -} -.JobDetail-panelHeader{ - display: flex; - height: 30px; -} -.JobDetail-expandContainer{ - flex: 1; - margin: 0px; - line-height: 30px; - white-space: nowrap; -} - -.JobDetail-panelHeaderText{ - color: @default-interface-txt; - flex: 1 0 auto; - font-size: 14px; - font-weight: bold; - margin-right: 10px; -} - -.JobDetail-panelHeaderText:hover{ - color: @default-interface-txt; - font-size: 14px; - font-weight: bold; - margin-right: 10px; - text-transform: uppercase; -} - -.JobDetail-expandArrow{ - color: @default-icon-hov; - font-size: 14px; - font-weight: bold; - margin-right: 10px; - text-transform: uppercase; - margin-left: 10px; -} - -.JobDetail-resultsDetails{ - display: flex; - flex-wrap: wrap; - flex-direction: row; - padding-top: 25px; - @media screen and(max-width: @breakpoint-sm){ - flex-direction: column; - } -} - -.JobDetail-resultRow{ - width: 100%; - display: flex; - padding-bottom: 10px; - flex-wrap: wrap; -} - -.JobDetail-resultRow--variables { - flex-direction: column; -} - -.JobDetail-resultRowLabel{ - text-transform: uppercase; - color: @default-interface-txt; - font-size: 14px; - font-weight: normal!important; - width: 30%; - margin-right: 20px; - @media screen and(max-width: @breakpoint-md){ - flex: 2.5 0 auto; - } -} - -.JobDetail-resultRowLabel--fullWidth { - width: 100%; - margin-right: 0px; -} - -.JobDetail-resultRowText{ - width: ~"calc(70% - 20px)"; - flex: 1 0 auto; - text-transform: none; - word-wrap: break-word; -} - -.JobDetail-resultRowText--fullWidth { - width: 100%; -} - -.JobDetail-searchHeaderRow{ - display: flex; - flex-wrap: wrap; - flex-direction: row; - height: 50px; - margin-top: 20px; - @media screen and(max-width: @breakpoint-sm){ - height: auto; - } -} - -.JobDetail-searchContainer{ - flex: 2 0 auto; - @media screen and(max-width: @breakpoint-sm){ - margin-bottom: 0px; - } -} - -.JobDetail-tableToggleContainer{ - flex: 1 0 auto; - display: flex; - justify-content: flex-end; -} - -.JobDetail-tableToggle{ - padding-left:10px; - padding-right: 10px; - border: 1px solid @d7grey; -} - -.JobDetail-tableToggle.active{ - background-color: @default-link; - border: 1px solid @default-link; - color: @default-bg; - - &:hover { - background-color: @default-link-hov; - } -} -.JobDetail .nvd3.nv-noData{ - color: @default-interface-txt; - font-size: 12px; - text-transform: uppercase; - font-family: 'Open Sans', sans-serif; -} -.JobDetail .nv-series{ - padding-right: 30px; - display: block; -} -.JobDetail-instructions .badge{ - background-color: @default-list-header-bg; - color: @default-interface-txt; -} -.JobDetail-tableToggle--left{ - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; -} - -.JobDetail-tableToggle--right{ - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; -} - -.JobDetail-searchInput{ - border-radius: 5px !important; -} - -.JobDetail-tableHeader:last-of-type{ - text-align:justify; -} - -.JobDetail-statusIcon{ - padding-right: 10px; - padding-left: 10px; -} - -.JobDetail-tableRow--selected, -.JobDetail-tableRow--selected > :first-child{ - border-left: 5px solid @list-row-select-bord; -} - -.JobDetail-tableRow--selected > :first-child > .JobDetail-statusIcon{ - margin-left: -5px; -} - -.JobDetails-table--noResults { - tr > td { - border-top: none !important; - } -} - -.JobDetail-statusIcon--results{ - padding-left: 0px; - padding-right: 10px; -} - -.JobDetail-graphSection{ - height: 320px; - width:100%; -} - -.JobDetail-stdoutActionButton--active{ - display: none; - visibility: hidden; - flex:none; - width:0px; - padding-right: 0px; -} - -.JobDetail-leftSide.JobDetail-stdoutActionButton--active { - margin-right: 0px; -} diff --git a/awx/ui/client/src/job-detail/job-detail.controller.js b/awx/ui/client/src/job-detail/job-detail.controller.js deleted file mode 100644 index 067c26aa1f..0000000000 --- a/awx/ui/client/src/job-detail/job-detail.controller.js +++ /dev/null @@ -1,1150 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:JobDetail - * @description This controller's for the Job Detail Page -*/ - -export default - [ '$location', '$rootScope', '$filter', '$scope', '$compile', '$state', '$stateParams', '$log', 'ClearScope', - 'GetBasePath', 'Wait', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'GetElapsed', 'JobIsFinished', - 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'RelaunchPlaybook', 'LoadPlays', 'LoadTasks', - 'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels', 'EditSchedule', - 'ParseTypeChange', 'JobDetailService', - function( - $location, $rootScope, $filter, $scope, $compile, $state, $stateParams, $log, ClearScope, - GetBasePath, Wait, ProcessErrors, SelectPlay, SelectTask, GetElapsed, JobIsFinished, - SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob, RelaunchPlaybook, LoadPlays, LoadTasks, - ParseVariableString, GetChoices, fieldChoices, fieldLabels, EditSchedule, - ParseTypeChange, JobDetailService - ) { - ClearScope(); - - var job_id = $stateParams.id, - scope = $scope, - api_complete = false, - refresh_count = 0, - lastEventId = 0, - verbosity_options, - job_type_options; - - scope.plays = []; - scope.parseType = 'yaml'; - scope.previousTaskFailed = false; - $scope.stdoutFullScreen = false; - - scope.$watch('job_status', function(job_status) { - if (job_status && job_status.explanation && job_status.explanation.split(":")[0] === "Previous Task Failed") { - scope.previousTaskFailed = true; - var taskObj = JSON.parse(job_status.explanation.substring(job_status.explanation.split(":")[0].length + 1)); - // return a promise from the options request with the permission type choices (including adhoc) as a param - var fieldChoice = fieldChoices({ - scope: $scope, - url: 'api/v1/unified_jobs/', - field: 'type' - }); - - // manipulate the choices from the options request to be set on - // scope and be usable by the list form - fieldChoice.then(function (choices) { - choices = - fieldLabels({ - choices: choices - }); - scope.explanation_fail_type = choices[taskObj.job_type]; - scope.explanation_fail_name = taskObj.job_name; - scope.explanation_fail_id = taskObj.job_id; - scope.task_detail = scope.explanation_fail_type + " failed for " + scope.explanation_fail_name + " with ID " + scope.explanation_fail_id + "."; - }); - } else { - scope.previousTaskFailed = false; - } - }, true); - - scope.$watch('plays', function(plays) { - for (var play in plays) { - if (plays[play].elapsed) { - plays[play].finishedTip = "Play completed at " + $filter("longDate")(plays[play].finished) + "."; - } else { - plays[play].finishedTip = "Play not completed."; - } - } - }); - scope.hosts = []; - scope.tasks = []; - scope.$watch('tasks', function(tasks) { - for (var task in tasks) { - if (tasks[task].elapsed) { - tasks[task].finishedTip = "Task completed at " + $filter("longDate")(tasks[task].finished) + "."; - } else { - tasks[task].finishedTip = "Task not completed."; - } - if (tasks[task].successfulCount) { - tasks[task].successfulCountTip = tasks[task].successfulCount; - tasks[task].successfulCountTip += (tasks[task].successfulCount === 1) ? " host event was" : " host events were"; - tasks[task].successfulCountTip += " ok."; - } else { - tasks[task].successfulCountTip = "No host events were ok."; - } - if (tasks[task].changedCount) { - tasks[task].changedCountTip = tasks[task].changedCount; - tasks[task].changedCountTip += (tasks[task].changedCount === 1) ? " host event" : " host events"; - tasks[task].changedCountTip += " changed."; - } else { - tasks[task].changedCountTip = "No host events changed."; - } - if (tasks[task].skippedCount) { - tasks[task].skippedCountTip = tasks[task].skippedCount; - tasks[task].skippedCountTip += (tasks[task].skippedCount === 1) ? " host event was" : " hosts events were"; - tasks[task].skippedCountTip += " skipped."; - } else { - tasks[task].skippedCountTip = "No host events were skipped."; - } - if (tasks[task].failedCount) { - tasks[task].failedCountTip = tasks[task].failedCount; - tasks[task].failedCountTip += (tasks[task].failedCount === 1) ? " host event" : " host events"; - tasks[task].failedCountTip += " failed."; - } else { - tasks[task].failedCountTip = "No host events failed."; - } - if (tasks[task].unreachableCount) { - tasks[task].unreachableCountTip = tasks[task].unreachableCount; - tasks[task].unreachableCountTip += (tasks[task].unreachableCount === 1) ? " host event was" : " hosts events were"; - tasks[task].unreachableCountTip += " unreachable."; - } else { - tasks[task].unreachableCountTip = "No host events were unreachable."; - } - if (tasks[task].missingCount) { - tasks[task].missingCountTip = tasks[task].missingCount; - tasks[task].missingCountTip += (tasks[task].missingCount === 1) ? " host event was" : " host events were"; - tasks[task].missingCountTip += " missing."; - } else { - tasks[task].missingCountTip = "No host events were missing."; - } - } - }); - scope.hostResults = []; - - scope.hostResultsMaxRows = 200; - scope.tasksMaxRows = 200; - scope.playsMaxRows = 200; - - // Set the following to true when 'Loading...' message desired - scope.playsLoading = true; - scope.tasksLoading = true; - scope.hostResultsLoading = true; - - // Turn on the 'Waiting...' message until events begin arriving - scope.waiting = true; - - scope.liveEventProcessing = true; // true while job is active and live events are arriving - scope.pauseLiveEvents = false; // control play/pause state of event processing - - scope.job_status = {}; - scope.job_id = job_id; - scope.auto_scroll = false; - - // @issue: OLD SEARCH - // scope.searchPlaysEnabled = true; - // scope.searchTasksEnabled = true; - // scope.searchHostsEnabled = true; - // scope.search_play_status = 'all'; - // scope.search_task_status = 'all'; - // scope.search_host_status = 'all'; - // - // scope.search_play_name = ''; - // scope.search_task_name = ''; - // scope.search_host_name = ''; - - scope.haltEventQueue = false; - scope.processing = false; - scope.lessStatus = false; - scope.lessDetail = false; - // pops the event summary panel open if we're in the host summary child state - //scope.lessEvents = ($state.current.name === 'jobDetail.host-summary' || $state.current.name === 'jobDetail.host-events') ? false : true; - if ($state.current.name === 'jobDetail.host-summary' ){ - scope.lessEvents = false; - } - else{ - scope.lessEvents = true; - } - scope.jobData = {}; - scope.jobData.hostSummaries = {}; - - verbosity_options = [ - { value: 0, label: 'Default' }, - { value: 1, label: 'Verbose' }, - { value: 3, label: 'Debug' } - ]; - - job_type_options = [ - { value: 'run', label: 'Run' }, - { value: 'check', label: 'Check' } - ]; - - GetChoices({ - scope: scope, - url: GetBasePath('unified_jobs'), - field: 'status', - variable: 'status_choices', - }); - - scope.eventsHelpText = "

Successful

\n" + - "

Changed

\n" + - "

Unreachable

\n" + - "

Failed

\n"; - - scope.$on(`ws-job_events-${job_id}`, function(e, data) { - // update elapsed time on each event received - scope.job_status.elapsed = GetElapsed({ - start: scope.job.created, - end: Date.now() - }); - if (api_complete && data.id > lastEventId) { - scope.waiting = false; - data.event = data.event_name; - DigestEvent({ scope: scope, event: data }); - } - UpdateDOM({ scope: scope }); - }); - - scope.$on(`ws-jobs`, function(e, data) { - // if we receive a status change event for the current job indicating the job - // is finished, stop event queue processing and reload - if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10)) { - if (data.status === 'failed' || data.status === 'canceled' || - data.status === 'error' || data.status === 'successful' || data.status === 'running') { - $scope.liveEventProcessing = false; - if (!scope.pauseLiveEvents) { - $scope.$emit('LoadJob'); //this is what is used for the refresh - } - } - } - }); - - scope.$on('ws-jobs-summary', function() { - // the job host summary should now be available from the API - $log.debug('Trigging reload of job_host_summaries'); - scope.$emit('InitialLoadComplete'); - }); - - if (scope.removeInitialLoadComplete) { - scope.removeInitialLoadComplete(); - } - scope.removeInitialLoadComplete = scope.$on('InitialLoadComplete', function() { - Wait('stop'); - - if (JobIsFinished(scope)) { - scope.liveEventProcessing = false; // signal that event processing is over and endless scroll - scope.pauseLiveEvents = false; // should be enabled - var params = { - event: 'playbook_on_stats' - }; - JobDetailService.getRelatedJobEvents(scope.job.id, params) - .success(function() { - UpdateDOM({ scope: scope }); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call failed. GET returned: ' + status }); - }); - $log.debug('Job completed!'); - $log.debug(scope.jobData); - } - else { - api_complete = true; //trigger events to start processing - UpdateDOM({ scope: scope}); - } - }); - - if (scope.removeLoadHosts) { - scope.removeLoadHosts(); - } - scope.removeLoadHosts = scope.$on('LoadHosts', function() { - if (scope.activeTask) { - - var play = scope.jobData.plays[scope.activePlay], - task; - if(play){ - task = play.tasks[scope.activeTask]; - } - if (play && task) { - var params = { - parent: task.id, - event__startswith: 'runner', - page_size: scope.hostResultsMaxRows - }; - JobDetailService.getRelatedJobEvents(scope.job.id, params) - .success(function(data) { - if (data.results.length > 0) { - lastEventId = data.results[0].id; - } - scope.next_host_results = data.next; - task.hostResults = JobDetailService.processHostEvents(data.results); - scope.$emit('InitialLoadComplete'); - }); - } else { - scope.$emit('InitialLoadComplete'); - } - } else { - scope.$emit('InitialLoadComplete'); - } - }); - - if (scope.removeLoadTasks) { - scope.removeLoadTasks(); - } - scope.removeLoadTasks = scope.$on('LoadTasks', function() { - if (scope.activePlay) { - var play = scope.jobData.plays[scope.activePlay]; - - if (play) { - var params = { - event_id: play.id, - page_size: scope.tasksMaxRows, - order: 'id' - }; - JobDetailService.getJobTasks(scope.job.id, params) - .success(function(data) { - scope.next_tasks = data.next; - if (data.results.length > 0) { - lastEventId = data.results[data.results.length - 1].id; - if (scope.liveEventProcessing) { - scope.activeTask = data.results[data.results.length - 1].id; - } - else { - scope.activeTask = data.results[0].id; - } - scope.selectedTask = scope.activeTask; - } - data.results.forEach(function(event, idx) { - var end, elapsed, status, status_text; - - if (play.firstTask === undefined || play.firstTask === null) { - play.firstTask = event.id; - play.hostCount = (event.host_count) ? event.host_count : 0; - } - - if (idx < data.results.length - 1) { - // end date = starting date of the next event - end = data.results[idx + 1].created; - } - else { - // no next event (task), get the end time of the play - if(scope.jobData.plays[scope.activePlay]){ - end = scope.jobData.plays[scope.activePlay].finished; - } - } - - if (end) { - elapsed = GetElapsed({ - start: event.created, - end: end - }); - } - else { - elapsed = '00:00:00'; - } - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - - play.tasks[event.id] = { - id: event.id, - play_id: scope.activePlay, - name: event.name, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - created: event.created, - modified: event.modified, - finished: end, - elapsed: elapsed, - hostCount: (event.host_count) ? event.host_count : 0, - reportedHosts: (event.reported_hosts) ? event.reported_hosts : 0, - successfulCount: (event.successful_count) ? event.successful_count : 0, - failedCount: (event.failed_count) ? event.failed_count : 0, - changedCount: (event.changed_count) ? event.changed_count : 0, - skippedCount: (event.skipped_count) ? event.skipped_count : 0, - unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, - taskActiveClass: '', - hostResults: {} - }; - if (play.firstTask !== event.id) { - // this is not the first task - play.tasks[event.id].hostCount = play.tasks[play.firstTask].hostCount; - } - if (play.tasks[event.id].reportedHosts === 0 && play.tasks[event.id].successfulCount === 0 && - play.tasks[event.id].failedCount === 0 && play.tasks[event.id].changedCount === 0 && - play.tasks[event.id].skippedCount === 0 && play.tasks[event.id].unreachableCount === 0) { - play.tasks[event.id].status = 'no-matching-hosts'; - play.tasks[event.id].status_text = 'No matching hosts'; - play.tasks[event.id].status_tip = "Event ID: " + event.id + "
Status: No matching hosts"; - } - play.taskCount++; - SetTaskStyles({ - task: play.tasks[event.id] - }); - }); - if (scope.activeTask && scope.jobData.plays[scope.activePlay] && scope.jobData.plays[scope.activePlay].tasks[scope.activeTask]) { - scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'JobDetail-tableRow--selected'; - } - scope.$emit('LoadHosts'); - }) - .error(function(data) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call failed. GET returned: ' + status }); - }); - } else { - scope.$emit('InitialLoadComplete'); - } - } else { - scope.$emit('InitialLoadComplete'); - } - }); - - if (scope.removeLoadPlays) { - scope.removeLoadPlays(); - } - scope.removeLoadPlays = scope.$on('LoadPlays', function(e, events_url) { - scope.jobData.plays = {}; - var params = { - order_by: 'id' - }; - - JobDetailService.getJobPlays(scope.job.id, params) - .success( function(data) { - scope.next_plays = data.next; - if (data.results.length > 0) { - lastEventId = data.results[data.results.length - 1].id; - if (scope.liveEventProcessing) { - scope.activePlay = data.results[data.results.length - 1].id; - } - else { - scope.activePlay = data.results[0].id; - } - scope.selectedPlay = scope.activePlay; - } else { - // if we are here, there are no plays and the job has failed, let the user know they may want to consult stdout - if ( (scope.job_status.status === 'failed' || scope.job_status.status === 'error') && - (!scope.job_status.explanation)) { - scope.job_status.explanation = "See standard out for more details"; - } - } - data.results.forEach(function(event, idx) { - var status, status_text, start, end, elapsed, ok, changed, failed, skipped; - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - start = event.started; - - if (idx < data.results.length - 1) { - // end date = starting date of the next event - end = data.results[idx + 1].started; - } - else if (JobIsFinished(scope)) { - // this is the last play and the job already finished - end = scope.job_status.finished; - } - if (end) { - elapsed = GetElapsed({ - start: start, - end: end - }); - } - else { - elapsed = '00:00:00'; - } - - scope.jobData.plays[event.id] = { - id: event.id, - name: event.play, - created: start, - finished: end, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - elapsed: elapsed, - hostCount: 0, - fistTask: null, - taskCount: 0, - playActiveClass: '', - unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, - tasks: {} - }; - - ok = (event.ok_count) ? event.ok_count : 0; - changed = (event.changed_count) ? event.changed_count : 0; - failed = (event.failed_count) ? event.failed_count : 0; - skipped = (event.skipped_count) ? event.skipped_count : 0; - - scope.jobData.plays[event.id].hostCount = ok + changed + failed + skipped; - - if (scope.jobData.plays[event.id].hostCount > 0 || event.unreachable_count > 0 || scope.job_status.status === 'successful' || - scope.job_status.status === 'failed' || scope.job_status.status === 'error' || scope.job_status.status === 'canceled') { - // force the play to be on the 'active' list - scope.jobData.plays[event.id].taskCount = 1; - } - - if (scope.jobData.plays[event.id].hostCount === 0 && event.unreachable_count === 0) { - scope.jobData.plays[event.id].status = 'no-matching-hosts'; - scope.jobData.plays[event.id].status_text = 'No matching hosts'; - scope.jobData.plays[event.id].status_tip = "Event ID: " + event.id + "
Status: No matching hosts"; - } - }); - if (scope.activePlay && scope.jobData.plays[scope.activePlay]) { - scope.jobData.plays[scope.activePlay].playActiveClass = 'JobDetail-tableRow--selected'; - } - scope.$emit('LoadTasks', events_url); - }); - }); - - - if (scope.removeLoadJob) { - scope.removeLoadJob(); - } - scope.removeLoadJobRow = scope.$on('LoadJob', function() { - Wait('start'); - scope.job_status = {}; - - scope.playsLoading = true; - scope.tasksLoading = true; - scope.hostResultsLoading = true; - - // Load the job record - JobDetailService.getJob({id: job_id}) - .success(function(res) { - var i, - data = res.results[0]; - scope.job = data; - scope.job_template_name = data.name; - scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : ''; - scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : ''; - scope.job_template_url = '/#/templates/' + data.unified_job_template; - scope.inventory_url = (scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : ''; - scope.project_url = (scope.project_name && data.project) ? '/#/projects/' + data.project : ''; - scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : ''; - scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : ''; - scope.playbook = data.playbook; - scope.credential = data.credential; - scope.cloud_credential = data.cloud_credential; - scope.forks = data.forks; - scope.limit = data.limit; - scope.verbosity = data.verbosity; - scope.job_tags = data.job_tags; - scope.variables = ParseVariableString(data.extra_vars); - - // If we get created_by back from the server then use it. This means that the job was kicked - // off by a user and not a schedule AND that the user still exists in the system. - if(data.summary_fields.created_by) { - scope.users_url = '/#/users/' + data.summary_fields.created_by.id; - scope.created_by = data.summary_fields.created_by.username; - } - else { - if(data.summary_fields.schedule) { - // Build the Launched By link to point to the schedule that kicked it off - scope.scheduled_by = (data.summary_fields.schedule.name) ? data.summary_fields.schedule.name.toString() : ''; - } - // If there is no schedule or created_by then we can assume that the job was - // created by a deleted user - } - - if (data.summary_fields.credential) { - scope.credential_name = data.summary_fields.credential.name; - scope.credential_url = data.related.credential - .replace('api/v1', '#'); - } else { - scope.credential_name = ""; - } - - if (data.summary_fields.cloud_credential) { - scope.cloud_credential_name = data.summary_fields.cloud_credential.name; - scope.cloud_credential_url = data.related.cloud_credential - .replace('api/v1', '#'); - } else { - scope.cloud_credential_name = ""; - } - - if (data.summary_fields.network_credential) { - scope.network_credential_name = data.summary_fields.network_credential.name; - scope.network_credential_url = data.related.network_credential - .replace('api/v1', '#'); - } else { - scope.network_credential_name = ""; - } - - for (i=0; i < verbosity_options.length; i++) { - if (verbosity_options[i].value === data.verbosity) { - scope.verbosity = verbosity_options[i].label; - } - } - - for (i=0; i < job_type_options.length; i++) { - if (job_type_options[i].value === data.job_type) { - scope.job_type = job_type_options[i].label; - } - } - - // In the case the job is already completed, or an error already happened, - // populate scope.job_status info - scope.job_status.status = (data.status === 'waiting' || data.status === 'new') ? 'pending' : data.status; - scope.job_status.started = data.started; - scope.job_status.status_class = ((data.status === 'error' || data.status === 'failed') && data.job_explanation) ? "alert alert-danger" : ""; - scope.job_status.explanation = data.job_explanation; - if(data.result_traceback) { - scope.job_status.traceback = data.result_traceback.trim().split('\n').join('
'); - } - if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') { - scope.job_status.finished = data.finished; - scope.liveEventProcessing = false; - scope.pauseLiveEvents = false; - scope.waiting = false; - scope.playsLoading = false; - scope.tasksLoading = false; - scope.hostResultsLoading = false; - } - else { - scope.job_status.finished = null; - } - - if (data.started && data.finished) { - scope.job_status.elapsed = GetElapsed({ - start: data.started, - end: data.finished - }); - } - else { - scope.job_status.elapsed = '00:00:00'; - } - scope.status_choices.every(function(status) { - if (status.value === scope.job.status) { - scope.job_status.status_label = status.label; - return false; - } - return true; - }); - //scope.setSearchAll('host'); - ParseTypeChange({ scope: scope, field_id: 'pre-formatted-variables', readOnly: true }); - scope.$emit('LoadPlays', data.related.job_events); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve job: ' + $stateParams.id + '. GET returned: ' + status }); - }); - }); - - - if (scope.removeRefreshCompleted) { - scope.removeRefreshCompleted(); - } - scope.removeRefreshCompleted = scope.$on('RefreshCompleted', function() { - refresh_count++; - if (refresh_count === 1) { - // First time. User just loaded page. - scope.$emit('LoadJob'); - } - }); - - scope.adjustSize = function() { - var height, ww = $(window).width(); - if (ww < 1024) { - $('#job-summary-container').hide(); - $('#job-detail-container').css({ "width": "100%", "padding-right": "15px" }); - $('#summary-button').show(); - } - else { - $('.overlay').hide(); - $('#summary-button').hide(); - $('#hide-summary-button').hide(); - $('#job-summary-container .job_well').css({ - 'box-shadow': 'none', - 'height': 'auto' - }); - $('#job-summary-container').css({ - "width": "41.66666667%", - "padding-left": "7px", - "padding-right": "15px", - "z-index": 0 - }); - setTimeout(function() { $('#job-summary-container .job_well').height($('#job-detail-container').height() - 18); }, 500); - $('#job-summary-container').show(); - } - - scope.lessStatus = false; // close the view more status option - - - height = $(window).height() - $('#main-menu-container .navbar').outerHeight() - - $('#job-detail-container').outerHeight() - 20; - scope.$emit('RefreshCompleted'); - }; - - setTimeout(function() { scope.adjustSize(); }, 500); - - // Use debounce for the underscore library to adjust after user resizes window. - $(window).resize(_.debounce(function(){ - scope.adjustSize(); - }, 500)); - - function flashPlayTip() { - setTimeout(function(){ - $('#play-help').popover('show'); - },500); - setTimeout(function() { - $('#play-help').popover('hide'); - }, 5000); - } - - scope.selectPlay = function(id) { - if (scope.liveEventProcessing && !scope.pauseLiveEvents) { - scope.pauseLiveEvents = true; - flashPlayTip(); - } - SelectPlay({ - scope: scope, - id: id - }); - }; - - scope.selectTask = function(id) { - if (scope.liveEventProcessing && !scope.pauseLiveEvents) { - scope.pauseLiveEvents = true; - flashPlayTip(); - } - SelectTask({ - scope: scope, - id: id - }); - }; - - scope.togglePlayButton = function() { - if (scope.pauseLiveEvents) { - scope.pauseLiveEvents = false; - scope.$emit('LoadJob'); - } - }; - - scope.objectIsEmpty = function(obj) { - if (angular.isObject(obj)) { - return (Object.keys(obj).length > 0) ? false : true; - } - return true; - }; - - scope.toggleLessEvents = function() { - if (!scope.lessEvents) { - $('#events-summary').slideUp(0); - scope.lessEvents = true; - } - else { - $('#events-summary').slideDown(0); - scope.lessEvents = false; - } - }; - - scope.toggleLessStatus = function() { - if (!scope.lessStatus) { - $('#job-status-form').slideUp(200); - scope.lessStatus = true; - } - else { - $('#job-status-form').slideDown(200); - scope.lessStatus = false; - } - }; - - scope.toggleLessDetail = function() { - if (!scope.lessDetail) { - $('#job-detail-details').slideUp(200); - scope.lessDetail = true; - } - else { - $('#job-detail-details').slideDown(200); - scope.lessDetail = false; - } - }; - - // @issue: OLD SEARCH - // scope.filterTaskStatus = function() { - // scope.search_task_status = (scope.search_task_status === 'all') ? 'failed' : 'all'; - // if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - // LoadTasks({ - // scope: scope - // }); - // } - // }; - // scope.filterPlayStatus = function() { - // scope.search_play_status = (scope.search_play_status === 'all') ? 'failed' : 'all'; - // if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - // LoadPlays({ - // scope: scope - // }); - // } - // }; - // scope.filterHostStatus = function(){ - // scope.search_host_status = (scope.search_host_status === 'all') ? 'failed' : 'all'; - // if (!scope.liveEventProcessing || scope.pauseLiveEvents){ - // if (scope.selectedTask !== null && scope.selectedPlay !== null){ - // var params = { - // parent: scope.selectedTask, - // page_size: scope.hostResultsMaxRows, - // order: 'host_name,counter', - // }; - // if (scope.search_host_status === 'failed'){ - // params.failed = true; - // } - // JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){ - // scope.hostResults = JobDetailService.processHostEvents(res.results); - // scope.hostResultsLoading = false; - // }); - // } - // } - // }; - // scope.searchPlays = function() { - // if (scope.search_play_name) { - // scope.searchPlaysEnabled = false; - // } - // else { - // scope.searchPlaysEnabled = true; - // } - // if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - // LoadPlays({ - // scope: scope - // }); - // } - // }; - // scope.searchPlaysKeyPress = function(e) { - // if (e.keyCode === 13) { - // scope.searchPlays(); - // e.stopPropagation(); - // } - // }; - // scope.searchTasks = function() { - // var params = {}; - // if (scope.search_task_name) { - // scope.searchTasksEnabled = false; - // } - // else { - // scope.searchTasksEnabled = true; - // } - // if (!scope.liveEventProcessing || scope.pauseLiveEvents) { - // if (scope.search_task_status === 'failed'){ - // params.failed = true; - // } - // LoadTasks({ - // scope: scope - // }); - // } - // }; - // scope.searchTasksKeyPress = function(e) { - // if (e.keyCode === 13) { - // scope.searchTasks(); - // e.stopPropagation(); - // } - // }; - // scope.searchHosts = function() { - // var params = {}; - // if (scope.search_host_name) { - // scope.searchHostsEnabled = false; - // } - // else { - // scope.searchHostsEnabled = true; - // } - // if ((!scope.liveEventProcessing || scope.pauseLiveEvents) && scope.selectedTask) { - // scope.hostResultsLoading = true; - // params = { - // parent: scope.selectedTask, - // event__startswith: 'runner', - // page_size: scope.hostResultsMaxRows, - // order: 'host_name,counter', - // host_name__icontains: scope.search_host_name - // }; - // if (scope.search_host_status === 'failed'){ - // params.failed = true; - // } - // JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){ - // scope.hostResults = JobDetailService.processHostEvents(res.results); - // scope.hostResultsLoading = false; - // }); - // } - // }; - // scope.searchHostsKeyPress = function(e) { - // if (e.keyCode === 13) { - // scope.searchHosts(); - // e.stopPropagation(); - // } - // }; - - if (scope.removeDeleteFinished) { - scope.removeDeleteFinished(); - } - scope.removeDeleteFinished = scope.$on('DeleteFinished', function(e, action) { - Wait('stop'); - if (action !== 'cancel') { - Wait('stop'); - $location.url('/jobs'); - } - }); - - scope.deleteJob = function() { - DeleteJob({ - scope: scope, - id: scope.job.id, - job: scope.job, - callback: 'DeleteFinished' - }); - }; - - scope.relaunchJob = function() { - RelaunchPlaybook({ - scope: scope, - id: scope.job.id - }); - }; - - scope.playsScrollDown = function() { - // check for more plays when user scrolls to bottom of play list... - if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_plays) { - $('#playsMoreRows').fadeIn(); - scope.playsLoading = true; - JobDetailService.getNextPage(scope.next_plays) - .success( function(data) { - scope.next_plays = data.next; - data.results.forEach(function(event, idx) { - var status, status_text, start, end, elapsed, ok, changed, failed, skipped; - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - start = event.started; - - if (idx < data.results.length - 1) { - // end date = starting date of the next event - end = data.results[idx + 1].started; - } - else if (JobIsFinished(scope)) { - // this is the last play and the job already finished - end = scope.job_status.finished; - } - if (end) { - elapsed = GetElapsed({ - start: start, - end: end - }); - } - else { - elapsed = '00:00:00'; - } - - scope.plays.push({ - id: event.id, - name: event.play, - created: start, - finished: end, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - elapsed: elapsed, - hostCount: 0, - fistTask: null, - playActiveClass: '', - unreachableCount: (event.unreachable_count) ? event.unreachable_count : 0, - }); - - ok = (event.ok_count) ? event.ok_count : 0; - changed = (event.changed_count) ? event.changed_count : 0; - failed = (event.failed_count) ? event.failed_count : 0; - skipped = (event.skipped_count) ? event.skipped_count : 0; - - scope.plays[scope.plays.length - 1].hostCount = ok + changed + failed + skipped; - scope.playsLoading = false; - }); - $('#playsMoreRows').fadeOut(400); - }) - .error( function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + scope.next_plays + '. GET returned: ' + status }); - }); - } - }; - - scope.tasksScrollDown = function() { - // check for more tasks when user scrolls to bottom of task list... - if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_tasks) { - $('#tasksMoreRows').fadeIn(); - scope.tasksLoading = true; - JobDetailService.getNextPage(scope.next_tasks) - .success(function(data) { - scope.next_tasks = data.next; - data.results.forEach(function(event, idx) { - var end, elapsed, status, status_text; - if (idx < data.results.length - 1) { - // end date = starting date of the next event - end = data.results[idx + 1].created; - } - else { - // no next event (task), get the end time of the play - scope.plays.every(function(p, j) { - if (p.id === scope.selectedPlay) { - end = scope.plays[j].finished; - return false; - } - return true; - }); - } - if (end) { - elapsed = GetElapsed({ - start: event.created, - end: end - }); - } - else { - elapsed = '00:00:00'; - } - - status = (event.failed) ? 'failed' : (event.changed) ? 'changed' : 'successful'; - status_text = (event.failed) ? 'Failed' : (event.changed) ? 'Changed' : 'OK'; - - scope.tasks.push({ - id: event.id, - play_id: scope.selectedPlay, - name: event.name, - status: status, - status_text: status_text, - status_tip: "Event ID: " + event.id + "
Status: " + status_text, - created: event.created, - modified: event.modified, - finished: end, - elapsed: elapsed, - hostCount: event.host_count, // hostCount, - reportedHosts: event.reported_hosts, - successfulCount: event.successful_count, - failedCount: event.failed_count, - changedCount: event.changed_count, - skippedCount: event.skipped_count, - taskActiveClass: '' - }); - SetTaskStyles({ - task: scope.tasks[scope.tasks.length - 1] - }); - }); - $('#tasksMoreRows').fadeOut(400); - scope.tasksLoading = false; - }) - .error(function(data, status) { - $('#tasksMoreRows').fadeOut(400); - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + scope.next_tasks + '. GET returned: ' + status }); - }); - } - }; - - scope.hostResultsScrollDown = function() { - // check for more hosts when user scrolls to bottom of host results list... - if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_host_results) { - $('#hostResultsMoreRows').fadeIn(); - scope.hostResultsLoading = true; - JobDetailService.getNextPage(scope.next_host_results) - .success(function(data) { - scope.next_host_results = data.next; - data.results.forEach(function(row) { - var status, status_text, item, msg; - if (row.event === "runner_on_skipped") { - status = 'skipped'; - } - else if (row.event === "runner_on_unreachable") { - status = 'unreachable'; - } - else { - status = (row.failed) ? 'failed' : (row.changed) ? 'changed' : 'successful'; - } - switch(status) { - case "successful": - status_text = 'OK'; - break; - case "changed": - status_text = "Changed"; - break; - case "failed": - status_text = "Failed"; - break; - case "unreachable": - status_text = "Unreachable"; - break; - case "skipped": - status_text = "Skipped"; - } - if (row.event_data && row.event_data.res) { - item = row.event_data.res.item; - if (typeof item === "object") { - item = JSON.stringify(item); - } - } - msg = ''; - if (row.event_data && row.event_data.res) { - if (typeof row.event_data.res === 'object') { - msg = row.event_data.res.msg; - } else { - msg = row.event_data.res; - } - } - scope.hostResults.push({ - id: row.id, - status: status, - status_text: status_text, - host_id: row.host, - task_id: row.parent, - name: row.event_data.host, - created: row.created, - msg: (row.event_data && row.event_data.res) ? row.event_data.res.msg : '', - item: item - }); - scope.hostResultsLoading = false; - }); - $('#hostResultsMoreRows').fadeOut(400); - }) - .error(function(data, status) { - $('#hostResultsMoreRows').fadeOut(400); - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + scope.next_host_results + '. GET returned: ' + status }); - }); - } - }; - - scope.refresh = function(){ - $scope.$emit('LoadJob'); - }; - - // Click binding for the expand/collapse button on the standard out log - $scope.toggleStdoutFullscreen = function() { - $scope.stdoutFullScreen = !$scope.stdoutFullScreen; - }; - - scope.editSchedule = function() { - // We need to get the schedule's ID out of the related links - // An example of the related schedule link looks like /api/v1/schedules/5 - // where 5 is the ID we are trying to capture - var regex = /\/api\/v1\/schedules\/(\d+)\//; - var id = scope.job.related.schedule.match(regex)[1]; - - if(scope.job.job_template && id) { - $state.go('jobTemplateSchedules.edit', {id: scope.job.job_template, schedule_id: id}); - } - }; - - // SchedulesRefresh is the callback string that we passed to the edit schedule modal - // When the modal successfully updates the schedule it will emit this event and pass - // the updated schedule object - if (scope.removeSchedulesRefresh) { - scope.removeSchedulesRefresh(); - } - scope.$on('SchedulesRefresh', function(e, data) { - if (data) { - scope.scheduled_by = data.name; - } - }); - } -]; diff --git a/awx/ui/client/src/job-detail/job-detail.partial.html b/awx/ui/client/src/job-detail/job-detail.partial.html deleted file mode 100644 index bd0b488b18..0000000000 --- a/awx/ui/client/src/job-detail/job-detail.partial.html +++ /dev/null @@ -1,435 +0,0 @@ -
-
-
- -
- -
-
- -
- - - -
-
- -
-
- -
{{ job_status.status_label }}
-
- -
- -
-
Previous Task Failed - - - - -
-
- -
- -
-
- -
- - -
- -
- -
{{ job_status.started | longDate }}
-
- -
- -
{{ job_type }}
-
- -
- -
{{ job_status.finished | longDate }}
-
- -
- - -
- -
- -
{{ job_status.elapsed }}
-
- -
- - -
- -
- - -
- -
- - -
- -
- -
{{ job.playbook }}
-
- -
- - -
- -
- - -
- -
- - -
- -
- -
{{ job.forks }}
-
- -
- -
{{ job.limit }}
-
- -
- -
{{ verbosity }}
-
- -
- -
{{ job.job_tags }}
-
- -
- -
{{ job.skip_tags }}
-
- -
- - -
- -
-
- - - -
- - -
-
-
1 Please select from a play below to view its associated tasks.
-
-
-
- - - -
-
-
-
- - -
-
-
- - -
- - - - - - - - -
PlaysStartedElapsed
-
-
- - - - - - - - - - - - - - - - - - -
{{ play.name }}{{ play.created | date: 'HH:mm:ss' }}{{ play.elapsed }}
Waiting...
Loading...
No matching plays
-
-
- -
-
- - -
-
2 Please select a task below to view its associated hosts
-
-
-
- - - -
-
-
-
- - -
-
-
- -
- - - - - - - - - -
TasksStartedElapsed
-
-
- - - - - - - - - - - - - - - - - - - -
{{ task.name }}{{ task.created | date: 'HH:mm:ss' }}{{ task.elapsed }}
Waiting...
Loading...
No matching tasks
-
-
-
- - -
-
3 Please select a host below to view associated task details.
-
-
-
- - - -
-
-
-
- - -
-
-
-
- - - - - - - - -
HostsItemMessage
-
- -
- - - - - - - - - - - - - - - - - -
- {{ result.name }}{{ result.name }} - {{ result.item }}{{ result.msg }}
Waiting...
Loading...
No matching host events
-
-
-
- -
-
- - - - - - - -
- - - -
-
-
-
STANDARD OUT
-
- - - - -
-
- -
-
- -
-
- - - -
diff --git a/awx/ui/client/src/job-detail/job-detail.route.js b/awx/ui/client/src/job-detail/job-detail.route.js deleted file mode 100644 index 94088c126b..0000000000 --- a/awx/ui/client/src/job-detail/job-detail.route.js +++ /dev/null @@ -1,91 +0,0 @@ -// <<<<<<< 4cf6a946a1aa14b7d64a8e1e8dabecfd3d056f27 -// //<<<<<<< bc59236851902d7c768aa26abdb7dc9c9dc27a5a -// /************************************************* -// * Copyright (c) 2016 Ansible, Inc. -// * -// * All Rights Reserved -// *************************************************/ -// -// // <<<<<<< a3d9eea2c9ddb4e16deec9ec38dea16bf37c559d -// // import { templateUrl } from '../shared/template-url/template-url.factory'; -// // -// // export default { -// // name: 'jobDetail', -// // url: '/jobs/{id: int}', -// // ncyBreadcrumb: { -// // parent: 'jobs', -// // label: "{{ job.id }} - {{ job.name }}" -// // }, -// // data: { -// // socket: { -// // "groups": { -// // "jobs": ["status_changed", "summary"], -// // "job_events": [] -// // } -// // } -// // }, -// // templateUrl: templateUrl('job-detail/job-detail'), -// // controller: 'JobDetailController' -// // }; -// // ======= -// // import {templateUrl} from '../shared/template-url/template-url.factory'; -// // -// // export default { -// // name: 'jobDetail', -// // url: '/jobs/:id', -// // ncyBreadcrumb: { -// // parent: 'jobs', -// // label: "{{ job.id }} - {{ job.name }}" -// // }, -// // socket: { -// // "groups":{ -// // "jobs": ["status_changed", "summary"], -// // "job_events": [] -// // } -// // }, -// // templateUrl: templateUrl('job-detail/job-detail'), -// // controller: 'JobDetailController' -// // }; -// //======= -// ======= -// >>>>>>> Rebase of devel (w/ channels) + socket rework for new job details -// // /************************************************* -// // * Copyright (c) 2016 Ansible, Inc. -// // * -// // * All Rights Reserved -// // *************************************************/ -// // -// // import {templateUrl} from '../shared/template-url/template-url.factory'; -// // -// // export default { -// // name: 'jobDetail', -// // url: '/jobs/:id', -// // ncyBreadcrumb: { -// // parent: 'jobs', -// // label: "{{ job.id }} - {{ job.name }}" -// // }, -// // socket: { -// // "groups":{ -// // "jobs": ["status_changed", "summary"], -// // "job_events": [] -// // } -// // }, -// // resolve: { -// // jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { -// // if (!$rootScope.event_socket) { -// // $rootScope.event_socket = Socket({ -// // scope: $rootScope, -// // endpoint: "job_events" -// // }); -// // $rootScope.event_socket.init(); -// // // returns should really be providing $rootScope.event_socket -// // // otherwise, we have to inject the entire $rootScope into the controller -// // return true; -// // } else { -// // return true; -// // } -// // }] -// // }, -// // templateUrl: templateUrl('job-detail/job-detail'), -// // controller: 'JobDetailController' -// // }; diff --git a/awx/ui/client/src/job-detail/job-detail.service.js b/awx/ui/client/src/job-detail/job-detail.service.js deleted file mode 100644 index 8e436a3e96..0000000000 --- a/awx/ui/client/src/job-detail/job-detail.service.js +++ /dev/null @@ -1,215 +0,0 @@ -export default - ['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', function($rootScope, Rest, GetBasePath, ProcessErrors){ - return { - stringifyParams: function(params){ - return _.reduce(params, (result, value, key) => { - return result + key + '=' + value + '&'; - }, ''); - }, - - // the the API passes through Ansible's event_data response - // we need to massage away the verbose & redundant stdout/stderr properties - processJson: function(data){ - // configure fields to ignore - var ignored = [ - 'type', - 'event_data', - 'related', - 'summary_fields', - 'url', - 'ansible_facts', - ]; - // remove ignored properties - var result = _.chain(data).cloneDeep().forEach(function(value, key, collection){ - if (ignored.indexOf(key) > -1){ - delete collection[key]; - } - }).value(); - return result; - }, - // Return Ansible's passed-through response msg on a job_event - processEventMsg: function(event){ - return typeof event.event_data.res === 'object' ? event.event_data.res.msg : event.event_data.res; - }, - // Return only Ansible's passed-through response item on a job_event - processEventItem: function(event){ - try{ - var item = event.event_data.res.item; - return typeof item === 'object' ? JSON.stringify(item) : item; - } - catch(err){return;} - }, - processsEventTip: function(event, status){ - try{ - var string = `Event ID: ${ event.id }
Status: ${ _.capitalize(status.status)}. Click for details`; - return typeof item === 'object' ? JSON.stringify(string) : string; - } - catch(err){return;} - }, - // Generate a helper class for job_event statuses - // the stack for which status to display is - // unreachable > failed > changed > ok - // uses the API's runner events and convenience properties .failed .changed to determine status. - // see: job_event_callback.py for more filters to support - processEventStatus: function(event){ - if (event.event === 'runner_on_unreachable'){ - return { - class: 'HostEvents-status--unreachable', - status: 'unreachable' - }; - } - // equiv to 'runner_on_error' && 'runner on failed' - if (event.failed){ - return { - class: 'HostEvents-status--failed', - status: 'failed' - }; - } - // catch the changed case before ok, because both can be true - if (event.changed){ - return { - class: 'HostEvents-status--changed', - status: 'changed' - }; - } - if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok'){ - return { - class: 'HostEvents-status--ok', - status: 'ok' - }; - } - if (event.event === 'runner_on_skipped'){ - return { - class: 'HostEvents-status--skipped', - status: 'skipped' - }; - } - }, - // Consumes a response from this.getRelatedJobEvents(id, params) - // returns an array for view logic to iterate over to build host result rows - processHostEvents: function(data){ - var self = this; - var results = []; - data.forEach(function(event){ - if (event.event !== 'runner_on_no_hosts'){ - var status = self.processEventStatus(event); - var msg = self.processEventMsg(event); - var item = self.processEventItem(event); - var tip = self.processsEventTip(event, status); - results.push({ - id: event.id, - status: status.status, - status_text: _.capitalize(status.status), - host_id: event.host, - task_id: event.parent, - name: event.event_data.host, - created: event.created, - tip: typeof tip === 'undefined' ? undefined : tip, - msg: typeof msg === 'undefined' ? undefined : msg, - item: typeof item === 'undefined' ? undefined : item - }); - } - }); - return results; - }, - // GET events related to a job run - // e.g. - // ?event=playbook_on_stats - // ?parent=206&event__startswith=runner&page_size=200&order=host_name,counter - getRelatedJobEvents: function(id, params){ - var url = GetBasePath('jobs'); - url = url + id + '/job_events/?' + this.stringifyParams(params); - Rest.setUrl(url); - return Rest.get() - .success(function(data){ - return data; - }) - .error(function(data, status) { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - }, - getJobEventChildren: function(uuid){ - var url = GetBasePath('job_events'); - url = `${url}?parent__uuid=${uuid}&order_by=host_name`; - Rest.setUrl(url); - return Rest.get() - .success(function(data){ - return data; - }) - .error(function(data, status) { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - }, - // GET job host summaries related to a job run - // e.g. ?page_size=200&order=host_name - getJobHostSummaries: function(id, params){ - var url = GetBasePath('jobs'); - url = url + id + '/job_host_summaries/?' + this.stringifyParams(params); - Rest.setUrl(url); - return Rest.get() - .success(function(data){ - return data; - }) - .error(function(data, status) { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - }, - // GET job plays related to a job run - // e.g. ?page_size=200 - getJobPlays: function(id, params){ - var url = GetBasePath('jobs'); - url = url + id + '/job_plays/?' + this.stringifyParams(params); - Rest.setUrl(url); - return Rest.get() - .success(function(data){ - return data; - }) - .error(function(data, status) { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - }, - getJobTasks: function(id, params){ - var url = GetBasePath('jobs'); - url = url + id + '/job_tasks/?' + this.stringifyParams(params); - Rest.setUrl(url); - return Rest.get() - .success(function(data){ - return data; - }) - .error(function(data, status) { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - }, - getJob: function(params){ - var url = GetBasePath('unified_jobs') + '?' + this.stringifyParams(params); - Rest.setUrl(url); - return Rest.get() - .success(function(data){ - return data; - }) - .error(function(data, status) { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - }, - // GET next set of paginated results - // expects 'next' param returned by the API e.g. - // "/api/v1/jobs/51/job_plays/?order_by=id&page=2&page_size=1" - getNextPage: function(url){ - Rest.setUrl(url); - return Rest.get() - .success(function(data){ - return data; - }) - .error(function(data, status) { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - } - }; - }]; diff --git a/awx/ui/client/src/job-detail/main.js b/awx/ui/client/src/job-detail/main.js deleted file mode 100644 index 628f537e43..0000000000 --- a/awx/ui/client/src/job-detail/main.js +++ /dev/null @@ -1,24 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -// import route from './job-detail.route'; -import controller from './job-detail.controller'; -import service from './job-detail.service'; -import hostEvents from './host-events/main'; -// import hostEvent from './host-event/main'; -import hostSummary from './host-summary/main'; - -export default - angular.module('jobDetail', [ - hostEvents.name, - // hostEvent.name, - hostSummary.name - ]) - .controller('JobDetailController', controller) - .service('JobDetailService', service); - // .run(['$stateExtender', function($stateExtender) { - // $stateExtender.addState(route); - // }]); diff --git a/awx/ui/client/src/job-results/host-event/host-event-modal.partial.html b/awx/ui/client/src/job-results/host-event/host-event-modal.partial.html index 96676dc56a..916add240d 100644 --- a/awx/ui/client/src/job-results/host-event/host-event-modal.partial.html +++ b/awx/ui/client/src/job-results/host-event/host-event-modal.partial.html @@ -9,7 +9,7 @@ {{event.host_name}} -
@@ -40,19 +40,19 @@
- - - @@ -64,7 +64,7 @@
- +
diff --git a/awx/ui/client/src/job-results/host-event/host-event.controller.js b/awx/ui/client/src/job-results/host-event/host-event.controller.js index 398ae70ab4..fe8a65b268 100644 --- a/awx/ui/client/src/job-results/host-event/host-event.controller.js +++ b/awx/ui/client/src/job-results/host-event/host-event.controller.js @@ -6,23 +6,15 @@ export default - ['$stateParams', '$scope', '$state', 'Wait', 'JobDetailService', 'hostEvent', 'hostResults', - function($stateParams, $scope, $state, Wait, JobDetailService, hostEvent, hostResults){ + ['$stateParams', '$scope', '$state', 'Wait', 'jobResultsService', 'hostEvent', + function($stateParams, $scope, $state, Wait, jobResultsService, hostEvent){ - $scope.processEventStatus = JobDetailService.processEventStatus; - $scope.hostResults = []; - // Avoid rendering objects in the details fieldset - // ng-if="processResults(value)" via host-event-details.partial.html + $scope.processEventStatus = jobResultsService.processEventStatus; $scope.processResults = function(value){ if (typeof value === 'object'){return false;} else {return true;} }; - $scope.isStdOut = function(){ - if ($state.current.name === 'jobDetail.host-event.stdout' || $state.current.name === 'jobDetail.host-event.stderr'){ - return 'StandardOut-preContainer StandardOut-preContent'; - } - }; - /*ignore jslint start*/ + var initCodeMirror = function(el, data, mode){ var container = document.getElementById(el); var editor = CodeMirror.fromTextArea(container, { // jshint ignore:line @@ -37,17 +29,9 @@ return $state.current.name === name; }; - $scope.getActiveHostIndex = function(){ - var result = $scope.hostResults.filter(function( obj ) { - return obj.id === $scope.event.id; - }); - return $scope.hostResults.indexOf(result[0]); - }; - var init = function(){ hostEvent.event_name = hostEvent.event; $scope.event = _.cloneDeep(hostEvent); - $scope.hostResults = hostResults; // grab standard out & standard error if present from the host // event's "res" object, for things like Ansible modules @@ -72,7 +56,7 @@ } // instantiate Codemirror // try/catch pattern prevents the abstract-state controller from complaining about element being null - if ($state.current.name === 'jobDetail.host-event.json'){ + if ($state.current.name === 'jobResult.host-event.json'){ try{ initCodeMirror('HostEvent-codemirror', JSON.stringify($scope.json, null, 4), {name: "javascript", json: true}); } @@ -80,7 +64,7 @@ // element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController } } - else if ($state.current.name === 'jobDetail.host-event.stdout'){ + else if ($state.current.name === 'jobResult.host-event.stdout'){ try{ initCodeMirror('HostEvent-codemirror', $scope.stdout, 'shell'); } @@ -88,7 +72,7 @@ // element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController } } - else if ($state.current.name === 'jobDetail.host-event.stderr'){ + else if ($state.current.name === 'jobResult.host-event.stderr'){ try{ initCodeMirror('HostEvent-codemirror', $scope.stderr, 'shell'); } diff --git a/awx/ui/client/src/job-results/host-event/host-event.route.js b/awx/ui/client/src/job-results/host-event/host-event.route.js index 23d5fe2451..650a82bb25 100644 --- a/awx/ui/client/src/job-results/host-event/host-event.route.js +++ b/awx/ui/client/src/job-results/host-event/host-event.route.js @@ -7,20 +7,17 @@ import { templateUrl } from '../../shared/template-url/template-url.factory'; var hostEventModal = { - name: 'jobDetail.host-event', + name: 'jobResult.host-event', url: '/host-event/:eventId', controller: 'HostEventController', templateUrl: templateUrl('job-results/host-event/host-event-modal'), 'abstract': false, resolve: { - hostEvent: ['JobDetailService', '$stateParams', function(JobDetailService, $stateParams) { - return JobDetailService.getRelatedJobEvents($stateParams.id, { + hostEvent: ['jobResultsService', '$stateParams', function(jobResultsService, $stateParams) { + return jobResultsService.getRelatedJobEvents($stateParams.id, { id: $stateParams.eventId }).then(function(res) { return res.data.results[0]; }); - }], - hostResults: ['JobDetailService', '$stateParams', function(JobDetailService, $stateParams) { - return JobDetailService.getJobEventChildren($stateParams.taskId).then(res => res.data.results); }] }, onExit: function() { @@ -34,21 +31,21 @@ var hostEventModal = { }; var hostEventJson = { - name: 'jobDetail.host-event.json', + name: 'jobResult.host-event.json', url: '/json', controller: 'HostEventController', templateUrl: templateUrl('job-results/host-event/host-event-codemirror') }; var hostEventStdout = { - name: 'jobDetail.host-event.stdout', + name: 'jobResult.host-event.stdout', url: '/stdout', controller: 'HostEventController', templateUrl: templateUrl('job-results/host-event/host-event-stdout') }; var hostEventStderr = { - name: 'jobDetail.host-event.stderr', + name: 'jobResult.host-event.stderr', url: '/stderr', controller: 'HostEventController', templateUrl: templateUrl('job-results/host-event/host-event-stderr') 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 c1c9211209..fa032c1d68 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -108,7 +108,7 @@ ng-show="!previousTaskFailed"> {{job.job_explanation}}
-
Previous Task Failed failed > changed > ok + // uses the API's runner events and convenience properties .failed .changed to determine status. + // see: job_event_callback.py for more filters to support + processEventStatus: function(event){ + if (event.event === 'runner_on_unreachable'){ + return { + class: 'HostEvent-status--unreachable', + status: 'unreachable' + }; + } + // equiv to 'runner_on_error' && 'runner on failed' + if (event.failed){ + return { + class: 'HostEvent-status--failed', + status: 'failed' + }; + } + // catch the changed case before ok, because both can be true + if (event.changed){ + return { + class: 'HostEvent-status--changed', + status: 'changed' + }; + } + if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok'){ + return { + class: 'HostEvent-status--ok', + status: 'ok' + }; + } + if (event.event === 'runner_on_skipped'){ + return { + class: 'HostEvent-status--skipped', + status: 'skipped' + }; + } + }, + // GET events related to a job run + // e.g. + // ?event=playbook_on_stats + // ?parent=206&event__startswith=runner&page_size=200&order=host_name,counter + getRelatedJobEvents: function(id, params){ + var url = GetBasePath('jobs'); + url = url + id + '/job_events/?' + this.stringifyParams(params); + Rest.setUrl(url); + return Rest.get() + .success(function(data){ + return data; + }) + .error(function(data, status) { + ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + }, + stringifyParams: function(params){ + return _.reduce(params, (result, value, key) => { + return result + key + '=' + value + '&'; + }, ''); + }, + // the the API passes through Ansible's event_data response + // we need to massage away the verbose & redundant stdout/stderr properties + processJson: function(data){ + // configure fields to ignore + var ignored = [ + 'type', + 'event_data', + 'related', + 'summary_fields', + 'url', + 'ansible_facts', + ]; + // remove ignored properties + var result = _.chain(data).cloneDeep().forEach(function(value, key, collection){ + if (ignored.indexOf(key) > -1){ + delete collection[key]; + } + }).value(); + return result; } }; return val; diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js index 03852eb185..14d804ab23 100644 --- a/awx/ui/client/src/job-results/parse-stdout.service.js +++ b/awx/ui/client/src/job-results/parse-stdout.service.js @@ -77,7 +77,7 @@ export default ['$log', 'moment', function($log, moment){ return `"`; } else{ - return ` JobResultsStdOut-stdoutColumn--clickable" ui-sref="jobDetail.host-event.json({eventId: ${event.id}, taskUuid: '${event.event_data.task_uuid}' })" aw-tool-tip="Event ID: ${event.id}
Status: ${event.event_display}
Click for details" data-placement="top"`; + return ` JobResultsStdOut-stdoutColumn--clickable" ui-sref="jobResult.host-event.json({eventId: ${event.id}, taskUuid: '${event.event_data.task_uuid}' })" aw-tool-tip="Event ID: ${event.id}
Status: ${event.event_display}
Click for details" data-placement="top"`; } }, diff --git a/awx/ui/client/src/job-submission/job-submission-factories/adhoc-run.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/adhoc-run.factory.js new file mode 100644 index 0000000000..bcc688936f --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/adhoc-run.factory.js @@ -0,0 +1,162 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name helpers.function:Adhoc + * @description These routines are shared by adhoc command related controllers. + * The content here is very similar to the JobSubmission helper, and in fact, + * certain services are pulled from that helper. This leads to an important + * point: if you need to create functionality that is shared between the command + * and playbook run process, put that code in the JobSubmission helper and make + * it into a reusable step (by specifying a callback parameter in the factory). + * For a good example of this, please see how the AdhocLaunch factory in this + * file utilizes the CheckPasswords factory from the JobSubmission helper. + * + * #AdhocRelaunch Step 1: preparing the GET to ad_hoc_commands/n/relaunch + * The adhoc relaunch process is called from the JobSubmission helper. It is a + * separate process from the initial adhoc run becuase of the way the API + * endpoints work. For AdhocRelaunch, we have access to the original run and + * we can pull the related relaunch URL by knowing the original Adhoc runs ID. + * + * #AdhocRelaunch Step 2: If we got passwords back, add them + * The relaunch URL gives us back the passwords we need to prompt for (if any). + * We'll go to step 3 if there are passwords, and step 4 if not. + * + * #AdhocRelaunch Step 3: PromptForPasswords and the CreateLaunchDialog + * + * #AdhocRelaunch Step 5: StartAdhocRun + * + * #AdhocRelaunch Step 6: LaunchJob and navigate to the standard out page. + + * **If you are + * TODO: once the API endpoint is figured out for running an adhoc command + * from the form is figured out, the rest work should probably be excised from + * the controller and moved into here. See the todo statements in the + * controller for more information about this. + */ + + export default + function AdhocRun($location, $stateParams, LaunchJob, PromptForPasswords, + Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, CreateLaunchDialog, $state) { + return function(params) { + var id = params.project_id, + scope = params.scope.$new(), + new_job_id, + html, + url; + + // this is used to cancel a running adhoc command from + // the jobs page + if (scope.removeCancelJob) { + scope.removeCancelJob(); + } + scope.removeCancelJob = scope.$on('CancelJob', function() { + // Delete the job + Wait('start'); + Rest.setUrl(GetBasePath('ad_hoc_commands') + new_job_id + '/'); + Rest.destroy() + .success(function() { + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, + null, { hdr: 'Error!', + msg: 'Call to ' + url + + ' failed. DELETE returned status: ' + + status }); + }); + }); + + if (scope.removeStartAdhocRun) { + scope.removeStartAdhocRun(); + } + + scope.removeStartAdhocRun = scope.$on('StartAdhocRun', function() { + var password, + postData={}; + for (password in scope.passwords) { + postData[scope.passwords[password]] = scope[ + scope.passwords[password] + ]; + } + // Re-launch the adhoc job + Rest.setUrl(url); + Rest.post(postData) + .success(function (data) { + Wait('stop'); + if($location.path().replace(/^\//, '').split('/')[0] !== 'jobs') { + $state.go('adHocJobStdout', {id: data.id}); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, { + hdr: 'Error!', + msg: 'Failed to launch adhoc command. POST ' + + 'returned status: ' + status }); + }); + }); + + // start routine only if passwords need to be prompted + if (scope.removeCreateLaunchDialog) { + scope.removeCreateLaunchDialog(); + } + scope.removeCreateLaunchDialog = scope.$on('CreateLaunchDialog', + function(e, html, url) { + CreateLaunchDialog({ + scope: scope, + html: html, + url: url, + callback: 'StartAdhocRun' + }); + }); + + if (scope.removePromptForPasswords) { + scope.removePromptForPasswords(); + } + scope.removePromptForPasswords = scope.$on('PromptForPasswords', + function(e, passwords_needed_to_start,html, url) { + PromptForPasswords({ + scope: scope, + passwords: passwords_needed_to_start, + callback: 'CreateLaunchDialog', + html: html, + url: url + }); + }); // end password prompting routine + + // start the adhoc relaunch routine + Wait('start'); + url = GetBasePath('ad_hoc_commands') + id + '/relaunch/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + new_job_id = data.id; + + scope.passwords_needed_to_start = data.passwords_needed_to_start; + if (!Empty(data.passwords_needed_to_start) && + data.passwords_needed_to_start.length > 0) { + // go through the password prompt routine before + // starting the adhoc run + scope.$emit('PromptForPasswords', data.passwords_needed_to_start, html, url); + } + else { + // no prompting of passwords needed + scope.$emit('StartAdhocRun'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get job template details. GET returned status: ' + status }); + }); + }; + } + + AdhocRun.$inject = + [ '$location','$stateParams', 'LaunchJob', + 'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors', + 'Wait', 'Empty', 'CreateLaunchDialog', '$state' + ]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/check-passwords.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/check-passwords.factory.js new file mode 100644 index 0000000000..4f6089f7e2 --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/check-passwords.factory.js @@ -0,0 +1,43 @@ +export default + function CheckPasswords(Rest, GetBasePath, ProcessErrors, Empty) { + return function(params) { + var scope = params.scope, + callback = params.callback, + credential = params.credential; + + var passwords = []; + if (!Empty(credential)) { + Rest.setUrl(GetBasePath('credentials')+credential); + Rest.get() + .success(function (data) { + if(data.kind === "ssh"){ + if(data.password === "ASK" ){ + passwords.push("ssh_password"); + } + if(data.ssh_key_unlock === "ASK"){ + passwords.push("ssh_key_unlock"); + } + if(data.become_password === "ASK"){ + passwords.push("become_password"); + } + if(data.vault_password === "ASK"){ + passwords.push("vault_password"); + } + } + scope.$emit(callback, passwords); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get job template details. GET returned status: ' + status }); + }); + } + + }; + } + +CheckPasswords.$inject = + [ 'Rest', + 'GetBasePath', + 'ProcessErrors', + 'Empty' + ]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/create-launch-dialog.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/create-launch-dialog.factory.js new file mode 100644 index 0000000000..9620807621 --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/create-launch-dialog.factory.js @@ -0,0 +1,75 @@ +export default + function CreateLaunchDialog($compile, CreateDialog, Wait, ParseTypeChange) { + return function(params) { + var buttons, + scope = params.scope, + html = params.html, + // job_launch_data = {}, + callback = params.callback || 'PlaybookLaunchFinished', + // url = params.url, + e; + + // html+='
job_launch_form.$valid = {{job_launch_form.$valid}}
'; + html+=''; + $('#password-modal').empty().html(html); + $('#password-modal').find('#job_extra_vars').before(scope.helpContainer); + e = angular.element(document.getElementById('password-modal')); + $compile(e)(scope); + + if(scope.prompt_for_vars===true){ + ParseTypeChange({ scope: scope, field_id: 'job_extra_vars' , variable: "extra_vars"}); + } + + buttons = [{ + label: "Cancel", + onClick: function() { + $('#password-modal').dialog('close'); + // scope.$emit('CancelJob'); + // scope.$destroy(); + }, + icon: "fa-times", + "class": "btn btn-default", + "id": "password-cancel-button" + },{ + label: "Launch", + onClick: function() { + scope.$emit(callback); + $('#password-modal').dialog('close'); + }, + icon: "fa-check", + "class": "btn btn-primary", + "id": "password-accept-button" + }]; + + CreateDialog({ + id: 'password-modal', + scope: scope, + buttons: buttons, + width: 620, + height: 700, //(scope.passwords.length > 1) ? 700 : 500, + minWidth: 500, + title: 'Launch Configuration', + callback: 'DialogReady', + onOpen: function(){ + Wait('stop'); + } + }); + + if (scope.removeDialogReady) { + scope.removeDialogReady(); + } + scope.removeDialogReady = scope.$on('DialogReady', function() { + $('#password-modal').dialog('open'); + $('#password-accept-button').attr('ng-disabled', 'job_launch_form.$invalid' ); + e = angular.element(document.getElementById('password-accept-button')); + $compile(e)(scope); + }); + }; + } + +CreateLaunchDialog.$inject = + [ '$compile', + 'CreateDialog', + 'Wait', + 'ParseTypeChange' + ]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/inventory-update.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/inventory-update.factory.js new file mode 100644 index 0000000000..1c3ba53d03 --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/inventory-update.factory.js @@ -0,0 +1,76 @@ +export default + function InventoryUpdate(PromptForPasswords, LaunchJob, Rest, GetBasePath, ProcessErrors, Alert, Wait) { + return function (params) { + + var scope = params.scope, + url = params.url, + inventory_source; + + if (scope.removeUpdateSubmitted) { + scope.removeUpdateSubmitted(); + } + scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function () { + Wait('stop'); + if (scope.socketStatus === 'error') { + Alert('Sync Started', '
The request to start the inventory sync process was submitted. ' + + 'To monitor the status refresh the page by clicking the button.
', 'alert-info', null, null, null, null, true); + if (scope.refreshGroups) { + // inventory detail page + scope.refreshGroups(); + } + else if (scope.refresh) { + scope.refresh(); + } + } + }); + + if (scope.removePromptForPasswords) { + scope.removePromptForPasswords(); + } + scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { + PromptForPasswords({ scope: scope, passwords: inventory_source.passwords_needed_to_update, callback: 'StartTheUpdate' }); + }); + + if (scope.removeStartTheUpdate) { + scope.removeStartTheUpdate(); + } + scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { + LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); + }); + + // Check to see if we have permission to perform the update and if any passwords are needed + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + inventory_source = data; + if (data.can_update) { + if (data.passwords_needed_to_update) { + Wait('stop'); + scope.$emit('PromptForPasswords'); + } + else { + scope.$emit('StartTheUpdate', {}); + } + } else { + Wait('stop'); + Alert('Permission Denied', 'You do not have access to run the inventory sync. Please contact your system administrator.', + 'alert-danger'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get inventory source ' + url + ' GET returned: ' + status }); + }); + }; + } + +InventoryUpdate.$inject = + [ 'PromptForPasswords', + 'LaunchJob', + 'Rest', + 'GetBasePath', + 'ProcessErrors', + 'Alert', + 'Wait' + ]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js index 3cfb80e7a3..8269cac26b 100644 --- a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js +++ b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js @@ -124,12 +124,12 @@ export default if($rootScope.portalMode===false && Empty(data.system_job) || (base === 'home')){ // use $state.go with reload: true option to re-instantiate sockets in - var goToJobDetails = function(state) { + var goTojobResults = function(state) { $state.go(state, {id: job}, {reload:true}); }; if(_.has(data, 'job')) { - goToJobDetails('jobDetail'); + goTojobResults('jobResult'); } else if(base === 'jobs'){ if(scope.clearDialog) { scope.clearDialog(); @@ -137,20 +137,20 @@ export default return; } else if(data.type && data.type === 'workflow_job') { job = data.id; - goToJobDetails('workflowResults'); + goTojobResults('workflowResults'); } else if(_.has(data, 'ad_hoc_command')) { - goToJobDetails('adHocJobStdout'); + goTojobResults('adHocJobStdout'); } else if(_.has(data, 'system_job')) { - goToJobDetails('managementJobStdout'); + goTojobResults('managementJobStdout'); } else if(_.has(data, 'project_update')) { // If we are on the projects list or any child state of that list // then we want to stay on that page. Otherwise go to the stdout // view. if(!$state.includes('projects')) { - goToJobDetails('scmUpdateStdout'); + goTojobResults('scmUpdateStdout'); } } else if(_.has(data, 'inventory_update')) { @@ -158,7 +158,7 @@ export default // page then we want to stay on that page. Otherwise go to the stdout // view. if(!$state.includes('inventoryManage')) { - goToJobDetails('inventorySyncStdout'); + goTojobResults('inventorySyncStdout'); } } } diff --git a/awx/ui/client/src/job-submission/job-submission-factories/project-update.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/project-update.factory.js new file mode 100644 index 0000000000..b089127600 --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/project-update.factory.js @@ -0,0 +1,78 @@ +export default + function ProjectUpdate(PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) { + return function (params) { + var scope = params.scope, + project_id = params.project_id, + url = GetBasePath('projects') + project_id + '/update/', + project; + + if (scope.removeUpdateSubmitted) { + scope.removeUpdateSubmitted(); + } + scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function() { + // Refresh the project list after update request submitted + Wait('stop'); + if (/\d$/.test($location.path())) { + //Request submitted from projects/N page. Navigate back to the list so user can see status + $location.path('/projects'); + } + if (scope.socketStatus === 'error') { + Alert('Update Started', '
The request to start the SCM update process was submitted. ' + + 'To monitor the update status, refresh the page by clicking the button.
', 'alert-info', null, null, null, null, true); + if (scope.refresh) { + scope.refresh(); + } + } + }); + + if (scope.removePromptForPasswords) { + scope.removePromptForPasswords(); + } + scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { + PromptForPasswords({ scope: scope, passwords: project.passwords_needed_to_update, callback: 'StartTheUpdate' }); + }); + + if (scope.removeStartTheUpdate) { + scope.removeStartTheUpdate(); + } + scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { + LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); + }); + + // Check to see if we have permission to perform the update and if any passwords are needed + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + project = data; + if (project.can_update) { + if (project.passwords_needed_to_updated) { + Wait('stop'); + scope.$emit('PromptForPasswords'); + } + else { + scope.$emit('StartTheUpdate', {}); + } + } + else { + Alert('Permission Denied', 'You do not have access to update this project. Please contact your system administrator.', + 'alert-danger'); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to lookup project ' + url + ' GET returned: ' + status }); + }); + }; + } + +ProjectUpdate.$inject = + [ 'PromptForPasswords', + 'LaunchJob', + 'Rest', + '$location', + 'GetBasePath', + 'ProcessErrors', + 'Alert', + 'Wait' + ]; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/prompt-for-passwords.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/prompt-for-passwords.factory.js new file mode 100644 index 0000000000..7efc827919 --- /dev/null +++ b/awx/ui/client/src/job-submission/job-submission-factories/prompt-for-passwords.factory.js @@ -0,0 +1,82 @@ +export default + function PromptForPasswords(CredentialForm) { + return function(params) { + var scope = params.scope, + callback = params.callback || 'PasswordsAccepted', + url = params.url, + form = CredentialForm, + fld, field, + html=params.html || ""; + + scope.passwords = params.passwords; + + html += "
Launching this job requires the passwords listed below. Enter and confirm each password before continuing.
\n"; + + scope.passwords.forEach(function(password) { + // Prompt for password + field = form.fields[password]; + fld = password; + scope[fld] = ''; + html += "
\n"; + html += "\n"; + html += "Please enter a password.
\n"; + html += "
\n"; + html += "
\n"; + + // Add the related confirm field + if (field.associated) { + fld = field.associated; + field = form.fields[field.associated]; + scope[fld] = ''; + html += "
\n"; + html += "\n"; + html += "Please confirm the password.\n"; + html += (field.awPassMatch) ? "This value does not match the password you entered previously. Please confirm that password.
\n" : ""; + html += "
\n"; + html += "
\n"; + } + }); + + scope.$emit(callback, html, url); + + // Password change + scope.clearPWConfirm = function (fld) { + // If password value changes, make sure password_confirm must be re-entered + scope[fld] = ''; + scope.job_launch_form[fld].$setValidity('awpassmatch', false); + scope.checkStatus(); + }; + + scope.checkStatus = function() { + if (!scope.job_launch_form.$invalid) { + $('#password-accept-button').removeAttr('disabled'); + } + else { + $('#password-accept-button').attr({ "disabled": "disabled" }); + } + }; + }; + } + +PromptForPasswords.$inject = + [ 'CredentialForm' ]; diff --git a/awx/ui/client/src/job-submission/job-submission.controller.js b/awx/ui/client/src/job-submission/job-submission.controller.js index 49fb8b7640..39e8404f71 100644 --- a/awx/ui/client/src/job-submission/job-submission.controller.js +++ b/awx/ui/client/src/job-submission/job-submission.controller.js @@ -136,7 +136,7 @@ export default // As of 3.0, the only place the user can relaunch a // playbook is on jobTemplates.edit (completed_jobs tab), - // jobs, and jobDetails $states. + // jobs, and jobResults $states. if (!$scope.submitJobRelaunch) { if($scope.submitJobType && $scope.submitJobType === 'job_template') { @@ -236,18 +236,18 @@ export default // Go out and get some of the job details like inv, cred, name Rest.setUrl(GetBasePath('jobs') + $scope.submitJobId); Rest.get() - .success(function (jobDetailData) { + .success(function (jobResultData) { $scope.job_template_data = { - name: jobDetailData.name + name: jobResultData.name }; $scope.defaults = {}; - if(jobDetailData.summary_fields.inventory) { - $scope.defaults.inventory = angular.copy(jobDetailData.summary_fields.inventory); - $scope.selected_inventory = angular.copy(jobDetailData.summary_fields.inventory); + if(jobResultData.summary_fields.inventory) { + $scope.defaults.inventory = angular.copy(jobResultData.summary_fields.inventory); + $scope.selected_inventory = angular.copy(jobResultData.summary_fields.inventory); } - if(jobDetailData.summary_fields.credential) { - $scope.defaults.credential = angular.copy(jobDetailData.summary_fields.credential); - $scope.selected_credential = angular.copy(jobDetailData.summary_fields.credential); + if(jobResultData.summary_fields.credential) { + $scope.defaults.credential = angular.copy(jobResultData.summary_fields.credential); + $scope.selected_credential = angular.copy(jobResultData.summary_fields.credential); updateRequiredPasswords(); } initiateModal(); diff --git a/awx/ui/client/src/job-submission/main.js b/awx/ui/client/src/job-submission/main.js index 71c4c5c0de..1204613560 100644 --- a/awx/ui/client/src/job-submission/main.js +++ b/awx/ui/client/src/job-submission/main.js @@ -7,6 +7,12 @@ import InitiatePlaybookRun from './job-submission-factories/initiateplaybookrun.factory'; import LaunchJob from './job-submission-factories/launchjob.factory'; import GetSurveyQuestions from './job-submission-factories/getsurveyquestions.factory'; +import AdhocRun from './job-submission-factories/adhoc-run.factory.js'; +import CheckPasswords from './job-submission-factories/check-passwords.factory'; +import CreateLaunchDialog from './job-submission-factories/create-launch-dialog.factory'; +import InventoryUpdate from './job-submission-factories/inventory-update.factory'; +import ProjectUpdate from './job-submission-factories/project-update.factory'; +import PromptForPasswords from './job-submission-factories/prompt-for-passwords.factory'; import submitJob from './job-submission.directive'; import credentialList from './lists/credential/job-sub-cred-list.directive'; import inventoryList from './lists/inventory/job-sub-inv-list.directive'; @@ -16,6 +22,12 @@ export default .factory('InitiatePlaybookRun', InitiatePlaybookRun) .factory('LaunchJob', LaunchJob) .factory('GetSurveyQuestions', GetSurveyQuestions) + .factory('AdhocRun', AdhocRun) + .factory('CheckPasswords', CheckPasswords) + .factory('CreateLaunchDialog', CreateLaunchDialog) + .factory('InventoryUpdate', InventoryUpdate) + .factory('ProjectUpdate', ProjectUpdate) + .factory('PromptForPasswords', PromptForPasswords) .directive('submitJob', submitJob) .directive('jobSubCredList', credentialList) .directive('jobSubInvList', inventoryList); diff --git a/awx/ui/client/src/jobs/factories/delete-job.factory.js b/awx/ui/client/src/jobs/factories/delete-job.factory.js new file mode 100644 index 0000000000..d82afe70fc --- /dev/null +++ b/awx/ui/client/src/jobs/factories/delete-job.factory.js @@ -0,0 +1,137 @@ +export default + function DeleteJob($state, Find, GetBasePath, Rest, Wait, ProcessErrors, Prompt, Alert, + $filter, i18n) { + return function(params) { + var scope = params.scope, + id = params.id, + job = params.job, + callback = params.callback, + action, jobs, url, action_label, hdr; + + if (!job) { + if (scope.completed_jobs) { + jobs = scope.completed_jobs; + } + else if (scope.running_jobs) { + jobs = scope.running_jobs; + } + else if (scope.queued_jobs) { + jobs = scope.queued_jobs; + } + else if (scope.all_jobs) { + jobs = scope.all_jobs; + } + else if (scope.jobs) { + jobs = scope.jobs; + } + job = Find({list: jobs, key: 'id', val: id }); + } + + if (job.status === 'pending' || job.status === 'running' || job.status === 'waiting') { + url = job.related.cancel; + action_label = 'cancel'; + hdr = i18n._('Cancel'); + } else { + url = job.url; + action_label = 'delete'; + hdr = i18n._('Delete'); + } + + action = function () { + Wait('start'); + Rest.setUrl(url); + if (action_label === 'cancel') { + Rest.post() + .success(function () { + $('#prompt-modal').modal('hide'); + if (callback) { + scope.$emit(callback, action_label); + } + else { + $state.reload(); + Wait('stop'); + } + }) + .error(function(obj, status) { + Wait('stop'); + $('#prompt-modal').modal('hide'); + if (status === 403) { + Alert('Error', obj.detail); + } + // Ignore the error. The job most likely already finished. + // ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + + // ' failed. POST returned status: ' + status }); + }); + } else { + Rest.destroy() + .success(function () { + $('#prompt-modal').modal('hide'); + if (callback) { + scope.$emit(callback, action_label); + } + else { + $state.reload(); + Wait('stop'); + } + }) + .error(function (obj, status) { + Wait('stop'); + $('#prompt-modal').modal('hide'); + if (status === 403) { + Alert('Error', obj.detail); + } + // Ignore the error. The job most likely already finished. + //ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + + // ' failed. DELETE returned status: ' + status }); + }); + } + }; + + if (scope.removeCancelNotAllowed) { + scope.removeCancelNotAllowed(); + } + scope.removeCancelNotAllowed = scope.$on('CancelNotAllowed', function() { + Wait('stop'); + Alert('Job Completed', 'The request to cancel the job could not be submitted. The job already completed.', 'alert-info'); + }); + + if (scope.removeCancelJob) { + scope.removeCancelJob(); + } + scope.removeCancelJob = scope.$on('CancelJob', function() { + var cancelBody = "
" + i18n._("Submit the request to cancel?") + "
"; + var deleteBody = "
" + i18n._("Are you sure you want to delete the job below?") + "
#" + id + " " + $filter('sanitize')(job.name) + "
"; + Prompt({ + hdr: hdr, + body: (action_label === 'cancel' || job.status === 'new') ? cancelBody : deleteBody, + action: action, + actionText: (action_label === 'cancel' || job.status === 'new') ? "OK" : "DELETE" + }); + }); + + if (action_label === 'cancel') { + Rest.setUrl(url); + Rest.get() + .success(function(data) { + if (data.can_cancel) { + scope.$emit('CancelJob'); + } + else { + scope.$emit('CancelNotAllowed'); + } + }) + .error(function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + + ' failed. GET returned: ' + status }); + }); + } + else { + scope.$emit('CancelJob'); + } + }; + } + +DeleteJob.$inject = + [ '$state', 'Find', 'GetBasePath', 'Rest', 'Wait', + 'ProcessErrors', 'Prompt', 'Alert', '$filter', 'i18n' + ]; diff --git a/awx/ui/client/src/jobs/factories/job-status-tool-tip.factory.js b/awx/ui/client/src/jobs/factories/job-status-tool-tip.factory.js new file mode 100644 index 0000000000..126c9977d3 --- /dev/null +++ b/awx/ui/client/src/jobs/factories/job-status-tool-tip.factory.js @@ -0,0 +1,31 @@ +export default + function JobStatusToolTip() { + return function(status) { + var toolTip; + switch (status) { + case 'successful': + case 'success': + toolTip = 'There were no failed tasks.'; + break; + case 'failed': + toolTip = 'Some tasks encountered errors.'; + break; + case 'canceled': + toolTip = 'Stopped by user request.'; + break; + case 'new': + toolTip = 'In queue, waiting on task manager.'; + break; + case 'waiting': + toolTip = 'SCM Update or Inventory Update is executing.'; + break; + case 'pending': + toolTip = 'Not in queue, waiting on task manager.'; + break; + case 'running': + toolTip = 'Playbook tasks executing.'; + break; + } + return toolTip; + }; + } diff --git a/awx/ui/client/src/jobs/factories/jobs-list-update.factory.js b/awx/ui/client/src/jobs/factories/jobs-list-update.factory.js new file mode 100644 index 0000000000..5f950e458c --- /dev/null +++ b/awx/ui/client/src/jobs/factories/jobs-list-update.factory.js @@ -0,0 +1,49 @@ +export default + function JobsListUpdate() { + return function(params) { + var scope = params.scope, + parent_scope = params.parent_scope, + list = params.list; + + scope[list.name].forEach(function(item, item_idx) { + var fld, field, + itm = scope[list.name][item_idx]; + + //if (item.type === 'inventory_update') { + // itm.name = itm.name.replace(/^.*?:/,'').replace(/^: /,''); + //} + + // Set the item type label + if (list.fields.type) { + parent_scope.type_choices.forEach(function(choice) { + if (choice.value === item.type) { + itm.type_label = choice.label; + } + }); + } + // Set the job status label + parent_scope.status_choices.forEach(function(status) { + if (status.value === item.status) { + itm.status_label = status.label; + } + }); + + if (list.name === 'completed_jobs' || list.name === 'running_jobs') { + itm.status_tip = itm.status_label + '. Click for details.'; + } + else if (list.name === 'queued_jobs') { + itm.status_tip = 'Pending'; + } + + // Copy summary_field values + for (field in list.fields) { + fld = list.fields[field]; + if (fld.sourceModel) { + if (itm.summary_fields[fld.sourceModel]) { + itm[field] = itm.summary_fields[fld.sourceModel][fld.sourceField]; + } + } + } + }); + }; + } diff --git a/awx/ui/client/src/jobs/factories/relaunch-adhoc.factory.js b/awx/ui/client/src/jobs/factories/relaunch-adhoc.factory.js new file mode 100644 index 0000000000..462501f097 --- /dev/null +++ b/awx/ui/client/src/jobs/factories/relaunch-adhoc.factory.js @@ -0,0 +1,11 @@ +export default + function RelaunchAdhoc(AdhocRun) { + return function(params) { + var scope = params.scope, + id = params.id; + AdhocRun({ scope: scope, project_id: id, relaunch: true }); + }; + } + +RelaunchAdhoc.$inject = + [ 'AdhocRun' ]; diff --git a/awx/ui/client/src/jobs/factories/relaunch-inventory.factory.js b/awx/ui/client/src/jobs/factories/relaunch-inventory.factory.js new file mode 100644 index 0000000000..eccd06a291 --- /dev/null +++ b/awx/ui/client/src/jobs/factories/relaunch-inventory.factory.js @@ -0,0 +1,30 @@ +export default + function RelaunchInventory(Find, Wait, Rest, InventoryUpdate, ProcessErrors, GetBasePath) { + return function(params) { + var scope = params.scope, + id = params.id, + url = GetBasePath('inventory_sources') + id + '/'; + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + InventoryUpdate({ + scope: scope, + url: data.related.update, + group_name: data.summary_fields.group.name, + group_source: data.source, + tree_id: null, + group_id: data.group + }); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + + url + ' GET returned: ' + status }); + }); + }; + } + +RelaunchInventory.$inject = + [ 'Find', 'Wait', 'Rest', + 'InventoryUpdate', 'ProcessErrors', 'GetBasePath' + ]; diff --git a/awx/ui/client/src/jobs/factories/relaunch-job.factory.js b/awx/ui/client/src/jobs/factories/relaunch-job.factory.js new file mode 100644 index 0000000000..0190cf9c79 --- /dev/null +++ b/awx/ui/client/src/jobs/factories/relaunch-job.factory.js @@ -0,0 +1,28 @@ +export default + function RelaunchJob(RelaunchInventory, RelaunchPlaybook, RelaunchSCM, RelaunchAdhoc) { + return function(params) { + var scope = params.scope, + id = params.id, + type = params.type, + name = params.name; + if (type === 'inventory_update') { + RelaunchInventory({ scope: scope, id: id}); + } + else if (type === 'ad_hoc_command') { + RelaunchAdhoc({ scope: scope, id: id, name: name }); + } + else if (type === 'job' || type === 'system_job' || type === 'workflow_job') { + RelaunchPlaybook({ scope: scope, id: id, name: name, job_type: type }); + } + else if (type === 'project_update') { + RelaunchSCM({ scope: scope, id: id }); + } + }; + } + +RelaunchJob.$inject = + [ 'RelaunchInventory', + 'RelaunchPlaybook', + 'RelaunchSCM', + 'RelaunchAdhoc' + ]; diff --git a/awx/ui/client/src/jobs/factories/relaunch-playbook.factory.js b/awx/ui/client/src/jobs/factories/relaunch-playbook.factory.js new file mode 100644 index 0000000000..78431b17be --- /dev/null +++ b/awx/ui/client/src/jobs/factories/relaunch-playbook.factory.js @@ -0,0 +1,12 @@ +export default + function RelaunchPlaybook(InitiatePlaybookRun) { + return function(params) { + var scope = params.scope, + id = params.id, + job_type = params.job_type; + InitiatePlaybookRun({ scope: scope, id: id, relaunch: true, job_type: job_type }); + }; + } + +RelaunchPlaybook.$inject = + [ 'InitiatePlaybookRun' ]; diff --git a/awx/ui/client/src/jobs/factories/relaunch-scm.factory.js b/awx/ui/client/src/jobs/factories/relaunch-scm.factory.js new file mode 100644 index 0000000000..b37e90e9ca --- /dev/null +++ b/awx/ui/client/src/jobs/factories/relaunch-scm.factory.js @@ -0,0 +1,11 @@ +export default + function RelaunchSCM(ProjectUpdate) { + return function(params) { + var scope = params.scope, + id = params.id; + ProjectUpdate({ scope: scope, project_id: id }); + }; + } + +RelaunchSCM.$inject = + [ 'ProjectUpdate' ]; diff --git a/awx/ui/client/src/jobs/jobs-list.controller.js b/awx/ui/client/src/jobs/jobs-list.controller.js index ac1ef684cd..1413488e0e 100644 --- a/awx/ui/client/src/jobs/jobs-list.controller.js +++ b/awx/ui/client/src/jobs/jobs-list.controller.js @@ -112,29 +112,29 @@ RelaunchJob({ scope: $scope, id: typeId, type: job.type, name: job.name }); }; - $scope.viewJobDetails = function(job) { + $scope.viewjobResults = function(job) { - var goToJobDetails = function(state) { + var goTojobResults = function(state) { $state.go(state, { id: job.id }, { reload: true }); }; switch (job.type) { case 'job': - goToJobDetails('jobDetail'); + goTojobResults('jobResult'); break; case 'ad_hoc_command': - goToJobDetails('adHocJobStdout'); + goTojobResults('adHocJobStdout'); break; case 'system_job': - goToJobDetails('managementJobStdout'); + goTojobResults('managementJobStdout'); break; case 'project_update': - goToJobDetails('scmUpdateStdout'); + goTojobResults('scmUpdateStdout'); break; case 'inventory_update': - goToJobDetails('inventorySyncStdout'); + goTojobResults('inventorySyncStdout'); break; case 'workflow_job': - goToJobDetails('workflowResults'); + goTojobResults('workflowResults'); break; } diff --git a/awx/ui/client/src/jobs/main.js b/awx/ui/client/src/jobs/main.js index b1782cead6..6045000ef0 100644 --- a/awx/ui/client/src/jobs/main.js +++ b/awx/ui/client/src/jobs/main.js @@ -6,10 +6,26 @@ import jobsList from './jobs-list.controller'; import jobsRoute from './jobs.route'; +import DeleteJob from './factories/delete-job.factory'; +import JobStatusToolTip from './factories/job-status-tool-tip.factory'; +import JobsListUpdate from './factories/jobs-list-update.factory'; +import RelaunchAdhoc from './factories/relaunch-adhoc.factory'; +import RelaunchInventory from './factories/relaunch-inventory.factory'; +import RelaunchJob from './factories/relaunch-job.factory'; +import RelaunchPlaybook from './factories/relaunch-playbook.factory'; +import RelaunchSCM from './factories/relaunch-scm.factory'; export default angular.module('JobsModule', []) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(jobsRoute); }]) - .controller('JobsList', jobsList); + .controller('JobsList', jobsList) + .factory('DeleteJob', DeleteJob) + .factory('JobStatusToolTip', JobStatusToolTip) + .factory('JobsListUpdate', JobsListUpdate) + .factory('RelaunchAdhoc', RelaunchAdhoc) + .factory('RelaunchInventory', RelaunchInventory) + .factory('RelaunchJob', RelaunchJob) + .factory('RelaunchPlaybook', RelaunchPlaybook) + .factory('RelaunchSCM', RelaunchSCM); diff --git a/awx/ui/client/src/lists/AllJobs.js b/awx/ui/client/src/lists/AllJobs.js index 24521d9c32..f3aacef09f 100644 --- a/awx/ui/client/src/lists/AllJobs.js +++ b/awx/ui/client/src/lists/AllJobs.js @@ -6,7 +6,7 @@ export default - angular.module('AllJobsDefinition', ['sanitizeFilter', 'capitalizeFilter']) + angular.module('AllJobsDefinition', []) .factory('AllJobsList', ['i18n', function(i18n) { return { @@ -30,12 +30,12 @@ export default dataTitle: "{{ job.status_popover_title }}", icon: 'icon-job-{{ job.status }}', iconOnly: true, - ngClick:"viewJobDetails(job)", + ngClick:"viewjobResults(job)", nosort: true }, id: { label: 'ID', - ngClick:"viewJobDetails(job)", + ngClick:"viewjobResults(job)", columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', awToolTip: "{{ job.status_tip }}", dataPlacement: 'top', @@ -44,7 +44,7 @@ export default name: { label: i18n._('Name'), columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6', - ngClick: "viewJobDetails(job)", + ngClick: "viewjobResults(job)", badgePlacement: 'right', badgeCustom: true, badgeIcon: `
add view @@ -30,11 +30,11 @@ export default dataTitle: "{{ completed_job.status_popover_title }}", icon: 'icon-job-{{ completed_job.status }}', iconOnly: true, - ngClick:"viewJobDetails(completed_job)", + ngClick:"viewjobResults(completed_job)", }, id: { label: 'ID', - ngClick:"viewJobDetails(completed_job)", + ngClick:"viewjobResults(completed_job)", columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', awToolTip: "{{ completed_job.status_tip }}", dataPlacement: 'top' @@ -42,7 +42,7 @@ export default name: { label: i18n._('Name'), columnClass: 'col-lg-4 col-md-4 col-sm-4 col-xs-6', - ngClick: "viewJobDetails(completed_job)", + ngClick: "viewjobResults(completed_job)", awToolTip: "{{ completed_job.name | sanitize }}", dataPlacement: 'top' }, diff --git a/awx/ui/client/src/lists/Jobs.js b/awx/ui/client/src/lists/Jobs.js index 455ce26175..e03b1f5511 100644 --- a/awx/ui/client/src/lists/Jobs.js +++ b/awx/ui/client/src/lists/Jobs.js @@ -20,7 +20,7 @@ export default fields: { id: { label: 'ID', - ngClick:"viewJobDetails(job)", + ngClick:"viewjobResults(job)", key: true, desc: true, columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2', @@ -35,7 +35,7 @@ export default dataTitle: "{{ job.status_popover_title }}", icon: 'icon-job-{{ job.status }}', iconOnly: true, - ngClick:"viewJobDetails(job)" + ngClick:"viewjobResults(job)" }, started: { label: 'Started', @@ -52,7 +52,7 @@ export default name: { label: 'Name', columnClass: 'col-md-3 col-xs-5', - ngClick: "viewJobDetails(job)", + ngClick: "viewjobResults(job)", } }, diff --git a/awx/ui/client/src/lists/ScheduledJobs.js b/awx/ui/client/src/lists/ScheduledJobs.js index 7d0acd4c25..f134dca1c8 100644 --- a/awx/ui/client/src/lists/ScheduledJobs.js +++ b/awx/ui/client/src/lists/ScheduledJobs.js @@ -6,7 +6,7 @@ export default - angular.module('ScheduledJobsDefinition', ['sanitizeFilter']) + angular.module('ScheduledJobsDefinition', []) .factory('ScheduledJobsList', ['i18n', function(i18n) { return { diff --git a/awx/ui/client/src/login/authenticationServices/authentication.service.js b/awx/ui/client/src/login/authenticationServices/authentication.service.js index 9771bc46c5..1b94e6bd93 100644 --- a/awx/ui/client/src/login/authenticationServices/authentication.service.js +++ b/awx/ui/client/src/login/authenticationServices/authentication.service.js @@ -15,20 +15,20 @@ */ export default - ['$http', '$rootScope', '$location', '$cookieStore', 'GetBasePath', 'Store', '$q', + ['$http', '$rootScope', '$location', '$cookies', 'GetBasePath', 'Store', '$q', '$injector', - function ($http, $rootScope, $location, $cookieStore, GetBasePath, Store, $q, + function ($http, $rootScope, $location, $cookies, GetBasePath, Store, $q, $injector) { return { setToken: function (token, expires) { // set the session cookie - $cookieStore.remove('token'); - $cookieStore.remove('token_expires'); - $cookieStore.remove('userLoggedIn'); - $cookieStore.put('token', token); - $cookieStore.put('token_expires', expires); - $cookieStore.put('userLoggedIn', true); - $cookieStore.put('sessionExpired', false); + $cookies.remove('token'); + $cookies.remove('token_expires'); + $cookies.remove('userLoggedIn'); + $cookies.put('token', token); + $cookies.put('token_expires', expires); + $cookies.put('userLoggedIn', true); + $cookies.put('sessionExpired', false); $rootScope.token = token; $rootScope.userLoggedIn = true; $rootScope.token_expires = expires; @@ -38,14 +38,14 @@ export default isUserLoggedIn: function () { if ($rootScope.userLoggedIn === undefined) { // Browser refresh may have occurred - $rootScope.userLoggedIn = $cookieStore.get('userLoggedIn'); - $rootScope.sessionExpired = $cookieStore.get('sessionExpired'); + $rootScope.userLoggedIn = $cookies.get('userLoggedIn'); + $rootScope.sessionExpired = $cookies.get('sessionExpired'); } return $rootScope.userLoggedIn; }, getToken: function () { - return ($rootScope.token) ? $rootScope.token : $cookieStore.get('token'); + return ($rootScope.token) ? $rootScope.token : $cookies.get('token'); }, retrieveToken: function (username, password) { @@ -83,17 +83,17 @@ export default scope.$destroy(); } - if($cookieStore.get('lastPath')==='/portal'){ - $cookieStore.put( 'lastPath', '/portal'); + if($cookies.get('lastPath')==='/portal'){ + $cookies.put( 'lastPath', '/portal'); $rootScope.lastPath = '/portal'; } - else if ($cookieStore.get('lastPath') !== '/home' || $cookieStore.get('lastPath') !== '/' || $cookieStore.get('lastPath') !== '/login' || $cookieStore.get('lastPath') !== '/logout'){ + else if ($cookies.get('lastPath') !== '/home' || $cookies.get('lastPath') !== '/' || $cookies.get('lastPath') !== '/login' || $cookies.get('lastPath') !== '/logout'){ // do nothing - $rootScope.lastPath = $cookieStore.get('lastPath'); + $rootScope.lastPath = $cookies.get('lastPath'); } else { // your last path was home - $cookieStore.remove('lastPath'); + $cookies.remove('lastPath'); $rootScope.lastPath = '/home'; } x = Store('sessionTime'); @@ -102,17 +102,17 @@ export default } Store('sessionTime', x); - if ($cookieStore.get('current_user')) { - $rootScope.lastUser = $cookieStore.get('current_user').id; + if ($cookies.getObject('current_user')) { + $rootScope.lastUser = $cookies.getObject('current_user').id; } ConfigService.delete(); SocketService.disconnect(); - $cookieStore.remove('token_expires'); - $cookieStore.remove('current_user'); - $cookieStore.remove('token'); - $cookieStore.put('userLoggedIn', false); - $cookieStore.put('sessionExpired', false); - $cookieStore.put('current_user', {}); + $cookies.remove('token_expires'); + $cookies.remove('current_user'); + $cookies.remove('token'); + $cookies.put('userLoggedIn', false); + $cookies.put('sessionExpired', false); + $cookies.putObject('current_user', {}); $rootScope.current_user = {}; $rootScope.license_tested = undefined; $rootScope.userLoggedIn = false; @@ -163,11 +163,11 @@ export default setUserInfo: function (response) { // store the response values in $rootScope so we can get to them later $rootScope.current_user = response.results[0]; - $cookieStore.put('current_user', response.results[0]); //keep in session cookie in the event of browser refresh + $cookies.putObject('current_user', response.results[0]); //keep in session cookie in the event of browser refresh }, restoreUserInfo: function () { - $rootScope.current_user = $cookieStore.get('current_user'); + $rootScope.current_user = $cookies.getObject('current_user'); }, getUserInfo: function (key) { @@ -177,7 +177,7 @@ export default return $rootScope.current_user[key]; } this.restoreUserInfo(); - cu = $cookieStore.get('current_user'); + cu = $cookies.getObject('current_user'); return cu[key]; } }; diff --git a/awx/ui/client/src/login/authenticationServices/checkAccess.factory.js b/awx/ui/client/src/login/authenticationServices/checkAccess.factory.js index 1be4bab4b8..781012b1e9 100644 --- a/awx/ui/client/src/login/authenticationServices/checkAccess.factory.js +++ b/awx/ui/client/src/login/authenticationServices/checkAccess.factory.js @@ -12,8 +12,8 @@ export default - ['$rootScope', 'Alert', 'Rest', 'GetBasePath', 'ProcessErrors', '$cookieStore', - function ($rootScope, Alert, Rest, GetBasePath, ProcessErrors, $cookieStore) { + ['$rootScope', 'Alert', 'Rest', 'GetBasePath', 'ProcessErrors', '$cookies', + function ($rootScope, Alert, Rest, GetBasePath, ProcessErrors, $cookies) { return function (params) { // set PermissionAddAllowed to true or false based on user access. admins and org admins are granted // accesss. @@ -22,7 +22,7 @@ export default me; // uer may have refreshed the browser, in which case retrieve current user info from session cookie - me = ($rootScope.current_user) ? $rootScope.current_user : $cookieStore.get('current_user'); + me = ($rootScope.current_user) ? $rootScope.current_user : $cookies.getObject('current_user'); if (me.is_superuser) { scope.PermissionAddAllowed = true; diff --git a/awx/ui/client/src/login/authenticationServices/timer.factory.js b/awx/ui/client/src/login/authenticationServices/timer.factory.js index ebdf440baf..37d61a9349 100644 --- a/awx/ui/client/src/login/authenticationServices/timer.factory.js +++ b/awx/ui/client/src/login/authenticationServices/timer.factory.js @@ -22,9 +22,9 @@ * @description */ export default - ['$rootScope', '$cookieStore', 'CreateDialog', 'Authorization', + ['$rootScope', '$cookies', 'CreateDialog', 'Authorization', 'Store', '$interval', '$state', '$q', 'i18n', - function ($rootScope, $cookieStore, CreateDialog, Authorization, + function ($rootScope, $cookies, CreateDialog, Authorization, Store, $interval, $state, $q, i18n) { return { @@ -81,7 +81,7 @@ export default } this.sessionTime = 0; this.clearTimers(); - $cookieStore.put('sessionExpired', true); + $cookies.put('sessionExpired', true); }, moveForward: function () { @@ -101,7 +101,7 @@ export default y[$rootScope.current_user.id] = x; Store('sessionTime' , y); $rootScope.sessionExpired = false; - $cookieStore.put('sessionExpired', false); + $cookies.put('sessionExpired', false); this.startTimers(); }, diff --git a/awx/ui/client/src/login/loginModal/loginModal.controller.js b/awx/ui/client/src/login/loginModal/loginModal.controller.js index 24074ac2a1..61ff6b756d 100644 --- a/awx/ui/client/src/login/loginModal/loginModal.controller.js +++ b/awx/ui/client/src/login/loginModal/loginModal.controller.js @@ -54,11 +54,11 @@ * This is usage information. */ -export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', +export default ['$log', '$cookies', '$compile', '$window', '$rootScope', '$location', 'Authorization', 'ToggleClass', 'Alert', 'Wait', 'Timer', 'Empty', 'ClearScope', '$scope', 'pendoService', 'ConfigService', 'CheckLicense', 'FeaturesService', 'SocketService', - function ($log, $cookieStore, $compile, $window, $rootScope, $location, + function ($log, $cookies, $compile, $window, $rootScope, $location, Authorization, ToggleClass, Alert, Wait, Timer, Empty, ClearScope, scope, pendoService, ConfigService, CheckLicense, FeaturesService, SocketService) { @@ -70,13 +70,13 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', }, 1000); }; - scope.sessionExpired = (Empty($rootScope.sessionExpired)) ? $cookieStore.get('sessionExpired') : $rootScope.sessionExpired; + scope.sessionExpired = (Empty($rootScope.sessionExpired)) ? $cookies.get('sessionExpired') : $rootScope.sessionExpired; scope.login_username = ''; scope.login_password = ''; lastPath = function () { - return (Empty($rootScope.lastPath)) ? $cookieStore.get('lastPath') : $rootScope.lastPath; + return (Empty($rootScope.lastPath)) ? $cookies.get('lastPath') : $rootScope.lastPath; }; lastUser = function(){ diff --git a/awx/ui/client/src/management-jobs/card/card.controller.js b/awx/ui/client/src/management-jobs/card/card.controller.js index 19e521804f..6bdb32bb0c 100644 --- a/awx/ui/client/src/management-jobs/card/card.controller.js +++ b/awx/ui/client/src/management-jobs/card/card.controller.js @@ -31,8 +31,7 @@ export default }); }; getManagementJobs(); - var scope = $rootScope.$new(), - parent_scope = scope; + var scope = $rootScope.$new(); scope.cleanupJob = true; // This handles the case where the user refreshes the management job notifications page. @@ -51,14 +50,6 @@ export default //ignore } - // @issue: OLD SEARCH - // if (scope.searchCleanup) { - // scope.searchCleanup(); - // } - // else { - // Wait('stop'); - // } - Wait('stop'); }; @@ -273,11 +264,6 @@ export default }); }; - parent_scope.refreshJobs = function(){ - // @issue: OLD SEARCH - // scope.search(SchedulesList.iterator); - }; - var cleanUpStateChangeListener = $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams) { if(toState.name === "managementJobsList") { // We are on the management job list view - nothing needs to be highlighted diff --git a/awx/ui/client/src/organizations/edit/organizations-edit.controller.js b/awx/ui/client/src/organizations/edit/organizations-edit.controller.js index 6d06679ffa..b6ecfae2a4 100644 --- a/awx/ui/client/src/organizations/edit/organizations-edit.controller.js +++ b/awx/ui/client/src/organizations/edit/organizations-edit.controller.js @@ -121,9 +121,6 @@ export default ['$scope', '$rootScope', '$location', '$log', '$stateParams', Rest.post({ id: itm_id, disassociate: 1 }) .success(function() { $('#prompt-modal').modal('hide'); - - // @issue: OLD SEARCH - // $scope.search(form.related[set].iterator); }) .error(function(data, status) { $('#prompt-modal').modal('hide'); diff --git a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js index 0a59ddbaef..5db0ffeac2 100644 --- a/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js +++ b/awx/ui/client/src/organizations/linkout/addUsers/addUsers.controller.js @@ -12,9 +12,9 @@ */ export default ['$scope', '$rootScope', 'ProcessErrors', 'GetBasePath', 'generateList', -'SelectionInit', 'templateUrl', '$state', 'Rest', '$q', 'Wait', '$window', 'QuerySet', 'UserList', +'templateUrl', '$state', 'Rest', '$q', 'Wait', '$window', 'QuerySet', 'UserList', function($scope, $rootScope, ProcessErrors, GetBasePath, generateList, - SelectionInit, templateUrl, $state, Rest, $q, Wait, $window, qs, UserList) { + templateUrl, $state, Rest, $q, Wait, $window, qs, UserList) { $scope.$on("linkLists", function() { if ($state.current.name.split(".")[1] === "users") { diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js index ae92d23058..6d171f98c2 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js @@ -5,11 +5,11 @@ *************************************************/ export default ['$scope', '$rootScope', '$location', '$log', - '$stateParams', '$compile', '$filter', 'sanitizeFilter', 'Rest', 'Alert', 'InventoryList', + '$stateParams', '$compile', '$filter', 'Rest', 'Alert', 'InventoryList', 'generateList', 'Prompt', 'ReturnToCaller', 'OrgInventoryDataset', 'OrgInventoryList', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'Wait', 'Find', 'Empty', '$state', function($scope, $rootScope, $location, $log, - $stateParams, $compile, $filter, sanitizeFilter, Rest, Alert, InventoryList, + $stateParams, $compile, $filter, Rest, Alert, InventoryList, generateList, Prompt, ReturnToCaller, Dataset, OrgInventoryList, ClearScope, ProcessErrors, GetBasePath, Wait, Find, Empty, $state) { diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js index 7032ef5f81..5f112f47c0 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js @@ -7,11 +7,11 @@ export default ['$scope', '$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'Prompt', 'ReturnToCaller', 'ClearScope', 'OrgProjectList', 'OrgProjectDataset', - 'ProcessErrors', 'GetBasePath', 'SelectionInit', 'ProjectUpdate', + 'ProcessErrors', 'GetBasePath', 'ProjectUpdate', 'Wait', 'GetChoices', 'Empty', 'Find', 'GetProjectIcon', 'GetProjectToolTip', '$filter', '$state', function($scope, $rootScope, $location, $log, $stateParams, Rest, Alert, Prompt, ReturnToCaller, ClearScope, OrgProjectList, Dataset, - ProcessErrors, GetBasePath, SelectionInit, ProjectUpdate, + ProcessErrors, GetBasePath, ProjectUpdate, Wait, GetChoices, Empty, Find, GetProjectIcon, GetProjectToolTip, $filter, $state) { var list = OrgProjectList, @@ -118,11 +118,7 @@ export default ['$scope', '$rootScope', '$location', '$log', // And we found the affected project $log.debug('Received event for project: ' + project.name); $log.debug('Status changed to: ' + data.status); - if (data.status === 'successful' || data.status === 'failed') { - // @issue: OLD SEARCH - // $scope.search(list.iterator, null, null, null, null, false); - - } else { + if (!(data.status === 'successful' || data.status === 'failed')) { project.scm_update_tooltip = "SCM update currently running"; project.scm_type_class = "btn-disabled"; } @@ -181,10 +177,10 @@ export default ['$scope', '$rootScope', '$location', '$log', $state.go('projects.edit', { project_id: id }); }; - if ($scope.removeGoToJobDetails) { - $scope.removeGoToJobDetails(); + if ($scope.removeGoTojobResults) { + $scope.removeGoTojobResults(); } - $scope.removeGoToJobDetails = $scope.$on('GoToJobDetails', function(e, data) { + $scope.removeGoTojobResults = $scope.$on('GoTojobResults', function(e, data) { if (data.summary_fields.current_update || data.summary_fields.last_update) { Wait('start'); @@ -211,7 +207,7 @@ export default ['$scope', '$rootScope', '$location', '$log', Rest.setUrl(project.url); Rest.get() .success(function(data) { - $scope.$emit('GoToJobDetails', data); + $scope.$emit('GoTojobResults', data); }) .error(function(data, status) { ProcessErrors($scope, data, status, null, { @@ -288,11 +284,6 @@ export default ['$scope', '$rootScope', '$location', '$log', }); }; - $scope.refresh = function() { - // @issue: OLD SEARCH - // $scope.search(list.iterator); - }; - $scope.SCMUpdate = function(project_id, event) { try { $(event.target).tooltip('hide'); @@ -301,13 +292,7 @@ export default ['$scope', '$rootScope', '$location', '$log', } $scope.projects.forEach(function(project) { if (project.id === project_id) { - if (project.scm_type === "Manual" || Empty(project.scm_type)) { - // Do not respond. Button appears greyed out as if it is disabled. Not disabled though, because we need mouse over event - // to work. So user can click, but we just won't do anything. - //Alert('Missing SCM Setup', 'Before running an SCM update, edit the project and provide the SCM access information.', 'alert-info'); - } else if (project.status === 'updating' || project.status === 'running' || project.status === 'pending') { - // Alert('Update in Progress', 'The SCM update process is running. Use the Refresh button to monitor the status.', 'alert-info'); - } else { + if (!((project.scm_type === "Manual" || Empty(project.scm_type)) || (project.status === 'updating' || project.status === 'running' || project.status === 'pending'))) { ProjectUpdate({ scope: $scope, project_id: project.id }); } } diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js index 654ed8ee59..3589328bb3 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-teams.controller.js @@ -6,12 +6,12 @@ export default ['$scope', '$rootScope', '$location', '$log', '$stateParams', 'OrgTeamList', 'Rest', 'Alert', 'Prompt', 'OrgTeamsDataset', 'ReturnToCaller', 'ClearScope', - 'ProcessErrors', 'SetTeamListeners', 'GetBasePath', - 'SelectionInit', 'Wait', '$state', + 'ProcessErrors', 'GetBasePath', + 'Wait', '$state', function($scope, $rootScope, $location, $log, $stateParams, OrgTeamList, Rest, Alert, Prompt, Dataset, ReturnToCaller, ClearScope, - ProcessErrors, SetTeamListeners, GetBasePath, - SelectionInit, Wait, $state) { + ProcessErrors, GetBasePath, + Wait, $state) { var list = OrgTeamList, orgBase = GetBasePath('organizations'); diff --git a/awx/ui/client/src/projects/add/projects-add.controller.js b/awx/ui/client/src/projects/add/projects-add.controller.js new file mode 100644 index 0000000000..4e513acb13 --- /dev/null +++ b/awx/ui/client/src/projects/add/projects-add.controller.js @@ -0,0 +1,154 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$compile', '$location', '$log', + '$stateParams', 'GenerateForm', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', + 'GetProjectPath', 'GetChoices', 'Wait', '$state', 'CreateSelect2', 'i18n', + function($scope, $rootScope, $compile, $location, $log, + $stateParams, GenerateForm, ProjectsForm, Rest, Alert, ProcessErrors, + GetBasePath, GetProjectPath, GetChoices, Wait, $state, CreateSelect2, i18n) { + + var form = ProjectsForm(), + base = $location.path().replace(/^\//, '').split('/')[0], + defaultUrl = GetBasePath('projects'), + master = {}; + + init(); + + function init() { + Rest.setUrl(GetBasePath('projects')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert(i18n._('Permission Error'), i18n._('You do not have permission to add a project.'), 'alert-info'); + } + }); + + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + } + + GetProjectPath({ scope: $scope, master: master }); + + if ($scope.removeChoicesReady) { + $scope.removeChoicesReady(); + } + $scope.removeChoicesReady = $scope.$on('choicesReady', function() { + var i; + for (i = 0; i < $scope.scm_type_options.length; i++) { + if ($scope.scm_type_options[i].value === '') { + $scope.scm_type_options[i].value = "manual"; + //$scope.scm_type = $scope.scm_type_options[i]; + break; + } + } + + CreateSelect2({ + element: '#project_scm_type', + multiple: false + }); + + $scope.scmRequired = false; + master.scm_type = $scope.scm_type; + }); + + // Load the list of options for Kind + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'scm_type', + variable: 'scm_type_options', + callback: 'choicesReady' + }); + CreateSelect2({ + element: '#local-path-select', + multiple: false + }); + + // Save + $scope.formSave = function() { + var i, fld, url, data = {}; + data = {}; + for (fld in form.fields) { + if (form.fields[fld].type === 'checkbox_group') { + for (i = 0; i < form.fields[fld].fields.length; i++) { + data[form.fields[fld].fields[i].name] = $scope[form.fields[fld].fields[i].name]; + } + } else { + if (form.fields[fld].type !== 'alertblock') { + data[fld] = $scope[fld]; + } + } + } + + if ($scope.scm_type.value === "manual") { + data.scm_type = ""; + data.local_path = $scope.local_path.value; + } else { + data.scm_type = $scope.scm_type.value; + delete data.local_path; + } + + url = (base === 'teams') ? GetBasePath('teams') + $stateParams.team_id + '/projects/' : defaultUrl; + Wait('start'); + Rest.setUrl(url); + Rest.post(data) + .success(function(data) { + $scope.addedItem = data.id; + $state.go('projects.edit', { project_id: data.id }, { reload: true }); + }) + .error(function(data, status) { + Wait('stop'); + ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), + msg: i18n._('Failed to create new project. POST returned status: ') + status }); + }); + }; + + $scope.scmChange = function() { + // When an scm_type is set, path is not required + if ($scope.scm_type) { + $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; + $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; + $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; + } + + // Dynamically update popover values + if ($scope.scm_type.value) { + switch ($scope.scm_type.value) { + case 'git': + $scope.urlPopover = '

' + + i18n._('Example URLs for GIT SCM include:') + + '

  • https://github.com/ansible/ansible.git
  • ' + + '
  • git@github.com:ansible/ansible.git
  • git://servername.example.com/ansible.git
' + + '

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

' + i18n._('Example URLs for Subversion SCM include:') + '

' + + '
  • https://github.com/ansible/ansible
  • svn://servername.example.com/path
  • ' + + '
  • svn+ssh://servername.example.com/path
'; + break; + case 'hg': + $scope.urlPopover = '

' + i18n._('Example URLs for Mercurial SCM include:') + '

' + + '
  • https://bitbucket.org/username/project
  • ssh://hg@bitbucket.org/username/project
  • ' + + '
  • ssh://server.example.com/path
' + + '

' + i18n.sprintf(i18n._('%sNote:%s Mercurial does not support password authentication for SSH. ' + + 'Do not put the username and key in the URL. ' + + 'If using Bitbucket and SSH, do not supply your Bitbucket username.'), '', ''); + break; + default: + $scope.urlPopover = '

' + i18n._('URL popover text'); + } + } + + }; + $scope.formCancel = function() { + $state.go('projects'); + }; + } +]; diff --git a/awx/ui/client/src/projects/edit/projects-edit.controller.js b/awx/ui/client/src/projects/edit/projects-edit.controller.js new file mode 100644 index 0000000000..05b7cf48e7 --- /dev/null +++ b/awx/ui/client/src/projects/edit/projects-edit.controller.js @@ -0,0 +1,295 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$compile', '$location', '$log', + '$stateParams', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GenerateForm', + 'Prompt', 'ClearScope', 'GetBasePath', 'GetProjectPath', 'Authorization', 'GetChoices', 'Empty', + 'DebugForm', 'Wait', 'ProjectUpdate', '$state', 'CreateSelect2', 'ToggleNotification', 'i18n', + function($scope, $rootScope, $compile, $location, $log, + $stateParams, ProjectsForm, Rest, Alert, ProcessErrors, GenerateForm, + Prompt, ClearScope, GetBasePath, GetProjectPath, Authorization, + GetChoices, Empty, DebugForm, Wait, ProjectUpdate, $state, CreateSelect2, ToggleNotification, i18n) { + + ClearScope('htmlTemplate'); + + var form = ProjectsForm(), + defaultUrl = GetBasePath('projects') + $stateParams.project_id + '/', + master = {}, + id = $stateParams.project_id; + + init(); + + function init() { + $scope.project_local_paths = []; + $scope.base_dir = ''; + } + + $scope.$watch('project_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + + if ($scope.pathsReadyRemove) { + $scope.pathsReadyRemove(); + } + $scope.pathsReadyRemove = $scope.$on('pathsReady', function () { + CreateSelect2({ + element: '#local-path-select', + multiple: false + }); + }); + + // After the project is loaded, retrieve each related set + if ($scope.projectLoadedRemove) { + $scope.projectLoadedRemove(); + } + $scope.projectLoadedRemove = $scope.$on('projectLoaded', function() { + var opts = []; + + if (Authorization.getUserInfo('is_superuser') === true) { + GetProjectPath({ scope: $scope, master: master }); + } else { + opts.push({ + label: $scope.local_path, + value: $scope.local_path + }); + $scope.project_local_paths = opts; + $scope.local_path = $scope.project_local_paths[0]; + $scope.base_dir = i18n._('You do not have access to view this property'); + $scope.$emit('pathsReady'); + } + + $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; + $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; + $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; + Wait('stop'); + + $scope.scmChange(); + }); + + if ($scope.removeChoicesReady) { + $scope.removeChoicesReady(); + } + $scope.removeChoicesReady = $scope.$on('choicesReady', function() { + let i; + for (i = 0; i < $scope.scm_type_options.length; i++) { + if ($scope.scm_type_options[i].value === '') { + $scope.scm_type_options[i].value = "manual"; + break; + } + } + // Retrieve detail record and prepopulate the form + Rest.setUrl(defaultUrl); + Rest.get({ params: { id: id } }) + .success(function(data) { + var fld, i; + for (fld in form.fields) { + if (form.fields[fld].type === 'checkbox_group') { + for (i = 0; i < form.fields[fld].fields.length; i++) { + $scope[form.fields[fld].fields[i].name] = data[form.fields[fld].fields[i].name]; + master[form.fields[fld].fields[i].name] = data[form.fields[fld].fields[i].name]; + } + } else { + if (data[fld] !== undefined) { + $scope[fld] = data[fld]; + master[fld] = data[fld]; + } + } + if (form.fields[fld].sourceModel && data.summary_fields && + data.summary_fields[form.fields[fld].sourceModel]) { + $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + } + } + + data.scm_type = (Empty(data.scm_type)) ? 'manual' : data.scm_type; + for (i = 0; i < $scope.scm_type_options.length; i++) { + if ($scope.scm_type_options[i].value === data.scm_type) { + $scope.scm_type = $scope.scm_type_options[i]; + break; + } + } + + if ($scope.scm_type.value !== 'manual') { + $scope.pathRequired = false; + $scope.scmRequired = true; + } else { + $scope.pathRequired = true; + $scope.scmRequired = false; + } + + master.scm_type = $scope.scm_type; + CreateSelect2({ + element: '#project_scm_type', + multiple: false + }); + + $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; + $scope.scm_update_tooltip = i18n._("Start an SCM update"); + $scope.scm_type_class = ""; + if (data.status === 'running' || data.status === 'updating') { + $scope.scm_update_tooltip = i18n._("SCM update currently running"); + $scope.scm_type_class = "btn-disabled"; + } + if (Empty(data.scm_type)) { + $scope.scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); + $scope.scm_type_class = "btn-disabled"; + } + + $scope.project_obj = data; + $scope.name = data.name; + $scope.$emit('projectLoaded'); + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), + msg: i18n.sprintf(i18n._('Failed to retrieve project: %s. GET status: '), id) + status + }); + }); + }); + + // Load the list of options for Kind + Wait('start'); + GetChoices({ + url: GetBasePath('projects'), + scope: $scope, + field: 'scm_type', + variable: 'scm_type_options', + callback: 'choicesReady' + }); + + $scope.toggleNotification = function(event, id, column) { + var notifier = this.notification; + try { + $(event.target).tooltip('hide'); + } catch (e) { + // ignore + } + ToggleNotification({ + scope: $scope, + url: $scope.project_obj.url, + notifier: notifier, + column: column, + callback: 'NotificationRefresh' + }); + }; + + // Save changes to the parent + $scope.formSave = function() { + var fld, i, params; + GenerateForm.clearApiErrors($scope); + Wait('start'); + $rootScope.flashMessage = null; + params = {}; + for (fld in form.fields) { + if (form.fields[fld].type === 'checkbox_group') { + for (i = 0; i < form.fields[fld].fields.length; i++) { + params[form.fields[fld].fields[i].name] = $scope[form.fields[fld].fields[i].name]; + } + } else { + if (form.fields[fld].type !== 'alertblock') { + params[fld] = $scope[fld]; + } + } + } + + if ($scope.scm_type.value === "manual") { + params.scm_type = ""; + params.local_path = $scope.local_path.value; + } else { + params.scm_type = $scope.scm_type.value; + delete params.local_path; + } + + Rest.setUrl(defaultUrl); + Rest.put(params) + .success(function() { + Wait('stop'); + $state.go($state.current, {}, { reload: true }); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Failed to update project: %s. PUT status: '), id) + status }); + }); + }; + + // Related set: Delete button + $scope['delete'] = function(set, itm_id, name, title) { + var action = function() { + var url = GetBasePath('projects') + id + '/' + set + '/'; + $rootScope.flashMessage = null; + Rest.setUrl(url); + Rest.post({ id: itm_id, disassociate: 1 }) + .success(function() { + $('#prompt-modal').modal('hide'); + }) + .error(function(data, status) { + $('#prompt-modal').modal('hide'); + ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. POST returned status: '), url) + status }); + }); + }; + + Prompt({ + hdr: i18n._('Delete'), + body: '

' + i18n.sprintf(i18n._('Are you sure you want to remove the %s below from %s?'), title, $scope.name) + '
' + '
' + name + '
', + action: action, + actionText: i18n._('DELETE') + }); + }; + + $scope.scmChange = function() { + if ($scope.scm_type) { + $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; + $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; + $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? i18n._('Revision #') : i18n._('SCM Branch'); + } + + // Dynamically update popover values + if ($scope.scm_type.value) { + switch ($scope.scm_type.value) { + case 'git': + $scope.urlPopover = '

' + i18n._('Example URLs for GIT SCM include:') + '

  • https://github.com/ansible/ansible.git
  • ' + + '
  • git@github.com:ansible/ansible.git
  • git://servername.example.com/ansible.git
' + + '

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

' + i18n._('Example URLs for Subversion SCM include:') + '

' + + '
  • https://github.com/ansible/ansible
  • svn://servername.example.com/path
  • ' + + '
  • svn+ssh://servername.example.com/path
'; + break; + case 'hg': + $scope.urlPopover = '

' + i18n._('Example URLs for Mercurial SCM include:') + '

' + + '
  • https://bitbucket.org/username/project
  • ssh://hg@bitbucket.org/username/project
  • ' + + '
  • ssh://server.example.com/path
' + + '

' + i18n.sprintf(i18n._('%sNote:%s Mercurial does not support password authentication for SSH. ' + + 'Do not put the username and key in the URL. ' + + 'If using Bitbucket and SSH, do not supply your Bitbucket username.'), '', ''); + break; + default: + $scope.urlPopover = '

' + i18n._('URL popover text'); + } + } + }; + + $scope.SCMUpdate = function() { + if ($scope.project_obj.scm_type === "Manual" || Empty($scope.project_obj.scm_type)) { + // ignore + } else if ($scope.project_obj.status === 'updating' || $scope.project_obj.status === 'running' || $scope.project_obj.status === 'pending') { + Alert(i18n._('Update in Progress'), i18n._('The SCM update process is running.'), 'alert-info'); + } else { + ProjectUpdate({ scope: $scope, project_id: $scope.project_obj.id }); + } + }; + + $scope.formCancel = function() { + $state.transitionTo('projects'); + }; + } +]; diff --git a/awx/ui/client/src/projects/factories/get-project-icon.factory.js b/awx/ui/client/src/projects/factories/get-project-icon.factory.js new file mode 100644 index 0000000000..5234041e38 --- /dev/null +++ b/awx/ui/client/src/projects/factories/get-project-icon.factory.js @@ -0,0 +1,30 @@ +export default + function GetProjectIcon() { + return function(status) { + var result = ''; + switch (status) { + case 'n/a': + case 'ok': + case 'never updated': + result = 'none'; + break; + case 'pending': + case 'waiting': + case 'new': + result = 'none'; + break; + case 'updating': + case 'running': + result = 'running'; + break; + case 'successful': + result = 'success'; + break; + case 'failed': + case 'missing': + case 'canceled': + result = 'error'; + } + return result; + }; + } diff --git a/awx/ui/client/src/projects/factories/get-project-path.factory.js b/awx/ui/client/src/projects/factories/get-project-path.factory.js new file mode 100644 index 0000000000..1a48772ff9 --- /dev/null +++ b/awx/ui/client/src/projects/factories/get-project-path.factory.js @@ -0,0 +1,78 @@ +export default + function GetProjectPath(Alert, Rest, GetBasePath, ProcessErrors) { + return function(params) { + var scope = params.scope, + master = params.master; + + function arraySort(data) { + //Sort nodes by name + var i, j, names = [], + newData = []; + for (i = 0; i < data.length; i++) { + names.push(data[i].value); + } + names.sort(); + for (j = 0; j < names.length; j++) { + for (i = 0; i < data.length; i++) { + if (data[i].value === names[j]) { + newData.push(data[i]); + } + } + } + return newData; + } + + scope.showMissingPlaybooksAlert = false; + + Rest.setUrl(GetBasePath('config')); + Rest.get() + .success(function (data) { + var opts = [], i; + if (data.project_local_paths) { + for (i = 0; i < data.project_local_paths.length; i++) { + opts.push({ + label: data.project_local_paths[i], + value: data.project_local_paths[i] + }); + } + } + if (scope.local_path) { + // List only includes paths not assigned to projects, so add the + // path assigned to the current project. + opts.push({ + label: scope.local_path, + value: scope.local_path + }); + } + scope.project_local_paths = arraySort(opts); + if (scope.local_path) { + for (i = 0; scope.project_local_paths.length; i++) { + if (scope.project_local_paths[i].value === scope.local_path) { + scope.local_path = scope.project_local_paths[i]; + break; + } + } + } + scope.base_dir = data.project_base_dir; + master.local_path = scope.local_path; + master.base_dir = scope.base_dir; // Keep in master object so that it doesn't get + // wiped out on form reset. + if (opts.length === 0) { + // trigger display of alert block when scm_type == manual + scope.showMissingPlaybooksAlert = true; + } + scope.$emit('pathsReady'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to access API config. GET status: ' + status }); + }); + }; + } + +GetProjectPath.$inject = + [ 'Alert', + 'Rest', + 'GetBasePath', + 'ProcessErrors' + ]; diff --git a/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js b/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js new file mode 100644 index 0000000000..6db397bed0 --- /dev/null +++ b/awx/ui/client/src/projects/factories/get-project-tool-tip.factory.js @@ -0,0 +1,38 @@ +export default + function GetProjectToolTip(i18n) { + return function(status) { + var result = ''; + switch (status) { + case 'n/a': + case 'ok': + case 'never updated': + result = i18n._('No SCM updates have run for this project'); + break; + case 'pending': + case 'waiting': + case 'new': + result = i18n._('Queued. Click for details'); + break; + case 'updating': + case 'running': + result = i18n._('Running! Click for details'); + break; + case 'successful': + result = i18n._('Success! Click for details'); + break; + case 'failed': + result = i18n._('Failed. Click for details'); + break; + case 'missing': + result = i18n._('Missing. Click for details'); + break; + case 'canceled': + result = i18n._('Canceled. Click for details'); + break; + } + return result; + }; + } + +GetProjectToolTip.$inject = + [ 'i18n' ]; diff --git a/awx/ui/client/src/projects/list/projects-list.controller.js b/awx/ui/client/src/projects/list/projects-list.controller.js new file mode 100644 index 0000000000..1304b5a21b --- /dev/null +++ b/awx/ui/client/src/projects/list/projects-list.controller.js @@ -0,0 +1,295 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$location', '$log', '$stateParams', + 'Rest', 'Alert', 'ProjectList', 'Prompt', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', + 'GetBasePath', 'ProjectUpdate', 'Wait', 'GetChoices', 'Empty', 'Find', 'GetProjectIcon', + 'GetProjectToolTip', '$filter', '$state', 'rbacUiControlService', 'Dataset', 'i18n', 'QuerySet', + function($scope, $rootScope, $location, $log, $stateParams, + Rest, Alert, ProjectList, Prompt, ReturnToCaller, ClearScope, ProcessErrors, + GetBasePath, ProjectUpdate, Wait, GetChoices, Empty, Find, GetProjectIcon, + GetProjectToolTip, $filter, $state, rbacUiControlService, Dataset, i18n, qs) { + + var list = ProjectList, + defaultUrl = GetBasePath('projects'); + + init(); + + function init() { + $scope.canAdd = false; + + rbacUiControlService.canAdd('projects') + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + _.forEach($scope[list.name], buildTooltips); + $rootScope.flashMessage = null; + } + + $scope.$on(`${list.iterator}_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + $scope.$watchCollection(`${$scope.list.name}`, function() { + 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] !== undefined) { + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + + // Set the item type label + if (list.fields.scm_type && $scope.options && + $scope.options.hasOwnProperty('scm_type')) { + $scope.options.scm_type.choices.forEach(function(choice) { + if (choice[0] === item.scm_type) { + itm.type_label = choice[1]; + } + }); + } + + buildTooltips(itm); + + }); + } + } + + function buildTooltips(project) { + project.statusIcon = GetProjectIcon(project.status); + project.statusTip = GetProjectToolTip(project.status); + project.scm_update_tooltip = i18n._("Start an SCM update"); + project.scm_schedule_tooltip = i18n._("Schedule future SCM updates"); + project.scm_type_class = ""; + + if (project.status === 'failed' && project.summary_fields.last_update && project.summary_fields.last_update.status === 'canceled') { + project.statusTip = i18n._('Canceled. Click for details'); + } + + if (project.status === 'running' || project.status === 'updating') { + project.scm_update_tooltip = i18n._("SCM update currently running"); + project.scm_type_class = "btn-disabled"; + } + if (project.scm_type === 'manual') { + project.scm_update_tooltip = i18n._('Manual projects do not require an SCM update'); + project.scm_schedule_tooltip = i18n._('Manual projects do not require a schedule'); + project.scm_type_class = 'btn-disabled'; + project.statusTip = i18n._('Not configured for SCM'); + project.statusIcon = 'none'; + } + } + + $scope.reloadList = function(){ + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + qs.search(path, $state.params[`${list.iterator}_search`]) + .then(function(searchResponse) { + $scope[`${list.iterator}_dataset`] = searchResponse.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + }); + }; + + $scope.$on(`ws-jobs`, function(e, data) { + var project; + $log.debug(data); + if ($scope.projects) { + // Assuming we have a list of projects available + project = Find({ list: $scope.projects, key: 'id', val: data.project_id }); + if (project) { + // And we found the affected project + $log.debug('Received event for project: ' + project.name); + $log.debug('Status changed to: ' + data.status); + if (data.status === 'successful' || data.status === 'failed') { + $scope.reloadList(); + } else { + project.scm_update_tooltip = "SCM update currently running"; + project.scm_type_class = "btn-disabled"; + } + project.status = data.status; + project.statusIcon = GetProjectIcon(data.status); + project.statusTip = GetProjectToolTip(data.status); + } + } + }); + + $scope.addProject = function() { + $state.go('projects.add'); + }; + + $scope.editProject = function(id) { + $state.go('projects.edit', { project_id: id }); + }; + + if ($scope.removeGoTojobResults) { + $scope.removeGoTojobResults(); + } + $scope.removeGoTojobResults = $scope.$on('GoTojobResults', function(e, data) { + if (data.summary_fields.current_update || data.summary_fields.last_update) { + + Wait('start'); + + // Grab the id from summary_fields + var id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id; + + $state.go('scmUpdateStdout', { id: id }); + + } else { + Alert(i18n._('No Updates Available'), i18n._('There is no SCM update information available for this project. An update has not yet been ' + + ' completed. If you have not already done so, start an update for this project.'), 'alert-info'); + } + }); + + $scope.showSCMStatus = function(id) { + // Refresh the project list + var project = Find({ list: $scope.projects, key: 'id', val: id }); + if (Empty(project.scm_type) || project.scm_type === 'Manual') { + Alert(i18n._('No SCM Configuration'), i18n._('The selected project is not configured for SCM. To configure for SCM, edit the project and provide SCM settings, ' + + 'and then run an update.'), 'alert-info'); + } else { + // Refresh what we have in memory to insure we're accessing the most recent status record + Rest.setUrl(project.url); + Rest.get() + .success(function(data) { + $scope.$emit('GoTojobResults', data); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), + msg: i18n._('Project lookup failed. GET returned: ') + status }); + }); + } + }; + + $scope.deleteProject = function(id, name) { + var action = function() { + $('#prompt-modal').modal('hide'); + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.destroy() + .success(function() { + if (parseInt($state.params.project_id) === id) { + $state.go("^", null, { reload: true }); + } else { + $state.go('.', null, {reload: true}); + } + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), + msg: i18n.sprintf(i18n._('Call to %s failed. DELETE returned status: '), url) + status }); + }) + .finally(function() { + Wait('stop'); + }); + }; + + Prompt({ + hdr: i18n._('Delete'), + body: '

' + i18n._('Are you sure you want to delete the project below?') + '
' + '
' + $filter('sanitize')(name) + '
', + action: action, + actionText: 'DELETE' + }); + }; + + if ($scope.removeCancelUpdate) { + $scope.removeCancelUpdate(); + } + $scope.removeCancelUpdate = $scope.$on('Cancel_Update', function(e, url) { + // Cancel the project update process + Rest.setUrl(url); + Rest.post() + .success(function () { + Alert(i18n._('SCM Update Cancel'), i18n._('Your request to cancel the update was submitted to the task manager.'), 'alert-info'); + $scope.refresh(); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. POST status: '), url) + status }); + }); + }); + + if ($scope.removeCheckCancel) { + $scope.removeCheckCancel(); + } + $scope.removeCheckCancel = $scope.$on('Check_Cancel', function(e, data) { + // Check that we 'can' cancel the update + var url = data.related.cancel; + Rest.setUrl(url); + Rest.get() + .success(function(data) { + if (data.can_cancel) { + $scope.$emit('Cancel_Update', url); + } else { + Alert(i18n._('Cancel Not Allowed'), '
' + i18n.sprintf(i18n._('Either you do not have access or the SCM update process completed. ' + + 'Click the %sRefresh%s button to view the latest status.'), '', '') + '
', 'alert-info', null, null, null, null, true); + } + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. GET status: '), url) + status }); + }); + }); + + $scope.cancelUpdate = function(id, name) { + Rest.setUrl(GetBasePath("projects") + id); + Rest.get() + .success(function(data) { + if (data.related.current_update) { + Rest.setUrl(data.related.current_update); + Rest.get() + .success(function(data) { + $scope.$emit('Check_Cancel', data); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), + msg: i18n.sprintf(i18n._('Call to %s failed. GET status: '), data.related.current_update) + status }); + }); + } else { + Alert(i18n._('Update Not Found'), '
' + i18n.sprintf(i18n._('An SCM update does not appear to be running for project: %s. Click the %sRefresh%s ' + + 'button to view the latest status.'), $filter('sanitize')(name), '', '') + '
', 'alert-info',undefined,undefined,undefined,undefined,true); + } + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), + msg: i18n._('Call to get project failed. GET status: ') + status }); + }); + }; + + $scope.SCMUpdate = function(project_id, event) { + try { + $(event.target).tooltip('hide'); + } catch (e) { + // ignore + } + $scope.projects.forEach(function(project) { + if (project.id === project_id) { + if (project.scm_type === "Manual" || Empty(project.scm_type)) { + // Do not respond. Button appears greyed out as if it is disabled. Not disabled though, because we need mouse over event + // to work. So user can click, but we just won't do anything. + //Alert('Missing SCM Setup', 'Before running an SCM update, edit the project and provide the SCM access information.', 'alert-info'); + } else if (project.status === 'updating' || project.status === 'running' || project.status === 'pending') { + // Alert('Update in Progress', 'The SCM update process is running. Use the Refresh button to monitor the status.', 'alert-info'); + } else { + ProjectUpdate({ scope: $scope, project_id: project.id }); + } + } + }); + }; + + $scope.editSchedules = function(id) { + var project = Find({ list: $scope.projects, key: 'id', val: id }); + if (!(project.scm_type === "Manual" || Empty(project.scm_type)) && !(project.status === 'updating' || project.status === 'running' || project.status === 'pending')) { + $state.go('projectSchedules', { id: id }); + } + }; + } +]; diff --git a/awx/ui/client/src/projects/main.js b/awx/ui/client/src/projects/main.js new file mode 100644 index 0000000000..2bb0c3cdcc --- /dev/null +++ b/awx/ui/client/src/projects/main.js @@ -0,0 +1,57 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import ProjectsList from './list/projects-list.controller'; +import ProjectsAdd from './add/projects-add.controller'; +import ProjectsEdit from './edit/projects-edit.controller'; +import { N_ } from '../i18n'; +import GetProjectPath from './factories/get-project-path.factory'; +import GetProjectIcon from './factories/get-project-icon.factory'; +import GetProjectToolTip from './factories/get-project-tool-tip.factory'; + +export default +angular.module('Projects', []) + .controller('ProjectsList', ProjectsList) + .controller('ProjectsAdd', ProjectsAdd) + .controller('ProjectsEdit', ProjectsEdit) + .factory('GetProjectPath', GetProjectPath) + .factory('GetProjectIcon', GetProjectIcon) + .factory('GetProjectToolTip', GetProjectToolTip) + .config(['$stateProvider', 'stateDefinitionsProvider', + function($stateProvider, stateDefinitionsProvider) { + let stateDefinitions = stateDefinitionsProvider.$get(); + + // lazily generate a tree of substates which will replace this node in ui-router's stateRegistry + // see: stateDefinition.factory for usage documentation + $stateProvider.state({ + name: 'projects', + url: '/projects', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'projects', // top-most node in the generated tree (will replace this state definition) + modes: ['add', 'edit'], + list: 'ProjectList', + form: 'ProjectsForm', + controllers: { + list: ProjectsList, // DI strings or objects + add: ProjectsAdd, + edit: ProjectsEdit + }, + data: { + activityStream: true, + activityStreamTarget: 'project', + socket: { + "groups": { + "jobs": ["status_changed"] + } + } + }, + ncyBreadcrumb: { + label: N_('PROJECTS') + } + }) + }); + } + ]); diff --git a/awx/ui/client/src/rest/restServices.factory.js b/awx/ui/client/src/rest/restServices.factory.js index 7e25c50999..b031237a67 100644 --- a/awx/ui/client/src/rest/restServices.factory.js +++ b/awx/ui/client/src/rest/restServices.factory.js @@ -55,8 +55,8 @@ */ export default - ['$http', '$rootScope', '$cookieStore', '$q', 'Authorization', - function ($http, $rootScope, $cookieStore, $q, Authorization) { + ['$http', '$rootScope', '$cookies', '$q', 'Authorization', + function ($http, $rootScope, $cookies, $q, Authorization) { return { headers: {}, diff --git a/awx/ui/client/src/scheduler/factories/add-schedule.factory.js b/awx/ui/client/src/scheduler/factories/add-schedule.factory.js new file mode 100644 index 0000000000..c1870dd469 --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/add-schedule.factory.js @@ -0,0 +1,135 @@ +export default + function AddSchedule($location, $rootScope, $stateParams, SchedulerInit, + Wait, GetBasePath, Empty, SchedulePost, $state, Rest, + ProcessErrors) { + return function(params) { + var scope = params.scope, + callback= params.callback, + base = params.base || $location.path().replace(/^\//, '').split('/')[0], + url = params.url || null, + scheduler, + job_type; + + job_type = scope.parentObject.job_type; + if (!Empty($stateParams.id) && base !== 'system_job_templates' && base !== 'inventories' && !url) { + url = GetBasePath(base) + $stateParams.id + '/schedules/'; + } + else if(base === "inventories"){ + if (!params.url){ + url = GetBasePath('groups') + $stateParams.id + '/'; + Rest.setUrl(url); + Rest.get(). + then(function (data) { + url = data.data.related.inventory_source + 'schedules/'; + }).catch(function (response) { + ProcessErrors(null, response.data, response.status, null, { + hdr: 'Error!', + msg: 'Failed to get inventory group info. GET returned status: ' + + response.status + }); + }); + } + else { + url = params.url; + } + } + else if (base === 'system_job_templates') { + url = GetBasePath(base) + $stateParams.id + '/schedules/'; + if(job_type === "cleanup_facts"){ + scope.isFactCleanup = true; + scope.keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + scope.granularity_keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + scope.prompt_for_days_facts_form.keep_amount.$setViewValue(30); + scope.prompt_for_days_facts_form.granularity_keep_amount.$setViewValue(1); + scope.keep_unit = scope.keep_unit_choices[0]; + scope.granularity_keep_unit = scope.granularity_keep_unit_choices[1]; + } + else { + scope.cleanupJob = true; + } + } + + Wait('start'); + $('#form-container').empty(); + scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); + if(scope.schedulerUTCTime) { + // The UTC time is already set + scope.processSchedulerEndDt(); + } + else { + // We need to wait for it to be set by angular-scheduler because the following function depends + // on it + var schedulerUTCTimeWatcher = scope.$watch('schedulerUTCTime', function(newVal) { + if(newVal) { + // Remove the watcher + schedulerUTCTimeWatcher(); + scope.processSchedulerEndDt(); + } + }); + } + scheduler.inject('form-container', false); + scheduler.injectDetail('occurrences', false); + scheduler.clear(); + scope.$on("htmlDetailReady", function() { + $rootScope.$broadcast("ScheduleFormCreated", scope); + }); + scope.showRRuleDetail = false; + + if (scope.removeScheduleSaved) { + scope.removeScheduleSaved(); + } + scope.removeScheduleSaved = scope.$on('ScheduleSaved', function(e, data) { + Wait('stop'); + if (callback) { + scope.$emit(callback, data); + } + $state.go("^", null, {reload: true}); + }); + scope.saveSchedule = function() { + SchedulePost({ + scope: scope, + url: url, + scheduler: scheduler, + callback: 'ScheduleSaved', + mode: 'add' + }); + }; + + $('#scheduler-tabs li a').on('shown.bs.tab', function(e) { + if ($(e.target).text() === 'Details') { + if (!scheduler.isValid()) { + $('#scheduler-tabs a:first').tab('show'); + } + } + }); + }; + } + +AddSchedule.$inject = + [ '$location', '$rootScope', '$stateParams', + 'SchedulerInit', 'Wait', 'GetBasePath', + 'Empty', 'SchedulePost', '$state', + 'Rest', 'ProcessErrors' + ]; diff --git a/awx/ui/client/src/scheduler/factories/delete-schedule.factory.js b/awx/ui/client/src/scheduler/factories/delete-schedule.factory.js new file mode 100644 index 0000000000..7c75882966 --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/delete-schedule.factory.js @@ -0,0 +1,61 @@ +export default + function DeleteSchedule(GetBasePath, Rest, Wait, $state, + ProcessErrors, Prompt, Find, $location, $filter) { + return function(params) { + var scope = params.scope, + id = params.id, + callback = params.callback, + action, schedule, list, url, hdr; + + if (scope.schedules) { + list = scope.schedules; + } + else if (scope.scheduled_jobs) { + list = scope.scheduled_jobs; + } + + url = GetBasePath('schedules') + id + '/'; + schedule = Find({list: list, key: 'id', val: id }); + hdr = 'Delete Schedule'; + + action = function () { + Wait('start'); + Rest.setUrl(url); + Rest.destroy() + .success(function () { + $('#prompt-modal').modal('hide'); + scope.$emit(callback, id); + if (new RegExp('/' + id + '$').test($location.$$url)) { + $location.url($location.url().replace(/[/][0-9]+$/, "")); // go to list view + } + else{ + $state.go('.', null, {reload: true}); + } + }) + .error(function (data, status) { + try { + $('#prompt-modal').modal('hide'); + } + catch(e) { + // ignore + } + ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + + ' failed. DELETE returned: ' + status }); + }); + }; + + Prompt({ + hdr: hdr, + body: '
Are you sure you want to delete the schedule below?
' + $filter('sanitize')(schedule.name) + '
', + action: action, + actionText: 'DELETE', + backdrop: false + }); + }; + } + +DeleteSchedule.$inject = + [ 'GetBasePath','Rest', 'Wait', '$state', + 'ProcessErrors', 'Prompt', 'Find', '$location', + '$filter' + ]; diff --git a/awx/ui/client/src/scheduler/factories/edit-schedule.factory.js b/awx/ui/client/src/scheduler/factories/edit-schedule.factory.js new file mode 100644 index 0000000000..de591aee08 --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/edit-schedule.factory.js @@ -0,0 +1,154 @@ +export default + function EditSchedule(SchedulerInit, $rootScope, Wait, Rest, ProcessErrors, + GetBasePath, SchedulePost, $state) { + return function(params) { + var scope = params.scope, + id = params.id, + callback = params.callback, + schedule, scheduler, + url = GetBasePath('schedules') + id + '/'; + + delete scope.isFactCleanup; + delete scope.cleanupJob; + + function setGranularity(){ + var a,b, prompt_for_days, + keep_unit, + granularity, + granularity_keep_unit; + + if(scope.cleanupJob){ + scope.schedulerPurgeDays = Number(schedule.extra_data.days); + // scope.scheduler_form.schedulerPurgeDays.$setViewValue( Number(schedule.extra_data.days)); + } + else if(scope.isFactCleanup){ + scope.keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + scope.granularity_keep_unit_choices = [{ + "label" : "Days", + "value" : "d" + }, + { + "label": "Weeks", + "value" : "w" + }, + { + "label" : "Years", + "value" : "y" + }]; + // the API returns something like 20w or 1y + a = schedule.extra_data.older_than; // "20y" + b = schedule.extra_data.granularity; // "1w" + prompt_for_days = Number(_.initial(a,1).join('')); // 20 + keep_unit = _.last(a); // "y" + granularity = Number(_.initial(b,1).join('')); // 1 + granularity_keep_unit = _.last(b); // "w" + + scope.keep_amount = prompt_for_days; + scope.granularity_keep_amount = granularity; + scope.keep_unit = _.find(scope.keep_unit_choices, function(i){ + return i.value === keep_unit; + }); + scope.granularity_keep_unit =_.find(scope.granularity_keep_unit_choices, function(i){ + return i.value === granularity_keep_unit; + }); + } + } + + if (scope.removeScheduleFound) { + scope.removeScheduleFound(); + } + scope.removeScheduleFound = scope.$on('ScheduleFound', function() { + $('#form-container').empty(); + scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); + scheduler.inject('form-container', false); + scheduler.injectDetail('occurrences', false); + + if (!/DTSTART/.test(schedule.rrule)) { + schedule.rrule += ";DTSTART=" + schedule.dtstart.replace(/\.\d+Z$/,'Z'); + } + schedule.rrule = schedule.rrule.replace(/ RRULE:/,';'); + schedule.rrule = schedule.rrule.replace(/DTSTART:/,'DTSTART='); + scope.$on("htmlDetailReady", function() { + scheduler.setRRule(schedule.rrule); + scheduler.setName(schedule.name); + $rootScope.$broadcast("ScheduleFormCreated", scope); + }); + scope.showRRuleDetail = false; + + scheduler.setRRule(schedule.rrule); + scheduler.setName(schedule.name); + if(scope.isFactCleanup || scope.cleanupJob){ + setGranularity(); + } + }); + + + if (scope.removeScheduleSaved) { + scope.removeScheduleSaved(); + } + scope.removeScheduleSaved = scope.$on('ScheduleSaved', function(e, data) { + Wait('stop'); + if (callback) { + scope.$emit(callback, data); + } + $state.go("^"); + }); + scope.saveSchedule = function() { + schedule.extra_data = scope.extraVars; + SchedulePost({ + scope: scope, + url: url, + scheduler: scheduler, + callback: 'ScheduleSaved', + mode: 'edit', + schedule: schedule + }); + }; + + Wait('start'); + + // Get the existing record + Rest.setUrl(url); + Rest.get() + .success(function(data) { + schedule = data; + try { + schedule.extra_data = JSON.parse(schedule.extra_data); + } catch(e) { + // do nothing + } + scope.extraVars = data.extra_data === '' ? '---' : '---\n' + jsyaml.safeDump(data.extra_data); + + if(schedule.extra_data.hasOwnProperty('granularity')){ + scope.isFactCleanup = true; + } + if (schedule.extra_data.hasOwnProperty('days')){ + scope.cleanupJob = true; + } + + scope.schedule_obj = data; + + scope.$emit('ScheduleFound'); + }) + .error(function(data,status){ + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve schedule ' + id + ' GET returned: ' + status }); + }); + }; + } + +EditSchedule.$inject = + [ 'SchedulerInit', '$rootScope', 'Wait', 'Rest', + 'ProcessErrors', 'GetBasePath', 'SchedulePost', '$state' + ]; diff --git a/awx/ui/client/src/scheduler/factories/r-rule-to-api.factory.js b/awx/ui/client/src/scheduler/factories/r-rule-to-api.factory.js new file mode 100644 index 0000000000..1e3b251d8a --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/r-rule-to-api.factory.js @@ -0,0 +1,10 @@ +export default + function RRuleToAPI() { + return function(rrule) { + var response; + response = rrule.replace(/(^.*(?=DTSTART))(DTSTART=.*?;)(.*$)/, function(str, p1, p2, p3) { + return p2.replace(/\;/,'').replace(/=/,':') + ' ' + 'RRULE:' + p1 + p3; + }); + return response; + }; + } diff --git a/awx/ui/client/src/scheduler/factories/schedule-post.factory.js b/awx/ui/client/src/scheduler/factories/schedule-post.factory.js new file mode 100644 index 0000000000..2c636f8b89 --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/schedule-post.factory.js @@ -0,0 +1,78 @@ +export default + function SchedulePost(Rest, ProcessErrors, RRuleToAPI, Wait) { + return function(params) { + var scope = params.scope, + url = params.url, + scheduler = params.scheduler, + mode = params.mode, + schedule = (params.schedule) ? params.schedule : {}, + callback = params.callback, + newSchedule, rrule, extra_vars; + if (scheduler.isValid()) { + Wait('start'); + newSchedule = scheduler.getValue(); + rrule = scheduler.getRRule(); + schedule.name = newSchedule.name; + schedule.rrule = RRuleToAPI(rrule.toString()); + schedule.description = (/error/.test(rrule.toText())) ? '' : rrule.toText(); + + if (scope.isFactCleanup) { + extra_vars = { + "older_than": scope.scheduler_form.keep_amount.$viewValue + scope.scheduler_form.keep_unit.$viewValue.value, + "granularity": scope.scheduler_form.granularity_keep_amount.$viewValue + scope.scheduler_form.granularity_keep_unit.$viewValue.value + }; + schedule.extra_data = JSON.stringify(extra_vars); + } else if (scope.cleanupJob) { + extra_vars = { + "days" : scope.scheduler_form.schedulerPurgeDays.$viewValue + }; + schedule.extra_data = JSON.stringify(extra_vars); + } + else if(scope.extraVars){ + schedule.extra_data = scope.parseType === 'yaml' ? + (scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars; + } + Rest.setUrl(url); + if (mode === 'add') { + Rest.post(schedule) + .success(function(){ + if (callback) { + scope.$emit(callback); + } + else { + Wait('stop'); + } + }) + .error(function(data, status){ + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'POST to ' + url + ' returned: ' + status }); + }); + } + else { + Rest.put(schedule) + .success(function(){ + if (callback) { + scope.$emit(callback, schedule); + } + else { + Wait('stop'); + } + }) + .error(function(data, status){ + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'POST to ' + url + ' returned: ' + status }); + }); + } + } + else { + return false; + } + }; + } + +SchedulePost.$inject = + [ 'Rest', + 'ProcessErrors', + 'RRuleToAPI', + 'Wait' + ]; diff --git a/awx/ui/client/src/scheduler/factories/toggle-schedule.factory.js b/awx/ui/client/src/scheduler/factories/toggle-schedule.factory.js new file mode 100644 index 0000000000..423c92227a --- /dev/null +++ b/awx/ui/client/src/scheduler/factories/toggle-schedule.factory.js @@ -0,0 +1,46 @@ +export default + function ToggleSchedule(Wait, GetBasePath, ProcessErrors, Rest, $state) { + return function(params) { + var scope = params.scope, + id = params.id, + url = GetBasePath('schedules') + id +'/'; + + // Perform the update + if (scope.removeScheduleFound) { + scope.removeScheduleFound(); + } + scope.removeScheduleFound = scope.$on('ScheduleFound', function(e, data) { + data.enabled = (data.enabled) ? false : true; + Rest.put(data) + .success( function() { + Wait('stop'); + $state.go('.', null, {reload: true}); + }) + .error( function(data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to update schedule ' + id + ' PUT returned: ' + status }); + }); + }); + + Wait('start'); + + // Get the schedule + Rest.setUrl(url); + Rest.get() + .success(function(data) { + scope.$emit('ScheduleFound', data); + }) + .error(function(data,status){ + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve schedule ' + id + ' GET returned: ' + status }); + }); + }; + } + +ToggleSchedule.$inject = + [ 'Wait', + 'GetBasePath', + 'ProcessErrors', + 'Rest', + '$state' + ]; diff --git a/awx/ui/client/src/scheduler/main.js b/awx/ui/client/src/scheduler/main.js index 34f8d4d3d3..4536261d0c 100644 --- a/awx/ui/client/src/scheduler/main.js +++ b/awx/ui/client/src/scheduler/main.js @@ -10,12 +10,24 @@ import editController from './schedulerEdit.controller'; import {templateUrl} from '../shared/template-url/template-url.factory'; import schedulerDatePicker from './schedulerDatePicker.directive'; import { N_ } from '../i18n'; +import AddSchedule from './factories/add-schedule.factory'; +import DeleteSchedule from './factories/delete-schedule.factory'; +import EditSchedule from './factories/edit-schedule.factory'; +import RRuleToAPI from './factories/r-rule-to-api.factory'; +import SchedulePost from './factories/schedule-post.factory'; +import ToggleSchedule from './factories/toggle-schedule.factory'; export default angular.module('scheduler', []) .controller('schedulerListController', listController) .controller('schedulerAddController', addController) .controller('schedulerEditController', editController) + .factory('AddSchedule', AddSchedule) + .factory('DeleteSchedule', DeleteSchedule) + .factory('EditSchedule', EditSchedule) + .factory('RRuleToAPI', RRuleToAPI) + .factory('SchedulePost', SchedulePost) + .factory('ToggleSchedule', ToggleSchedule) .directive('schedulerDatePicker', schedulerDatePicker) .run(['$stateExtender', function($stateExtender) { // Inventory sync schedule states registered in: awx/ui/client/src/inventories/manage/groups/main.js diff --git a/awx/ui/client/src/shared/Modal.js b/awx/ui/client/src/shared/Modal.js index e6890ac367..86c2da80a9 100644 --- a/awx/ui/client/src/shared/Modal.js +++ b/awx/ui/client/src/shared/Modal.js @@ -17,7 +17,7 @@ export default -angular.module('ModalDialog', ['Utilities', 'ParseHelper']) +angular.module('ModalDialog', ['Utilities']) /** * @ngdoc method diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 0cd6632721..3d3646e64d 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -18,7 +18,7 @@ export default -angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter']) +angular.module('Utilities', ['RestServices', 'Utilities']) /** * @ngdoc method @@ -179,8 +179,8 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter']) * @methodOf shared.function:Utilities * @description For handling errors that are returned from the API */ -.factory('ProcessErrors', ['$rootScope', '$cookieStore', '$log', '$location', 'Alert', 'Wait', - function($rootScope, $cookieStore, $log, $location, Alert, Wait) { +.factory('ProcessErrors', ['$rootScope', '$cookies', '$log', '$location', 'Alert', 'Wait', + function($rootScope, $cookies, $log, $location, Alert, Wait) { return function(scope, data, status, form, defaultMsg) { var field, fieldErrors, msg, keys; Wait('stop'); diff --git a/awx/ui/client/src/shared/capitalize.filter.js b/awx/ui/client/src/shared/capitalize.filter.js deleted file mode 100644 index 598be4218c..0000000000 --- a/awx/ui/client/src/shared/capitalize.filter.js +++ /dev/null @@ -1,13 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export default - angular.module('capitalizeFilter', []).filter('capitalize', function() { - return function(input) { - input = input.charAt(0).toUpperCase() + input.substr(1).toLowerCase(); - return input; - }; - }); diff --git a/awx/ui/client/src/shared/directives.js b/awx/ui/client/src/shared/directives.js index a39205ad81..941e8e49f3 100644 --- a/awx/ui/client/src/shared/directives.js +++ b/awx/ui/client/src/shared/directives.js @@ -14,7 +14,7 @@ */ export default -angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper']) +angular.module('AWDirectives', ['RestServices', 'Utilities']) // awpassmatch: Add to password_confirm field. Will test if value // matches that of 'input[name="password"]' diff --git a/awx/ui/client/src/shared/filters.js b/awx/ui/client/src/shared/filters.js deleted file mode 100644 index 50014c06ce..0000000000 --- a/awx/ui/client/src/shared/filters.js +++ /dev/null @@ -1,112 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name shared.function:filters - * @description - * Custom filters - * - */ - - - -export default -angular.module('AWFilters', []) - - // Object is empty / undefined / null - .filter('isEmpty', function () { - var key; - return function (obj) { - for (key in obj) { - if (obj.hasOwnProperty(key)) { - return false; - } - } - return true; - }; - }) - - // - // capitalize -capitalize the first letter of each word - // - .filter('capitalize', [ function () { - return function (input) { - var values, result, i; - if (input) { - values = input.replace(/\_/g, ' ').split(" "); - result = ""; - for (i = 0; i < values.length; i++) { - result += values[i].charAt(0).toUpperCase() + values[i].substr(1) + ' '; - } - return result.trim(); - } - }; - }]) - - // - // Filter an object of objects by id using an array of id values - // Created specifically for Filter Events on job detail page. - // - .filter('FilterById', [ function() { - return function(input, list) { - var results = []; - if (input && list.length > 0) { - list.forEach(function(itm) { - input.forEach(function(row) { - if (row.id === itm) { - results.push(row); - } - }); - }); - return results; - } - return input; - }; - }]) - - .filter('FilterByField', [ function() { - return function(input, list) { - var fld, key, search = {}, results = {}; - for (fld in list) { - if (list[fld]) { - search[fld] = list[fld]; - } - } - if (Object.keys(search).length > 0) { - for (fld in search) { - for (key in input) { - if (input[key][fld] === search[fld]) { - results[key] = input[key]; - } - } - } - return results; - } - return input; - }; - }]) - - .filter('FilterFailedEvents', [ function() { - return function(input, liveEventProcessing, searchAllStatus) { - var results = []; - if (liveEventProcessing) { - // while live events are happening, we don't want angular to filter out anything - return input; - } - else if (searchAllStatus === 'failed') { - // filter by failed - input.forEach(function(row) { - if (row.status === 'failed') { - results.push(row); - } - }); - return results; - } - return input; - }; - }]); diff --git a/awx/ui/client/src/shared/filters/append.filter.js b/awx/ui/client/src/shared/filters/append.filter.js new file mode 100644 index 0000000000..1493cf5997 --- /dev/null +++ b/awx/ui/client/src/shared/filters/append.filter.js @@ -0,0 +1,15 @@ +export default function() { + return function(string, append) { + if (string) { + if (append) { + return string + append; + } + else { + return string; + } + } + else { + return ""; + } + }; +} diff --git a/awx/ui/client/src/shared/filters/capitalize.filter.js b/awx/ui/client/src/shared/filters/capitalize.filter.js new file mode 100644 index 0000000000..859dd367e1 --- /dev/null +++ b/awx/ui/client/src/shared/filters/capitalize.filter.js @@ -0,0 +1,12 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default [function() { + return function(input) { + input = input.charAt(0).toUpperCase() + input.substr(1).toLowerCase(); + return input; + }; + }]; diff --git a/awx/ui/client/src/shared/format-epoch/format-epoch.filter.js b/awx/ui/client/src/shared/filters/format-epoch.filter.js similarity index 99% rename from awx/ui/client/src/shared/format-epoch/format-epoch.filter.js rename to awx/ui/client/src/shared/filters/format-epoch.filter.js index 2216249732..9d8fee351f 100644 --- a/awx/ui/client/src/shared/format-epoch/format-epoch.filter.js +++ b/awx/ui/client/src/shared/filters/format-epoch.filter.js @@ -12,5 +12,3 @@ export default }; } ]; - - diff --git a/awx/ui/client/src/shared/filters/is-empty.filter.js b/awx/ui/client/src/shared/filters/is-empty.filter.js new file mode 100644 index 0000000000..a0ece767f1 --- /dev/null +++ b/awx/ui/client/src/shared/filters/is-empty.filter.js @@ -0,0 +1,17 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default function() { + return function (obj) { + var key; + for (key in obj) { + if (obj.hasOwnProperty(key)) { + return false; + } + } + return true; + }; + } diff --git a/awx/ui/client/src/shared/filters/long-date.filter.js b/awx/ui/client/src/shared/filters/long-date.filter.js new file mode 100644 index 0000000000..f2d058e3b2 --- /dev/null +++ b/awx/ui/client/src/shared/filters/long-date.filter.js @@ -0,0 +1,17 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default ['moment', function(moment) { + return function(input) { + var date; + if(input === null || input === undefined){ + return ""; + }else { + date = moment(input); + return date.format('l LTS'); + } + }; + }]; diff --git a/awx/ui/client/src/shared/filters/main.js b/awx/ui/client/src/shared/filters/main.js new file mode 100644 index 0000000000..2298bb0723 --- /dev/null +++ b/awx/ui/client/src/shared/filters/main.js @@ -0,0 +1,17 @@ +import prepend from './prepend.filter'; +import append from './append.filter'; +import isEmpty from './is-empty.filter'; +import capitalize from './capitalize.filter'; +import longDate from './long-date.filter'; +import sanitize from './xss-sanitizer.filter'; +import formatEpoch from './format-epoch.filter'; + +export default + angular.module('stringFilters', []) + .filter('prepend', prepend) + .filter('append', append) + .filter('isEmpty', isEmpty) + .filter('capitalize', capitalize) + .filter('longDate', longDate) + .filter('sanitize', sanitize) + .filter('formatEpoch', formatEpoch); diff --git a/awx/ui/client/src/shared/filters/prepend.filter.js b/awx/ui/client/src/shared/filters/prepend.filter.js new file mode 100644 index 0000000000..93bad2889d --- /dev/null +++ b/awx/ui/client/src/shared/filters/prepend.filter.js @@ -0,0 +1,15 @@ +export default function() { + return function(string, prepend) { + if (string) { + if(prepend) { + return prepend + string; + } + else { + return string; + } + } + else { + return ""; + } + }; +} diff --git a/awx/ui/client/src/widgets.js b/awx/ui/client/src/shared/filters/xss-sanitizer.filter.js similarity index 50% rename from awx/ui/client/src/widgets.js rename to awx/ui/client/src/shared/filters/xss-sanitizer.filter.js index dae0b52e0f..ef1c16d32d 100644 --- a/awx/ui/client/src/widgets.js +++ b/awx/ui/client/src/shared/filters/xss-sanitizer.filter.js @@ -4,8 +4,9 @@ * All Rights Reserved *************************************************/ -import "./widgets/InventorySyncStatus"; -import "./widgets/JobStatus"; -import "./widgets/ObjectCount"; -import "./widgets/SCMSyncStatus"; -import "./widgets/Stream"; \ No newline at end of file + export default [function() { + return function(input) { + input = $("").text(input)[0].innerHTML; + return input; + }; + }]; diff --git a/awx/ui/client/src/shared/format-epoch/main.js b/awx/ui/client/src/shared/format-epoch/main.js deleted file mode 100644 index df93378aff..0000000000 --- a/awx/ui/client/src/shared/format-epoch/main.js +++ /dev/null @@ -1,9 +0,0 @@ -import formatEpoch from './format-epoch.filter'; -import moment from '../moment/main'; - -export default - angular.module('formatEpoch', - [ moment.name - ]) - .filter('formatEpoch', formatEpoch); - diff --git a/awx/ui/client/src/helpers/LoadConfig.js b/awx/ui/client/src/shared/load-config/load-config.factory.js similarity index 82% rename from awx/ui/client/src/helpers/LoadConfig.js rename to awx/ui/client/src/shared/load-config/load-config.factory.js index 93d381079b..6394acd288 100644 --- a/awx/ui/client/src/helpers/LoadConfig.js +++ b/awx/ui/client/src/shared/load-config/load-config.factory.js @@ -1,30 +1,5 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:LoadConfig - * @description Attempts to load local_config.js. If not found, loads config.js. Then evaluates the loaded - * javascript, putting the result in $AnsibleConfig. - * LoadConfigHelper - * - * - * - */ - -/*jshint evil:true */ - - - export default -angular.module('LoadConfigHelper', ['Utilities']) - -.factory('LoadConfig', ['$log', '$rootScope', '$http', '$location', 'GetBasePath', - 'ProcessErrors', 'Rest', 'Store', - function($log, $rootScope, $http, $location, GetBasePath, ProcessErrors, Rest, Store) { + function LoadConfig($log, $rootScope, $http, $location, GetBasePath, ProcessErrors, Rest, Store) { return function() { // These ettings used to be found in config.js, hardcoded now. @@ -105,4 +80,8 @@ angular.module('LoadConfigHelper', ['Utilities']) }; } -]); + +LoadConfig.$inject = + [ '$log', '$rootScope', '$http', '$location', + 'GetBasePath', 'ProcessErrors', 'Rest', 'Store' + ]; diff --git a/awx/ui/client/src/shared/load-config/main.js b/awx/ui/client/src/shared/load-config/main.js new file mode 100644 index 0000000000..3accf42ffa --- /dev/null +++ b/awx/ui/client/src/shared/load-config/main.js @@ -0,0 +1,5 @@ +import LoadConfig from './load-config.factory'; + +export default + angular.module('loadconfig', []) + .factory('LoadConfig', LoadConfig); diff --git a/awx/ui/client/src/shared/long-date.filter.js b/awx/ui/client/src/shared/long-date.filter.js deleted file mode 100644 index f48b014f56..0000000000 --- a/awx/ui/client/src/shared/long-date.filter.js +++ /dev/null @@ -1,25 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -function longDateFilter(moment, input) { - var date; - if(input === null){ - return ""; - }else { - date = moment(input); - return date.format('l LTS'); - } -} - -export default - angular.module('longDateFilter', []) - .filter('longDate', - [ 'moment', - function(moment) { - return _.partial(longDateFilter, moment); - } - ]); - diff --git a/awx/ui/client/src/shared/main.js b/awx/ui/client/src/shared/main.js index 5b68027dde..82daaf238d 100644 --- a/awx/ui/client/src/shared/main.js +++ b/awx/ui/client/src/shared/main.js @@ -11,7 +11,7 @@ import smartSearch from './smart-search/main'; import paginate from './paginate/main'; import columnSort from './column-sort/main'; import lodashAsPromised from './lodash-as-promised'; -import stringFilters from './string-filters/main'; +import filters from './filters/main'; import truncatedText from './truncated-text.directive'; import stateExtender from './stateExtender.provider'; import rbacUiControl from './rbacUiControl'; @@ -20,6 +20,15 @@ import templateUrl from './template-url/main'; import RestServices from '../rest/main'; import stateDefinitions from './stateDefinitions.factory'; import apiLoader from './api-loader'; +import variables from './variables/main'; +import parse from './parse/main'; +import loadconfig from './load-config/main'; +import Modal from './Modal'; +import moment from './moment/main'; +import config from './config/main'; +import PromptDialog from './prompt-dialog'; +import directives from './directives'; +import features from './features/main'; import 'angular-duration-format'; export default @@ -29,13 +38,23 @@ angular.module('shared', [listGenerator.name, smartSearch.name, paginate.name, columnSort.name, - stringFilters.name, + filters.name, 'ui.router', rbacUiControl.name, socket.name, templateUrl.name, RestServices.name, apiLoader.name, + variables.name, + parse.name, + loadconfig.name, + Modal.name, + moment.name, + config.name, + PromptDialog.name, + directives.name, + filters.name, + features.name, require('angular-cookies'), 'angular-duration-format' ]) diff --git a/awx/ui/client/src/shared/parse/main.js b/awx/ui/client/src/shared/parse/main.js new file mode 100644 index 0000000000..962bf14bcf --- /dev/null +++ b/awx/ui/client/src/shared/parse/main.js @@ -0,0 +1,5 @@ +import ParseTypeChange from './parse-type-change.factory'; + +export default + angular.module('parse', []) + .factory('ParseTypeChange', ParseTypeChange); diff --git a/awx/ui/client/src/shared/parse/parse-type-change.factory.js b/awx/ui/client/src/shared/parse/parse-type-change.factory.js new file mode 100644 index 0000000000..678c91abe9 --- /dev/null +++ b/awx/ui/client/src/shared/parse/parse-type-change.factory.js @@ -0,0 +1,93 @@ +import 'codemirror/lib/codemirror.js'; +import 'codemirror/mode/javascript/javascript.js'; +import 'codemirror/mode/yaml/yaml.js'; +import 'codemirror/addon/lint/lint.js'; +import 'angular-codemirror/lib/yaml-lint.js'; +import 'codemirror/addon/edit/closebrackets.js'; +import 'codemirror/addon/edit/matchbrackets.js'; +import 'codemirror/addon/selection/active-line.js'; + +export default + function ParseTypeChange(Alert, AngularCodeMirror) { + return function(params) { + var scope = params.scope, + field_id = params.field_id, + fld = (params.variable) ? params.variable : 'variables', + pfld = (params.parse_variable) ? params.parse_variable : 'parseType', + onReady = params.onReady, + onChange = params.onChange, + readOnly = params.readOnly; + + function removeField(fld) { + //set our model to the last change in CodeMirror and then destroy CodeMirror + scope[fld] = scope[fld + 'codeMirror'].getValue(); + $('#cm-' + fld + '-container > .CodeMirror').empty().remove(); + } + + function createField(onChange, onReady, fld) { + //hide the textarea and show a fresh CodeMirror with the current mode (json or yaml) + + scope[fld + 'codeMirror'] = AngularCodeMirror(readOnly); + scope[fld + 'codeMirror'].addModes(global.$AnsibleConfig.variable_edit_modes); + scope[fld + 'codeMirror'].showTextArea({ + scope: scope, + model: fld, + element: field_id, + lineNumbers: true, + mode: scope[pfld], + onReady: onReady, + onChange: onChange + }); + } + + // Hide the textarea and show a CodeMirror editor + createField(onChange, onReady, fld); + + + // Toggle displayed variable string between JSON and YAML + scope.parseTypeChange = function(model, fld) { + var json_obj; + if (scope[model] === 'json') { + // converting yaml to json + try { + removeField(fld); + json_obj = jsyaml.load(scope[fld]); + if ($.isEmptyObject(json_obj)) { + scope[fld] = "{}"; + } + else { + scope[fld] = JSON.stringify(json_obj, null, " "); + } + createField(onReady, onChange, fld); + } + catch (e) { + Alert('Parse Error', 'Failed to parse valid YAML. ' + e.message); + setTimeout( function() { scope.$apply( function() { scope[model] = 'yaml'; createField(onReady, onChange, fld); }); }, 500); + } + } + else { + // convert json to yaml + try { + removeField(fld); + json_obj = JSON.parse(scope[fld]); + if ($.isEmptyObject(json_obj)) { + scope[fld] = '---'; + } + else { + scope[fld] = jsyaml.safeDump(json_obj); + } + createField(onReady, onChange, fld); + } + catch (e) { + Alert('Parse Error', 'Failed to parse valid JSON. ' + e.message); + setTimeout( function() { scope.$apply( function() { scope[model] = 'json'; createField(onReady, onChange, fld); }); }, 500 ); + } + } + }; + }; + } + +ParseTypeChange.$inject = + [ 'Alert', + 'AngularCodeMirror' + ]; diff --git a/awx/ui/client/src/shared/prompt-dialog.js b/awx/ui/client/src/shared/prompt-dialog.js index f0c7794e47..96152c0b89 100644 --- a/awx/ui/client/src/shared/prompt-dialog.js +++ b/awx/ui/client/src/shared/prompt-dialog.js @@ -30,7 +30,7 @@ */ export default -angular.module('PromptDialog', ['Utilities', 'sanitizeFilter']) +angular.module('PromptDialog', ['Utilities']) .factory('Prompt', [ function () { return function (params) { diff --git a/awx/ui/client/src/shared/smart-search/smart-search.block.less b/awx/ui/client/src/shared/smart-search/smart-search.block.less index 3163ec945b..bd295d5b0d 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.block.less +++ b/awx/ui/client/src/shared/smart-search/smart-search.block.less @@ -160,7 +160,7 @@ } .SmartSearch-keyToggle { margin-right: auto; - margin-left: 12px; + margin-left: 20px; text-transform: uppercase; background-color: @default-bg; border-radius: 5px; @@ -192,7 +192,7 @@ font-size: 12px; width: 100%; padding: 15px; - margin-bottom: 15px; + margin-bottom: 20px; border-radius: 4px; border: 1px solid @login-notice-border; background-color: @login-notice-bg; diff --git a/awx/ui/client/src/shared/string-filters/append.filter.js b/awx/ui/client/src/shared/string-filters/append.filter.js deleted file mode 100644 index 42860cd499..0000000000 --- a/awx/ui/client/src/shared/string-filters/append.filter.js +++ /dev/null @@ -1,10 +0,0 @@ -export default function() { - return function(string, append) { - if (string) { - return string + append; - } - - return ""; - }; -} - diff --git a/awx/ui/client/src/shared/string-filters/main.js b/awx/ui/client/src/shared/string-filters/main.js deleted file mode 100644 index 3c51d184d2..0000000000 --- a/awx/ui/client/src/shared/string-filters/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import prepend from './prepend.filter'; -import append from './append.filter'; - -export default - angular.module('stringFilters', []) - .filter('prepend', prepend) - .filter('append', append); diff --git a/awx/ui/client/src/shared/string-filters/prepend.filter.js b/awx/ui/client/src/shared/string-filters/prepend.filter.js deleted file mode 100644 index a9d87be94c..0000000000 --- a/awx/ui/client/src/shared/string-filters/prepend.filter.js +++ /dev/null @@ -1,9 +0,0 @@ -export default function() { - return function(string, prepend) { - if (string) { - return prepend + string; - } - - return ""; - }; -} diff --git a/awx/ui/client/src/shared/variables/main.js b/awx/ui/client/src/shared/variables/main.js new file mode 100644 index 0000000000..29942a7ff9 --- /dev/null +++ b/awx/ui/client/src/shared/variables/main.js @@ -0,0 +1,9 @@ +import ParseVariableString from './parse-variable-string.factory'; +import SortVariables from './sort-variables.factory'; +import ToJSON from './to-json.factory'; + +export default + angular.module('variables', []) + .factory('ParseVariableString', ParseVariableString) + .factory('SortVariables', SortVariables) + .factory('ToJSON', ToJSON); diff --git a/awx/ui/client/src/shared/variables/parse-variable-string.factory.js b/awx/ui/client/src/shared/variables/parse-variable-string.factory.js new file mode 100644 index 0000000000..71e145897f --- /dev/null +++ b/awx/ui/client/src/shared/variables/parse-variable-string.factory.js @@ -0,0 +1,55 @@ +export default + function ParseVariableString($log, ProcessErrors, SortVariables) { + return function (variables) { + var result = "---", json_obj; + if (typeof variables === 'string') { + if (variables === "{}" || variables === "null" || variables === "" || variables === "\"\"") { + // String is empty, return --- + } else { + try { + json_obj = JSON.parse(variables); + json_obj = SortVariables(json_obj); + result = jsyaml.safeDump(json_obj); + + } + catch (e) { + $log.debug('Attempt to parse extra_vars as JSON failed. Check that the variables parse as yaml. Set the raw string as the result.'); + try { + // do safeLoad, which well error if not valid yaml + json_obj = jsyaml.safeLoad(variables); + // but just send the variables + result = variables; + } + catch(e2) { + ProcessErrors(null, variables, e2.message, null, { hdr: 'Error!', + msg: 'Attempts to parse variables as JSON and YAML failed. Last attempt returned: ' + e2.message }); + } + } + } + } + else { + if ($.isEmptyObject(variables) || variables === null) { + // Empty object, return --- + } + else { + // convert object to yaml + try { + json_obj = SortVariables(variables); + result = jsyaml.safeDump(json_obj); + // result = variables; + } + catch(e3) { + ProcessErrors(null, variables, e3.message, null, { hdr: 'Error!', + msg: 'Attempt to convert JSON object to YAML document failed: ' + e3.message }); + } + } + } + return result; + }; + } + +ParseVariableString.$inject = + [ '$log', + 'ProcessErrors', + 'SortVariables' + ]; diff --git a/awx/ui/client/src/shared/variables/sort-variables.factory.js b/awx/ui/client/src/shared/variables/sort-variables.factory.js new file mode 100644 index 0000000000..a9cb3a363b --- /dev/null +++ b/awx/ui/client/src/shared/variables/sort-variables.factory.js @@ -0,0 +1,23 @@ +export default + function SortVariables() { + return function(variableObj) { + var newObj; + function sortIt(objToSort) { + var i, + keys = Object.keys(objToSort), + newObj = {}; + keys = keys.sort(); + for (i=0; i < keys.length; i++) { + if (typeof objToSort[keys[i]] === 'object' && objToSort[keys[i]] !== null && !Array.isArray(objToSort[keys[i]])) { + newObj[keys[i]] = sortIt(objToSort[keys[i]]); + } + else { + newObj[keys[i]] = objToSort[keys[i]]; + } + } + return newObj; + } + newObj = sortIt(variableObj); + return newObj; + }; + } diff --git a/awx/ui/client/src/shared/variables/to-json.factory.js b/awx/ui/client/src/shared/variables/to-json.factory.js new file mode 100644 index 0000000000..70996d489e --- /dev/null +++ b/awx/ui/client/src/shared/variables/to-json.factory.js @@ -0,0 +1,80 @@ +export default + function ToJSON($log, ProcessErrors) { + return function(parseType, variables, stringify, reviver) { + var json_data, + result, + tmp; + // bracketVar, + // key, + // lines, i, newVars = []; + if (parseType === 'json') { + try { + // perform a check to see if the user cleared the field completly + if(variables.trim() === "" || variables.trim() === "{" || variables.trim() === "}" ){ + variables = "{}"; + } + //parse a JSON string + if (reviver) { + json_data = JSON.parse(variables, reviver); + } + else { + json_data = JSON.parse(variables); + } + } + catch(e) { + json_data = {}; + $log.error('Failed to parse JSON string. Parser returned: ' + e.message); + ProcessErrors(null, variables, e.message, null, { hdr: 'Error!', + msg: 'Failed to parse JSON string. Parser returned: ' + e.message }); + throw 'Parse error. Failed to parse variables.'; + } + } else { + try { + if(variables.trim() === "" || variables.trim() === "-" || variables.trim() === "--"){ + variables = '---'; + } + json_data = jsyaml.safeLoad(variables); + if(json_data!==null){ + // unparsing just to make sure no weird characters are included. + tmp = jsyaml.dump(json_data); + if(tmp.indexOf('[object Object]')!==-1){ + throw "Failed to parse YAML string. Parser returned' + key + ' : ' +value + '.' "; + } + } + } + catch(e) { + json_data = undefined; // {}; + // $log.error('Failed to parse YAML string. Parser returned undefined'); + ProcessErrors(null, variables, e.message, null, { hdr: 'Error!', + msg: 'Failed to parse YAML string. Parser returned undefined'}); + } + } + // Make sure our JSON is actually an object + if (typeof json_data !== 'object') { + ProcessErrors(null, variables, null, null, { hdr: 'Error!', + msg: 'Failed to parse variables. Attempted to parse ' + parseType + '. Parser did not return an object.' }); + // setTimeout( function() { + throw 'Parse error. Failed to parse variables.'; + // }, 1000); + } + result = json_data; + if (stringify) { + if(json_data === undefined){ + result = undefined; + } + else if ($.isEmptyObject(json_data)) { + result = ""; + } else { + // utilize the parsing to get here + // but send the raw variable string + result = variables; + } + } + return result; + }; + } + +ToJSON.$inject = + [ '$log', + 'ProcessErrors' + ]; diff --git a/awx/ui/client/src/shared/xss-sanitizer.filter.js b/awx/ui/client/src/shared/xss-sanitizer.filter.js deleted file mode 100644 index daa9a71d68..0000000000 --- a/awx/ui/client/src/shared/xss-sanitizer.filter.js +++ /dev/null @@ -1,13 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export default - angular.module('sanitizeFilter', []).filter('sanitize', function() { - return function(input) { - input = $("").text(input)[0].innerHTML; - return input; - }; - }); diff --git a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html index c2baf24ee9..69a84dacfd 100644 --- a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html +++ b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html @@ -8,9 +8,9 @@ RESULTS
- - - + + +
diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html index 451f10c5e8..5df1bd84c7 100644 --- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html +++ b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html @@ -8,9 +8,9 @@ RESULTS
- - - + + +
diff --git a/awx/ui/client/src/standard-out/log/standard-out-log.controller.js b/awx/ui/client/src/standard-out/log/standard-out-log.controller.js index 45564fffc6..bf8a8f89ef 100644 --- a/awx/ui/client/src/standard-out/log/standard-out-log.controller.js +++ b/awx/ui/client/src/standard-out/log/standard-out-log.controller.js @@ -20,7 +20,7 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce // Open up a socket for events depending on the type of job function openSockets() { - if ($state.current.name === 'jobDetail') { + if ($state.current.name === 'jobResult') { $log.debug("socket watching on job_events-" + job_id); $scope.$on(`ws-job_events-${job_id}`, function() { $log.debug("socket fired on job_events-" + job_id); diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html index bb7a6c1e64..8c9740a5eb 100644 --- a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html +++ b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html @@ -8,8 +8,8 @@ RESULTS
- - + +
diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html index 458b2c6dd2..249cb0bf72 100644 --- a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html +++ b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html @@ -8,9 +8,9 @@ RESULTS
- - - + + +
diff --git a/awx/ui/client/src/standard-out/standard-out.controller.js b/awx/ui/client/src/standard-out/standard-out.controller.js index 64f34cb68f..0cabd74389 100644 --- a/awx/ui/client/src/standard-out/standard-out.controller.js +++ b/awx/ui/client/src/standard-out/standard-out.controller.js @@ -33,14 +33,14 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, if (data.status === 'failed' || data.status === 'canceled' || data.status === 'error' || data.status === 'successful') { // Go out and refresh the job details - getJobDetails(); + getjobResults(); } }); // Set the parse type so that CodeMirror knows how to display extra params YAML/JSON $scope.parseType = 'yaml'; - function getJobDetails() { + function getjobResults() { // Go out and get the job details based on the job type. jobType gets defined // in the data block of the route declaration for each of the different types @@ -260,7 +260,7 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, RelaunchJob({ scope: $scope, id: typeId, type: job.type, name: job.name }); }; - getJobDetails(); + getjobResults(); } diff --git a/awx/ui/client/src/system-tracking/compare-facts/fact-template.js b/awx/ui/client/src/system-tracking/compare-facts/fact-template.js index 3dcc29a92a..506e893f2c 100644 --- a/awx/ui/client/src/system-tracking/compare-facts/fact-template.js +++ b/awx/ui/client/src/system-tracking/compare-facts/fact-template.js @@ -1,7 +1,4 @@ -import stringFilters from '../../shared/string-filters/main'; -import formatEpoch from '../../shared/format-epoch/main'; - -var $injector = angular.injector(['ng', stringFilters.name, formatEpoch.name]); +var $injector = angular.injector(['ng']); var $interpolate = $injector.get('$interpolate'); function FactTemplate(templateString) { diff --git a/awx/ui/client/src/teams/add/teams-add.controller.js b/awx/ui/client/src/teams/add/teams-add.controller.js new file mode 100644 index 0000000000..7f09f166a7 --- /dev/null +++ b/awx/ui/client/src/teams/add/teams-add.controller.js @@ -0,0 +1,68 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'GenerateForm', + 'Rest', 'Alert', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'Wait', '$state', + function($scope, $rootScope, $stateParams, TeamForm, GenerateForm, Rest, Alert, ProcessErrors, + ClearScope, GetBasePath, Wait, $state) { + + ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior + //$scope. + + Rest.setUrl(GetBasePath('teams')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert('Permission Error', 'You do not have permission to add a team.', 'alert-info'); + } + }); + + // Inject dynamic view + var defaultUrl = GetBasePath('teams'), + form = TeamForm; + + init(); + + function init() { + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + + $rootScope.flashMessage = null; + } + + // Save + $scope.formSave = function() { + var fld, data; + GenerateForm.clearApiErrors($scope); + Wait('start'); + Rest.setUrl(defaultUrl); + data = {}; + for (fld in form.fields) { + data[fld] = $scope[fld]; + } + Rest.post(data) + .success(function(data) { + Wait('stop'); + $rootScope.flashMessage = "New team successfully created!"; + $rootScope.$broadcast("EditIndicatorChange", "users", data.id); + $state.go('teams.edit', { team_id: data.id }, { reload: true }); + }) + .error(function(data, status) { + Wait('stop'); + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to add new team. Post returned status: ' + + status + }); + }); + }; + + $scope.formCancel = function() { + $state.go('teams'); + }; + } +]; diff --git a/awx/ui/client/src/teams/edit/teams-edit.controller.js b/awx/ui/client/src/teams/edit/teams-edit.controller.js new file mode 100644 index 0000000000..c4b1d1fa4c --- /dev/null +++ b/awx/ui/client/src/teams/edit/teams-edit.controller.js @@ -0,0 +1,97 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Rest', + 'ProcessErrors', 'ClearScope', 'GetBasePath', 'Wait', '$state', + function($scope, $rootScope, $stateParams, + TeamForm, Rest, ProcessErrors, ClearScope, GetBasePath, Wait, $state) { + + ClearScope(); + + var form = TeamForm, + id = $stateParams.team_id, + defaultUrl = GetBasePath('teams') + id; + + init(); + + function init() { + $scope.team_id = id; + Rest.setUrl(defaultUrl); + Wait('start'); + Rest.get(defaultUrl).success(function(data) { + setScopeFields(data); + $scope.organization_name = data.summary_fields.organization.name; + + $scope.team_obj = data; + Wait('stop'); + }); + + $scope.$watch('team_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + + + } + + // @issue I think all this really want to do is _.forEach(form.fields, (field) =>{ $scope[field] = data[field]}) + function setScopeFields(data) { + _(data) + .pick(function(value, key) { + return form.fields.hasOwnProperty(key) === true; + }) + .forEach(function(value, key) { + $scope[key] = value; + }) + .value(); + return; + } + + // prepares a data payload for a PUT request to the API + function processNewData(fields) { + var data = {}; + _.forEach(fields, function(value, key) { + if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined) { + data[key] = $scope[key]; + } + }); + return data; + } + + $scope.formCancel = function() { + $state.go('teams', null, { reload: true }); + }; + + $scope.formSave = function() { + $rootScope.flashMessage = null; + if ($scope[form.name + '_form'].$valid) { + var data = processNewData(form.fields); + Rest.setUrl(defaultUrl); + Rest.put(data).success(function() { + $state.go($state.current, null, { reload: true }); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Failed to retrieve user: ' + + $stateParams.id + '. GET status: ' + status + }); + }); + } + }; + + init(); + + $scope.convertApiUrl = function(str) { + if (str) { + return str.replace("api/v1", "#"); + } else { + return null; + } + }; + } +]; diff --git a/awx/ui/client/src/teams/list/teams-list.controller.js b/awx/ui/client/src/teams/list/teams-list.controller.js new file mode 100644 index 0000000000..a4793de0ec --- /dev/null +++ b/awx/ui/client/src/teams/list/teams-list.controller.js @@ -0,0 +1,81 @@ +/************************************************* + * Copyright (c) 2017 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$log', + '$stateParams', 'Rest', 'Alert', 'TeamList', 'Prompt', 'ClearScope', + 'ProcessErrors', 'GetBasePath', 'Wait', '$state', '$filter', 'rbacUiControlService', 'Dataset', + function($scope, $rootScope, $log, $stateParams, + Rest, Alert, TeamList, Prompt, ClearScope, ProcessErrors, + GetBasePath, Wait, $state, $filter, rbacUiControlService, Dataset) { + + ClearScope(); + + var list = TeamList, + defaultUrl = GetBasePath('teams'); + + init(); + + function init() { + $scope.canAdd = false; + + rbacUiControlService.canAdd('teams') + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + _.forEach($scope[list.name], (team) => { + team.organization_name = team.summary_fields.organization.name; + }); + + $scope.selected = []; + } + + $scope.addTeam = function() { + $state.go('teams.add'); + }; + + $scope.editTeam = function(id) { + $state.go('teams.edit', { team_id: id }); + }; + + $scope.deleteTeam = function(id, name) { + + var action = function() { + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.destroy() + .success(function() { + Wait('stop'); + $('#prompt-modal').modal('hide'); + if (parseInt($state.params.team_id) === id) { + $state.go('^', null, { reload: true }); + } else { + $state.go('.', null, { reload: true }); + } + }) + .error(function(data, status) { + Wait('stop'); + $('#prompt-modal').modal('hide'); + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status + }); + }); + }; + + Prompt({ + hdr: 'Delete', + body: '
Are you sure you want to delete the team below?
' + $filter('sanitize')(name) + '
', + action: action, + actionText: 'DELETE' + }); + }; + } +]; diff --git a/awx/ui/client/src/teams/main.js b/awx/ui/client/src/teams/main.js new file mode 100644 index 0000000000..cd800ddb11 --- /dev/null +++ b/awx/ui/client/src/teams/main.js @@ -0,0 +1,47 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import TeamsList from './list/teams-list.controller'; +import TeamsAdd from './add/teams-add.controller'; +import TeamsEdit from './edit/teams-edit.controller'; +import { N_ } from '../i18n'; + +export default +angular.module('Teams', []) + .controller('TeamsList', TeamsList) + .controller('TeamsAdd', TeamsAdd) + .controller('TeamsEdit', TeamsEdit) + .config(['$stateProvider', 'stateDefinitionsProvider', + function($stateProvider, stateDefinitionsProvider) { + let stateDefinitions = stateDefinitionsProvider.$get(); + + // lazily generate a tree of substates which will replace this node in ui-router's stateRegistry + // see: stateDefinition.factory for usage documentation + $stateProvider.state({ + name: 'teams', + url: '/teams', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'teams', + modes: ['add', 'edit'], + list: 'TeamList', + form: 'TeamForm', + controllers: { + list: TeamsList, + add: TeamsAdd, + edit: TeamsEdit + }, + data: { + activityStream: true, + activityStreamTarget: 'team' + }, + ncyBreadcrumb: { + parent: 'setup', + label: N_('TEAMS') + } + }) + }); + } + ]); diff --git a/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js b/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js new file mode 100644 index 0000000000..891d391f95 --- /dev/null +++ b/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js @@ -0,0 +1,156 @@ +export default + function CallbackHelpInit($location, GetBasePath, Rest, JobTemplateForm, GenerateForm, $stateParams, ProcessErrors, ParseTypeChange, + ParseVariableString, Empty, InventoryList, CredentialList, ProjectList, Wait) { + return function(params) { + var scope = params.scope, + defaultUrl = GetBasePath('job_templates'), + // generator = GenerateForm, + form = JobTemplateForm(), + // loadingFinishedCount = 0, + // base = $location.path().replace(/^\//, '').split('/')[0], + master = {}, + id = $stateParams.job_template_id; + // checkSCMStatus, getPlaybooks, callback, + // choicesCount = 0; + + CredentialList = _.cloneDeep(CredentialList); + + // The form uses awPopOverWatch directive to 'watch' scope.callback_help for changes. Each time the + // popover is activated, a function checks the value of scope.callback_help before constructing the content. + scope.setCallbackHelp = function() { + scope.callback_help = "

With a provisioning callback URL and a host config key a host can contact Tower and request a configuration update using this job " + + "template. The request from the host must be a POST. Here is an example using curl:

\n" + + "
curl --data \"host_config_key=" + scope.example_config_key + "\" " +
+                    scope.callback_server_path + GetBasePath('job_templates') + scope.example_template_id + "/callback/
\n" + + "

Note the requesting host must be defined in the inventory associated with the job template. If Tower fails to " + + "locate the host, the request will be denied.

" + + "

Successful requests create an entry on the Jobs page, where results and history can be viewed.

"; + }; + + // The md5 helper emits NewMD5Generated whenever a new key is available + if (scope.removeNewMD5Generated) { + scope.removeNewMD5Generated(); + } + scope.removeNewMD5Generated = scope.$on('NewMD5Generated', function() { + scope.configKeyChange(); + }); + + // Fired when user enters a key value + scope.configKeyChange = function() { + scope.example_config_key = scope.host_config_key; + scope.setCallbackHelp(); + }; + + // Set initial values and construct help text + scope.callback_server_path = $location.protocol() + '://' + $location.host() + (($location.port()) ? ':' + $location.port() : ''); + scope.example_config_key = '5a8ec154832b780b9bdef1061764ae5a'; + scope.example_template_id = 'N'; + scope.setCallbackHelp(); + + // this fills the job template form both on copy of the job template + // and on edit + scope.fillJobTemplate = function(){ + // id = id || $rootScope.copy.id; + // Retrieve detail record and prepopulate the form + Rest.setUrl(defaultUrl + id); + Rest.get() + .success(function (data) { + scope.job_template_obj = data; + scope.name = data.name; + var fld, i; + for (fld in form.fields) { + if (fld !== 'variables' && fld !== 'survey' && data[fld] !== null && data[fld] !== undefined) { + if (form.fields[fld].type === 'select') { + if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) { + for (i = 0; i < scope[fld + '_options'].length; i++) { + if (data[fld] === scope[fld + '_options'][i].value) { + scope[fld] = scope[fld + '_options'][i]; + } + } + } else { + scope[fld] = data[fld]; + } + } else { + scope[fld] = data[fld]; + if(!Empty(data.summary_fields.survey)) { + scope.survey_exists = true; + } + } + master[fld] = scope[fld]; + } + if (fld === 'variables') { + // Parse extra_vars, converting to YAML. + scope.variables = ParseVariableString(data.extra_vars); + master.variables = scope.variables; + } + if (form.fields[fld].type === 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; + } + if (form.fields[fld].type === 'checkbox_group') { + for(var j=0; j maxLength) { - return text.substring(0,maxLength) + '...'; + if(text && text.length > maxNodeTextLength) { + return text.substring(0,maxNodeTextLength) + '...'; } else { return text; @@ -216,7 +214,7 @@ export default [ '$state','moment', '$timeout', '$window', links = tree.links(nodes); let node = svgGroup.selectAll("g.node") .data(nodes, function(d) { - d.y = d.depth * 180; + d.y = d.depth * 240; return d.id || (d.id = ++i); }); @@ -347,11 +345,32 @@ export default [ '$state','moment', '$timeout', '$window', .call(edit_node) .on("mouseover", function(d) { if(!d.isStartNode) { + let resourceName = (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : ""; + if(resourceName && resourceName.length > maxNodeTextLength) { + // Render the tooltip quickly in the dom and then remove. This lets us know how big the tooltip is so that we can place + // it properly on the workflow + let tooltipDimensionChecker = $(""); + $('body').append(tooltipDimensionChecker); + let tipWidth = $(tooltipDimensionChecker).outerWidth(); + let tipHeight = $(tooltipDimensionChecker).outerHeight(); + $(tooltipDimensionChecker).remove(); + + thisNode.append("foreignObject") + .attr("x", (nodeW / 2) - (tipWidth / 2)) + .attr("y", (tipHeight + 15) * -1) + .attr("width", tipWidth) + .attr("height", tipHeight+20) + .attr("class", "WorkflowChart-tooltip") + .html(function(){ + return "
" + resourceName + "
"; + }); + } d3.select("#node-" + d.id) .classed("hovering", true); } }) .on("mouseout", function(d){ + $('.WorkflowChart-tooltip').remove(); if(!d.isStartNode) { d3.select("#node-" + d.id) .classed("hovering", false); @@ -830,7 +849,7 @@ export default [ '$state','moment', '$timeout', '$window', this.on("click", function(d) { if(d.job.id && d.unifiedJobTemplate) { if(d.unifiedJobTemplate.unified_job_type === 'job') { - $state.go('jobDetail', {id: d.job.id}); + $state.go('jobResult', {id: d.job.id}); } else if(d.unifiedJobTemplate.unified_job_type === 'inventory_update') { $state.go('inventorySyncStdout', {id: d.job.id}); diff --git a/awx/ui/client/src/users/add/users-add.controller.js b/awx/ui/client/src/users/add/users-add.controller.js new file mode 100644 index 0000000000..6f7b8b75b3 --- /dev/null +++ b/awx/ui/client/src/users/add/users-add.controller.js @@ -0,0 +1,119 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import { N_ } from "../../i18n"; + +const user_type_options = [ + { type: 'normal', label: N_('Normal User') }, + { type: 'system_auditor', label: N_('System Auditor') }, + { type: 'system_administrator', label: N_('System Administrator') }, +]; + +export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'GenerateForm', + 'Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'ClearScope', 'GetBasePath', + 'Wait', 'CreateSelect2', '$state', '$location', 'i18n', + function($scope, $rootScope, $stateParams, UserForm, + GenerateForm, Rest, Alert, ProcessErrors, ReturnToCaller, ClearScope, + GetBasePath, Wait, CreateSelect2, $state, $location, i18n) { + + ClearScope(); + + var defaultUrl = GetBasePath('organizations'), + form = UserForm; + + init(); + + function init() { + // apply form definition's default field values + GenerateForm.applyDefaults(form, $scope); + + $scope.ldap_user = false; + $scope.not_ldap_user = !$scope.ldap_user; + $scope.ldap_dn = null; + $scope.socialAuthUser = false; + $scope.external_account = null; + + Rest.setUrl(GetBasePath('users')); + Rest.options() + .success(function(data) { + if (!data.actions.POST) { + $state.go("^"); + Alert(i18n._('Permission Error'), i18n._('You do not have permission to add a user.'), 'alert-info'); + } + }); + + $scope.user_type_options = user_type_options; + $scope.user_type = user_type_options[0]; + $scope.$watch('user_type', user_type_sync($scope)); + CreateSelect2({ + element: '#user_user_type', + multiple: false + }); + } + + function user_type_sync($scope) { + return (type_option) => { + $scope.is_superuser = false; + $scope.is_system_auditor = false; + switch (type_option.type) { + case 'system_administrator': + $scope.is_superuser = true; + break; + case 'system_auditor': + $scope.is_system_auditor = true; + break; + } + }; + } + + // Save + $scope.formSave = function() { + var fld, data = {}; + if ($scope[form.name + '_form'].$valid) { + if ($scope.organization !== undefined && $scope.organization !== null && $scope.organization !== '') { + Rest.setUrl(defaultUrl + $scope.organization + '/users/'); + for (fld in form.fields) { + if (form.fields[fld].realName) { + data[form.fields[fld].realName] = $scope[fld]; + } else { + data[fld] = $scope[fld]; + } + } + data.is_superuser = $scope.is_superuser; + data.is_system_auditor = $scope.is_system_auditor; + Wait('start'); + Rest.post(data) + .success(function(data) { + var base = $location.path().replace(/^\//, '').split('/')[0]; + if (base === 'users') { + $rootScope.flashMessage = i18n._('New user successfully created!'); + $rootScope.$broadcast("EditIndicatorChange", "users", data.id); + $state.go('users.edit', { user_id: data.id }, { reload: true }); + } else { + ReturnToCaller(1); + } + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, form, { hdr: i18n._('Error!'), msg: i18n._('Failed to add new user. POST returned status: ') + status }); + }); + } else { + $scope.organization_name_api_error = i18n._('A value is required'); + } + } + }; + + $scope.formCancel = function() { + $state.go('users'); + }; + + // Password change + $scope.clearPWConfirm = function(fld) { + // If password value changes, make sure password_confirm must be re-entered + $scope[fld] = ''; + $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); + }; + } +]; diff --git a/awx/ui/client/src/users/edit/users-edit.controller.js b/awx/ui/client/src/users/edit/users-edit.controller.js new file mode 100644 index 0000000000..a1255398ed --- /dev/null +++ b/awx/ui/client/src/users/edit/users-edit.controller.js @@ -0,0 +1,179 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import { N_ } from "../../i18n"; + +const user_type_options = [ + { type: 'normal', label: N_('Normal User') }, + { type: 'system_auditor', label: N_('System Auditor') }, + { type: 'system_administrator', label: N_('System Administrator') }, +]; + +export default ['$scope', '$rootScope', '$location', + '$stateParams', 'UserForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath', + 'Wait', 'CreateSelect2', '$state', 'i18n', + function($scope, $rootScope, $location, + $stateParams, UserForm, Rest, ProcessErrors, + ClearScope, GetBasePath, Wait, CreateSelect2, $state, i18n) { + + for (var i = 0; i < user_type_options.length; i++) { + user_type_options[i].label = i18n._(user_type_options[i].label); + } + ClearScope(); + + var form = UserForm, + master = {}, + id = $stateParams.user_id, + defaultUrl = GetBasePath('users') + id; + + init(); + + function init() { + $scope.hidePagination = false; + $scope.hideSmartSearch = false; + $scope.user_type_options = user_type_options; + $scope.user_type = user_type_options[0]; + $scope.$watch('user_type', user_type_sync($scope)); + $scope.$watch('is_superuser', hidePermissionsTabSmartSearchAndPaginationIfSuperUser($scope)); + Rest.setUrl(defaultUrl); + Wait('start'); + Rest.get(defaultUrl).success(function(data) { + $scope.user_id = id; + $scope.ldap_user = (data.ldap_dn !== null && data.ldap_dn !== undefined && data.ldap_dn !== '') ? true : false; + $scope.not_ldap_user = !$scope.ldap_user; + master.ldap_user = $scope.ldap_user; + $scope.socialAuthUser = (data.auth.length > 0) ? true : false; + $scope.external_account = data.external_account; + + $scope.user_type = $scope.user_type_options[0]; + $scope.is_system_auditor = false; + $scope.is_superuser = false; + if (data.is_system_auditor) { + $scope.user_type = $scope.user_type_options[1]; + $scope.is_system_auditor = true; + } + if (data.is_superuser) { + $scope.user_type = $scope.user_type_options[2]; + $scope.is_superuser = true; + } + + $scope.user_obj = data; + $scope.name = data.username; + + CreateSelect2({ + element: '#user_user_type', + multiple: false + }); + + $scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) { + if (val === false) { + $scope.canAdd = false; + } + }); + + setScopeFields(data); + Wait('stop'); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: i18n._('Error!'), + msg: i18n.sprintf(i18n._('Failed to retrieve user: %s. GET status: '), $stateParams.id) + status + }); + }); + } + + function user_type_sync($scope) { + return (type_option) => { + $scope.is_superuser = false; + $scope.is_system_auditor = false; + switch (type_option.type) { + case 'system_administrator': + $scope.is_superuser = true; + break; + case 'system_auditor': + $scope.is_system_auditor = true; + break; + } + }; + } + + // Organizations and Teams tab pagination is hidden through other mechanism + function hidePermissionsTabSmartSearchAndPaginationIfSuperUser(scope) { + return function(isSuperuserNewValue) { + let shouldHide = isSuperuserNewValue; + if (shouldHide === true) { + scope.hidePagination = true; + scope.hideSmartSearch = true; + } else if (shouldHide === false) { + scope.hidePagination = false; + scope.hideSmartSearch = false; + } + }; + } + + + function setScopeFields(data) { + _(data) + .pick(function(value, key) { + return form.fields.hasOwnProperty(key) === true; + }) + .forEach(function(value, key) { + $scope[key] = value; + }) + .value(); + return; + } + + $scope.convertApiUrl = function(str) { + if (str) { + return str.replace("api/v1", "#"); + } else { + return null; + } + }; + + // prepares a data payload for a PUT request to the API + var processNewData = function(fields) { + var data = {}; + _.forEach(fields, function(value, key) { + if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined) { + data[key] = $scope[key]; + } + }); + data.is_superuser = $scope.is_superuser; + data.is_system_auditor = $scope.is_system_auditor; + return data; + }; + + $scope.formCancel = function() { + $state.go('users', null, { reload: true }); + }; + + $scope.formSave = function() { + $rootScope.flashMessage = null; + if ($scope[form.name + '_form'].$valid) { + Rest.setUrl(defaultUrl + '/'); + var data = processNewData(form.fields); + Rest.put(data).success(function() { + $state.go($state.current, null, { reload: true }); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: i18n._('Error!'), + msg: i18n.sprintf(i18n._('Failed to retrieve user: %s. GET status: '), $stateParams.id) + status + }); + }); + } + }; + + $scope.clearPWConfirm = function(fld) { + // If password value changes, make sure password_confirm must be re-entered + $scope[fld] = ''; + $scope[form.name + '_form'][fld].$setValidity('awpassmatch', false); + $rootScope.flashMessage = null; + }; + } +]; diff --git a/awx/ui/client/src/users/list/users-list.controller.js b/awx/ui/client/src/users/list/users-list.controller.js new file mode 100644 index 0000000000..437690da83 --- /dev/null +++ b/awx/ui/client/src/users/list/users-list.controller.js @@ -0,0 +1,90 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import { N_ } from "../../i18n"; + +const user_type_options = [ + { type: 'normal', label: N_('Normal User') }, + { type: 'system_auditor', label: N_('System Auditor') }, + { type: 'system_administrator', label: N_('System Administrator') }, +]; + +export default ['$scope', '$rootScope', '$stateParams', + 'Rest', 'Alert', 'UserList', 'Prompt', 'ClearScope', 'ProcessErrors', 'GetBasePath', + 'Wait', '$state', '$filter', 'rbacUiControlService', 'Dataset', 'i18n', + function($scope, $rootScope, $stateParams, + Rest, Alert, UserList, Prompt, ClearScope, ProcessErrors, GetBasePath, + Wait, $state, $filter, rbacUiControlService, Dataset, i18n) { + + for (var i = 0; i < user_type_options.length; i++) { + user_type_options[i].label = i18n._(user_type_options[i].label); + } + + ClearScope(); + + var list = UserList, + defaultUrl = GetBasePath('users'); + + init(); + + function init() { + $scope.canAdd = false; + + rbacUiControlService.canAdd('users') + .then(function(canAdd) { + $scope.canAdd = canAdd; + }); + + // search init + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + + + $rootScope.flashMessage = null; + $scope.selected = []; + } + + $scope.addUser = function() { + $state.go('users.add'); + }; + + $scope.editUser = function(id) { + $state.go('users.edit', { user_id: id }); + }; + + $scope.deleteUser = function(id, name) { + + var action = function() { + $('#prompt-modal').modal('hide'); + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.destroy() + .success(function() { + if (parseInt($state.params.user_id) === id) { + $state.go('^', null, { reload: true }); + } else { + $state.go('.', null, { reload: true }); + } + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: i18n._('Error!'), + msg: i18n.sprintf(i18n._('Call to %s failed. DELETE returned status: '), url) + status + }); + }); + }; + + Prompt({ + hdr: i18n._('Delete'), + body: '
' + i18n._('Are you sure you want to delete the user below?') + '
' + $filter('sanitize')(name) + '
', + action: action, + actionText: i18n._('DELETE') + }); + }; + } +]; diff --git a/awx/ui/client/src/users/main.js b/awx/ui/client/src/users/main.js new file mode 100644 index 0000000000..1d978a091b --- /dev/null +++ b/awx/ui/client/src/users/main.js @@ -0,0 +1,47 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import UsersList from './list/users-list.controller'; +import UsersAdd from './add/users-add.controller'; +import UsersEdit from './edit/users-edit.controller'; +import { N_ } from '../i18n'; + +export default +angular.module('Users', []) + .controller('UsersList', UsersList) + .controller('UsersAdd', UsersAdd) + .controller('UsersEdit', UsersEdit) + .config(['$stateProvider', 'stateDefinitionsProvider', + function($stateProvider, stateDefinitionsProvider) { + let stateDefinitions = stateDefinitionsProvider.$get(); + + // lazily generate a tree of substates which will replace this node in ui-router's stateRegistry + // see: stateDefinition.factory for usage documentation + $stateProvider.state({ + name: 'users', + url: '/users', + lazyLoad: () => stateDefinitions.generateTree({ + parent: 'users', + modes: ['add', 'edit'], + list: 'UserList', + form: 'UserForm', + controllers: { + list: UsersList, + add: UsersAdd, + edit: UsersEdit + }, + data: { + activityStream: true, + activityStreamTarget: 'user' + }, + ncyBreadcrumb: { + parent: 'setup', + label: N_('USERS') + } + }) + }); + } + ]); diff --git a/awx/ui/client/src/widgets/InventorySyncStatus.js b/awx/ui/client/src/widgets/InventorySyncStatus.js deleted file mode 100644 index 75438f03de..0000000000 --- a/awx/ui/client/src/widgets/InventorySyncStatus.js +++ /dev/null @@ -1,115 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name widgets.function:InventorySyncStatus - * @description - * InventorySyncStatus.js - * - * Dashboard widget showing object counts and license availability. - * - */ - - - -angular.module('InventorySyncStatusWidget', ['RestServices', 'Utilities']) - .factory('InventorySyncStatus', ['$rootScope', '$compile', function ($rootScope, $compile) { - return function (params) { - - var scope = params.scope, - target = params.target, - dashboard = params.dashboard, - html, group_total, group_fail, element, src; - - html = "
\n"; - html += "
Inventory Sync Status
\n"; - html += "
\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - - function makeRow(params) { - var label = params.label, - count = params.count, - fail = params.fail, - link = params.link, - fail_link = params.fail_link, - html = "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - return html; - } - - html += makeRow({ - label: 'Inventories', - count: (dashboard.inventories && dashboard.inventories.total_with_inventory_source) ? - dashboard.inventories.total_with_inventory_source : 0, - fail: (dashboard.inventories && dashboard.inventories.inventory_failed) ? dashboard.inventories.inventory_failed : 0, - link: '/#/inventories/?has_inventory_sources=true', - fail_link: '/#/inventories/?inventory_sources_with_failures=true' - }); - - group_total = 0; - group_fail = 0; - if (dashboard.inventory_sources) { - for (src in dashboard.inventory_sources) { - group_total += (dashboard.inventory_sources[src].total) ? dashboard.inventory_sources[src].total : 0; - group_fail += (dashboard.inventory_sources[src].failed) ? dashboard.inventory_sources[src].failed : 0; - } - } - - html += makeRow({ - label: 'Groups', - count: group_total, - fail: group_fail, - link: '/#/home/groups/?has_external_source=true', - fail_link: '/#/home/groups/?status=failed' - }); - - // Each inventory source - for (src in dashboard.inventory_sources) { - if (dashboard.inventory_sources[src].total) { - html += makeRow({ - label: dashboard.inventory_sources[src].label, - count: (dashboard.inventory_sources[src].total) ? dashboard.inventory_sources[src].total : 0, - fail: (dashboard.inventory_sources[src].failed) ? dashboard.inventory_sources[src].failed : 0, - link: '/#/home/groups/?source=' + src, - fail_link: '/#/home/groups/?status=failed&source=' + src - }); - } - } - - html += "\n"; - html += "
FailedTotal
0) ? 'failed-column' : 'zero-column'; - html += " text-right\">"; - html += "" + fail + ""; - html += ""; - html += "" + count + ""; - html += "
\n"; - html += "
\n"; - html += "
\n"; - html += "
\n"; - - element = angular.element(document.getElementById(target)); - element.html(html); - $compile(element)(scope); - scope.$emit('WidgetLoaded'); - - }; - } -]); diff --git a/awx/ui/client/src/widgets/JobStatus.js b/awx/ui/client/src/widgets/JobStatus.js deleted file mode 100644 index 2ec89936a3..0000000000 --- a/awx/ui/client/src/widgets/JobStatus.js +++ /dev/null @@ -1,107 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name widgets.function:JobStatus - * @description -* JobStatus.js -* -* Dashboard widget showing object counts and license availability. -* -*/ - - - -angular.module('JobStatusWidget', ['RestServices', 'Utilities']) - .factory('JobStatus', ['$rootScope', '$compile', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', - function ($rootScope, $compile) { - return function (params) { - - var scope = params.scope, - target = params.target, - dashboard = params.dashboard, - html = '', element; - - html = "
\n"; - html += "
Job Status
\n"; - html += "
\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - - function makeRow(params) { - var html = '', - label = params.label, - link = params.link, - fail_link = params.fail_link, - count = params.count, - fail = params.fail; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - return html; - } - - html += makeRow({ - label: 'Jobs', - link: '/#/jobs', - count: (dashboard.jobs && dashboard.jobs.total) ? dashboard.jobs.total : 0, - fail: (dashboard.jobs && dashboard.jobs.failed) ? dashboard.jobs.failed : 0, - fail_link: '/#/jobs/?status=failed' - }); - html += makeRow({ - label: 'Inventories', - link: '/#/inventories', - count: (dashboard.inventories && dashboard.inventories.total) ? dashboard.inventories.total : 0, - fail: (dashboard.inventories && dashboard.inventories.job_failed) ? dashboard.inventories.job_failed : 0, - fail_link: '/#/inventories/?has_active_failures=true' - }); - html += makeRow({ - label: 'Groups', - link: '/#/home/groups', - count: (dashboard.groups && dashboard.groups.total) ? dashboard.groups.total : 0, - fail: (dashboard.groups && dashboard.groups.job_failed) ? dashboard.groups.job_failed : 0, - fail_link: '/#/home/groups/?has_active_failures=true' - }); - html += makeRow({ - label: 'Hosts', - link: '/#/home/hosts', - count: (dashboard.hosts && dashboard.hosts.total) ? dashboard.hosts.total : 0, - fail: (dashboard.hosts && dashboard.hosts.failed) ? dashboard.hosts.failed : 0, - fail_link: '/#/home/hosts/?has_active_failures=true' - }); - - html += "\n"; - html += "
FailedTotal
0) ? 'failed-column' : 'zero-column'; - html += " text-right\">"; - html += "" + fail + ""; - html += ""; - html += "" + count + ""; - html += "
\n"; - html += "
\n"; - html += "
\n"; - html += "\n"; - - element = angular.element(document.getElementById(target)); - element.html(html); - $compile(element)(scope); - scope.$emit('WidgetLoaded'); - - }; - } - ]); diff --git a/awx/ui/client/src/widgets/ObjectCount.js b/awx/ui/client/src/widgets/ObjectCount.js deleted file mode 100644 index 84ddd6fb80..0000000000 --- a/awx/ui/client/src/widgets/ObjectCount.js +++ /dev/null @@ -1,78 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name widgets.function:ObjectCount - * @description - * ObjectCount.js - * - * Dashboard widget showing object counts and license availability. - * - */ - - - -angular.module('ObjectCountWidget', ['RestServices', 'Utilities']) - .factory('ObjectCount', ['$rootScope', '$compile', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', - function ($rootScope, $compile) { - return function (params) { - - var scope = params.scope, - target = params.target, - dashboard = params.dashboard, - keys = ['organizations', 'users', 'teams', 'credentials', 'projects', 'inventories', 'groups', 'hosts', - 'job_templates', 'jobs' - ], - i, html, element; - - html = "
\n"; - html += "
System Summary
\n"; - html += "
\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - - function makeRow(params) { - var html = '', - label = params.label, - link = params.link, - count = params.count; - html += "\n"; - html += "\n"; - html += "\n"; - return html; - } - - for (i = 0; i < keys.length; i++) { - html += makeRow({ - label: keys[i], - link: '/#/' + ((keys[i] === 'hosts' || keys[i] === 'groups') ? 'home/' + keys[i] : keys[i]), - count: (dashboard[keys[i]] && dashboard[keys[i]].total) ? dashboard[keys[i]].total : 0 - }); - } - - html += "\n"; - html += "
Total
"; - html += "" + count + ""; - html += "
\n"; - html += "
\n"; - html += "
\n"; - element = angular.element(document.getElementById(target)); - element.html(html); - $compile(element)(scope); - scope.$emit('WidgetLoaded'); - }; - } - ]); diff --git a/awx/ui/client/src/widgets/SCMSyncStatus.js b/awx/ui/client/src/widgets/SCMSyncStatus.js deleted file mode 100644 index b4d452f06a..0000000000 --- a/awx/ui/client/src/widgets/SCMSyncStatus.js +++ /dev/null @@ -1,108 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name widgets.function:SCMSyncStatus - * @description - * SCMSyncStatus.js - * - * Dashboard widget showing object counts and license availability. - * - */ - - -angular.module('SCMSyncStatusWidget', ['RestServices', 'Utilities']) - .factory('SCMSyncStatus', ['$rootScope', '$compile', - function ($rootScope, $compile) { - return function (params) { - - var scope = params.scope, - target = params.target, - dashboard = params.dashboard, - i, html, total_count, element, type, labelList; - - html = "
\n"; - html += "
Project SCM Status
\n"; - html += "
\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - - function makeRow(params) { - var html = '', - label = params.label, - link = params.link, - fail_link = params.fail_link, - count = params.count, - fail = params.fail; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - return html; - } - - total_count = 0; - if (dashboard.scm_types) { - for (type in dashboard.scm_types) { - total_count += (dashboard.scm_types[type].total) ? dashboard.scm_types[type].total : 0; - } - } - - html += makeRow({ - label: 'Projects', - link: '/#/projects', - count: total_count, - fail: (dashboard.projects && dashboard.projects.failed) ? dashboard.projects.failed : 0, - fail_link: '/#/projects/?status=failed' - }); - - labelList = []; - for (type in dashboard.scm_types) { - labelList.push(type); - } - labelList.sort(); - for (i = 0; i < labelList.length; i++) { - type = labelList[i]; - if (dashboard.scm_types[type].total) { - html += makeRow({ - label: dashboard.scm_types[type].label, - link: '/#/projects/?scm_type=' + type, - count: (dashboard.scm_types[type].total) ? dashboard.scm_types[type].total : 0, - fail: (dashboard.scm_types[type].failed) ? dashboard.scm_types[type].failed : 0, - fail_link: '/#/projects/?scm_type=' + type + '&status=failed' - }); - } - } - - html += "\n"; - html += "
FailedTotal
" + label + " 0) ? 'failed-column' : 'zero-column'; - html += " text-right\">"; - html += "" + fail + ""; - html += ""; - html += "" + count + ""; - html += "
\n"; - html += "
\n"; - html += "
\n"; - html += "\n"; - - element = angular.element(document.getElementById(target)); - element.html(html); - $compile(element)(scope); - scope.$emit('WidgetLoaded'); - - }; - } - ]); diff --git a/awx/ui/client/src/widgets/Stream.js b/awx/ui/client/src/widgets/Stream.js deleted file mode 100644 index 64315e7953..0000000000 --- a/awx/ui/client/src/widgets/Stream.js +++ /dev/null @@ -1,317 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name widgets.function:Stream - * @description - * Stream.js - * - * Activity stream widget that can be called from anywhere - * - */ - -import listGenerator from '../shared/list-generator/main'; - - -angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefinition', listGenerator.name, 'StreamWidget', -]) - -.factory('BuildAnchor', [ '$log', '$filter', - // Returns a full
resource_name HTML string if link can be derived from supplied context - // returns name of resource if activity stream object doesn't contain enough data to build a UI url - // arguments are: a summary_field object, a resource type, an activity stream object - function ($log, $filter) { - return function (obj, resource, activity) { - var url = '/#/'; - // try/except pattern asserts that: - // if we encounter a case where a UI url can't or shouldn't be generated, just supply the name of the resource - try { - // catch-all case to avoid generating urls if a resource has been deleted - // if a resource still exists, it'll be serialized in the activity's summary_fields - if (!activity.summary_fields[resource]){ - throw {name : 'ResourceDeleted', message: 'The referenced resource no longer exists'}; - } - switch (resource) { - case 'custom_inventory_script': - url += 'inventory_scripts/' + obj.id + '/'; - break; - case 'group': - if (activity.operation === 'create' || activity.operation === 'delete'){ - // the API formats the changes.inventory field as str 'myInventoryName-PrimaryKey' - var inventory_id = _.last(activity.changes.inventory.split('-')); - url += 'inventories/' + inventory_id + '/manage?group=' + activity.changes.id; - } - else { - url += 'inventories/' + activity.summary_fields.inventory[0].id + '/manage?group=' + (activity.changes.id || activity.changes.object1_pk); - } - break; - case 'host': - url += 'home/hosts/' + obj.id; - break; - case 'job': - url += 'jobs/' + obj.id; - break; - case 'inventory': - url += 'inventories/' + obj.id + '/'; - break; - case 'schedule': - // schedule urls depend on the resource they're associated with - if (activity.summary_fields.job_template){ - url += 'job_templates/' + activity.summary_fields.job_template.id + '/schedules/' + obj.id; - } - else if (activity.summary_fields.project){ - url += 'projects/' + activity.summary_fields.project.id + '/schedules/' + obj.id; - } - else if (activity.summary_fields.system_job_template){ - url += 'management_jobs/' + activity.summary_fields.system_job_template.id + '/schedules/edit/' + obj.id; - } - // urls for inventory sync schedules currently depend on having an inventory id and group id - else { - throw {name : 'NotImplementedError', message : 'activity.summary_fields to build this url not implemented yet'}; - } - break; - case 'notification_template': - url += `notification_templates/${obj.id}`; - break; - case 'role': - throw {name : 'NotImplementedError', message : 'role object management is not consolidated to a single UI view'}; - case 'job_template': - url += `templates/job_template/${obj.id}`; - break; - case 'workflow_job_template': - url += `templates/workflow_job_template/${obj.id}`; - break; - default: - url += resource + 's/' + obj.id + '/'; - } - return ' ' + $filter('sanitize')(obj.name || obj.username) + ' '; - } - catch(err){ - $log.debug(err); - return ' ' + $filter('sanitize')(obj.name || obj.username || '') + ' '; - } - }; - } -]) - -.factory('BuildDescription', ['BuildAnchor', '$log', 'i18n', - function (BuildAnchor, $log, i18n) { - return function (activity) { - - var pastTense = function(operation){ - return (/e$/.test(activity.operation)) ? operation + 'd ' : operation + 'ed '; - }; - // convenience method to see if dis+association operation involves 2 groups - // the group cases are slightly different because groups can be dis+associated into each other - var isGroupRelationship = function(activity){ - return activity.object1 === 'group' && activity.object2 === 'group' && activity.summary_fields.group.length > 1; - }; - - // Activity stream objects will outlive the resources they reference - // in that case, summary_fields will not be available - show generic error text instead - try { - activity.description = pastTense(activity.operation); - switch(activity.object_association){ - // explicit role dis+associations - case 'role': - // object1 field is resource targeted by the dis+association - // object2 field is the resource the role is inherited from - // summary_field.role[0] contains ref info about the role - switch(activity.operation){ - // expected outcome: "disassociated role_name from " - case 'disassociate': - if (isGroupRelationship(activity)){ - activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' from ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); - } - else{ - activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - } - break; - // expected outcome: "associated role_name to " - case 'associate': - if (isGroupRelationship(activity)){ - activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' to ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); - } - else{ - activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - } - break; - } - break; - // inherited role dis+associations (logic identical to case 'role') - case 'parents': - // object1 field is resource targeted by the dis+association - // object2 field is the resource the role is inherited from - // summary_field.role[0] contains ref info about the role - switch(activity.operation){ - // expected outcome: "disassociated role_name from " - case 'disassociate': - if (isGroupRelationship(activity)){ - activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + - 'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); - } - else{ - activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - } - break; - // expected outcome: "associated role_name to " - case 'associate': - if (isGroupRelationship(activity)){ - activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) + - 'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity); - } - else{ - activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - } - break; - } - break; - // CRUD operations / resource on resource dis+associations - default: - switch(activity.operation){ - // expected outcome: "disassociated from " - case 'disassociate' : - if (isGroupRelationship(activity)){ - activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + - 'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); - } - else { - activity.description += activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + - 'from ' + activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - } - break; - // expected outcome "associated to " - case 'associate': - // groups are the only resource that can be associated/disassociated into each other - if (isGroupRelationship(activity)){ - activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) + - 'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity); - } - else { - activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity) + - 'to ' + activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity); - } - break; - case 'delete': - activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity); - break; - // expected outcome: "operation " - case 'update': - activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - break; - case 'create': - activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity); - break; - } - break; - } - } - catch(err){ - $log.debug(err); - activity.description = i18n._('Event summary not available'); - } - }; - } -]) - -.factory('ShowDetail', ['$filter', '$rootScope', 'Rest', 'Alert', 'GenerateForm', 'ProcessErrors', 'GetBasePath', 'FormatDate', - 'ActivityDetailForm', 'Empty', 'Find', - function ($filter, $rootScope, Rest, Alert, GenerateForm, ProcessErrors, GetBasePath, FormatDate, ActivityDetailForm, Empty, Find) { - return function (params, scope) { - - var activity_id = params.activity_id, - activity = Find({ list: params.scope.activities, key: 'id', val: activity_id }), - element; - - if (activity) { - - // Grab our element out of the dom - element = angular.element(document.getElementById('stream-detail-modal')); - - // Grab the modal's scope so that we can set a few variables - scope = element.scope(); - - scope.changes = activity.changes; - scope.user = ((activity.summary_fields.actor) ? activity.summary_fields.actor.username : 'system') + - ' on ' + $filter('longDate')(activity.timestamp); - scope.operation = activity.description; - scope.header = "Event " + activity.id; - - // Open the modal - $('#stream-detail-modal').modal({ - show: true, - backdrop: 'static', - keyboard: true - }); - - if (!scope.$$phase) { - scope.$digest(); - } - } - }; - } -]) - -.factory('Stream', ['$rootScope', '$location', '$state', 'Rest', 'GetBasePath', - 'ProcessErrors', 'Wait', 'StreamList', 'generateList', 'FormatDate', 'BuildDescription', - 'ShowDetail', - function ($rootScope, $location, $state, Rest, GetBasePath, ProcessErrors, - Wait, StreamList, GenerateList, FormatDate, - BuildDescription, ShowDetail) { - return function (params) { - - var scope = params.scope; - - $rootScope.flashMessage = null; - - // descriptive title describing what AS is showing - scope.streamTitle = (params && params.title) ? params.title : null; - - scope.refreshStream = function () { - $state.go('.', null, {reload: true}); - }; - - scope.showDetail = function (id) { - ShowDetail({ - scope: scope, - activity_id: id - }); - }; - - if(scope.activities && scope.activities.length > 0) { - buildUserAndDescription(); - } - - scope.$watch('activities', function(){ - // Watch for future update to scope.activities (like page change, column sort, search, etc) - buildUserAndDescription(); - }); - - function buildUserAndDescription(){ - scope.activities.forEach(function(activity, i) { - // build activity.user - if (scope.activities[i].summary_fields.actor) { - scope.activities[i].user = "" + - scope.activities[i].summary_fields.actor.username + ""; - } else { - scope.activities[i].user = 'system'; - } - // build description column / action text - BuildDescription(scope.activities[i]); - - }); - } - }; - } -]); diff --git a/awx/ui/tests/spec/shared/filters/append.filter-test.js b/awx/ui/tests/spec/shared/filters/append.filter-test.js new file mode 100644 index 0000000000..f8a1ddcd47 --- /dev/null +++ b/awx/ui/tests/spec/shared/filters/append.filter-test.js @@ -0,0 +1,25 @@ +'use strict'; + +describe('Filter: append', () => { + var filter; + + beforeEach(angular.mock.module('Tower')); + + beforeEach(function(){ + inject(function($injector){ + filter = $injector.get('$filter')('append'); + }); + }); + + it('should append the two parameters passed', function(){ + expect(filter("foo", "bar")).toBe("foobar"); + }); + + it('should return string if no append param passed', function(){ + expect(filter("foo")).toBe("foo"); + }); + + it('should return empty string if no params passed', function(){ + expect(filter()).toBe(""); + }); +}); diff --git a/awx/ui/tests/spec/shared/filters/capitalize.filter-test.js b/awx/ui/tests/spec/shared/filters/capitalize.filter-test.js new file mode 100644 index 0000000000..d0f939c480 --- /dev/null +++ b/awx/ui/tests/spec/shared/filters/capitalize.filter-test.js @@ -0,0 +1,20 @@ +'use strict'; + +describe('Filter: capitalize', () => { + var filter; + + beforeEach(angular.mock.module('Tower')); + + beforeEach(function(){ + inject(function($injector){ + filter = $injector.get('$filter')('capitalize'); + }); + }); + + it('should capitalize the first letter', function(){ + expect(filter("foo")).toBe("Foo"); + expect(filter("Foo")).toBe("Foo"); + expect(filter("FOO")).toBe("Foo"); + expect(filter("FoO")).toBe("Foo"); + }); +}); diff --git a/awx/ui/tests/spec/shared/filters/format-epoch.filter-test.js b/awx/ui/tests/spec/shared/filters/format-epoch.filter-test.js new file mode 100644 index 0000000000..6bc23aa51b --- /dev/null +++ b/awx/ui/tests/spec/shared/filters/format-epoch.filter-test.js @@ -0,0 +1,18 @@ +'use strict'; + +describe('Filter: formatEpoch', () => { + var filter; + + beforeEach(angular.mock.module('Tower')); + + beforeEach(function(){ + inject(function($injector){ + filter = $injector.get('$filter')('formatEpoch'); + }); + }); + + it('should convert epoch to datetime string', function(){ + expect(filter(11111)).toBe("Dec 31, 1969 10:05 PM"); + expect(filter(610430400)).toBe("May 6, 1989 12:00 AM"); + }); +}); diff --git a/awx/ui/tests/spec/shared/filters/is-empty.filter-test.js b/awx/ui/tests/spec/shared/filters/is-empty.filter-test.js new file mode 100644 index 0000000000..bbb156bf48 --- /dev/null +++ b/awx/ui/tests/spec/shared/filters/is-empty.filter-test.js @@ -0,0 +1,18 @@ +'use strict'; + +describe('Filter: isEmpty', () => { + var filter; + + beforeEach(angular.mock.module('Tower')); + + beforeEach(function(){ + inject(function($injector){ + filter = $injector.get('$filter')('isEmpty'); + }); + }); + + it('check if an object is empty', function(){ + expect(filter({})).toBe(true); + expect(filter({foo: 'bar'})).toBe(false); + }); +}); diff --git a/awx/ui/tests/spec/shared/filters/long-date.filter-test.js b/awx/ui/tests/spec/shared/filters/long-date.filter-test.js new file mode 100644 index 0000000000..579962492c --- /dev/null +++ b/awx/ui/tests/spec/shared/filters/long-date.filter-test.js @@ -0,0 +1,20 @@ +'use strict'; + +describe('Filter: longDate', () => { + var filter; + + beforeEach(angular.mock.module('Tower')); + + beforeEach(function(){ + inject(function($injector){ + filter = $injector.get('$filter')('longDate'); + }); + }); + + it('should convert the timestamp to a UI friendly date and time', function(){ + expect(filter("2017-02-13T22:00:14.106Z")).toBe("2/13/2017 5:00:14 PM"); + }); + it('should return an empty string if no timestamp is passed', function(){ + expect(filter()).toBe(""); + }); +}); diff --git a/awx/ui/tests/spec/shared/filters/prepend.filter-test.js b/awx/ui/tests/spec/shared/filters/prepend.filter-test.js new file mode 100644 index 0000000000..f22c077ecd --- /dev/null +++ b/awx/ui/tests/spec/shared/filters/prepend.filter-test.js @@ -0,0 +1,25 @@ +'use strict'; + +describe('Filter: prepend', () => { + var filter; + + beforeEach(angular.mock.module('Tower')); + + beforeEach(function(){ + inject(function($injector){ + filter = $injector.get('$filter')('prepend'); + }); + }); + + it('should prepend the second param to the first', function(){ + expect(filter("foo", "bar")).toBe("barfoo"); + }); + + it('should return string if no prepend param passed', function(){ + expect(filter("foo")).toBe("foo"); + }); + + it('should return empty string if no params passed', function(){ + expect(filter()).toBe(""); + }); +}); diff --git a/awx/ui/tests/spec/shared/filters/xss-sanitizer.filter-test.js b/awx/ui/tests/spec/shared/filters/xss-sanitizer.filter-test.js new file mode 100644 index 0000000000..88cb3458e1 --- /dev/null +++ b/awx/ui/tests/spec/shared/filters/xss-sanitizer.filter-test.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Filter: sanitize', () => { + var filter; + + beforeEach(angular.mock.module('Tower')); + + beforeEach(function(){ + inject(function($injector){ + filter = $injector.get('$filter')('sanitize'); + }); + }); + + it('should sanitize xss-vulnerable strings', function(){ + expect(filter("
foobar
")).toBe("<div>foobar</div>"); + }); +}); diff --git a/awx/ui/tests/spec/workflow--results/workflow-results.controller-test.js b/awx/ui/tests/spec/workflow--results/workflow-results.controller-test.js index 90bc6f582f..91616b8a03 100644 --- a/awx/ui/tests/spec/workflow--results/workflow-results.controller-test.js +++ b/awx/ui/tests/spec/workflow--results/workflow-results.controller-test.js @@ -7,7 +7,6 @@ describe('Controller: workflowResults', () => { let $controller; let workflowResults; let $rootScope; - let ParseVariableString; let workflowResultsService; let $interval; @@ -17,10 +16,8 @@ describe('Controller: workflowResults', () => { } }; - beforeEach(angular.mock.module('VariablesHelper')); - beforeEach(angular.mock.module('workflowResults', ($provide) => { - ['PromptDialog', 'Prompt', 'Wait', 'Rest', '$state', 'ProcessErrors', + ['PromptDialog', 'Prompt', 'Wait', 'Rest', '$state', 'ProcessErrors', 'InitiatePlaybookRun', 'jobLabels', 'workflowNodes', 'count', ].forEach((item) => { $provide.value(item, {}); @@ -30,8 +27,9 @@ describe('Controller: workflowResults', () => { $provide.value('workflowData', workflow_job_json); $provide.value('workflowDataOptions', workflow_job_options_json); $provide.value('ParseTypeChange', function() {}); + $provide.value('ParseVariableString', function() {}); $provide.value('i18n', { '_': (a) => { return a; } }); - $provide.provider('$stateProvider', { '$get': function() { return function() {} } }); + $provide.provider('$stateProvider', { '$get': function() { return function() {}; } }); $provide.service('WorkflowService', function($q) { return { buildTree: function() { @@ -39,14 +37,13 @@ describe('Controller: workflowResults', () => { deferred.resolve(treeData); return deferred.promise; } - } + }; }); })); - beforeEach(angular.mock.inject(function(_$controller_, _$rootScope_, _ParseVariableString_, _workflowResultsService_, _$interval_){ + beforeEach(angular.mock.inject(function(_$controller_, _$rootScope_, _workflowResultsService_, _$interval_){ $controller = _$controller_; $rootScope = _$rootScope_; - ParseVariableString = _ParseVariableString_; workflowResultsService = _workflowResultsService_; $interval = _$interval_; @@ -102,7 +99,7 @@ describe('Controller: workflowResults', () => { describe('job waiting', () => { beforeEach(() => { jobWaitingWorkflowResultsControllerFixture(null, 'waiting'); - }); + }); it('should not start elapsed timer', () => { expect(workflowResultsService.createOneSecondTimer).not.toHaveBeenCalled(); @@ -133,4 +130,4 @@ describe('Controller: workflowResults', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000000..831959c46f --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,5 @@ +3.2.0 +===== +* added a new API endpoint - `/api/v1/settings/logging/test/` - for testing + external log aggregrator connectivity + [[#5164](https://github.com/ansible/ansible-tower/issues/5164)] diff --git a/tools/docker-compose/unit-tests/Dockerfile b/tools/docker-compose/unit-tests/Dockerfile index d1fec4e583..2de3715f7b 100644 --- a/tools/docker-compose/unit-tests/Dockerfile +++ b/tools/docker-compose/unit-tests/Dockerfile @@ -1,26 +1,11 @@ FROM gcr.io/ansible-tower-engineering/tower_devel:latest +# For UI tests RUN yum install -y bzip2 gcc-c++ -# We need to install dependencies somewhere other than /ansible-tower. -# Anything in /ansible-tower will be overwritten by the bind-mount. -# We switch the WORKDIR to /ansible-tower further down. -WORKDIR "/tmp/" - -COPY awx/ui/package.json awx/ui/ - RUN npm set progress=false -# Copy __init__.py so the Makefile can retrieve `awx.__version__` -COPY awx/__init__.py awx/ -RUN make ui-deps - WORKDIR "/tower_devel" -# This entrypoint script takes care of moving the node_modules -# into the bind-mount, then exec's to whatever was passed as the CMD. -ADD tools/docker-compose/unit-tests/entrypoint /usr/bin/ -RUN chmod +x /usr/bin/entrypoint - -ENTRYPOINT ["entrypoint"] +ENTRYPOINT ["/bin/bash", "-c"] CMD ["bash"] diff --git a/tools/docker-compose/unit-tests/entrypoint b/tools/docker-compose/unit-tests/entrypoint deleted file mode 100644 index cb7fbe142e..0000000000 --- a/tools/docker-compose/unit-tests/entrypoint +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -e - -mkdir -p /tower_devel/awx/ui/ -mv -n /tmp/awx/ui/node_modules /tmp/awx/ui/.deps_built /tower_devel/awx/ui - -exec $@