mirror of
https://github.com/ansible/awx.git
synced 2026-05-20 07:17:40 -02:30
Merge remote-tracking branch 'downstream/release_3.3.0' into devel
# Conflicts: # awx/main/notifications/slack_backend.py
This commit is contained in:
1
Makefile
1
Makefile
@@ -382,6 +382,7 @@ test:
|
|||||||
. $(VENV_BASE)/awx/bin/activate; \
|
. $(VENV_BASE)/awx/bin/activate; \
|
||||||
fi; \
|
fi; \
|
||||||
py.test -n auto $(TEST_DIRS)
|
py.test -n auto $(TEST_DIRS)
|
||||||
|
awx-manage check_migrations --dry-run --check -n 'vNNN_missing_migration_file'
|
||||||
|
|
||||||
test_combined: test_ansible test
|
test_combined: test_ansible test
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class DeprecatedCredentialField(serializers.IntegerField):
|
|||||||
kwargs['allow_null'] = True
|
kwargs['allow_null'] = True
|
||||||
kwargs['default'] = None
|
kwargs['default'] = None
|
||||||
kwargs['min_value'] = 1
|
kwargs['min_value'] = 1
|
||||||
kwargs['help_text'] = 'This resource has been deprecated and will be removed in a future release'
|
kwargs.setdefault('help_text', 'This resource has been deprecated and will be removed in a future release')
|
||||||
super(DeprecatedCredentialField, self).__init__(**kwargs)
|
super(DeprecatedCredentialField, self).__init__(**kwargs)
|
||||||
|
|
||||||
def to_internal_value(self, pk):
|
def to_internal_value(self, pk):
|
||||||
|
|||||||
@@ -390,7 +390,6 @@ class GenericAPIView(generics.GenericAPIView, APIView):
|
|||||||
]:
|
]:
|
||||||
d[key] = self.metadata_class().get_serializer_info(serializer, method=method)
|
d[key] = self.metadata_class().get_serializer_info(serializer, method=method)
|
||||||
d['settings'] = settings
|
d['settings'] = settings
|
||||||
d['has_named_url'] = self.model in settings.NAMED_URL_GRAPH
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1903,7 +1903,9 @@ class CustomInventoryScriptSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class InventorySourceOptionsSerializer(BaseSerializer):
|
class InventorySourceOptionsSerializer(BaseSerializer):
|
||||||
credential = DeprecatedCredentialField()
|
credential = DeprecatedCredentialField(
|
||||||
|
help_text=_('Cloud credential to use for inventory updates.')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential',
|
fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential',
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ within all designated text fields of a model.
|
|||||||
|
|
||||||
?search=findme
|
?search=findme
|
||||||
|
|
||||||
_Added in AWX 1.4_
|
|
||||||
|
|
||||||
(_Added in Ansible Tower 3.1.0_) Search across related fields:
|
(_Added in Ansible Tower 3.1.0_) Search across related fields:
|
||||||
|
|
||||||
?related__search=findme
|
?related__search=findme
|
||||||
@@ -84,7 +82,7 @@ To exclude results matching certain criteria, prefix the field parameter with
|
|||||||
|
|
||||||
?not__field=value
|
?not__field=value
|
||||||
|
|
||||||
(_Added in AWX 1.4_) By default, all query string filters are AND'ed together, so
|
By default, all query string filters are AND'ed together, so
|
||||||
only the results matching *all* filters will be returned. To combine results
|
only the results matching *all* filters will be returned. To combine results
|
||||||
matching *any* one of multiple criteria, prefix each query string parameter
|
matching *any* one of multiple criteria, prefix each query string parameter
|
||||||
with `or__`:
|
with `or__`:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ object containing groups, including the hosts, children and variables for each
|
|||||||
group. The response data is equivalent to that returned by passing the
|
group. The response data is equivalent to that returned by passing the
|
||||||
`--list` argument to an inventory script.
|
`--list` argument to an inventory script.
|
||||||
|
|
||||||
_(Added in AWX 1.3)_ Specify a query string of `?hostvars=1` to retrieve the JSON
|
Specify a query string of `?hostvars=1` to retrieve the JSON
|
||||||
object above including all host variables. The `['_meta']['hostvars']` object
|
object above including all host variables. The `['_meta']['hostvars']` object
|
||||||
in the response contains an entry for each host with its variables. This
|
in the response contains an entry for each host with its variables. This
|
||||||
response format can be used with Ansible 1.3 and later to avoid making a
|
response format can be used with Ansible 1.3 and later to avoid making a
|
||||||
@@ -18,11 +18,16 @@ separate API request for each host. Refer to
|
|||||||
[Tuning the External Inventory Script](http://docs.ansible.com/developing_inventory.html#tuning-the-external-inventory-script)
|
[Tuning the External Inventory Script](http://docs.ansible.com/developing_inventory.html#tuning-the-external-inventory-script)
|
||||||
for more information on this feature.
|
for more information on this feature.
|
||||||
|
|
||||||
_(Added in AWX 1.4)_ By default, the inventory script will only return hosts that
|
By default, the inventory script will only return hosts that
|
||||||
are enabled in the inventory. This feature allows disabled hosts to be skipped
|
are enabled in the inventory. This feature allows disabled hosts to be skipped
|
||||||
when running jobs without removing them from the inventory. Specify a query
|
when running jobs without removing them from the inventory. Specify a query
|
||||||
string of `?all=1` to return all hosts, including disabled ones.
|
string of `?all=1` to return all hosts, including disabled ones.
|
||||||
|
|
||||||
|
Specify a query string of `?towervars=1` to add variables
|
||||||
|
to the hostvars of each host that specifies its enabled state and database ID.
|
||||||
|
|
||||||
|
To apply multiple query strings, join them with the `&` character, like `?hostvars=1&all=1`.
|
||||||
|
|
||||||
## Host Response
|
## Host Response
|
||||||
|
|
||||||
Make a GET request to this resource with a query string similar to
|
Make a GET request to this resource with a query string similar to
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
{% if has_named_url %}
|
|
||||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||||
|
|
||||||
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
|
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
{% if has_named_url %}
|
|
||||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% ifmeth GET %}
|
{% ifmeth GET %}
|
||||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
{% if has_named_url %}
|
|
||||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% ifmeth GET %}
|
{% ifmeth GET %}
|
||||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
{% if has_named_url %}
|
|
||||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% ifmeth GET %}
|
{% ifmeth GET %}
|
||||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||||
|
|
||||||
|
|||||||
@@ -115,9 +115,10 @@ class ActivityStreamEnforcementMixin(object):
|
|||||||
Mixin to check that license supports activity streams.
|
Mixin to check that license supports activity streams.
|
||||||
'''
|
'''
|
||||||
def check_permissions(self, request):
|
def check_permissions(self, request):
|
||||||
|
ret = super(ActivityStreamEnforcementMixin, self).check_permissions(request)
|
||||||
if not feature_enabled('activity_streams'):
|
if not feature_enabled('activity_streams'):
|
||||||
raise LicenseForbids(_('Your license does not allow use of the activity stream.'))
|
raise LicenseForbids(_('Your license does not allow use of the activity stream.'))
|
||||||
return super(ActivityStreamEnforcementMixin, self).check_permissions(request)
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class SystemTrackingEnforcementMixin(object):
|
class SystemTrackingEnforcementMixin(object):
|
||||||
@@ -125,9 +126,10 @@ class SystemTrackingEnforcementMixin(object):
|
|||||||
Mixin to check that license supports system tracking.
|
Mixin to check that license supports system tracking.
|
||||||
'''
|
'''
|
||||||
def check_permissions(self, request):
|
def check_permissions(self, request):
|
||||||
|
ret = super(SystemTrackingEnforcementMixin, self).check_permissions(request)
|
||||||
if not feature_enabled('system_tracking'):
|
if not feature_enabled('system_tracking'):
|
||||||
raise LicenseForbids(_('Your license does not permit use of system tracking.'))
|
raise LicenseForbids(_('Your license does not permit use of system tracking.'))
|
||||||
return super(SystemTrackingEnforcementMixin, self).check_permissions(request)
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class WorkflowsEnforcementMixin(object):
|
class WorkflowsEnforcementMixin(object):
|
||||||
@@ -135,9 +137,10 @@ class WorkflowsEnforcementMixin(object):
|
|||||||
Mixin to check that license supports workflows.
|
Mixin to check that license supports workflows.
|
||||||
'''
|
'''
|
||||||
def check_permissions(self, request):
|
def check_permissions(self, request):
|
||||||
|
ret = super(WorkflowsEnforcementMixin, self).check_permissions(request)
|
||||||
if not feature_enabled('workflows') and request.method not in ('GET', 'OPTIONS', 'DELETE'):
|
if not feature_enabled('workflows') and request.method not in ('GET', 'OPTIONS', 'DELETE'):
|
||||||
raise LicenseForbids(_('Your license does not allow use of workflows.'))
|
raise LicenseForbids(_('Your license does not allow use of workflows.'))
|
||||||
return super(WorkflowsEnforcementMixin, self).check_permissions(request)
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class UnifiedJobDeletionMixin(object):
|
class UnifiedJobDeletionMixin(object):
|
||||||
@@ -442,9 +445,9 @@ class ApiV1ConfigView(APIView):
|
|||||||
data.update(dict(
|
data.update(dict(
|
||||||
project_base_dir = settings.PROJECTS_ROOT,
|
project_base_dir = settings.PROJECTS_ROOT,
|
||||||
project_local_paths = Project.get_local_path_choices(),
|
project_local_paths = Project.get_local_path_choices(),
|
||||||
|
custom_virtualenvs = get_custom_venv_choices()
|
||||||
))
|
))
|
||||||
|
elif JobTemplate.accessible_objects(request.user, 'admin_role').exists():
|
||||||
if JobTemplate.accessible_objects(request.user, 'admin_role').exists():
|
|
||||||
data['custom_virtualenvs'] = get_custom_venv_choices()
|
data['custom_virtualenvs'] = get_custom_venv_choices()
|
||||||
|
|
||||||
return Response(data)
|
return Response(data)
|
||||||
@@ -2883,17 +2886,14 @@ class InventorySourceCredentialsList(SubListAttachDetachAPIView):
|
|||||||
relationship = 'credentials'
|
relationship = 'credentials'
|
||||||
|
|
||||||
def is_valid_relation(self, parent, sub, created=False):
|
def is_valid_relation(self, parent, sub, created=False):
|
||||||
|
# Inventory source credentials are exclusive with all other credentials
|
||||||
|
# subject to change for https://github.com/ansible/awx/issues/277
|
||||||
|
# or https://github.com/ansible/awx/issues/223
|
||||||
|
if parent.credentials.exists():
|
||||||
|
return {'msg': _("Source already has credential assigned.")}
|
||||||
error = InventorySource.cloud_credential_validation(parent.source, sub)
|
error = InventorySource.cloud_credential_validation(parent.source, sub)
|
||||||
if error:
|
if error:
|
||||||
return {'msg': error}
|
return {'msg': error}
|
||||||
if sub.credential_type == 'vault':
|
|
||||||
# TODO: support this
|
|
||||||
return {"msg": _("Vault credentials are not yet supported for inventory sources.")}
|
|
||||||
else:
|
|
||||||
# Cloud credentials are exclusive with all other cloud credentials
|
|
||||||
cloud_cred_qs = parent.credentials.exclude(credential_type__kind='vault')
|
|
||||||
if cloud_cred_qs.exists():
|
|
||||||
return {'msg': _("Source already has cloud credential assigned.")}
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ import os
|
|||||||
import pwd
|
import pwd
|
||||||
|
|
||||||
# PSUtil
|
# PSUtil
|
||||||
import psutil
|
try:
|
||||||
|
import psutil
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError('psutil is missing; {}bin/pip install psutil'.format(
|
||||||
|
os.environ['VIRTUAL_ENV']
|
||||||
|
))
|
||||||
|
|
||||||
__all__ = []
|
__all__ = []
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,13 @@ import os
|
|||||||
import stat
|
import stat
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
import memcache
|
|
||||||
|
try:
|
||||||
|
import memcache
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError('python-memcached is missing; {}bin/pip install python-memcached'.format(
|
||||||
|
os.environ['VIRTUAL_ENV']
|
||||||
|
))
|
||||||
|
|
||||||
from six.moves import xrange
|
from six.moves import xrange
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-08-03 19:04+0000\n"
|
"POT-Creation-Date: 2018-08-14 13:52+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -125,15 +125,15 @@ msgid ""
|
|||||||
"our REST API, the Content-Type must be application/json"
|
"our REST API, the Content-Type must be application/json"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/api/generics.py:629 awx/api/generics.py:691
|
#: awx/api/generics.py:635 awx/api/generics.py:697
|
||||||
msgid "\"id\" field must be an integer."
|
msgid "\"id\" field must be an integer."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/api/generics.py:688
|
#: awx/api/generics.py:694
|
||||||
msgid "\"id\" is required to disassociate"
|
msgid "\"id\" is required to disassociate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/api/generics.py:739
|
#: awx/api/generics.py:745
|
||||||
msgid "{} 'id' field is missing."
|
msgid "{} 'id' field is missing."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1642,86 +1642,86 @@ msgstr ""
|
|||||||
msgid "Bad data found in related field %s."
|
msgid "Bad data found in related field %s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:304
|
#: awx/main/access.py:302
|
||||||
msgid "License is missing."
|
msgid "License is missing."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:306
|
#: awx/main/access.py:304
|
||||||
msgid "License has expired."
|
msgid "License has expired."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:314
|
#: awx/main/access.py:312
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "License count of %s instances has been reached."
|
msgid "License count of %s instances has been reached."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:316
|
#: awx/main/access.py:314
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "License count of %s instances has been exceeded."
|
msgid "License count of %s instances has been exceeded."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:318
|
#: awx/main/access.py:316
|
||||||
msgid "Host count exceeds available instances."
|
msgid "Host count exceeds available instances."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:322
|
#: awx/main/access.py:320
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Feature %s is not enabled in the active license."
|
msgid "Feature %s is not enabled in the active license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:324
|
#: awx/main/access.py:322
|
||||||
msgid "Features not found in active license."
|
msgid "Features not found in active license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:837
|
#: awx/main/access.py:835
|
||||||
msgid "Unable to change inventory on a host."
|
msgid "Unable to change inventory on a host."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:854 awx/main/access.py:899
|
#: awx/main/access.py:852 awx/main/access.py:897
|
||||||
msgid "Cannot associate two items from different inventories."
|
msgid "Cannot associate two items from different inventories."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:887
|
#: awx/main/access.py:885
|
||||||
msgid "Unable to change inventory on a group."
|
msgid "Unable to change inventory on a group."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:1148
|
#: awx/main/access.py:1146
|
||||||
msgid "Unable to change organization on a team."
|
msgid "Unable to change organization on a team."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:1165
|
#: awx/main/access.py:1163
|
||||||
msgid "The {} role cannot be assigned to a team"
|
msgid "The {} role cannot be assigned to a team"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:1167
|
#: awx/main/access.py:1165
|
||||||
msgid "The admin_role for a User cannot be assigned to a team"
|
msgid "The admin_role for a User cannot be assigned to a team"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:1533 awx/main/access.py:1967
|
#: awx/main/access.py:1531 awx/main/access.py:1965
|
||||||
msgid "Job was launched with prompts provided by another user."
|
msgid "Job was launched with prompts provided by another user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:1553
|
#: awx/main/access.py:1551
|
||||||
msgid "Job has been orphaned from its job template."
|
msgid "Job has been orphaned from its job template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:1555
|
#: awx/main/access.py:1553
|
||||||
msgid "Job was launched with unknown prompted fields."
|
msgid "Job was launched with unknown prompted fields."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:1557
|
#: awx/main/access.py:1555
|
||||||
msgid "Job was launched with prompted fields."
|
msgid "Job was launched with prompted fields."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:1559
|
#: awx/main/access.py:1557
|
||||||
msgid " Organization level permissions required."
|
msgid " Organization level permissions required."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:1561
|
#: awx/main/access.py:1559
|
||||||
msgid " You do not have permission to related resources."
|
msgid " You do not have permission to related resources."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/access.py:1981
|
#: awx/main/access.py:1979
|
||||||
msgid ""
|
msgid ""
|
||||||
"You do not have permission to the workflow job resources required for "
|
"You do not have permission to the workflow job resources required for "
|
||||||
"relaunch."
|
"relaunch."
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -611,7 +611,8 @@ class OAuth2ApplicationAccess(BaseAccess):
|
|||||||
select_related = ('user',)
|
select_related = ('user',)
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(organization__in=self.user.organizations)
|
org_access_qs = Organization.accessible_objects(self.user, 'member_role')
|
||||||
|
return self.model.objects.filter(organization__in=org_access_qs)
|
||||||
|
|
||||||
def can_change(self, obj, data):
|
def can_change(self, obj, data):
|
||||||
return self.user.is_superuser or self.check_related('organization', Organization, data, obj=obj,
|
return self.user.is_superuser or self.check_related('organization', Organization, data, obj=obj,
|
||||||
|
|||||||
@@ -117,10 +117,10 @@ class IsolatedManager(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def awx_playbook_path(cls):
|
def awx_playbook_path(cls):
|
||||||
return os.path.join(
|
return os.path.abspath(os.path.join(
|
||||||
os.path.dirname(awx.__file__),
|
os.path.dirname(awx.__file__),
|
||||||
'playbooks'
|
'playbooks'
|
||||||
)
|
))
|
||||||
|
|
||||||
def path_to(self, *args):
|
def path_to(self, *args):
|
||||||
return os.path.join(self.private_data_dir, *args)
|
return os.path.join(self.private_data_dir, *args)
|
||||||
|
|||||||
@@ -208,6 +208,12 @@ def run_isolated_job(private_data_dir, secrets, logfile=sys.stdout):
|
|||||||
env['AWX_ISOLATED_DATA_DIR'] = private_data_dir
|
env['AWX_ISOLATED_DATA_DIR'] = private_data_dir
|
||||||
env['PYTHONPATH'] = env.get('PYTHONPATH', '') + callback_dir + ':'
|
env['PYTHONPATH'] = env.get('PYTHONPATH', '') + callback_dir + ':'
|
||||||
|
|
||||||
|
venv_path = env.get('VIRTUAL_ENV')
|
||||||
|
if venv_path and not os.path.exists(venv_path):
|
||||||
|
raise RuntimeError(
|
||||||
|
'a valid Python virtualenv does not exist at {}'.format(venv_path)
|
||||||
|
)
|
||||||
|
|
||||||
return run_pexpect(args, cwd, env, logfile,
|
return run_pexpect(args, cwd, env, logfile,
|
||||||
expect_passwords=expect_passwords,
|
expect_passwords=expect_passwords,
|
||||||
idle_timeout=idle_timeout,
|
idle_timeout=idle_timeout,
|
||||||
|
|||||||
12
awx/main/management/commands/check_migrations.py
Normal file
12
awx/main/management/commands/check_migrations.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.db import connections
|
||||||
|
from django.db.backends.sqlite3.base import DatabaseWrapper
|
||||||
|
from django.core.management.commands.makemigrations import Command as MakeMigrations
|
||||||
|
|
||||||
|
|
||||||
|
class Command(MakeMigrations):
|
||||||
|
|
||||||
|
def execute(self, *args, **options):
|
||||||
|
settings = connections['default'].settings_dict.copy()
|
||||||
|
settings['ENGINE'] = 'sqlite3'
|
||||||
|
connections['default'] = DatabaseWrapper(settings)
|
||||||
|
return MakeMigrations().execute(*args, **options)
|
||||||
@@ -491,7 +491,7 @@ class Command(BaseCommand):
|
|||||||
for host in hosts_qs.filter(pk__in=del_pks):
|
for host in hosts_qs.filter(pk__in=del_pks):
|
||||||
host_name = host.name
|
host_name = host.name
|
||||||
host.delete()
|
host.delete()
|
||||||
logger.info('Deleted host "%s"', host_name)
|
logger.debug('Deleted host "%s"', host_name)
|
||||||
if settings.SQL_DEBUG:
|
if settings.SQL_DEBUG:
|
||||||
logger.warning('host deletions took %d queries for %d hosts',
|
logger.warning('host deletions took %d queries for %d hosts',
|
||||||
len(connection.queries) - queries_before,
|
len(connection.queries) - queries_before,
|
||||||
@@ -528,7 +528,7 @@ class Command(BaseCommand):
|
|||||||
group_name = group.name
|
group_name = group.name
|
||||||
with ignore_inventory_computed_fields():
|
with ignore_inventory_computed_fields():
|
||||||
group.delete()
|
group.delete()
|
||||||
logger.info('Group "%s" deleted', group_name)
|
logger.debug('Group "%s" deleted', group_name)
|
||||||
if settings.SQL_DEBUG:
|
if settings.SQL_DEBUG:
|
||||||
logger.warning('group deletions took %d queries for %d groups',
|
logger.warning('group deletions took %d queries for %d groups',
|
||||||
len(connection.queries) - queries_before,
|
len(connection.queries) - queries_before,
|
||||||
@@ -549,7 +549,7 @@ class Command(BaseCommand):
|
|||||||
db_groups = self.inventory_source.groups
|
db_groups = self.inventory_source.groups
|
||||||
for db_group in db_groups.all():
|
for db_group in db_groups.all():
|
||||||
if self.inventory_source.deprecated_group_id == db_group.id: # TODO: remove in 3.3
|
if self.inventory_source.deprecated_group_id == db_group.id: # TODO: remove in 3.3
|
||||||
logger.info(
|
logger.debug(
|
||||||
'Group "%s" from v1 API child group/host connections preserved',
|
'Group "%s" from v1 API child group/host connections preserved',
|
||||||
db_group.name
|
db_group.name
|
||||||
)
|
)
|
||||||
@@ -566,7 +566,7 @@ class Command(BaseCommand):
|
|||||||
for db_child in db_children.filter(pk__in=child_group_pks):
|
for db_child in db_children.filter(pk__in=child_group_pks):
|
||||||
group_group_count += 1
|
group_group_count += 1
|
||||||
db_group.children.remove(db_child)
|
db_group.children.remove(db_child)
|
||||||
logger.info('Group "%s" removed from group "%s"',
|
logger.debug('Group "%s" removed from group "%s"',
|
||||||
db_child.name, db_group.name)
|
db_child.name, db_group.name)
|
||||||
# FIXME: Inventory source group relationships
|
# FIXME: Inventory source group relationships
|
||||||
# Delete group/host relationships not present in imported data.
|
# Delete group/host relationships not present in imported data.
|
||||||
@@ -594,7 +594,7 @@ class Command(BaseCommand):
|
|||||||
if db_host not in db_group.hosts.all():
|
if db_host not in db_group.hosts.all():
|
||||||
continue
|
continue
|
||||||
db_group.hosts.remove(db_host)
|
db_group.hosts.remove(db_host)
|
||||||
logger.info('Host "%s" removed from group "%s"',
|
logger.debug('Host "%s" removed from group "%s"',
|
||||||
db_host.name, db_group.name)
|
db_host.name, db_group.name)
|
||||||
if settings.SQL_DEBUG:
|
if settings.SQL_DEBUG:
|
||||||
logger.warning('group-group and group-host deletions took %d queries for %d relationships',
|
logger.warning('group-group and group-host deletions took %d queries for %d relationships',
|
||||||
@@ -614,9 +614,9 @@ class Command(BaseCommand):
|
|||||||
if db_variables != all_obj.variables_dict:
|
if db_variables != all_obj.variables_dict:
|
||||||
all_obj.variables = json.dumps(db_variables)
|
all_obj.variables = json.dumps(db_variables)
|
||||||
all_obj.save(update_fields=['variables'])
|
all_obj.save(update_fields=['variables'])
|
||||||
logger.info('Inventory variables updated from "all" group')
|
logger.debug('Inventory variables updated from "all" group')
|
||||||
else:
|
else:
|
||||||
logger.info('Inventory variables unmodified')
|
logger.debug('Inventory variables unmodified')
|
||||||
|
|
||||||
def _create_update_groups(self):
|
def _create_update_groups(self):
|
||||||
'''
|
'''
|
||||||
@@ -648,11 +648,11 @@ class Command(BaseCommand):
|
|||||||
group.variables = json.dumps(db_variables)
|
group.variables = json.dumps(db_variables)
|
||||||
group.save(update_fields=['variables'])
|
group.save(update_fields=['variables'])
|
||||||
if self.overwrite_vars:
|
if self.overwrite_vars:
|
||||||
logger.info('Group "%s" variables replaced', group.name)
|
logger.debug('Group "%s" variables replaced', group.name)
|
||||||
else:
|
else:
|
||||||
logger.info('Group "%s" variables updated', group.name)
|
logger.debug('Group "%s" variables updated', group.name)
|
||||||
else:
|
else:
|
||||||
logger.info('Group "%s" variables unmodified', group.name)
|
logger.debug('Group "%s" variables unmodified', group.name)
|
||||||
existing_group_names.add(group.name)
|
existing_group_names.add(group.name)
|
||||||
self._batch_add_m2m(self.inventory_source.groups, group)
|
self._batch_add_m2m(self.inventory_source.groups, group)
|
||||||
for group_name in all_group_names:
|
for group_name in all_group_names:
|
||||||
@@ -666,7 +666,7 @@ class Command(BaseCommand):
|
|||||||
'description':'imported'
|
'description':'imported'
|
||||||
}
|
}
|
||||||
)[0]
|
)[0]
|
||||||
logger.info('Group "%s" added', group.name)
|
logger.debug('Group "%s" added', group.name)
|
||||||
self._batch_add_m2m(self.inventory_source.groups, group)
|
self._batch_add_m2m(self.inventory_source.groups, group)
|
||||||
self._batch_add_m2m(self.inventory_source.groups, flush=True)
|
self._batch_add_m2m(self.inventory_source.groups, flush=True)
|
||||||
if settings.SQL_DEBUG:
|
if settings.SQL_DEBUG:
|
||||||
@@ -705,24 +705,24 @@ class Command(BaseCommand):
|
|||||||
if update_fields:
|
if update_fields:
|
||||||
db_host.save(update_fields=update_fields)
|
db_host.save(update_fields=update_fields)
|
||||||
if 'name' in update_fields:
|
if 'name' in update_fields:
|
||||||
logger.info('Host renamed from "%s" to "%s"', old_name, mem_host.name)
|
logger.debug('Host renamed from "%s" to "%s"', old_name, mem_host.name)
|
||||||
if 'instance_id' in update_fields:
|
if 'instance_id' in update_fields:
|
||||||
if old_instance_id:
|
if old_instance_id:
|
||||||
logger.info('Host "%s" instance_id updated', mem_host.name)
|
logger.debug('Host "%s" instance_id updated', mem_host.name)
|
||||||
else:
|
else:
|
||||||
logger.info('Host "%s" instance_id added', mem_host.name)
|
logger.debug('Host "%s" instance_id added', mem_host.name)
|
||||||
if 'variables' in update_fields:
|
if 'variables' in update_fields:
|
||||||
if self.overwrite_vars:
|
if self.overwrite_vars:
|
||||||
logger.info('Host "%s" variables replaced', mem_host.name)
|
logger.debug('Host "%s" variables replaced', mem_host.name)
|
||||||
else:
|
else:
|
||||||
logger.info('Host "%s" variables updated', mem_host.name)
|
logger.debug('Host "%s" variables updated', mem_host.name)
|
||||||
else:
|
else:
|
||||||
logger.info('Host "%s" variables unmodified', mem_host.name)
|
logger.debug('Host "%s" variables unmodified', mem_host.name)
|
||||||
if 'enabled' in update_fields:
|
if 'enabled' in update_fields:
|
||||||
if enabled:
|
if enabled:
|
||||||
logger.info('Host "%s" is now enabled', mem_host.name)
|
logger.debug('Host "%s" is now enabled', mem_host.name)
|
||||||
else:
|
else:
|
||||||
logger.info('Host "%s" is now disabled', mem_host.name)
|
logger.debug('Host "%s" is now disabled', mem_host.name)
|
||||||
self._batch_add_m2m(self.inventory_source.hosts, db_host)
|
self._batch_add_m2m(self.inventory_source.hosts, db_host)
|
||||||
|
|
||||||
def _create_update_hosts(self):
|
def _create_update_hosts(self):
|
||||||
@@ -796,9 +796,9 @@ class Command(BaseCommand):
|
|||||||
host_attrs['instance_id'] = instance_id
|
host_attrs['instance_id'] = instance_id
|
||||||
db_host = self.inventory.hosts.update_or_create(name=mem_host_name, defaults=host_attrs)[0]
|
db_host = self.inventory.hosts.update_or_create(name=mem_host_name, defaults=host_attrs)[0]
|
||||||
if enabled is False:
|
if enabled is False:
|
||||||
logger.info('Host "%s" added (disabled)', mem_host_name)
|
logger.debug('Host "%s" added (disabled)', mem_host_name)
|
||||||
else:
|
else:
|
||||||
logger.info('Host "%s" added', mem_host_name)
|
logger.debug('Host "%s" added', mem_host_name)
|
||||||
self._batch_add_m2m(self.inventory_source.hosts, db_host)
|
self._batch_add_m2m(self.inventory_source.hosts, db_host)
|
||||||
|
|
||||||
self._batch_add_m2m(self.inventory_source.hosts, flush=True)
|
self._batch_add_m2m(self.inventory_source.hosts, flush=True)
|
||||||
@@ -827,10 +827,10 @@ class Command(BaseCommand):
|
|||||||
child_names = all_child_names[offset2:(offset2 + self._batch_size)]
|
child_names = all_child_names[offset2:(offset2 + self._batch_size)]
|
||||||
db_children_qs = self.inventory.groups.filter(name__in=child_names)
|
db_children_qs = self.inventory.groups.filter(name__in=child_names)
|
||||||
for db_child in db_children_qs.filter(children__id=db_group.id):
|
for db_child in db_children_qs.filter(children__id=db_group.id):
|
||||||
logger.info('Group "%s" already child of group "%s"', db_child.name, db_group.name)
|
logger.debug('Group "%s" already child of group "%s"', db_child.name, db_group.name)
|
||||||
for db_child in db_children_qs.exclude(children__id=db_group.id):
|
for db_child in db_children_qs.exclude(children__id=db_group.id):
|
||||||
self._batch_add_m2m(db_group.children, db_child)
|
self._batch_add_m2m(db_group.children, db_child)
|
||||||
logger.info('Group "%s" added as child of "%s"', db_child.name, db_group.name)
|
logger.debug('Group "%s" added as child of "%s"', db_child.name, db_group.name)
|
||||||
self._batch_add_m2m(db_group.children, flush=True)
|
self._batch_add_m2m(db_group.children, flush=True)
|
||||||
if settings.SQL_DEBUG:
|
if settings.SQL_DEBUG:
|
||||||
logger.warning('Group-group updates took %d queries for %d group-group relationships',
|
logger.warning('Group-group updates took %d queries for %d group-group relationships',
|
||||||
@@ -854,19 +854,19 @@ class Command(BaseCommand):
|
|||||||
host_names = all_host_names[offset2:(offset2 + self._batch_size)]
|
host_names = all_host_names[offset2:(offset2 + self._batch_size)]
|
||||||
db_hosts_qs = self.inventory.hosts.filter(name__in=host_names)
|
db_hosts_qs = self.inventory.hosts.filter(name__in=host_names)
|
||||||
for db_host in db_hosts_qs.filter(groups__id=db_group.id):
|
for db_host in db_hosts_qs.filter(groups__id=db_group.id):
|
||||||
logger.info('Host "%s" already in group "%s"', db_host.name, db_group.name)
|
logger.debug('Host "%s" already in group "%s"', db_host.name, db_group.name)
|
||||||
for db_host in db_hosts_qs.exclude(groups__id=db_group.id):
|
for db_host in db_hosts_qs.exclude(groups__id=db_group.id):
|
||||||
self._batch_add_m2m(db_group.hosts, db_host)
|
self._batch_add_m2m(db_group.hosts, db_host)
|
||||||
logger.info('Host "%s" added to group "%s"', db_host.name, db_group.name)
|
logger.debug('Host "%s" added to group "%s"', db_host.name, db_group.name)
|
||||||
all_instance_ids = sorted([h.instance_id for h in mem_group.hosts if h.instance_id])
|
all_instance_ids = sorted([h.instance_id for h in mem_group.hosts if h.instance_id])
|
||||||
for offset2 in xrange(0, len(all_instance_ids), self._batch_size):
|
for offset2 in xrange(0, len(all_instance_ids), self._batch_size):
|
||||||
instance_ids = all_instance_ids[offset2:(offset2 + self._batch_size)]
|
instance_ids = all_instance_ids[offset2:(offset2 + self._batch_size)]
|
||||||
db_hosts_qs = self.inventory.hosts.filter(instance_id__in=instance_ids)
|
db_hosts_qs = self.inventory.hosts.filter(instance_id__in=instance_ids)
|
||||||
for db_host in db_hosts_qs.filter(groups__id=db_group.id):
|
for db_host in db_hosts_qs.filter(groups__id=db_group.id):
|
||||||
logger.info('Host "%s" already in group "%s"', db_host.name, db_group.name)
|
logger.debug('Host "%s" already in group "%s"', db_host.name, db_group.name)
|
||||||
for db_host in db_hosts_qs.exclude(groups__id=db_group.id):
|
for db_host in db_hosts_qs.exclude(groups__id=db_group.id):
|
||||||
self._batch_add_m2m(db_group.hosts, db_host)
|
self._batch_add_m2m(db_group.hosts, db_host)
|
||||||
logger.info('Host "%s" added to group "%s"', db_host.name, db_group.name)
|
logger.debug('Host "%s" added to group "%s"', db_host.name, db_group.name)
|
||||||
self._batch_add_m2m(db_group.hosts, flush=True)
|
self._batch_add_m2m(db_group.hosts, flush=True)
|
||||||
if settings.SQL_DEBUG:
|
if settings.SQL_DEBUG:
|
||||||
logger.warning('Group-host updates took %d queries for %d group-host relationships',
|
logger.warning('Group-host updates took %d queries for %d group-host relationships',
|
||||||
|
|||||||
@@ -6,6 +6,22 @@ from django.core.management.base import BaseCommand
|
|||||||
import six
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
class Ungrouped(object):
|
||||||
|
|
||||||
|
name = 'ungrouped'
|
||||||
|
policy_instance_percentage = None
|
||||||
|
policy_instance_minimum = None
|
||||||
|
controller = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instances(self):
|
||||||
|
return Instance.objects.filter(rampart_groups__isnull=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def capacity(self):
|
||||||
|
return sum([x.capacity for x in self.instances])
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""List instances from the Tower database
|
"""List instances from the Tower database
|
||||||
"""
|
"""
|
||||||
@@ -13,12 +29,28 @@ class Command(BaseCommand):
|
|||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
super(Command, self).__init__()
|
super(Command, self).__init__()
|
||||||
|
|
||||||
for instance in Instance.objects.all():
|
groups = list(InstanceGroup.objects.all())
|
||||||
print(six.text_type(
|
ungrouped = Ungrouped()
|
||||||
"hostname: {0.hostname}; created: {0.created}; "
|
if len(ungrouped.instances):
|
||||||
"heartbeat: {0.modified}; capacity: {0.capacity}").format(instance))
|
groups.append(ungrouped)
|
||||||
for instance_group in InstanceGroup.objects.all():
|
|
||||||
print(six.text_type(
|
for instance_group in groups:
|
||||||
"Instance Group: {0.name}; created: {0.created}; "
|
fmt = '[{0.name} capacity={0.capacity}'
|
||||||
"capacity: {0.capacity}; members: {1}").format(instance_group,
|
if instance_group.policy_instance_percentage:
|
||||||
[x.hostname for x in instance_group.instances.all()]))
|
fmt += ' policy={0.policy_instance_percentage}%'
|
||||||
|
if instance_group.policy_instance_minimum:
|
||||||
|
fmt += ' policy>={0.policy_instance_minimum}'
|
||||||
|
if instance_group.controller:
|
||||||
|
fmt += ' controller={0.controller.name}'
|
||||||
|
print(six.text_type(fmt + ']').format(instance_group))
|
||||||
|
for x in instance_group.instances.all():
|
||||||
|
color = '\033[92m'
|
||||||
|
if x.capacity == 0 or x.enabled is False:
|
||||||
|
color = '\033[91m'
|
||||||
|
fmt = '\t' + color + '{0.hostname} capacity={0.capacity} version={1}'
|
||||||
|
if x.last_isolated_check:
|
||||||
|
fmt += ' last_isolated_check="{0.last_isolated_check:%Y-%m-%d %H:%M:%S}"'
|
||||||
|
if x.capacity:
|
||||||
|
fmt += ' heartbeat="{0.modified:%Y-%m-%d %H:%M:%S}"'
|
||||||
|
print(six.text_type(fmt + '\033[0m').format(x, x.version or '?'))
|
||||||
|
print('')
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class ReplayJobEvents():
|
|||||||
raise RuntimeError("Job is of type {} and replay is not yet supported.".format(type(job)))
|
raise RuntimeError("Job is of type {} and replay is not yet supported.".format(type(job)))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def run(self, job_id, speed=1.0, verbosity=0, skip=0):
|
def run(self, job_id, speed=1.0, verbosity=0, skip_range=[]):
|
||||||
stats = {
|
stats = {
|
||||||
'events_ontime': {
|
'events_ontime': {
|
||||||
'total': 0,
|
'total': 0,
|
||||||
@@ -127,7 +127,7 @@ class ReplayJobEvents():
|
|||||||
|
|
||||||
je_previous = None
|
je_previous = None
|
||||||
for n, je_current in enumerate(job_events):
|
for n, je_current in enumerate(job_events):
|
||||||
if n < skip:
|
if je_current.counter in skip_range:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not je_previous:
|
if not je_previous:
|
||||||
@@ -193,19 +193,29 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
help = 'Replay job events over websockets ordered by created on date.'
|
help = 'Replay job events over websockets ordered by created on date.'
|
||||||
|
|
||||||
|
def _parse_slice_range(self, slice_arg):
|
||||||
|
slice_arg = tuple([int(n) for n in slice_arg.split(':')])
|
||||||
|
slice_obj = slice(*slice_arg)
|
||||||
|
|
||||||
|
start = slice_obj.start or 0
|
||||||
|
stop = slice_obj.stop or -1
|
||||||
|
step = slice_obj.step or 1
|
||||||
|
|
||||||
|
return range(start, stop, step)
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('--job_id', dest='job_id', type=int, metavar='j',
|
parser.add_argument('--job_id', dest='job_id', type=int, metavar='j',
|
||||||
help='Id of the job to replay (job or adhoc)')
|
help='Id of the job to replay (job or adhoc)')
|
||||||
parser.add_argument('--speed', dest='speed', type=int, metavar='s',
|
parser.add_argument('--speed', dest='speed', type=int, metavar='s',
|
||||||
help='Speedup factor.')
|
help='Speedup factor.')
|
||||||
parser.add_argument('--skip', dest='skip', type=int, metavar='k',
|
parser.add_argument('--skip-range', dest='skip_range', type=str, metavar='k',
|
||||||
help='Number of events to skip.')
|
default='0:-1:1', help='Range of events to skip')
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
job_id = options.get('job_id')
|
job_id = options.get('job_id')
|
||||||
speed = options.get('speed') or 1
|
speed = options.get('speed') or 1
|
||||||
verbosity = options.get('verbosity') or 0
|
verbosity = options.get('verbosity') or 0
|
||||||
skip = options.get('skip') or 0
|
skip = self._parse_slice_range(options.get('skip_range'))
|
||||||
|
|
||||||
replayer = ReplayJobEvents()
|
replayer = ReplayJobEvents()
|
||||||
replayer.run(job_id, speed, verbosity, skip)
|
replayer.run(job_id, speed, verbosity, skip)
|
||||||
|
|||||||
@@ -64,15 +64,22 @@ class CallbackBrokerWorker(ConsumerMixin):
|
|||||||
return _handler
|
return _handler
|
||||||
|
|
||||||
if use_workers:
|
if use_workers:
|
||||||
django_connection.close()
|
|
||||||
django_cache.close()
|
|
||||||
for idx in range(settings.JOB_EVENT_WORKERS):
|
for idx in range(settings.JOB_EVENT_WORKERS):
|
||||||
queue_actual = MPQueue(settings.JOB_EVENT_MAX_QUEUE_SIZE)
|
queue_actual = MPQueue(settings.JOB_EVENT_MAX_QUEUE_SIZE)
|
||||||
w = Process(target=self.callback_worker, args=(queue_actual, idx,))
|
w = Process(target=self.callback_worker, args=(queue_actual, idx,))
|
||||||
w.start()
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
logger.info('Started worker %s' % str(idx))
|
logger.info('Starting worker %s' % str(idx))
|
||||||
self.worker_queues.append([0, queue_actual, w])
|
self.worker_queues.append([0, queue_actual, w])
|
||||||
|
|
||||||
|
# It's important to close these _right before_ we fork; we
|
||||||
|
# don't want the forked processes to inherit the open sockets
|
||||||
|
# for the DB and memcached connections (that way lies race
|
||||||
|
# conditions)
|
||||||
|
django_connection.close()
|
||||||
|
django_cache.close()
|
||||||
|
for _, _, w in self.worker_queues:
|
||||||
|
w.start()
|
||||||
|
|
||||||
elif settings.DEBUG:
|
elif settings.DEBUG:
|
||||||
logger.warn('Started callback receiver (no workers)')
|
logger.warn('Started callback receiver (no workers)')
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Copyright (c) 2015 Ansible, Inc.
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
@@ -9,12 +11,15 @@ import time
|
|||||||
import cProfile
|
import cProfile
|
||||||
import pstats
|
import pstats
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.db.migrations.executor import MigrationExecutor
|
from django.db.migrations.executor import MigrationExecutor
|
||||||
from django.db import IntegrityError, connection
|
from django.db import IntegrityError, connection
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
@@ -128,8 +133,9 @@ class SessionTimeoutMiddleware(object):
|
|||||||
def process_response(self, request, response):
|
def process_response(self, request, response):
|
||||||
req_session = getattr(request, 'session', None)
|
req_session = getattr(request, 'session', None)
|
||||||
if req_session and not req_session.is_empty():
|
if req_session and not req_session.is_empty():
|
||||||
request.session.set_expiry(request.session.get_expiry_age())
|
expiry = int(settings.SESSION_COOKIE_AGE)
|
||||||
response['Session-Timeout'] = int(settings.SESSION_COOKIE_AGE)
|
request.session.set_expiry(expiry)
|
||||||
|
response['Session-Timeout'] = expiry
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -203,6 +209,56 @@ class URLModificationMiddleware(object):
|
|||||||
request.path_info = new_path
|
request.path_info = new_path
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedAuthTokenMiddleware(object):
|
||||||
|
"""
|
||||||
|
Used to emulate support for the old Auth Token endpoint to ease the
|
||||||
|
transition to OAuth2.0. Specifically, this middleware:
|
||||||
|
|
||||||
|
1. Intercepts POST requests to `/api/v2/authtoken/` (which now no longer
|
||||||
|
_actually_ exists in our urls.py)
|
||||||
|
2. Rewrites `request.path` to `/api/v2/users/N/personal_tokens/`
|
||||||
|
3. Detects the username and password in the request body (either in JSON,
|
||||||
|
or form-encoded variables) and builds an appropriate HTTP_AUTHORIZATION
|
||||||
|
Basic header
|
||||||
|
"""
|
||||||
|
|
||||||
|
def process_request(self, request):
|
||||||
|
if re.match('^/api/v[12]/authtoken/?$', request.path):
|
||||||
|
if request.method != 'POST':
|
||||||
|
return HttpResponse('HTTP {} is not allowed.'.format(request.method), status=405)
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.body)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
payload = request.POST
|
||||||
|
if 'username' not in payload or 'password' not in payload:
|
||||||
|
return HttpResponse('Unable to login with provided credentials.', status=401)
|
||||||
|
username = payload['username']
|
||||||
|
password = payload['password']
|
||||||
|
try:
|
||||||
|
pk = User.objects.get(username=username).pk
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return HttpResponse('Unable to login with provided credentials.', status=401)
|
||||||
|
new_path = reverse('api:user_personal_token_list', kwargs={
|
||||||
|
'pk': pk,
|
||||||
|
'version': 'v2'
|
||||||
|
})
|
||||||
|
request._body = ''
|
||||||
|
request.META['CONTENT_TYPE'] = 'application/json'
|
||||||
|
request.path = request.path_info = new_path
|
||||||
|
auth = ' '.join([
|
||||||
|
'Basic',
|
||||||
|
base64.b64encode(
|
||||||
|
six.text_type('{}:{}').format(username, password)
|
||||||
|
)
|
||||||
|
])
|
||||||
|
request.environ['HTTP_AUTHORIZATION'] = auth
|
||||||
|
logger.warn(
|
||||||
|
'The Auth Token API (/api/v2/authtoken/) is deprecated and will '
|
||||||
|
'be replaced with OAuth2.0 in the next version of Ansible Tower '
|
||||||
|
'(see /api/o/ for more details).'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MigrationRanCheckMiddleware(object):
|
class MigrationRanCheckMiddleware(object):
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ class Migration(migrations.Migration):
|
|||||||
('status', models.CharField(default=b'pending', max_length=20, editable=False, choices=[(b'pending', 'Pending'), (b'successful', 'Successful'), (b'failed', 'Failed')])),
|
('status', models.CharField(default=b'pending', max_length=20, editable=False, choices=[(b'pending', 'Pending'), (b'successful', 'Successful'), (b'failed', 'Failed')])),
|
||||||
('error', models.TextField(default=b'', editable=False, blank=True)),
|
('error', models.TextField(default=b'', editable=False, blank=True)),
|
||||||
('notifications_sent', models.IntegerField(default=0, editable=False)),
|
('notifications_sent', models.IntegerField(default=0, editable=False)),
|
||||||
('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'mattermost', 'Mattermost'), (b'irc', 'IRC')])),
|
('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'mattermost', 'Mattermost'), (b'rocketchat', 'Rocket.Chat'), (b'irc', 'IRC')])),
|
||||||
('recipients', models.TextField(default=b'', editable=False, blank=True)),
|
('recipients', models.TextField(default=b'', editable=False, blank=True)),
|
||||||
('subject', models.TextField(default=b'', editable=False, blank=True)),
|
('subject', models.TextField(default=b'', editable=False, blank=True)),
|
||||||
('body', jsonfield.fields.JSONField(default=dict, blank=True)),
|
('body', jsonfield.fields.JSONField(default=dict, blank=True)),
|
||||||
@@ -174,7 +174,7 @@ class Migration(migrations.Migration):
|
|||||||
('modified', models.DateTimeField(default=None, editable=False)),
|
('modified', models.DateTimeField(default=None, editable=False)),
|
||||||
('description', models.TextField(default=b'', blank=True)),
|
('description', models.TextField(default=b'', blank=True)),
|
||||||
('name', models.CharField(unique=True, max_length=512)),
|
('name', models.CharField(unique=True, max_length=512)),
|
||||||
('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'mattermost', 'Mattermost'), (b'irc', 'IRC')])),
|
('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'mattermost', 'Mattermost'), (b'rocketchat', 'Rocket.Chat'), (b'irc', 'IRC')])),
|
||||||
('notification_configuration', jsonfield.fields.JSONField(default=dict)),
|
('notification_configuration', jsonfield.fields.JSONField(default=dict)),
|
||||||
('created_by', models.ForeignKey(related_name="{u'class': 'notificationtemplate', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
('created_by', models.ForeignKey(related_name="{u'class': 'notificationtemplate', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||||
('modified_by', models.ForeignKey(related_name="{u'class': 'notificationtemplate', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
('modified_by', models.ForeignKey(related_name="{u'class': 'notificationtemplate', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
|
|||||||
('verbosity', models.PositiveIntegerField(default=0, editable=False)),
|
('verbosity', models.PositiveIntegerField(default=0, editable=False)),
|
||||||
('start_line', models.PositiveIntegerField(default=0, editable=False)),
|
('start_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||||
('end_line', models.PositiveIntegerField(default=0, editable=False)),
|
('end_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||||
('inventory_update', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='generic_command_events', to='main.InventoryUpdate')),
|
('inventory_update', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_update_events', to='main.InventoryUpdate')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ('-pk',),
|
'ordering': ('-pk',),
|
||||||
@@ -53,7 +53,7 @@ class Migration(migrations.Migration):
|
|||||||
('verbosity', models.PositiveIntegerField(default=0, editable=False)),
|
('verbosity', models.PositiveIntegerField(default=0, editable=False)),
|
||||||
('start_line', models.PositiveIntegerField(default=0, editable=False)),
|
('start_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||||
('end_line', models.PositiveIntegerField(default=0, editable=False)),
|
('end_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||||
('project_update', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='generic_command_events', to='main.ProjectUpdate')),
|
('project_update', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='project_update_events', to='main.ProjectUpdate')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ('pk',),
|
'ordering': ('pk',),
|
||||||
@@ -72,12 +72,24 @@ class Migration(migrations.Migration):
|
|||||||
('verbosity', models.PositiveIntegerField(default=0, editable=False)),
|
('verbosity', models.PositiveIntegerField(default=0, editable=False)),
|
||||||
('start_line', models.PositiveIntegerField(default=0, editable=False)),
|
('start_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||||
('end_line', models.PositiveIntegerField(default=0, editable=False)),
|
('end_line', models.PositiveIntegerField(default=0, editable=False)),
|
||||||
('system_job', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='generic_command_events', to='main.SystemJob')),
|
('system_job', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='system_job_events', to='main.SystemJob')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ('-pk',),
|
'ordering': ('-pk',),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.AlterIndexTogether(
|
||||||
|
name='inventoryupdateevent',
|
||||||
|
index_together=set([('inventory_update', 'start_line'), ('inventory_update', 'uuid'), ('inventory_update', 'end_line')]),
|
||||||
|
),
|
||||||
|
migrations.AlterIndexTogether(
|
||||||
|
name='projectupdateevent',
|
||||||
|
index_together=set([('project_update', 'event'), ('project_update', 'end_line'), ('project_update', 'start_line'), ('project_update', 'uuid')]),
|
||||||
|
),
|
||||||
|
migrations.AlterIndexTogether(
|
||||||
|
name='systemjobevent',
|
||||||
|
index_together=set([('system_job', 'end_line'), ('system_job', 'uuid'), ('system_job', 'start_line')]),
|
||||||
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='unifiedjob',
|
model_name='unifiedjob',
|
||||||
name='result_stdout_file',
|
name='result_stdout_file',
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='activitystream',
|
model_name='activitystream',
|
||||||
name='o_auth2_access_token',
|
name='o_auth2_access_token',
|
||||||
field=models.ManyToManyField(to='main.OAuth2AccessToken', blank=True, related_name='main_o_auth2_accesstoken'),
|
field=models.ManyToManyField(to='main.OAuth2AccessToken', blank=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='activitystream',
|
model_name='activitystream',
|
||||||
name='o_auth2_application',
|
name='o_auth2_application',
|
||||||
field=models.ManyToManyField(to='main.OAuth2Application', blank=True, related_name='main_o_auth2_application'),
|
field=models.ManyToManyField(to='main.OAuth2Application', blank=True),
|
||||||
),
|
),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='oauth2accesstoken',
|
model_name='oauth2accesstoken',
|
||||||
name='scope',
|
name='scope',
|
||||||
field=models.TextField(blank=True, help_text="Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write']."),
|
field=models.TextField(blank=True, default=b'write', help_text="Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write']."),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='oauth2accesstoken',
|
model_name='oauth2accesstoken',
|
||||||
name='modified',
|
name='modified',
|
||||||
field=models.DateTimeField(editable=False),
|
field=models.DateTimeField(editable=False, auto_now=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.11 on 2018-08-16 16:46
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0047_v330_activitystream_instance'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='credential',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'credential', u'model_name': 'credential'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='credential',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'credential', u'model_name': 'credential'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='credentialtype',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'credentialtype', u'model_name': 'credentialtype'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='credentialtype',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'credentialtype', u'model_name': 'credentialtype'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='custominventoryscript',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'custominventoryscript', u'model_name': 'custominventoryscript'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='custominventoryscript',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'custominventoryscript', u'model_name': 'custominventoryscript'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='group',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'group', u'model_name': 'group'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='group',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'group', u'model_name': 'group'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='host',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'host', u'model_name': 'host'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='host',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'host', u'model_name': 'host'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventory',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'inventory', u'model_name': 'inventory'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventory',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'inventory', u'model_name': 'inventory'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='label',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'label', u'model_name': 'label'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='label',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'label', u'model_name': 'label'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='notificationtemplate',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'notificationtemplate', u'model_name': 'notificationtemplate'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='notificationtemplate',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'notificationtemplate', u'model_name': 'notificationtemplate'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='organization',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'organization', u'model_name': 'organization'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='organization',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'organization', u'model_name': 'organization'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='schedule',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'schedule', u'model_name': 'schedule'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='schedule',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'schedule', u'model_name': 'schedule'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='team',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'team', u'model_name': 'team'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='team',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'team', u'model_name': 'team'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='unifiedjob',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'unifiedjob', u'model_name': 'unifiedjob'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='unifiedjob',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'unifiedjob', u'model_name': 'unifiedjob'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='unifiedjobtemplate',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'unifiedjobtemplate', u'model_name': 'unifiedjobtemplate'}(class)s_created+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='unifiedjobtemplate',
|
||||||
|
name='modified_by',
|
||||||
|
field=models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{u'app_label': 'main', u'class': 'unifiedjobtemplate', u'model_name': 'unifiedjobtemplate'}(class)s_modified+", to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.11 on 2018-08-17 16:13
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0048_v330_django_created_modified_by_model_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='instance',
|
||||||
|
name='capacity_adjustment',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=Decimal('1'), max_digits=3, validators=[django.core.validators.MinValueValidator(0)]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -35,9 +35,9 @@ def sanitize_event_keys(kwargs, valid_keys):
|
|||||||
for key in [
|
for key in [
|
||||||
'play', 'role', 'task', 'playbook'
|
'play', 'role', 'task', 'playbook'
|
||||||
]:
|
]:
|
||||||
if isinstance(kwargs.get(key), six.string_types):
|
if isinstance(kwargs.get('event_data', {}).get(key), six.string_types):
|
||||||
if len(kwargs[key]) > 1024:
|
if len(kwargs['event_data'][key]) > 1024:
|
||||||
kwargs[key] = Truncator(kwargs[key]).chars(1024)
|
kwargs['event_data'][key] = Truncator(kwargs['event_data'][key]).chars(1024)
|
||||||
|
|
||||||
|
|
||||||
def create_host_status_counts(event_data):
|
def create_host_status_counts(event_data):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import random
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models, connection
|
from django.db import models, connection
|
||||||
from django.db.models.signals import post_save, post_delete
|
from django.db.models.signals import post_save, post_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
@@ -81,6 +82,7 @@ class Instance(HasPolicyEditsMixin, BaseModel):
|
|||||||
default=Decimal(1.0),
|
default=Decimal(1.0),
|
||||||
max_digits=3,
|
max_digits=3,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
|
validators=[MinValueValidator(0)]
|
||||||
)
|
)
|
||||||
enabled = models.BooleanField(
|
enabled = models.BooleanField(
|
||||||
default=True
|
default=True
|
||||||
|
|||||||
@@ -1262,6 +1262,11 @@ class InventorySourceOptions(BaseModel):
|
|||||||
'Credentials of type machine, source control, insights and vault are '
|
'Credentials of type machine, source control, insights and vault are '
|
||||||
'disallowed for custom inventory sources.'
|
'disallowed for custom inventory sources.'
|
||||||
)
|
)
|
||||||
|
elif source == 'scm' and cred and cred.credential_type.kind in ('insights', 'vault'):
|
||||||
|
return _(
|
||||||
|
'Credentials of type insights and vault are '
|
||||||
|
'disallowed for scm inventory sources.'
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_inventory_plugin_name(self):
|
def get_inventory_plugin_name(self):
|
||||||
|
|||||||
@@ -238,11 +238,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
|
|
||||||
host_config_key = models.CharField(
|
host_config_key = prevent_search(models.CharField(
|
||||||
max_length=1024,
|
max_length=1024,
|
||||||
blank=True,
|
blank=True,
|
||||||
default='',
|
default='',
|
||||||
)
|
))
|
||||||
ask_diff_mode_on_launch = AskForField(
|
ask_diff_mode_on_launch = AskForField(
|
||||||
blank=True,
|
blank=True,
|
||||||
default=False,
|
default=False,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class SlackBackend(AWXBaseEmailBackend):
|
|||||||
if self.color:
|
if self.color:
|
||||||
ret = connection.api_call("chat.postMessage",
|
ret = connection.api_call("chat.postMessage",
|
||||||
channel=r,
|
channel=r,
|
||||||
|
as_user=True,
|
||||||
attachments=[{
|
attachments=[{
|
||||||
"color": self.color,
|
"color": self.color,
|
||||||
"text": m.subject
|
"text": m.subject
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ class TaskManager():
|
|||||||
inventory_updates_qs = InventoryUpdate.objects.filter(
|
inventory_updates_qs = InventoryUpdate.objects.filter(
|
||||||
status__in=status_list).exclude(source='file').prefetch_related('inventory_source', 'instance_group')
|
status__in=status_list).exclude(source='file').prefetch_related('inventory_source', 'instance_group')
|
||||||
inventory_updates = [i for i in inventory_updates_qs]
|
inventory_updates = [i for i in inventory_updates_qs]
|
||||||
project_updates = [p for p in ProjectUpdate.objects.filter(status__in=status_list).prefetch_related('instance_group')]
|
# Notice the job_type='check': we want to prevent implicit project updates from blocking our jobs.
|
||||||
|
project_updates = [p for p in ProjectUpdate.objects.filter(status__in=status_list, job_type='check').prefetch_related('instance_group')]
|
||||||
system_jobs = [s for s in SystemJob.objects.filter(status__in=status_list).prefetch_related('instance_group')]
|
system_jobs = [s for s in SystemJob.objects.filter(status__in=status_list).prefetch_related('instance_group')]
|
||||||
ad_hoc_commands = [a for a in AdHocCommand.objects.filter(status__in=status_list).prefetch_related('instance_group')]
|
ad_hoc_commands = [a for a in AdHocCommand.objects.filter(status__in=status_list).prefetch_related('instance_group')]
|
||||||
workflow_jobs = [w for w in WorkflowJob.objects.filter(status__in=status_list)]
|
workflow_jobs = [w for w in WorkflowJob.objects.filter(status__in=status_list)]
|
||||||
@@ -678,9 +679,9 @@ class TaskManager():
|
|||||||
return finished_wfjs
|
return finished_wfjs
|
||||||
|
|
||||||
def schedule(self):
|
def schedule(self):
|
||||||
with transaction.atomic():
|
|
||||||
# Lock
|
# Lock
|
||||||
with advisory_lock('task_manager_lock', wait=False) as acquired:
|
with advisory_lock('task_manager_lock', wait=False) as acquired:
|
||||||
|
with transaction.atomic():
|
||||||
if acquired is False:
|
if acquired is False:
|
||||||
logger.debug("Not running scheduler, another task holds lock")
|
logger.debug("Not running scheduler, another task holds lock")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ except Exception:
|
|||||||
from kombu import Queue, Exchange
|
from kombu import Queue, Exchange
|
||||||
from kombu.common import Broadcast
|
from kombu.common import Broadcast
|
||||||
from celery import Task, shared_task
|
from celery import Task, shared_task
|
||||||
from celery.signals import celeryd_init, worker_shutdown, celeryd_after_setup
|
from celery.signals import celeryd_init, worker_shutdown
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -108,6 +108,31 @@ def log_celery_failure(self, exc, task_id, args, kwargs, einfo):
|
|||||||
|
|
||||||
@celeryd_init.connect
|
@celeryd_init.connect
|
||||||
def celery_startup(conf=None, **kwargs):
|
def celery_startup(conf=None, **kwargs):
|
||||||
|
#
|
||||||
|
# When celeryd starts, if the instance cannot be found in the database,
|
||||||
|
# automatically register it. This is mostly useful for openshift-based
|
||||||
|
# deployments where:
|
||||||
|
#
|
||||||
|
# 2 Instances come online
|
||||||
|
# Instance B encounters a network blip, Instance A notices, and
|
||||||
|
# deprovisions it
|
||||||
|
# Instance B's connectivity is restored, celeryd starts, and it
|
||||||
|
# re-registers itself
|
||||||
|
#
|
||||||
|
# In traditional container-less deployments, instances don't get
|
||||||
|
# deprovisioned when they miss their heartbeat, so this code is mostly a
|
||||||
|
# no-op.
|
||||||
|
#
|
||||||
|
if kwargs['instance'].hostname != 'celery@{}'.format(settings.CLUSTER_HOST_ID):
|
||||||
|
error = six.text_type('celery -n {} does not match settings.CLUSTER_HOST_ID={}').format(
|
||||||
|
instance.hostname, settings.CLUSTER_HOST_ID
|
||||||
|
)
|
||||||
|
logger.error(error)
|
||||||
|
raise RuntimeError(error)
|
||||||
|
(changed, tower_instance) = Instance.objects.get_or_register()
|
||||||
|
if changed:
|
||||||
|
logger.info(six.text_type("Registered tower node '{}'").format(tower_instance.hostname))
|
||||||
|
|
||||||
startup_logger = logging.getLogger('awx.main.tasks')
|
startup_logger = logging.getLogger('awx.main.tasks')
|
||||||
startup_logger.info("Syncing Schedules")
|
startup_logger.info("Syncing Schedules")
|
||||||
for sch in Schedule.objects.all():
|
for sch in Schedule.objects.all():
|
||||||
@@ -147,9 +172,17 @@ def inform_cluster_of_shutdown(*args, **kwargs):
|
|||||||
|
|
||||||
@shared_task(bind=True, queue=settings.CELERY_DEFAULT_QUEUE)
|
@shared_task(bind=True, queue=settings.CELERY_DEFAULT_QUEUE)
|
||||||
def apply_cluster_membership_policies(self):
|
def apply_cluster_membership_policies(self):
|
||||||
|
started_waiting = time.time()
|
||||||
with advisory_lock('cluster_policy_lock', wait=True):
|
with advisory_lock('cluster_policy_lock', wait=True):
|
||||||
|
lock_time = time.time() - started_waiting
|
||||||
|
if lock_time > 1.0:
|
||||||
|
to_log = logger.info
|
||||||
|
else:
|
||||||
|
to_log = logger.debug
|
||||||
|
to_log('Waited {} seconds to obtain lock name: cluster_policy_lock'.format(lock_time))
|
||||||
|
started_compute = time.time()
|
||||||
all_instances = list(Instance.objects.order_by('id'))
|
all_instances = list(Instance.objects.order_by('id'))
|
||||||
all_groups = list(InstanceGroup.objects.all())
|
all_groups = list(InstanceGroup.objects.prefetch_related('instances'))
|
||||||
iso_hostnames = set([])
|
iso_hostnames = set([])
|
||||||
for ig in all_groups:
|
for ig in all_groups:
|
||||||
if ig.controller_id is not None:
|
if ig.controller_id is not None:
|
||||||
@@ -159,28 +192,32 @@ def apply_cluster_membership_policies(self):
|
|||||||
total_instances = len(considered_instances)
|
total_instances = len(considered_instances)
|
||||||
actual_groups = []
|
actual_groups = []
|
||||||
actual_instances = []
|
actual_instances = []
|
||||||
Group = namedtuple('Group', ['obj', 'instances'])
|
Group = namedtuple('Group', ['obj', 'instances', 'prior_instances'])
|
||||||
Node = namedtuple('Instance', ['obj', 'groups'])
|
Node = namedtuple('Instance', ['obj', 'groups'])
|
||||||
|
|
||||||
# Process policy instance list first, these will represent manually managed memberships
|
# Process policy instance list first, these will represent manually managed memberships
|
||||||
instance_hostnames_map = {inst.hostname: inst for inst in all_instances}
|
instance_hostnames_map = {inst.hostname: inst for inst in all_instances}
|
||||||
for ig in all_groups:
|
for ig in all_groups:
|
||||||
group_actual = Group(obj=ig, instances=[])
|
group_actual = Group(obj=ig, instances=[], prior_instances=[
|
||||||
|
instance.pk for instance in ig.instances.all() # obtained in prefetch
|
||||||
|
])
|
||||||
for hostname in ig.policy_instance_list:
|
for hostname in ig.policy_instance_list:
|
||||||
if hostname not in instance_hostnames_map:
|
if hostname not in instance_hostnames_map:
|
||||||
|
logger.info(six.text_type("Unknown instance {} in {} policy list").format(hostname, ig.name))
|
||||||
continue
|
continue
|
||||||
inst = instance_hostnames_map[hostname]
|
inst = instance_hostnames_map[hostname]
|
||||||
logger.info(six.text_type("Policy List, adding Instance {} to Group {}").format(inst.hostname, ig.name))
|
|
||||||
group_actual.instances.append(inst.id)
|
group_actual.instances.append(inst.id)
|
||||||
# NOTE: arguable behavior: policy-list-group is not added to
|
# NOTE: arguable behavior: policy-list-group is not added to
|
||||||
# instance's group count for consideration in minimum-policy rules
|
# instance's group count for consideration in minimum-policy rules
|
||||||
|
if group_actual.instances:
|
||||||
|
logger.info(six.text_type("Policy List, adding Instances {} to Group {}").format(group_actual.instances, ig.name))
|
||||||
|
|
||||||
if ig.controller_id is None:
|
if ig.controller_id is None:
|
||||||
actual_groups.append(group_actual)
|
actual_groups.append(group_actual)
|
||||||
else:
|
else:
|
||||||
# For isolated groups, _only_ apply the policy_instance_list
|
# For isolated groups, _only_ apply the policy_instance_list
|
||||||
# do not add to in-memory list, so minimum rules not applied
|
# do not add to in-memory list, so minimum rules not applied
|
||||||
logger.info('Committing instances {} to isolated group {}'.format(group_actual.instances, ig.name))
|
logger.info('Committing instances to isolated group {}'.format(ig.name))
|
||||||
ig.instances.set(group_actual.instances)
|
ig.instances.set(group_actual.instances)
|
||||||
|
|
||||||
# Process Instance minimum policies next, since it represents a concrete lower bound to the
|
# Process Instance minimum policies next, since it represents a concrete lower bound to the
|
||||||
@@ -189,6 +226,7 @@ def apply_cluster_membership_policies(self):
|
|||||||
logger.info("Total non-isolated instances:{} available for policy: {}".format(
|
logger.info("Total non-isolated instances:{} available for policy: {}".format(
|
||||||
total_instances, len(actual_instances)))
|
total_instances, len(actual_instances)))
|
||||||
for g in sorted(actual_groups, cmp=lambda x,y: len(x.instances) - len(y.instances)):
|
for g in sorted(actual_groups, cmp=lambda x,y: len(x.instances) - len(y.instances)):
|
||||||
|
policy_min_added = []
|
||||||
for i in sorted(actual_instances, cmp=lambda x,y: len(x.groups) - len(y.groups)):
|
for i in sorted(actual_instances, cmp=lambda x,y: len(x.groups) - len(y.groups)):
|
||||||
if len(g.instances) >= g.obj.policy_instance_minimum:
|
if len(g.instances) >= g.obj.policy_instance_minimum:
|
||||||
break
|
break
|
||||||
@@ -196,12 +234,15 @@ def apply_cluster_membership_policies(self):
|
|||||||
# If the instance is already _in_ the group, it was
|
# If the instance is already _in_ the group, it was
|
||||||
# applied earlier via the policy list
|
# applied earlier via the policy list
|
||||||
continue
|
continue
|
||||||
logger.info(six.text_type("Policy minimum, adding Instance {} to Group {}").format(i.obj.hostname, g.obj.name))
|
|
||||||
g.instances.append(i.obj.id)
|
g.instances.append(i.obj.id)
|
||||||
i.groups.append(g.obj.id)
|
i.groups.append(g.obj.id)
|
||||||
|
policy_min_added.append(i.obj.id)
|
||||||
|
if policy_min_added:
|
||||||
|
logger.info(six.text_type("Policy minimum, adding Instances {} to Group {}").format(policy_min_added, g.obj.name))
|
||||||
|
|
||||||
# Finally, process instance policy percentages
|
# Finally, process instance policy percentages
|
||||||
for g in sorted(actual_groups, cmp=lambda x,y: len(x.instances) - len(y.instances)):
|
for g in sorted(actual_groups, cmp=lambda x,y: len(x.instances) - len(y.instances)):
|
||||||
|
policy_per_added = []
|
||||||
for i in sorted(actual_instances, cmp=lambda x,y: len(x.groups) - len(y.groups)):
|
for i in sorted(actual_instances, cmp=lambda x,y: len(x.groups) - len(y.groups)):
|
||||||
if i.obj.id in g.instances:
|
if i.obj.id in g.instances:
|
||||||
# If the instance is already _in_ the group, it was
|
# If the instance is already _in_ the group, it was
|
||||||
@@ -209,15 +250,34 @@ def apply_cluster_membership_policies(self):
|
|||||||
continue
|
continue
|
||||||
if 100 * float(len(g.instances)) / len(actual_instances) >= g.obj.policy_instance_percentage:
|
if 100 * float(len(g.instances)) / len(actual_instances) >= g.obj.policy_instance_percentage:
|
||||||
break
|
break
|
||||||
logger.info(six.text_type("Policy percentage, adding Instance {} to Group {}").format(i.obj.hostname, g.obj.name))
|
|
||||||
g.instances.append(i.obj.id)
|
g.instances.append(i.obj.id)
|
||||||
i.groups.append(g.obj.id)
|
i.groups.append(g.obj.id)
|
||||||
|
policy_per_added.append(i.obj.id)
|
||||||
|
if policy_per_added:
|
||||||
|
logger.info(six.text_type("Policy percentage, adding Instances {} to Group {}").format(policy_per_added, g.obj.name))
|
||||||
|
|
||||||
|
# Determine if any changes need to be made
|
||||||
|
needs_change = False
|
||||||
|
for g in actual_groups:
|
||||||
|
if set(g.instances) != set(g.prior_instances):
|
||||||
|
needs_change = True
|
||||||
|
break
|
||||||
|
if not needs_change:
|
||||||
|
logger.info('Cluster policy no-op finished in {} seconds'.format(time.time() - started_compute))
|
||||||
|
return
|
||||||
|
|
||||||
# On a differential basis, apply instances to non-isolated groups
|
# On a differential basis, apply instances to non-isolated groups
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for g in actual_groups:
|
for g in actual_groups:
|
||||||
logger.info('Committing instances {} to group {}'.format(g.instances, g.obj.name))
|
instances_to_add = set(g.instances) - set(g.prior_instances)
|
||||||
g.obj.instances.set(g.instances)
|
instances_to_remove = set(g.prior_instances) - set(g.instances)
|
||||||
|
if instances_to_add:
|
||||||
|
logger.info('Adding instances {} to group {}'.format(list(instances_to_add), g.obj.name))
|
||||||
|
g.obj.instances.add(*instances_to_add)
|
||||||
|
if instances_to_remove:
|
||||||
|
logger.info('Removing instances {} from group {}'.format(list(instances_to_remove), g.obj.name))
|
||||||
|
g.obj.instances.remove(*instances_to_remove)
|
||||||
|
logger.info('Cluster policy computation finished in {} seconds'.format(time.time() - started_compute))
|
||||||
|
|
||||||
|
|
||||||
@shared_task(exchange='tower_broadcast_all', bind=True)
|
@shared_task(exchange='tower_broadcast_all', bind=True)
|
||||||
@@ -233,34 +293,6 @@ def handle_setting_changes(self, setting_keys):
|
|||||||
cache.delete_many(cache_keys)
|
cache.delete_many(cache_keys)
|
||||||
|
|
||||||
|
|
||||||
@celeryd_after_setup.connect
|
|
||||||
def auto_register_ha_instance(sender, instance, **kwargs):
|
|
||||||
#
|
|
||||||
# When celeryd starts, if the instance cannot be found in the database,
|
|
||||||
# automatically register it. This is mostly useful for openshift-based
|
|
||||||
# deployments where:
|
|
||||||
#
|
|
||||||
# 2 Instances come online
|
|
||||||
# Instance B encounters a network blip, Instance A notices, and
|
|
||||||
# deprovisions it
|
|
||||||
# Instance B's connectivity is restored, celeryd starts, and it
|
|
||||||
# re-registers itself
|
|
||||||
#
|
|
||||||
# In traditional container-less deployments, instances don't get
|
|
||||||
# deprovisioned when they miss their heartbeat, so this code is mostly a
|
|
||||||
# no-op.
|
|
||||||
#
|
|
||||||
if instance.hostname != 'celery@{}'.format(settings.CLUSTER_HOST_ID):
|
|
||||||
error = six.text_type('celery -n {} does not match settings.CLUSTER_HOST_ID={}').format(
|
|
||||||
instance.hostname, settings.CLUSTER_HOST_ID
|
|
||||||
)
|
|
||||||
logger.error(error)
|
|
||||||
raise RuntimeError(error)
|
|
||||||
(changed, tower_instance) = Instance.objects.get_or_register()
|
|
||||||
if changed:
|
|
||||||
logger.info(six.text_type("Registered tower node '{}'").format(tower_instance.hostname))
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue=settings.CELERY_DEFAULT_QUEUE)
|
@shared_task(queue=settings.CELERY_DEFAULT_QUEUE)
|
||||||
def send_notifications(notification_list, job_id=None):
|
def send_notifications(notification_list, job_id=None):
|
||||||
if not isinstance(notification_list, list):
|
if not isinstance(notification_list, list):
|
||||||
@@ -761,12 +793,12 @@ class BaseTask(Task):
|
|||||||
os.chmod(path, stat.S_IRUSR)
|
os.chmod(path, stat.S_IRUSR)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def add_ansible_venv(self, venv_path, env, add_awx_lib=True):
|
def add_ansible_venv(self, venv_path, env, add_awx_lib=True, **kwargs):
|
||||||
env['VIRTUAL_ENV'] = venv_path
|
env['VIRTUAL_ENV'] = venv_path
|
||||||
env['PATH'] = os.path.join(venv_path, "bin") + ":" + env['PATH']
|
env['PATH'] = os.path.join(venv_path, "bin") + ":" + env['PATH']
|
||||||
venv_libdir = os.path.join(venv_path, "lib")
|
venv_libdir = os.path.join(venv_path, "lib")
|
||||||
|
|
||||||
if not os.path.exists(venv_libdir):
|
if not kwargs.get('isolated', False) and not os.path.exists(venv_libdir):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'a valid Python virtualenv does not exist at {}'.format(venv_path)
|
'a valid Python virtualenv does not exist at {}'.format(venv_path)
|
||||||
)
|
)
|
||||||
@@ -1179,7 +1211,7 @@ class RunJob(BaseTask):
|
|||||||
plugin_dirs.extend(settings.AWX_ANSIBLE_CALLBACK_PLUGINS)
|
plugin_dirs.extend(settings.AWX_ANSIBLE_CALLBACK_PLUGINS)
|
||||||
plugin_path = ':'.join(plugin_dirs)
|
plugin_path = ':'.join(plugin_dirs)
|
||||||
env = super(RunJob, self).build_env(job, **kwargs)
|
env = super(RunJob, self).build_env(job, **kwargs)
|
||||||
env = self.add_ansible_venv(job.ansible_virtualenv_path, env, add_awx_lib=kwargs.get('isolated', False))
|
env = self.add_ansible_venv(job.ansible_virtualenv_path, env, add_awx_lib=kwargs.get('isolated', False), **kwargs)
|
||||||
# Set environment variables needed for inventory and job event
|
# Set environment variables needed for inventory and job event
|
||||||
# callbacks to work.
|
# callbacks to work.
|
||||||
env['JOB_ID'] = str(job.pk)
|
env['JOB_ID'] = str(job.pk)
|
||||||
@@ -2129,8 +2161,7 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
elif src == 'scm':
|
elif src == 'scm':
|
||||||
args.append(inventory_update.get_actual_source_path())
|
args.append(inventory_update.get_actual_source_path())
|
||||||
elif src == 'custom':
|
elif src == 'custom':
|
||||||
runpath = tempfile.mkdtemp(prefix='awx_inventory_', dir=settings.AWX_PROOT_BASE_PATH)
|
handle, path = tempfile.mkstemp(dir=kwargs['private_data_dir'])
|
||||||
handle, path = tempfile.mkstemp(dir=runpath)
|
|
||||||
f = os.fdopen(handle, 'w')
|
f = os.fdopen(handle, 'w')
|
||||||
if inventory_update.source_script is None:
|
if inventory_update.source_script is None:
|
||||||
raise RuntimeError('Inventory Script does not exist')
|
raise RuntimeError('Inventory Script does not exist')
|
||||||
@@ -2139,7 +2170,6 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
||||||
args.append(path)
|
args.append(path)
|
||||||
args.append("--custom")
|
args.append("--custom")
|
||||||
self.cleanup_paths.append(runpath)
|
|
||||||
args.append('-v%d' % inventory_update.verbosity)
|
args.append('-v%d' % inventory_update.verbosity)
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
args.append('--traceback')
|
args.append('--traceback')
|
||||||
|
|||||||
@@ -365,6 +365,116 @@ def test_inventory_source_vars_prohibition(post, inventory, admin_user):
|
|||||||
assert 'FOOBAR' in r.data['source_vars'][0]
|
assert 'FOOBAR' in r.data['source_vars'][0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestInventorySourceCredential:
|
||||||
|
def test_need_cloud_credential(self, inventory, admin_user, post):
|
||||||
|
"""Test that a cloud-based source requires credential"""
|
||||||
|
r = post(
|
||||||
|
url=reverse('api:inventory_source_list'),
|
||||||
|
data={'inventory': inventory.pk, 'name': 'foo', 'source': 'openstack'},
|
||||||
|
expect=400,
|
||||||
|
user=admin_user
|
||||||
|
)
|
||||||
|
assert 'Credential is required for a cloud source' in r.data['credential'][0]
|
||||||
|
|
||||||
|
def test_ec2_no_credential(self, inventory, admin_user, post):
|
||||||
|
"""Test that an ec2 inventory source can be added with no credential"""
|
||||||
|
post(
|
||||||
|
url=reverse('api:inventory_source_list'),
|
||||||
|
data={'inventory': inventory.pk, 'name': 'fobar', 'source': 'ec2'},
|
||||||
|
expect=201,
|
||||||
|
user=admin_user
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_validating_credential_type(self, organization, inventory, admin_user, post):
|
||||||
|
"""Test that cloud sources must use their respective credential type"""
|
||||||
|
from awx.main.models.credential import Credential, CredentialType
|
||||||
|
openstack = CredentialType.defaults['openstack']()
|
||||||
|
openstack.save()
|
||||||
|
os_cred = Credential.objects.create(
|
||||||
|
credential_type=openstack, name='bar', organization=organization)
|
||||||
|
r = post(
|
||||||
|
url=reverse('api:inventory_source_list'),
|
||||||
|
data={
|
||||||
|
'inventory': inventory.pk, 'name': 'fobar', 'source': 'ec2',
|
||||||
|
'credential': os_cred.pk
|
||||||
|
},
|
||||||
|
expect=400,
|
||||||
|
user=admin_user
|
||||||
|
)
|
||||||
|
assert 'Cloud-based inventory sources (such as ec2)' in r.data['credential'][0]
|
||||||
|
assert 'require credentials for the matching cloud service' in r.data['credential'][0]
|
||||||
|
|
||||||
|
def test_vault_credential_not_allowed(self, project, inventory, vault_credential, admin_user, post):
|
||||||
|
"""Vault credentials cannot be associated via the deprecated field"""
|
||||||
|
# TODO: when feature is added, add tests to use the related credentials
|
||||||
|
# endpoint for multi-vault attachment
|
||||||
|
r = post(
|
||||||
|
url=reverse('api:inventory_source_list'),
|
||||||
|
data={
|
||||||
|
'inventory': inventory.pk, 'name': 'fobar', 'source': 'scm',
|
||||||
|
'source_project': project.pk, 'source_path': '',
|
||||||
|
'credential': vault_credential.pk
|
||||||
|
},
|
||||||
|
expect=400,
|
||||||
|
user=admin_user
|
||||||
|
)
|
||||||
|
assert 'Credentials of type insights and vault' in r.data['credential'][0]
|
||||||
|
assert 'disallowed for scm inventory sources' in r.data['credential'][0]
|
||||||
|
|
||||||
|
def test_vault_credential_not_allowed_via_related(
|
||||||
|
self, project, inventory, vault_credential, admin_user, post):
|
||||||
|
"""Vault credentials cannot be associated via related endpoint"""
|
||||||
|
inv_src = InventorySource.objects.create(
|
||||||
|
inventory=inventory, name='foobar', source='scm',
|
||||||
|
source_project=project, source_path=''
|
||||||
|
)
|
||||||
|
r = post(
|
||||||
|
url=reverse('api:inventory_source_credentials_list', kwargs={'pk': inv_src.pk}),
|
||||||
|
data={
|
||||||
|
'id': vault_credential.pk
|
||||||
|
},
|
||||||
|
expect=400,
|
||||||
|
user=admin_user
|
||||||
|
)
|
||||||
|
assert 'Credentials of type insights and vault' in r.data['msg']
|
||||||
|
assert 'disallowed for scm inventory sources' in r.data['msg']
|
||||||
|
|
||||||
|
def test_credentials_relationship_mapping(self, project, inventory, organization, admin_user, post, patch):
|
||||||
|
"""The credentials relationship is used to manage the cloud credential
|
||||||
|
this test checks that replacement works"""
|
||||||
|
from awx.main.models.credential import Credential, CredentialType
|
||||||
|
openstack = CredentialType.defaults['openstack']()
|
||||||
|
openstack.save()
|
||||||
|
os_cred = Credential.objects.create(
|
||||||
|
credential_type=openstack, name='bar', organization=organization)
|
||||||
|
r = post(
|
||||||
|
url=reverse('api:inventory_source_list'),
|
||||||
|
data={
|
||||||
|
'inventory': inventory.pk, 'name': 'fobar', 'source': 'scm',
|
||||||
|
'source_project': project.pk, 'source_path': '',
|
||||||
|
'credential': os_cred.pk
|
||||||
|
},
|
||||||
|
expect=201,
|
||||||
|
user=admin_user
|
||||||
|
)
|
||||||
|
aws = CredentialType.defaults['aws']()
|
||||||
|
aws.save()
|
||||||
|
aws_cred = Credential.objects.create(
|
||||||
|
credential_type=aws, name='bar2', organization=organization)
|
||||||
|
inv_src = InventorySource.objects.get(pk=r.data['id'])
|
||||||
|
assert list(inv_src.credentials.values_list('id', flat=True)) == [os_cred.pk]
|
||||||
|
patch(
|
||||||
|
url=inv_src.get_absolute_url(),
|
||||||
|
data={
|
||||||
|
'credential': aws_cred.pk
|
||||||
|
},
|
||||||
|
expect=200,
|
||||||
|
user=admin_user
|
||||||
|
)
|
||||||
|
assert list(inv_src.credentials.values_list('id', flat=True)) == [aws_cred.pk]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestControlledBySCM:
|
class TestControlledBySCM:
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import json
|
|||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
|
from django.core.urlresolvers import resolve
|
||||||
|
from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
|
from awx.main.middleware import DeprecatedAuthTokenMiddleware
|
||||||
from awx.main.utils.encryption import decrypt_value, get_encryption_key
|
from awx.main.utils.encryption import decrypt_value, get_encryption_key
|
||||||
from awx.api.versioning import reverse, drf_reverse
|
from awx.api.versioning import reverse, drf_reverse
|
||||||
from awx.main.models.oauth import (OAuth2Application as Application,
|
from awx.main.models.oauth import (OAuth2Application as Application,
|
||||||
@@ -262,36 +265,6 @@ def test_oauth_list_user_tokens(oauth_application, post, get, admin, alice):
|
|||||||
assert response.data['count'] == 1
|
assert response.data['count'] == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_refresh_accesstoken(oauth_application, post, get, delete, admin):
|
|
||||||
response = post(
|
|
||||||
reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}),
|
|
||||||
{'scope': 'read'}, admin, expect=201
|
|
||||||
)
|
|
||||||
token = AccessToken.objects.get(token=response.data['token'])
|
|
||||||
refresh_token = RefreshToken.objects.get(token=response.data['refresh_token'])
|
|
||||||
assert AccessToken.objects.count() == 1
|
|
||||||
assert RefreshToken.objects.count() == 1
|
|
||||||
|
|
||||||
refresh_url = drf_reverse('api:oauth_authorization_root_view') + 'token/'
|
|
||||||
response = post(
|
|
||||||
refresh_url,
|
|
||||||
data='grant_type=refresh_token&refresh_token=' + refresh_token.token,
|
|
||||||
content_type='application/x-www-form-urlencoded',
|
|
||||||
HTTP_AUTHORIZATION='Basic ' + base64.b64encode(':'.join([
|
|
||||||
oauth_application.client_id, oauth_application.client_secret
|
|
||||||
]))
|
|
||||||
)
|
|
||||||
|
|
||||||
new_token = json.loads(response._container[0])['access_token']
|
|
||||||
new_refresh_token = json.loads(response._container[0])['refresh_token']
|
|
||||||
assert token not in AccessToken.objects.all()
|
|
||||||
assert AccessToken.objects.get(token=new_token) != 0
|
|
||||||
assert RefreshToken.objects.get(token=new_refresh_token) != 0
|
|
||||||
refresh_token = RefreshToken.objects.get(token=refresh_token)
|
|
||||||
assert refresh_token.revoked
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_implicit_authorization(oauth_application, admin):
|
def test_implicit_authorization(oauth_application, admin):
|
||||||
oauth_application.client_type = 'confidential'
|
oauth_application.client_type = 'confidential'
|
||||||
@@ -314,3 +287,117 @@ def test_implicit_authorization(oauth_application, admin):
|
|||||||
assert 'http://test.com' in response.url and 'access_token' in response.url
|
assert 'http://test.com' in response.url and 'access_token' in response.url
|
||||||
# Make sure no refresh token is created for app with implicit grant type.
|
# Make sure no refresh token is created for app with implicit grant type.
|
||||||
assert refresh_token_count == RefreshToken.objects.count()
|
assert refresh_token_count == RefreshToken.objects.count()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_refresh_accesstoken(oauth_application, post, get, delete, admin):
|
||||||
|
response = post(
|
||||||
|
reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}),
|
||||||
|
{'scope': 'read'}, admin, expect=201
|
||||||
|
)
|
||||||
|
assert AccessToken.objects.count() == 1
|
||||||
|
assert RefreshToken.objects.count() == 1
|
||||||
|
token = AccessToken.objects.get(token=response.data['token'])
|
||||||
|
refresh_token = RefreshToken.objects.get(token=response.data['refresh_token'])
|
||||||
|
|
||||||
|
refresh_url = drf_reverse('api:oauth_authorization_root_view') + 'token/'
|
||||||
|
response = post(
|
||||||
|
refresh_url,
|
||||||
|
data='grant_type=refresh_token&refresh_token=' + refresh_token.token,
|
||||||
|
content_type='application/x-www-form-urlencoded',
|
||||||
|
HTTP_AUTHORIZATION='Basic ' + base64.b64encode(':'.join([
|
||||||
|
oauth_application.client_id, oauth_application.client_secret
|
||||||
|
]))
|
||||||
|
)
|
||||||
|
assert RefreshToken.objects.filter(token=refresh_token).exists()
|
||||||
|
original_refresh_token = RefreshToken.objects.get(token=refresh_token)
|
||||||
|
assert token not in AccessToken.objects.all()
|
||||||
|
assert AccessToken.objects.count() == 1
|
||||||
|
# the same RefreshToken remains but is marked revoked
|
||||||
|
assert RefreshToken.objects.count() == 2
|
||||||
|
new_token = json.loads(response._container[0])['access_token']
|
||||||
|
new_refresh_token = json.loads(response._container[0])['refresh_token']
|
||||||
|
assert AccessToken.objects.filter(token=new_token).count() == 1
|
||||||
|
# checks that RefreshTokens are rotated (new RefreshToken issued)
|
||||||
|
assert RefreshToken.objects.filter(token=new_refresh_token).count() == 1
|
||||||
|
assert original_refresh_token.revoked # is not None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_revoke_access_then_refreshtoken(oauth_application, post, get, delete, admin):
|
||||||
|
response = post(
|
||||||
|
reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}),
|
||||||
|
{'scope': 'read'}, admin, expect=201
|
||||||
|
)
|
||||||
|
token = AccessToken.objects.get(token=response.data['token'])
|
||||||
|
refresh_token = RefreshToken.objects.get(token=response.data['refresh_token'])
|
||||||
|
assert AccessToken.objects.count() == 1
|
||||||
|
assert RefreshToken.objects.count() == 1
|
||||||
|
|
||||||
|
token.revoke()
|
||||||
|
assert AccessToken.objects.count() == 0
|
||||||
|
assert RefreshToken.objects.count() == 1
|
||||||
|
assert not refresh_token.revoked
|
||||||
|
|
||||||
|
refresh_token.revoke()
|
||||||
|
assert AccessToken.objects.count() == 0
|
||||||
|
assert RefreshToken.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_revoke_refreshtoken(oauth_application, post, get, delete, admin):
|
||||||
|
response = post(
|
||||||
|
reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}),
|
||||||
|
{'scope': 'read'}, admin, expect=201
|
||||||
|
)
|
||||||
|
refresh_token = RefreshToken.objects.get(token=response.data['refresh_token'])
|
||||||
|
assert AccessToken.objects.count() == 1
|
||||||
|
assert RefreshToken.objects.count() == 1
|
||||||
|
|
||||||
|
refresh_token.revoke()
|
||||||
|
assert AccessToken.objects.count() == 0
|
||||||
|
# the same RefreshToken is recycled
|
||||||
|
new_refresh_token = RefreshToken.objects.all().first()
|
||||||
|
assert refresh_token == new_refresh_token
|
||||||
|
assert new_refresh_token.revoked
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize('fmt', ['json', 'multipart'])
|
||||||
|
def test_deprecated_authtoken_support(alice, fmt):
|
||||||
|
kwargs = {
|
||||||
|
'data': {'username': 'alice', 'password': 'alice'},
|
||||||
|
'format': fmt
|
||||||
|
}
|
||||||
|
request = getattr(APIRequestFactory(), 'post')('/api/v2/authtoken/', **kwargs)
|
||||||
|
DeprecatedAuthTokenMiddleware().process_request(request)
|
||||||
|
assert request.path == request.path_info == '/api/v2/users/{}/personal_tokens/'.format(alice.pk)
|
||||||
|
view, view_args, view_kwargs = resolve(request.path)
|
||||||
|
resp = view(request, *view_args, **view_kwargs)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert 'token' in resp.data
|
||||||
|
assert resp.data['refresh_token'] is None
|
||||||
|
assert resp.data['scope'] == 'write'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_deprecated_authtoken_invalid_username(alice):
|
||||||
|
kwargs = {
|
||||||
|
'data': {'username': 'nobody', 'password': 'nobody'},
|
||||||
|
'format': 'json'
|
||||||
|
}
|
||||||
|
request = getattr(APIRequestFactory(), 'post')('/api/v2/authtoken/', **kwargs)
|
||||||
|
resp = DeprecatedAuthTokenMiddleware().process_request(request)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_deprecated_authtoken_missing_credentials(alice):
|
||||||
|
kwargs = {
|
||||||
|
'data': {},
|
||||||
|
'format': 'json'
|
||||||
|
}
|
||||||
|
request = getattr(APIRequestFactory(), 'post')('/api/v2/authtoken/', **kwargs)
|
||||||
|
resp = DeprecatedAuthTokenMiddleware().process_request(request)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ class TestOAuth2Application:
|
|||||||
)
|
)
|
||||||
assert access.can_read(app) is can_access
|
assert access.can_read(app) is can_access
|
||||||
|
|
||||||
|
def test_admin_only_can_read(self, user, organization):
|
||||||
|
user = user('org-admin', False)
|
||||||
|
organization.admin_role.members.add(user)
|
||||||
|
access = OAuth2ApplicationAccess(user)
|
||||||
|
app = Application.objects.create(
|
||||||
|
name='test app for {}'.format(user.username), user=user,
|
||||||
|
client_type='confidential', authorization_grant_type='password', organization=organization
|
||||||
|
)
|
||||||
|
assert access.can_read(app) is True
|
||||||
|
|
||||||
def test_app_activity_stream(self, org_admin, alice, organization):
|
def test_app_activity_stream(self, org_admin, alice, organization):
|
||||||
app = Application.objects.create(
|
app = Application.objects.create(
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ def test_really_long_event_fields(field):
|
|||||||
with mock.patch.object(JobEvent, 'objects') as manager:
|
with mock.patch.object(JobEvent, 'objects') as manager:
|
||||||
JobEvent.create_from_data(**{
|
JobEvent.create_from_data(**{
|
||||||
'job_id': 123,
|
'job_id': 123,
|
||||||
field: 'X' * 4096
|
'event_data': {field: 'X' * 4096}
|
||||||
})
|
})
|
||||||
manager.create.assert_called_with(**{
|
manager.create.assert_called_with(**{
|
||||||
'job_id': 123,
|
'job_id': 123,
|
||||||
field: 'X' * 1021 + '...'
|
'event_data': {field: 'X' * 1021 + '...'}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
# Python
|
# Python
|
||||||
import pytest
|
import pytest
|
||||||
import mock
|
import mock
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.utils.filters import SmartFilter, ExternalLoggerEnabled
|
from awx.main.utils.filters import SmartFilter, ExternalLoggerEnabled
|
||||||
@@ -44,8 +43,26 @@ def test_log_configurable_severity(level, expect, dummy_log_record):
|
|||||||
assert filter.filter(dummy_log_record) is expect
|
assert filter.filter(dummy_log_record) is expect
|
||||||
|
|
||||||
|
|
||||||
Field = namedtuple('Field', 'name')
|
class Field(object):
|
||||||
Meta = namedtuple('Meta', 'fields')
|
|
||||||
|
def __init__(self, name, related_model=None, __prevent_search__=None):
|
||||||
|
self.name = name
|
||||||
|
self.related_model = related_model
|
||||||
|
self.__prevent_search__ = __prevent_search__
|
||||||
|
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
|
||||||
|
def __init__(self, fields):
|
||||||
|
self._fields = {
|
||||||
|
f.name: f for f in fields
|
||||||
|
}
|
||||||
|
self.object_name = 'Host'
|
||||||
|
self.fields_map = {}
|
||||||
|
self.fields = self._fields.values()
|
||||||
|
|
||||||
|
def get_field(self, f):
|
||||||
|
return self._fields.get(f)
|
||||||
|
|
||||||
|
|
||||||
class mockObjects:
|
class mockObjects:
|
||||||
@@ -53,15 +70,32 @@ class mockObjects:
|
|||||||
return Q(*args, **kwargs)
|
return Q(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class mockUser:
|
||||||
|
def __init__(self):
|
||||||
|
print("Host user created")
|
||||||
|
self._meta = Meta(fields=[
|
||||||
|
Field(name='password', __prevent_search__=True)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
class mockHost:
|
class mockHost:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
print("Host mock created")
|
print("Host mock created")
|
||||||
self.objects = mockObjects()
|
self.objects = mockObjects()
|
||||||
self._meta = Meta(fields=(Field(name='name'), Field(name='description')))
|
fields = [
|
||||||
|
Field(name='name'),
|
||||||
|
Field(name='description'),
|
||||||
|
Field(name='created_by', related_model=mockUser())
|
||||||
|
]
|
||||||
|
self._meta = Meta(fields=fields)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('awx.main.utils.filters.get_model', return_value=mockHost())
|
@mock.patch('awx.main.utils.filters.get_model', return_value=mockHost())
|
||||||
class TestSmartFilterQueryFromString():
|
class TestSmartFilterQueryFromString():
|
||||||
|
@mock.patch(
|
||||||
|
'awx.api.filters.get_field_from_path',
|
||||||
|
lambda model, path: (model, path) # disable field filtering, because a__b isn't a real Host field
|
||||||
|
)
|
||||||
@pytest.mark.parametrize("filter_string,q_expected", [
|
@pytest.mark.parametrize("filter_string,q_expected", [
|
||||||
('facts__facts__blank=""', Q(**{u"facts__facts__blank": u""})),
|
('facts__facts__blank=""', Q(**{u"facts__facts__blank": u""})),
|
||||||
('"facts__facts__ space "="f"', Q(**{u"facts__facts__ space ": u"f"})),
|
('"facts__facts__ space "="f"', Q(**{u"facts__facts__ space ": u"f"})),
|
||||||
@@ -88,6 +122,16 @@ class TestSmartFilterQueryFromString():
|
|||||||
SmartFilter.query_from_string(filter_string)
|
SmartFilter.query_from_string(filter_string)
|
||||||
assert e.value.message == u"Invalid query " + filter_string
|
assert e.value.message == u"Invalid query " + filter_string
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("filter_string", [
|
||||||
|
'created_by__password__icontains=pbkdf2'
|
||||||
|
'search=foo or created_by__password__icontains=pbkdf2',
|
||||||
|
'created_by__password__icontains=pbkdf2 or search=foo',
|
||||||
|
])
|
||||||
|
def test_forbidden_filter_string(self, mock_get_host_model, filter_string):
|
||||||
|
with pytest.raises(Exception) as e:
|
||||||
|
SmartFilter.query_from_string(filter_string)
|
||||||
|
"Filtering on password is not allowed." in str(e)
|
||||||
|
|
||||||
@pytest.mark.parametrize("filter_string,q_expected", [
|
@pytest.mark.parametrize("filter_string,q_expected", [
|
||||||
(u'(a=abc\u1F5E3def)', Q(**{u"a": u"abc\u1F5E3def"})),
|
(u'(a=abc\u1F5E3def)', Q(**{u"a": u"abc\u1F5E3def"})),
|
||||||
(u'(ansible_facts__a=abc\u1F5E3def)', Q(**{u"ansible_facts__contains": {u"a": u"abc\u1F5E3def"}})),
|
(u'(ansible_facts__a=abc\u1F5E3def)', Q(**{u"ansible_facts__contains": {u"a": u"abc\u1F5E3def"}})),
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright (c) 2017 Ansible Tower by Red Hat
|
|
||||||
# All Rights Reserved.
|
|
||||||
|
|
||||||
# python
|
|
||||||
import pytest
|
|
||||||
import mock
|
|
||||||
|
|
||||||
# AWX
|
|
||||||
from awx.main.utils.ha import (
|
|
||||||
AWXCeleryRouter,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestAddRemoveCeleryWorkerQueues():
|
|
||||||
@pytest.fixture
|
|
||||||
def instance_generator(self, mocker):
|
|
||||||
def fn(hostname='east-1'):
|
|
||||||
groups=['east', 'west', 'north', 'south']
|
|
||||||
instance = mocker.MagicMock()
|
|
||||||
instance.hostname = hostname
|
|
||||||
instance.rampart_groups = mocker.MagicMock()
|
|
||||||
instance.rampart_groups.values_list = mocker.MagicMock(return_value=groups)
|
|
||||||
|
|
||||||
return instance
|
|
||||||
return fn
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def worker_queues_generator(self, mocker):
|
|
||||||
def fn(queues=['east', 'west']):
|
|
||||||
return [dict(name=n, alias='') for n in queues]
|
|
||||||
return fn
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_app(self, mocker):
|
|
||||||
app = mocker.MagicMock()
|
|
||||||
app.control = mocker.MagicMock()
|
|
||||||
app.control.cancel_consumer = mocker.MagicMock()
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
class TestUpdateCeleryWorkerRouter():
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_controller,expected_routes", [
|
|
||||||
(False, {
|
|
||||||
'awx.main.tasks.cluster_node_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'},
|
|
||||||
'awx.main.tasks.purge_old_stdout_files': {'queue': 'east-1', 'routing_key': 'east-1'}
|
|
||||||
}),
|
|
||||||
(True, {
|
|
||||||
'awx.main.tasks.cluster_node_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'},
|
|
||||||
'awx.main.tasks.purge_old_stdout_files': {'queue': 'east-1', 'routing_key': 'east-1'},
|
|
||||||
'awx.main.tasks.awx_isolated_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'},
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
def test_update_celery_worker_routes(self, mocker, is_controller, expected_routes):
|
|
||||||
def get_or_register():
|
|
||||||
instance = mock.MagicMock()
|
|
||||||
instance.hostname = 'east-1'
|
|
||||||
instance.is_controller = mock.MagicMock(return_value=is_controller)
|
|
||||||
return (False, instance)
|
|
||||||
|
|
||||||
with mock.patch('awx.main.models.Instance.objects.get_or_register', get_or_register):
|
|
||||||
router = AWXCeleryRouter()
|
|
||||||
|
|
||||||
for k,v in expected_routes.iteritems():
|
|
||||||
assert router.route_for_task(k) == v
|
|
||||||
|
|
||||||
@@ -147,6 +147,10 @@ class SmartFilter(object):
|
|||||||
q = reduce(lambda x, y: x | y, [models.Q(**{u'%s__icontains' % _k:_v}) for _k, _v in kwargs.items()])
|
q = reduce(lambda x, y: x | y, [models.Q(**{u'%s__icontains' % _k:_v}) for _k, _v in kwargs.items()])
|
||||||
self.result = Host.objects.filter(q)
|
self.result = Host.objects.filter(q)
|
||||||
else:
|
else:
|
||||||
|
# detect loops and restrict access to sensitive fields
|
||||||
|
# this import is intentional here to avoid a circular import
|
||||||
|
from awx.api.filters import FieldLookupBackend
|
||||||
|
FieldLookupBackend().get_field_from_lookup(Host, k)
|
||||||
kwargs[k] = v
|
kwargs[k] = v
|
||||||
self.result = Host.objects.filter(**kwargs)
|
self.result = Host.objects.filter(**kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,15 @@
|
|||||||
# Copyright (c) 2017 Ansible Tower by Red Hat
|
# Copyright (c) 2017 Ansible Tower by Red Hat
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
from awx.main.models import Instance
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
class AWXCeleryRouter(object):
|
class AWXCeleryRouter(object):
|
||||||
def route_for_task(self, task, args=None, kwargs=None):
|
def route_for_task(self, task, args=None, kwargs=None):
|
||||||
(changed, instance) = Instance.objects.get_or_register()
|
|
||||||
tasks = [
|
tasks = [
|
||||||
'awx.main.tasks.cluster_node_heartbeat',
|
'awx.main.tasks.cluster_node_heartbeat',
|
||||||
'awx.main.tasks.purge_old_stdout_files',
|
'awx.main.tasks.purge_old_stdout_files',
|
||||||
]
|
|
||||||
isolated_tasks = [
|
|
||||||
'awx.main.tasks.awx_isolated_heartbeat',
|
'awx.main.tasks.awx_isolated_heartbeat',
|
||||||
]
|
]
|
||||||
if task in tasks:
|
if task in tasks:
|
||||||
return {'queue': instance.hostname.encode("utf8"), 'routing_key': instance.hostname.encode("utf8")}
|
return {'queue': settings.CLUSTER_HOST_ID, 'routing_key': settings.CLUSTER_HOST_ID}
|
||||||
|
|
||||||
if instance.is_controller() and task in isolated_tasks:
|
|
||||||
return {'queue': instance.hostname.encode("utf8"), 'routing_key': instance.hostname.encode("utf8")}
|
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ MIDDLEWARE_CLASSES = ( # NOQA
|
|||||||
'awx.sso.middleware.SocialAuthMiddleware',
|
'awx.sso.middleware.SocialAuthMiddleware',
|
||||||
'crum.CurrentRequestUserMiddleware',
|
'crum.CurrentRequestUserMiddleware',
|
||||||
'awx.main.middleware.URLModificationMiddleware',
|
'awx.main.middleware.URLModificationMiddleware',
|
||||||
|
'awx.main.middleware.DeprecatedAuthTokenMiddleware',
|
||||||
'awx.main.middleware.SessionTimeoutMiddleware',
|
'awx.main.middleware.SessionTimeoutMiddleware',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1177,16 +1178,13 @@ LOGGING = {
|
|||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'awx.main.access': {
|
'awx.main.access': {
|
||||||
'handlers': ['null'],
|
'level': 'INFO', # very verbose debug-level logs
|
||||||
'propagate': False,
|
|
||||||
},
|
},
|
||||||
'awx.main.signals': {
|
'awx.main.signals': {
|
||||||
'handlers': ['null'],
|
'level': 'INFO', # very verbose debug-level logs
|
||||||
'propagate': False,
|
|
||||||
},
|
},
|
||||||
'awx.api.permissions': {
|
'awx.api.permissions': {
|
||||||
'handlers': ['null'],
|
'level': 'INFO', # very verbose debug-level logs
|
||||||
'propagate': False,
|
|
||||||
},
|
},
|
||||||
'awx.analytics': {
|
'awx.analytics': {
|
||||||
'handlers': ['external_logger'],
|
'handlers': ['external_logger'],
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
@import 'portalMode/_index';
|
@import 'portalMode/_index';
|
||||||
@import 'output/_index';
|
@import 'output/_index';
|
||||||
@import 'users/tokens/_index';
|
|
||||||
|
/** @define Popup Modal after create new token and applicaiton and save form */
|
||||||
|
.PopupModal {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PopupModal-label {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PopupModal-value {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
function AddApplicationsController (models, $state, strings, $scope) {
|
function AddApplicationsController (models, $state, strings, $scope, Alert, $filter) {
|
||||||
const vm = this || {};
|
const vm = this || {};
|
||||||
|
|
||||||
const { application, me, organization } = models;
|
const { application, me, organization } = models;
|
||||||
@@ -60,6 +60,41 @@ function AddApplicationsController (models, $state, strings, $scope) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vm.form.onSaveSuccess = res => {
|
vm.form.onSaveSuccess = res => {
|
||||||
|
if (res.data && res.data.client_id) {
|
||||||
|
const name = res.data.name ?
|
||||||
|
`<div class="PopupModal">
|
||||||
|
<div class="PopupModal-label">
|
||||||
|
${strings.get('add.NAME_LABEL')}
|
||||||
|
</div>
|
||||||
|
<div class="PopupModal-value">
|
||||||
|
${$filter('sanitize')(res.data.name)}
|
||||||
|
</div>
|
||||||
|
</div>` : '';
|
||||||
|
const clientId = res.data.client_id ?
|
||||||
|
`<div class="PopupModal">
|
||||||
|
<div class="PopupModal-label">
|
||||||
|
${strings.get('add.CLIENT_ID_LABEL')}
|
||||||
|
</div>
|
||||||
|
<div class="PopupModal-value">
|
||||||
|
${res.data.client_id}
|
||||||
|
</div>
|
||||||
|
</div>` : '';
|
||||||
|
const clientSecret = res.data.client_secret ?
|
||||||
|
`<div class="PopupModal">
|
||||||
|
<div class="PopupModal-label">
|
||||||
|
${strings.get('add.CLIENT_SECRECT_LABEL')}
|
||||||
|
</div>
|
||||||
|
<div class="PopupModal-value">
|
||||||
|
${res.data.client_secret}
|
||||||
|
</div>
|
||||||
|
</div>` : '';
|
||||||
|
|
||||||
|
Alert(strings.get('add.MODAL_HEADER'), `
|
||||||
|
${name}
|
||||||
|
${clientId}
|
||||||
|
${clientSecret}
|
||||||
|
`, null, null, null, null, null, true);
|
||||||
|
}
|
||||||
$state.go('applications.edit', { application_id: res.data.id }, { reload: true });
|
$state.go('applications.edit', { application_id: res.data.id }, { reload: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,7 +109,9 @@ AddApplicationsController.$inject = [
|
|||||||
'resolvedModels',
|
'resolvedModels',
|
||||||
'$state',
|
'$state',
|
||||||
'ApplicationsStrings',
|
'ApplicationsStrings',
|
||||||
'$scope'
|
'$scope',
|
||||||
|
'Alert',
|
||||||
|
'$filter',
|
||||||
];
|
];
|
||||||
|
|
||||||
export default AddApplicationsController;
|
export default AddApplicationsController;
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ function ApplicationsStrings (BaseString) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ns.add = {
|
ns.add = {
|
||||||
PANEL_TITLE: t.s('NEW APPLICATION')
|
PANEL_TITLE: t.s('NEW APPLICATION'),
|
||||||
|
CLIENT_ID_LABEL: t.s('CLIENT ID'),
|
||||||
|
CLIENT_SECRECT_LABEL: t.s('CLIENT SECRET'),
|
||||||
|
MODAL_HEADER: t.s('APPLICATION INFORMATION'),
|
||||||
|
NAME_LABEL: t.s('NAME'),
|
||||||
};
|
};
|
||||||
|
|
||||||
ns.list = {
|
ns.list = {
|
||||||
|
|||||||
@@ -38,6 +38,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-menuIcon--md {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @at-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-menuIcon--lg {
|
&-menuIcon--lg {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
line-height: 12px;
|
line-height: 12px;
|
||||||
@@ -94,6 +104,10 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-line--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&-event {
|
&-event {
|
||||||
.at-mixin-event();
|
.at-mixin-event();
|
||||||
}
|
}
|
||||||
@@ -138,6 +152,10 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
@media screen and (max-width: @breakpoint-md) {
|
||||||
|
max-height: calc(100vh - 30px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-borderHeader {
|
&-borderHeader {
|
||||||
|
|||||||
@@ -13,9 +13,30 @@ export const JOB_STATUS_INCOMPLETE = ['canceled', 'error'];
|
|||||||
export const JOB_STATUS_UNSUCCESSFUL = ['failed'].concat(JOB_STATUS_INCOMPLETE);
|
export const JOB_STATUS_UNSUCCESSFUL = ['failed'].concat(JOB_STATUS_INCOMPLETE);
|
||||||
export const JOB_STATUS_FINISHED = JOB_STATUS_COMPLETE.concat(JOB_STATUS_INCOMPLETE);
|
export const JOB_STATUS_FINISHED = JOB_STATUS_COMPLETE.concat(JOB_STATUS_INCOMPLETE);
|
||||||
|
|
||||||
|
export const OUTPUT_ANSI_COLORMAP = {
|
||||||
|
0: '#000',
|
||||||
|
1: '#A00',
|
||||||
|
2: '#0A0',
|
||||||
|
3: '#F0AD4E',
|
||||||
|
4: '#00A',
|
||||||
|
5: '#A0A',
|
||||||
|
6: '#0AA',
|
||||||
|
7: '#AAA',
|
||||||
|
8: '#555',
|
||||||
|
9: '#F55',
|
||||||
|
10: '#5F5',
|
||||||
|
11: '#FF5',
|
||||||
|
12: '#55F',
|
||||||
|
13: '#F5F',
|
||||||
|
14: '#5FF',
|
||||||
|
15: '#FFF'
|
||||||
|
};
|
||||||
export const OUTPUT_ELEMENT_CONTAINER = '.at-Stdout-container';
|
export const OUTPUT_ELEMENT_CONTAINER = '.at-Stdout-container';
|
||||||
export const OUTPUT_ELEMENT_TBODY = '#atStdoutResultTable';
|
export const OUTPUT_ELEMENT_TBODY = '#atStdoutResultTable';
|
||||||
|
export const OUTPUT_ELEMENT_LAST = '#atStdoutMenuLast';
|
||||||
|
export const OUTPUT_MAX_BUFFER_LENGTH = 1000;
|
||||||
export const OUTPUT_MAX_LAG = 120;
|
export const OUTPUT_MAX_LAG = 120;
|
||||||
|
export const OUTPUT_NO_COUNT_JOB_TYPES = ['ad_hoc_command', 'system_job', 'inventory_update'];
|
||||||
export const OUTPUT_ORDER_BY = 'counter';
|
export const OUTPUT_ORDER_BY = 'counter';
|
||||||
export const OUTPUT_PAGE_CACHE = true;
|
export const OUTPUT_PAGE_CACHE = true;
|
||||||
export const OUTPUT_PAGE_LIMIT = 5;
|
export const OUTPUT_PAGE_LIMIT = 5;
|
||||||
@@ -23,8 +44,8 @@ export const OUTPUT_PAGE_SIZE = 50;
|
|||||||
export const OUTPUT_SCROLL_DELAY = 100;
|
export const OUTPUT_SCROLL_DELAY = 100;
|
||||||
export const OUTPUT_SCROLL_THRESHOLD = 0.1;
|
export const OUTPUT_SCROLL_THRESHOLD = 0.1;
|
||||||
export const OUTPUT_SEARCH_DOCLINK = 'https://docs.ansible.com/ansible-tower/3.3.0/html/userguide/search_sort.html';
|
export const OUTPUT_SEARCH_DOCLINK = 'https://docs.ansible.com/ansible-tower/3.3.0/html/userguide/search_sort.html';
|
||||||
export const OUTPUT_SEARCH_FIELDS = ['changed', 'created', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play'];
|
export const OUTPUT_SEARCH_FIELDS = ['changed', 'created', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play', 'start_line', 'end_line'];
|
||||||
export const OUTPUT_SEARCH_KEY_EXAMPLES = ['host_name:localhost', 'task:set', 'created:>=2000-01-01'];
|
export const OUTPUT_SEARCH_KEY_EXAMPLES = ['host_name:localhost', 'task:set', 'created:>=2000-01-01', 'start_line:>=9000'];
|
||||||
export const OUTPUT_EVENT_LIMIT = OUTPUT_PAGE_LIMIT * OUTPUT_PAGE_SIZE;
|
export const OUTPUT_EVENT_LIMIT = OUTPUT_PAGE_LIMIT * OUTPUT_PAGE_SIZE;
|
||||||
|
|
||||||
export const WS_PREFIX = 'ws';
|
export const WS_PREFIX = 'ws';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
EVENT_START_PLAY,
|
EVENT_START_PLAY,
|
||||||
EVENT_START_TASK,
|
EVENT_START_TASK,
|
||||||
|
OUTPUT_ELEMENT_LAST,
|
||||||
OUTPUT_PAGE_SIZE,
|
OUTPUT_PAGE_SIZE,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
@@ -16,61 +17,21 @@ let scroll;
|
|||||||
let status;
|
let status;
|
||||||
let slide;
|
let slide;
|
||||||
let stream;
|
let stream;
|
||||||
|
let page;
|
||||||
|
|
||||||
let vm;
|
let vm;
|
||||||
|
|
||||||
const bufferState = [0, 0]; // [length, count]
|
|
||||||
const listeners = [];
|
const listeners = [];
|
||||||
const rx = [];
|
let lockFrames = false;
|
||||||
|
|
||||||
function bufferInit () {
|
|
||||||
rx.length = 0;
|
|
||||||
|
|
||||||
bufferState[0] = 0;
|
|
||||||
bufferState[1] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bufferAdd (event) {
|
|
||||||
rx.push(event);
|
|
||||||
|
|
||||||
bufferState[0] += 1;
|
|
||||||
bufferState[1] += 1;
|
|
||||||
|
|
||||||
return bufferState[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
function bufferEmpty (min, max) {
|
|
||||||
let count = 0;
|
|
||||||
let removed = [];
|
|
||||||
|
|
||||||
for (let i = bufferState[0] - 1; i >= 0; i--) {
|
|
||||||
if (rx[i].counter <= max) {
|
|
||||||
removed = removed.concat(rx.splice(i, 1));
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferState[0] -= count;
|
|
||||||
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lockFrames;
|
|
||||||
function onFrames (events) {
|
function onFrames (events) {
|
||||||
if (lockFrames) {
|
|
||||||
events.forEach(bufferAdd);
|
|
||||||
return $q.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
events = slide.pushFrames(events);
|
events = slide.pushFrames(events);
|
||||||
const popCount = events.length - slide.getCapacity();
|
|
||||||
const isAttached = events.length > 0;
|
|
||||||
|
|
||||||
if (!isAttached) {
|
if (lockFrames) {
|
||||||
stopFollowing();
|
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const popCount = events.length - render.getCapacity();
|
||||||
|
|
||||||
if (!vm.isFollowing && canStartFollowing()) {
|
if (!vm.isFollowing && canStartFollowing()) {
|
||||||
startFollowing();
|
startFollowing();
|
||||||
}
|
}
|
||||||
@@ -85,13 +46,13 @@ function onFrames (events) {
|
|||||||
scroll.scrollToBottom();
|
scroll.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
return slide.popBack(popCount)
|
return render.popBack(popCount)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (vm.isFollowing) {
|
if (vm.isFollowing) {
|
||||||
scroll.scrollToBottom();
|
scroll.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
return slide.pushFront(events);
|
return render.pushFront(events);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (vm.isFollowing) {
|
if (vm.isFollowing) {
|
||||||
@@ -104,27 +65,44 @@ function onFrames (events) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function first () {
|
//
|
||||||
|
// Menu Controls (Running)
|
||||||
|
//
|
||||||
|
|
||||||
|
function firstRange () {
|
||||||
if (scroll.isPaused()) {
|
if (scroll.isPaused()) {
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopFollowing();
|
||||||
|
lockFollow = true;
|
||||||
|
|
||||||
|
if (slide.isOnFirstPage()) {
|
||||||
|
scroll.resetScrollPosition();
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
scroll.pause();
|
scroll.pause();
|
||||||
lockFrames = true;
|
lockFrames = true;
|
||||||
|
|
||||||
stopFollowing();
|
return render.clear()
|
||||||
|
.then(() => slide.getFirst())
|
||||||
|
.then(results => render.pushFront(results))
|
||||||
|
.then(() => slide.getNext())
|
||||||
|
.then(results => {
|
||||||
|
const popCount = results.length - render.getCapacity();
|
||||||
|
|
||||||
return slide.getFirst()
|
return render.popBack(popCount)
|
||||||
.then(() => {
|
.then(() => render.pushFront(results));
|
||||||
scroll.resetScrollPosition();
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
scroll.resume();
|
scroll.resume();
|
||||||
lockFrames = false;
|
lockFollow = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function next () {
|
function nextRange () {
|
||||||
if (vm.isFollowing) {
|
if (vm.isFollowing) {
|
||||||
scroll.scrollToBottom();
|
scroll.scrollToBottom();
|
||||||
|
|
||||||
@@ -135,34 +113,49 @@ function next () {
|
|||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slide.getTailCounter() >= slide.getMaxCounter()) {
|
|
||||||
return $q.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
scroll.pause();
|
scroll.pause();
|
||||||
lockFrames = true;
|
lockFrames = true;
|
||||||
|
|
||||||
return slide.getNext()
|
return slide.getNext()
|
||||||
|
.then(results => {
|
||||||
|
const popCount = results.length - render.getCapacity();
|
||||||
|
|
||||||
|
return render.popBack(popCount)
|
||||||
|
.then(() => render.pushFront(results));
|
||||||
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
scroll.resume();
|
scroll.resume();
|
||||||
lockFrames = false;
|
lockFrames = false;
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function previous () {
|
function previousRange () {
|
||||||
if (scroll.isPaused()) {
|
if (scroll.isPaused()) {
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
scroll.pause();
|
scroll.pause();
|
||||||
|
stopFollowing();
|
||||||
lockFrames = true;
|
lockFrames = true;
|
||||||
|
|
||||||
stopFollowing();
|
let initialPosition;
|
||||||
|
let popHeight;
|
||||||
const initialPosition = scroll.getScrollPosition();
|
|
||||||
|
|
||||||
return slide.getPrevious()
|
return slide.getPrevious()
|
||||||
.then(popHeight => {
|
.then(results => {
|
||||||
|
const popCount = results.length - render.getCapacity();
|
||||||
|
initialPosition = scroll.getScrollPosition();
|
||||||
|
|
||||||
|
return render.popFront(popCount)
|
||||||
|
.then(() => {
|
||||||
|
popHeight = scroll.getScrollHeight();
|
||||||
|
|
||||||
|
return render.pushBack(results);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
const currentHeight = scroll.getScrollHeight();
|
const currentHeight = scroll.getScrollHeight();
|
||||||
scroll.setScrollPosition(currentHeight - popHeight + initialPosition);
|
scroll.setScrollPosition(currentHeight - popHeight + initialPosition);
|
||||||
|
|
||||||
@@ -171,10 +164,12 @@ function previous () {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
scroll.resume();
|
scroll.resume();
|
||||||
lockFrames = false;
|
lockFrames = false;
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function last () {
|
function lastRange () {
|
||||||
if (scroll.isPaused()) {
|
if (scroll.isPaused()) {
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
@@ -182,16 +177,39 @@ function last () {
|
|||||||
scroll.pause();
|
scroll.pause();
|
||||||
lockFrames = true;
|
lockFrames = true;
|
||||||
|
|
||||||
return slide.getLast()
|
return render.clear()
|
||||||
|
.then(() => slide.getLast())
|
||||||
|
.then(results => render.pushFront(results))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
|
stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
|
||||||
|
|
||||||
scroll.scrollToBottom();
|
scroll.scrollToBottom();
|
||||||
|
lockFrames = false;
|
||||||
|
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
scroll.resume();
|
scroll.resume();
|
||||||
lockFrames = false;
|
|
||||||
|
return $q.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function menuLastRange () {
|
||||||
|
if (vm.isFollowing) {
|
||||||
|
lockFollow = true;
|
||||||
|
stopFollowing();
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
lockFollow = false;
|
||||||
|
|
||||||
|
return lastRange()
|
||||||
|
.then(() => {
|
||||||
|
startFollowing();
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,8 +228,7 @@ function canStartFollowing () {
|
|||||||
|
|
||||||
if (followOnce && // one-time activation from top of first page
|
if (followOnce && // one-time activation from top of first page
|
||||||
scroll.isBeyondUpperThreshold() &&
|
scroll.isBeyondUpperThreshold() &&
|
||||||
slide.getHeadCounter() === 1 &&
|
slide.getTailCounter() - slide.getHeadCounter() >= OUTPUT_PAGE_SIZE) {
|
||||||
slide.getTailCounter() >= OUTPUT_PAGE_SIZE) {
|
|
||||||
followOnce = false;
|
followOnce = false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -234,27 +251,166 @@ function stopFollowing () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scroll.unlock();
|
||||||
|
scroll.unhide();
|
||||||
|
|
||||||
vm.isFollowing = false;
|
vm.isFollowing = false;
|
||||||
vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
|
vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
|
||||||
}
|
}
|
||||||
|
|
||||||
function menuLast () {
|
//
|
||||||
if (vm.isFollowing) {
|
// Menu Controls (Page Mode)
|
||||||
lockFollow = true;
|
//
|
||||||
stopFollowing();
|
|
||||||
|
|
||||||
|
function firstPage () {
|
||||||
|
if (scroll.isPaused()) {
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
lockFollow = false;
|
scroll.pause();
|
||||||
|
|
||||||
if (slide.isOnLastPage()) {
|
return render.clear()
|
||||||
|
.then(() => page.getFirst())
|
||||||
|
.then(results => render.pushFront(results))
|
||||||
|
.then(() => page.getNext())
|
||||||
|
.then(results => {
|
||||||
|
const popCount = page.trimHead();
|
||||||
|
|
||||||
|
return render.popBack(popCount)
|
||||||
|
.then(() => render.pushFront(results));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
scroll.resume();
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function lastPage () {
|
||||||
|
if (scroll.isPaused()) {
|
||||||
|
return $q.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll.pause();
|
||||||
|
|
||||||
|
return render.clear()
|
||||||
|
.then(() => page.getLast())
|
||||||
|
.then(results => render.pushBack(results))
|
||||||
|
.then(() => page.getPrevious())
|
||||||
|
.then(results => {
|
||||||
|
const popCount = page.trimTail();
|
||||||
|
|
||||||
|
return render.popFront(popCount)
|
||||||
|
.then(() => render.pushBack(results));
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
scroll.scrollToBottom();
|
scroll.scrollToBottom();
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
scroll.resume();
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPage () {
|
||||||
|
if (scroll.isPaused()) {
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
return last();
|
scroll.pause();
|
||||||
|
|
||||||
|
return page.getNext()
|
||||||
|
.then(results => {
|
||||||
|
const popCount = page.trimHead();
|
||||||
|
|
||||||
|
return render.popBack(popCount)
|
||||||
|
.then(() => render.pushFront(results));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
scroll.resume();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousPage () {
|
||||||
|
if (scroll.isPaused()) {
|
||||||
|
return $q.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll.pause();
|
||||||
|
|
||||||
|
let initialPosition;
|
||||||
|
let popHeight;
|
||||||
|
|
||||||
|
return page.getPrevious()
|
||||||
|
.then(results => {
|
||||||
|
const popCount = page.trimTail();
|
||||||
|
initialPosition = scroll.getScrollPosition();
|
||||||
|
|
||||||
|
return render.popFront(popCount)
|
||||||
|
.then(() => {
|
||||||
|
popHeight = scroll.getScrollHeight();
|
||||||
|
|
||||||
|
return render.pushBack(results);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
const currentHeight = scroll.getScrollHeight();
|
||||||
|
scroll.setScrollPosition(currentHeight - popHeight + initialPosition);
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
scroll.resume();
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Menu Controls
|
||||||
|
//
|
||||||
|
|
||||||
|
function first () {
|
||||||
|
if (vm.isProcessingFinished) {
|
||||||
|
return firstPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function last () {
|
||||||
|
if (vm.isProcessingFinished) {
|
||||||
|
return lastPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function next () {
|
||||||
|
if (vm.isProcessingFinished) {
|
||||||
|
return nextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function previous () {
|
||||||
|
if (vm.isProcessingFinished) {
|
||||||
|
return previousPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function menuLast () {
|
||||||
|
if (vm.isProcessingFinished) {
|
||||||
|
return lastPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return menuLastRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
function down () {
|
function down () {
|
||||||
@@ -269,64 +425,125 @@ function togglePanelExpand () {
|
|||||||
vm.isPanelExpanded = !vm.isPanelExpanded;
|
vm.isPanelExpanded = !vm.isPanelExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMenuExpand () {
|
//
|
||||||
|
// Line Interaction
|
||||||
|
//
|
||||||
|
|
||||||
|
const iconCollapsed = 'fa-angle-right';
|
||||||
|
const iconExpanded = 'fa-angle-down';
|
||||||
|
const iconSelector = '.at-Stdout-toggle > i';
|
||||||
|
const lineCollapsed = 'hidden';
|
||||||
|
|
||||||
|
function toggleCollapseAll () {
|
||||||
if (scroll.isPaused()) return;
|
if (scroll.isPaused()) return;
|
||||||
|
|
||||||
const recordList = Object.keys(render.record).map(key => render.record[key]);
|
const records = Object.keys(render.records).map(key => render.records[key]);
|
||||||
const playRecords = recordList.filter(({ name }) => name === EVENT_START_PLAY);
|
const plays = records.filter(({ name }) => name === EVENT_START_PLAY);
|
||||||
const playIds = playRecords.map(({ uuid }) => uuid);
|
const tasks = records.filter(({ name }) => name === EVENT_START_TASK);
|
||||||
|
|
||||||
// get any task record that does not have a parent play record
|
const orphanLines = records
|
||||||
const orphanTaskRecords = recordList
|
.filter(({ level }) => level === 3)
|
||||||
.filter(({ name }) => name === EVENT_START_TASK)
|
.filter(({ parents }) => !records[parents[0]]);
|
||||||
.filter(({ parents }) => !parents.some(uuid => playIds.indexOf(uuid) >= 0));
|
|
||||||
|
|
||||||
const toggled = playRecords.concat(orphanTaskRecords)
|
const orphanLineParents = orphanLines
|
||||||
.map(({ uuid }) => getToggleElements(uuid))
|
.map(({ parents }) => ({ uuid: parents[0] }));
|
||||||
.filter(({ icon }) => icon.length > 0)
|
|
||||||
.map(({ icon, lines }) => setExpanded(icon, lines, !vm.isMenuExpanded));
|
|
||||||
|
|
||||||
if (toggled.length > 0) {
|
plays.concat(tasks).forEach(({ uuid }) => {
|
||||||
vm.isMenuExpanded = !vm.isMenuExpanded;
|
const icon = $(`#${uuid} ${iconSelector}`);
|
||||||
|
|
||||||
|
if (vm.isMenuCollapsed) {
|
||||||
|
icon.removeClass(iconCollapsed);
|
||||||
|
icon.addClass(iconExpanded);
|
||||||
|
} else {
|
||||||
|
icon.removeClass(iconExpanded);
|
||||||
|
icon.addClass(iconCollapsed);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
function toggleLineExpand (uuid) {
|
tasks.concat(orphanLineParents).forEach(({ uuid }) => {
|
||||||
if (scroll.isPaused()) return;
|
|
||||||
|
|
||||||
const { icon, lines } = getToggleElements(uuid);
|
|
||||||
const isExpanded = icon.hasClass('fa-angle-down');
|
|
||||||
|
|
||||||
setExpanded(icon, lines, !isExpanded);
|
|
||||||
|
|
||||||
vm.isMenuExpanded = !isExpanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getToggleElements (uuid) {
|
|
||||||
const record = render.record[uuid];
|
|
||||||
const lines = $(`.child-of-${uuid}`);
|
const lines = $(`.child-of-${uuid}`);
|
||||||
|
|
||||||
const iconSelector = '.at-Stdout-toggle > i';
|
if (vm.isMenuCollapsed) {
|
||||||
const additionalSelector = `#${(record.children || []).join(', #')}`;
|
lines.removeClass(lineCollapsed);
|
||||||
|
} else {
|
||||||
let icon = $(`#${uuid} ${iconSelector}`);
|
lines.addClass(lineCollapsed);
|
||||||
if (additionalSelector) {
|
|
||||||
icon = icon.add($(additionalSelector).find(iconSelector));
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return { icon, lines };
|
vm.isMenuCollapsed = !vm.isMenuCollapsed;
|
||||||
|
render.setCollapseAll(vm.isMenuCollapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setExpanded (icon, lines, expanded) {
|
function toggleCollapse (uuid) {
|
||||||
if (expanded) {
|
if (scroll.isPaused()) return;
|
||||||
icon.removeClass('fa-angle-right');
|
|
||||||
icon.addClass('fa-angle-down');
|
const record = render.records[uuid];
|
||||||
lines.removeClass('hidden');
|
|
||||||
} else {
|
if (record.name === EVENT_START_PLAY) {
|
||||||
icon.removeClass('fa-angle-down');
|
togglePlayCollapse(uuid);
|
||||||
icon.addClass('fa-angle-right');
|
|
||||||
lines.addClass('hidden');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (record.name === EVENT_START_TASK) {
|
||||||
|
toggleTaskCollapse(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePlayCollapse (uuid) {
|
||||||
|
const record = render.records[uuid];
|
||||||
|
const descendants = record.children || [];
|
||||||
|
|
||||||
|
const icon = $(`#${uuid} ${iconSelector}`);
|
||||||
|
const lines = $(`.child-of-${uuid}`);
|
||||||
|
const taskIcons = $(`#${descendants.join(', #')}`).find(iconSelector);
|
||||||
|
|
||||||
|
const isCollapsed = icon.hasClass(iconCollapsed);
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
icon.removeClass(iconCollapsed);
|
||||||
|
icon.addClass(iconExpanded);
|
||||||
|
|
||||||
|
taskIcons.removeClass(iconExpanded);
|
||||||
|
taskIcons.addClass(iconCollapsed);
|
||||||
|
lines.removeClass(lineCollapsed);
|
||||||
|
|
||||||
|
descendants
|
||||||
|
.map(item => $(`.child-of-${item}`))
|
||||||
|
.forEach(line => line.addClass(lineCollapsed));
|
||||||
|
} else {
|
||||||
|
icon.removeClass(iconExpanded);
|
||||||
|
icon.addClass(iconCollapsed);
|
||||||
|
|
||||||
|
taskIcons.removeClass(iconExpanded);
|
||||||
|
taskIcons.addClass(iconCollapsed);
|
||||||
|
|
||||||
|
lines.addClass(lineCollapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
descendants
|
||||||
|
.map(item => render.records[item])
|
||||||
|
.filter(({ name }) => name === EVENT_START_TASK)
|
||||||
|
.forEach(rec => { render.records[rec.uuid].isCollapsed = true; });
|
||||||
|
|
||||||
|
render.records[uuid].isCollapsed = !isCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTaskCollapse (uuid) {
|
||||||
|
const icon = $(`#${uuid} ${iconSelector}`);
|
||||||
|
const lines = $(`.child-of-${uuid}`);
|
||||||
|
|
||||||
|
const isCollapsed = icon.hasClass(iconCollapsed);
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
icon.removeClass(iconCollapsed);
|
||||||
|
icon.addClass(iconExpanded);
|
||||||
|
lines.removeClass(lineCollapsed);
|
||||||
|
} else {
|
||||||
|
icon.removeClass(iconExpanded);
|
||||||
|
icon.addClass(iconCollapsed);
|
||||||
|
lines.addClass(lineCollapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
render.records[uuid].isCollapsed = !isCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compile (html) {
|
function compile (html) {
|
||||||
@@ -337,6 +554,60 @@ function showHostDetails (id, uuid) {
|
|||||||
$state.go('output.host-event.json', { eventId: id, taskUuid: uuid });
|
$state.go('output.host-event.json', { eventId: id, taskUuid: uuid });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showMissingEvents (uuid) {
|
||||||
|
const record = render.records[uuid];
|
||||||
|
|
||||||
|
const min = Math.min(...record.counters);
|
||||||
|
const max = Math.min(Math.max(...record.counters), min + OUTPUT_PAGE_SIZE);
|
||||||
|
|
||||||
|
const selector = `#${uuid}`;
|
||||||
|
const clicked = $(selector);
|
||||||
|
|
||||||
|
return resource.events.getRange([min, max])
|
||||||
|
.then(results => {
|
||||||
|
const counters = results.map(({ counter }) => counter);
|
||||||
|
|
||||||
|
for (let i = min; i <= max; i++) {
|
||||||
|
if (counters.indexOf(i) < 0) {
|
||||||
|
results = results.filter(({ counter }) => counter < i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lines = 0;
|
||||||
|
let untrusted = '';
|
||||||
|
|
||||||
|
for (let i = 0; i <= results.length - 1; i++) {
|
||||||
|
const { html, count } = render.transformEvent(results[i]);
|
||||||
|
|
||||||
|
lines += count;
|
||||||
|
untrusted += html;
|
||||||
|
|
||||||
|
const shifted = render.records[uuid].counters.shift();
|
||||||
|
delete render.uuids[shifted];
|
||||||
|
}
|
||||||
|
|
||||||
|
const trusted = render.trustHtml(untrusted);
|
||||||
|
const elements = angular.element(trusted);
|
||||||
|
|
||||||
|
return render
|
||||||
|
.requestAnimationFrame(() => {
|
||||||
|
elements.insertBefore(clicked);
|
||||||
|
|
||||||
|
if (render.records[uuid].counters.length === 0) {
|
||||||
|
clicked.remove();
|
||||||
|
delete render.records[uuid];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => render.compile(elements))
|
||||||
|
.then(() => lines);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Event Handling
|
||||||
|
//
|
||||||
|
|
||||||
let streaming;
|
let streaming;
|
||||||
function stopListening () {
|
function stopListening () {
|
||||||
streaming = null;
|
streaming = null;
|
||||||
@@ -361,7 +632,7 @@ function handleJobEvent (data) {
|
|||||||
streaming = streaming || resource.events
|
streaming = streaming || resource.events
|
||||||
.getRange([Math.max(1, data.counter - 50), data.counter + 50])
|
.getRange([Math.max(1, data.counter - 50), data.counter + 50])
|
||||||
.then(results => {
|
.then(results => {
|
||||||
results = results.concat(data);
|
results.push(data);
|
||||||
|
|
||||||
const counters = results.map(({ counter }) => counter);
|
const counters = results.map(({ counter }) => counter);
|
||||||
const min = Math.min(...counters);
|
const min = Math.min(...counters);
|
||||||
@@ -379,12 +650,13 @@ function handleJobEvent (data) {
|
|||||||
results = results.filter(({ counter }) => counter > maxMissing);
|
results = results.filter(({ counter }) => counter > maxMissing);
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.setMissingCounterThreshold(max + 1);
|
|
||||||
results.forEach(item => {
|
results.forEach(item => {
|
||||||
stream.pushJobEvent(item);
|
stream.pushJobEvent(item);
|
||||||
status.pushJobEvent(item);
|
status.pushJobEvent(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stream.setMissingCounterThreshold(min);
|
||||||
|
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -406,12 +678,36 @@ function handleSummaryEvent (data) {
|
|||||||
stream.setFinalCounter(data.final_counter);
|
stream.setFinalCounter(data.final_counter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Search
|
||||||
|
//
|
||||||
|
|
||||||
function reloadState (params) {
|
function reloadState (params) {
|
||||||
params.isPanelExpanded = vm.isPanelExpanded;
|
params.isPanelExpanded = vm.isPanelExpanded;
|
||||||
|
|
||||||
return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' });
|
return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Debug Mode
|
||||||
|
//
|
||||||
|
|
||||||
|
function clear () {
|
||||||
|
stopListening();
|
||||||
|
render.clear();
|
||||||
|
|
||||||
|
followOnce = true;
|
||||||
|
lockFollow = false;
|
||||||
|
lockFrames = false;
|
||||||
|
|
||||||
|
stream.bufferInit();
|
||||||
|
status.init(resource);
|
||||||
|
slide.init(resource.events, render);
|
||||||
|
status.subscribe(data => { vm.status = data.status; });
|
||||||
|
|
||||||
|
startListening();
|
||||||
|
}
|
||||||
|
|
||||||
function OutputIndexController (
|
function OutputIndexController (
|
||||||
_$compile_,
|
_$compile_,
|
||||||
_$q_,
|
_$q_,
|
||||||
@@ -428,7 +724,8 @@ function OutputIndexController (
|
|||||||
strings,
|
strings,
|
||||||
$stateParams,
|
$stateParams,
|
||||||
) {
|
) {
|
||||||
const { isPanelExpanded } = $stateParams;
|
const { isPanelExpanded, _debug } = $stateParams;
|
||||||
|
const isProcessingFinished = !_debug && _resource_.model.get('event_processing_finished');
|
||||||
|
|
||||||
$compile = _$compile_;
|
$compile = _$compile_;
|
||||||
$q = _$q_;
|
$q = _$q_;
|
||||||
@@ -440,7 +737,8 @@ function OutputIndexController (
|
|||||||
render = _render_;
|
render = _render_;
|
||||||
status = _status_;
|
status = _status_;
|
||||||
stream = _stream_;
|
stream = _stream_;
|
||||||
slide = resource.model.get('event_processing_finished') ? _page_ : _slide_;
|
slide = _slide_;
|
||||||
|
page = _page_;
|
||||||
|
|
||||||
vm = this || {};
|
vm = this || {};
|
||||||
|
|
||||||
@@ -451,24 +749,27 @@ function OutputIndexController (
|
|||||||
vm.resource = resource;
|
vm.resource = resource;
|
||||||
vm.reloadState = reloadState;
|
vm.reloadState = reloadState;
|
||||||
vm.isPanelExpanded = isPanelExpanded;
|
vm.isPanelExpanded = isPanelExpanded;
|
||||||
|
vm.isProcessingFinished = isProcessingFinished;
|
||||||
vm.togglePanelExpand = togglePanelExpand;
|
vm.togglePanelExpand = togglePanelExpand;
|
||||||
|
|
||||||
// Stdout Navigation
|
// Stdout Navigation
|
||||||
vm.menu = { last: menuLast, first, down, up };
|
vm.menu = { last: menuLast, first, down, up, clear };
|
||||||
vm.isMenuExpanded = true;
|
vm.isMenuCollapsed = false;
|
||||||
vm.isFollowing = false;
|
vm.isFollowing = false;
|
||||||
vm.toggleMenuExpand = toggleMenuExpand;
|
vm.toggleCollapseAll = toggleCollapseAll;
|
||||||
vm.toggleLineExpand = toggleLineExpand;
|
vm.toggleCollapse = toggleCollapse;
|
||||||
vm.showHostDetails = showHostDetails;
|
vm.showHostDetails = showHostDetails;
|
||||||
|
vm.showMissingEvents = showMissingEvents;
|
||||||
vm.toggleLineEnabled = resource.model.get('type') === 'job';
|
vm.toggleLineEnabled = resource.model.get('type') === 'job';
|
||||||
vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
|
vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
|
||||||
|
vm.debug = _debug;
|
||||||
|
|
||||||
render.requestAnimationFrame(() => {
|
render.requestAnimationFrame(() => {
|
||||||
bufferInit();
|
render.init({ compile, toggles: vm.toggleLineEnabled });
|
||||||
|
|
||||||
status.init(resource);
|
status.init(resource);
|
||||||
slide.init(render, resource.events, scroll);
|
page.init(resource.events);
|
||||||
render.init({ compile, toggles: vm.toggleLineEnabled });
|
slide.init(resource.events, render);
|
||||||
|
|
||||||
scroll.init({
|
scroll.init({
|
||||||
next,
|
next,
|
||||||
@@ -482,10 +783,29 @@ function OutputIndexController (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let showFollowTip = true;
|
||||||
|
const rates = [];
|
||||||
stream.init({
|
stream.init({
|
||||||
bufferAdd,
|
|
||||||
bufferEmpty,
|
|
||||||
onFrames,
|
onFrames,
|
||||||
|
onFrameRate (rate) {
|
||||||
|
rates.push(rate);
|
||||||
|
rates.splice(0, rates.length - 5);
|
||||||
|
|
||||||
|
if (rates.every(value => value === 1)) {
|
||||||
|
scroll.unlock();
|
||||||
|
scroll.unhide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rate > 1 && vm.isFollowing) {
|
||||||
|
scroll.lock();
|
||||||
|
scroll.hide();
|
||||||
|
|
||||||
|
if (showFollowTip) {
|
||||||
|
showFollowTip = false;
|
||||||
|
$(OUTPUT_ELEMENT_LAST).trigger('mouseenter');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onStop () {
|
onStop () {
|
||||||
lockFollow = true;
|
lockFollow = true;
|
||||||
stopFollowing();
|
stopFollowing();
|
||||||
@@ -493,11 +813,12 @@ function OutputIndexController (
|
|||||||
status.updateStats();
|
status.updateStats();
|
||||||
status.dispatch();
|
status.dispatch();
|
||||||
status.sync();
|
status.sync();
|
||||||
scroll.stop();
|
scroll.unlock();
|
||||||
|
scroll.unhide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resource.model.get('event_processing_finished')) {
|
if (isProcessingFinished) {
|
||||||
followOnce = false;
|
followOnce = false;
|
||||||
lockFollow = true;
|
lockFollow = true;
|
||||||
lockFrames = true;
|
lockFrames = true;
|
||||||
@@ -511,8 +832,21 @@ function OutputIndexController (
|
|||||||
startListening();
|
startListening();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_debug) {
|
||||||
|
return render.clear();
|
||||||
|
}
|
||||||
|
|
||||||
return last();
|
return last();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.$on('$destroy', () => {
|
||||||
|
stopListening();
|
||||||
|
|
||||||
|
render.clear();
|
||||||
|
render.el.remove();
|
||||||
|
slide.clear();
|
||||||
|
stream.bufferInit();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
OutputIndexController.$inject = [
|
OutputIndexController.$inject = [
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ function resolveResource (
|
|||||||
order_by: OUTPUT_ORDER_BY,
|
order_by: OUTPUT_ORDER_BY,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (job_event_search) { // eslint-disable-line camelcase
|
if (job_event_search) {
|
||||||
const query = qs.encodeQuerysetObject(qs.decodeArr(job_event_search));
|
const query = qs.encodeQuerysetObject(qs.decodeArr(job_event_search));
|
||||||
Object.assign(params, query);
|
Object.assign(params, query);
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ function JobsRun ($stateRegistry, $filter, strings) {
|
|||||||
const sanitize = $filter('sanitize');
|
const sanitize = $filter('sanitize');
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
url: '/:type/:id?job_event_search',
|
url: '/:type/:id?job_event_search?_debug',
|
||||||
name: 'output',
|
name: 'output',
|
||||||
parent,
|
parent,
|
||||||
ncyBreadcrumb,
|
ncyBreadcrumb,
|
||||||
|
|||||||
@@ -21,13 +21,14 @@
|
|||||||
reload="vm.reloadState">
|
reload="vm.reloadState">
|
||||||
</at-job-search>
|
</at-job-search>
|
||||||
<div class="at-Stdout-menuTop">
|
<div class="at-Stdout-menuTop">
|
||||||
<div class="pull-left" ng-click="vm.toggleMenuExpand()">
|
<div class="pull-left" ng-click="vm.toggleCollapseAll()">
|
||||||
<i class="at-Stdout-menuIcon fa" ng-if="vm.toggleLineEnabled"
|
<i class="at-Stdout-menuIcon fa" ng-if="vm.toggleLineEnabled"
|
||||||
ng-class="{ 'fa-minus': vm.isMenuExpanded, 'fa-plus': !vm.isMenuExpanded }"></i>
|
ng-class="{ 'fa-minus': !vm.isMenuCollapsed, 'fa-plus': vm.isMenuCollapsed }"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right" ng-click="vm.menu.last()">
|
<div class="pull-right" ng-click="vm.menu.last()">
|
||||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
|
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
|
||||||
ng-class="{ 'at-Stdout-menuIcon--active': vm.isFollowing }"
|
ng-class="{ 'at-Stdout-menuIcon--active': vm.isFollowing }"
|
||||||
|
id="atStdoutMenuLast"
|
||||||
data-placement="top"
|
data-placement="top"
|
||||||
data-trigger="hover"
|
data-trigger="hover"
|
||||||
data-tip-watch="vm.followTooltip"
|
data-tip-watch="vm.followTooltip"
|
||||||
@@ -46,6 +47,9 @@
|
|||||||
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"
|
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"
|
||||||
data-placement="top" aw-tool-tip="{{:: vm.strings.get('tooltips.MENU_UP') }}"></i>
|
data-placement="top" aw-tool-tip="{{:: vm.strings.get('tooltips.MENU_UP') }}"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pull-right" ng-if="vm.debug" ng-click="vm.menu.clear()">
|
||||||
|
<i class="at-Stdout-menuIcon--md fa fa-undo"></i>
|
||||||
|
</div>
|
||||||
<div class="at-u-clear"></div>
|
<div class="at-u-clear"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="at-Stdout-container">
|
<div class="at-Stdout-container">
|
||||||
|
|||||||
@@ -2,244 +2,153 @@
|
|||||||
import { OUTPUT_PAGE_LIMIT } from './constants';
|
import { OUTPUT_PAGE_LIMIT } from './constants';
|
||||||
|
|
||||||
function PageService ($q) {
|
function PageService ($q) {
|
||||||
this.init = (storage, api, { getScrollHeight }) => {
|
this.init = ({ getPage, getFirst, getLast, getLastPageNumber }) => {
|
||||||
const { prepend, append, shift, pop, deleteRecord } = storage;
|
|
||||||
const { getPage, getFirst, getLast, getLastPageNumber, getMaxCounter } = api;
|
|
||||||
|
|
||||||
this.api = {
|
this.api = {
|
||||||
getPage,
|
getPage,
|
||||||
getFirst,
|
getFirst,
|
||||||
getLast,
|
getLast,
|
||||||
getLastPageNumber,
|
getLastPageNumber,
|
||||||
getMaxCounter,
|
|
||||||
};
|
};
|
||||||
|
this.pages = {};
|
||||||
this.storage = {
|
this.state = { head: 0, tail: 0 };
|
||||||
prepend,
|
|
||||||
append,
|
|
||||||
shift,
|
|
||||||
pop,
|
|
||||||
deleteRecord,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.hooks = {
|
|
||||||
getScrollHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.records = {};
|
|
||||||
this.uuids = {};
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
head: 0,
|
|
||||||
tail: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.chain = $q.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pushFront = (results, key) => {
|
|
||||||
if (!results) {
|
|
||||||
return $q.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.storage.append(results)
|
|
||||||
.then(() => {
|
|
||||||
const tail = key || ++this.state.tail;
|
|
||||||
|
|
||||||
this.records[tail] = {};
|
|
||||||
results.forEach(({ counter, start_line, end_line, uuid }) => {
|
|
||||||
this.records[tail][counter] = { start_line, end_line };
|
|
||||||
this.uuids[counter] = uuid;
|
|
||||||
});
|
|
||||||
|
|
||||||
return $q.resolve();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pushBack = (results, key) => {
|
|
||||||
if (!results) {
|
|
||||||
return $q.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.storage.prepend(results)
|
|
||||||
.then(() => {
|
|
||||||
const head = key || --this.state.head;
|
|
||||||
|
|
||||||
this.records[head] = {};
|
|
||||||
results.forEach(({ counter, start_line, end_line, uuid }) => {
|
|
||||||
this.records[head][counter] = { start_line, end_line };
|
|
||||||
this.uuids[counter] = uuid;
|
|
||||||
});
|
|
||||||
|
|
||||||
return $q.resolve();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.popBack = () => {
|
|
||||||
if (this.getRecordCount() === 0) {
|
|
||||||
return $q.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageRecord = this.records[this.state.head] || {};
|
|
||||||
|
|
||||||
let lines = 0;
|
|
||||||
const counters = [];
|
|
||||||
|
|
||||||
Object.keys(pageRecord)
|
|
||||||
.forEach(counter => {
|
|
||||||
lines += pageRecord[counter].end_line - pageRecord[counter].start_line;
|
|
||||||
counters.push(counter);
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.storage.shift(lines)
|
|
||||||
.then(() => {
|
|
||||||
counters.forEach(counter => {
|
|
||||||
this.storage.deleteRecord(this.uuids[counter]);
|
|
||||||
delete this.uuids[counter];
|
|
||||||
});
|
|
||||||
|
|
||||||
delete this.records[this.state.head++];
|
|
||||||
|
|
||||||
return $q.resolve();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.popFront = () => {
|
|
||||||
if (this.getRecordCount() === 0) {
|
|
||||||
return $q.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageRecord = this.records[this.state.tail] || {};
|
|
||||||
|
|
||||||
let lines = 0;
|
|
||||||
const counters = [];
|
|
||||||
|
|
||||||
Object.keys(pageRecord)
|
|
||||||
.forEach(counter => {
|
|
||||||
lines += pageRecord[counter].end_line - pageRecord[counter].start_line;
|
|
||||||
counters.push(counter);
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.storage.pop(lines)
|
|
||||||
.then(() => {
|
|
||||||
counters.forEach(counter => {
|
|
||||||
this.storage.deleteRecord(this.uuids[counter]);
|
|
||||||
delete this.uuids[counter];
|
|
||||||
});
|
|
||||||
|
|
||||||
delete this.records[this.state.tail--];
|
|
||||||
|
|
||||||
return $q.resolve();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getNext = () => {
|
this.getNext = () => {
|
||||||
const lastPageNumber = this.api.getLastPageNumber();
|
const lastPageNumber = this.api.getLastPageNumber();
|
||||||
const number = Math.min(this.state.tail + 1, lastPageNumber);
|
const number = Math.min(this.state.tail + 1, lastPageNumber);
|
||||||
|
|
||||||
const isLoaded = (number >= this.state.head && number <= this.state.tail);
|
if (number < 1) {
|
||||||
const isValid = (number >= 1 && number <= lastPageNumber);
|
return $q.resolve([]);
|
||||||
|
|
||||||
let popHeight = this.hooks.getScrollHeight();
|
|
||||||
|
|
||||||
if (!isValid || isLoaded) {
|
|
||||||
this.chain = this.chain
|
|
||||||
.then(() => $q.resolve(popHeight));
|
|
||||||
|
|
||||||
return this.chain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageCount = this.state.head - this.state.tail;
|
if (number > lastPageNumber) {
|
||||||
|
return $q.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
if (pageCount >= OUTPUT_PAGE_LIMIT) {
|
let promise;
|
||||||
this.chain = this.chain
|
|
||||||
.then(() => this.popBack())
|
|
||||||
.then(() => {
|
|
||||||
popHeight = this.hooks.getScrollHeight();
|
|
||||||
|
|
||||||
return $q.resolve();
|
if (this.pages[number]) {
|
||||||
|
promise = $q.resolve(this.pages[number]);
|
||||||
|
} else {
|
||||||
|
promise = this.api.getPage(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
.then(results => {
|
||||||
|
if (results.length <= 0) {
|
||||||
|
return $q.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.tail = number;
|
||||||
|
this.pages[number] = results;
|
||||||
|
|
||||||
|
return $q.resolve(results);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
this.chain = this.chain
|
|
||||||
.then(() => this.api.getPage(number))
|
|
||||||
.then(events => this.pushFront(events))
|
|
||||||
.then(() => $q.resolve(popHeight));
|
|
||||||
|
|
||||||
return this.chain;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getPrevious = () => {
|
this.getPrevious = () => {
|
||||||
const number = Math.max(this.state.head - 1, 1);
|
const number = Math.max(this.state.head - 1, 1);
|
||||||
|
|
||||||
const isLoaded = (number >= this.state.head && number <= this.state.tail);
|
if (number < 1) {
|
||||||
const isValid = (number >= 1 && number <= this.api.getLastPageNumber());
|
return $q.resolve([]);
|
||||||
|
|
||||||
let popHeight = this.hooks.getScrollHeight();
|
|
||||||
|
|
||||||
if (!isValid || isLoaded) {
|
|
||||||
this.chain = this.chain
|
|
||||||
.then(() => $q.resolve(popHeight));
|
|
||||||
|
|
||||||
return this.chain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageCount = this.state.head - this.state.tail;
|
if (number > this.api.getLastPageNumber()) {
|
||||||
|
return $q.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
if (pageCount >= OUTPUT_PAGE_LIMIT) {
|
let promise;
|
||||||
this.chain = this.chain
|
|
||||||
.then(() => this.popFront())
|
|
||||||
.then(() => {
|
|
||||||
popHeight = this.hooks.getScrollHeight();
|
|
||||||
|
|
||||||
return $q.resolve();
|
if (this.pages[number]) {
|
||||||
|
promise = $q.resolve(this.pages[number]);
|
||||||
|
} else {
|
||||||
|
promise = this.api.getPage(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
.then(results => {
|
||||||
|
if (results.length <= 0) {
|
||||||
|
return $q.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.head = number;
|
||||||
|
this.pages[number] = results;
|
||||||
|
|
||||||
|
return $q.resolve(results);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
this.chain = this.chain
|
|
||||||
.then(() => this.api.getPage(number))
|
|
||||||
.then(events => this.pushBack(events))
|
|
||||||
.then(() => $q.resolve(popHeight));
|
|
||||||
|
|
||||||
return this.chain;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.clear = () => {
|
this.getLast = () => this.api.getLast()
|
||||||
const count = this.getRecordCount();
|
.then(results => {
|
||||||
|
if (results.length <= 0) {
|
||||||
for (let i = 0; i <= count; ++i) {
|
return $q.resolve([]);
|
||||||
this.chain = this.chain.then(() => this.popBack());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.chain;
|
const number = this.api.getLastPageNumber();
|
||||||
};
|
|
||||||
|
|
||||||
this.getLast = () => this.clear()
|
this.state.head = number;
|
||||||
.then(() => this.api.getLast())
|
this.state.tail = number;
|
||||||
.then(events => {
|
this.pages[number] = results;
|
||||||
const lastPage = this.api.getLastPageNumber();
|
|
||||||
|
|
||||||
this.state.head = lastPage;
|
return $q.resolve(results);
|
||||||
this.state.tail = lastPage;
|
});
|
||||||
|
|
||||||
return this.pushBack(events, lastPage);
|
this.getFirst = () => this.api.getFirst()
|
||||||
})
|
.then(results => {
|
||||||
.then(() => this.getPrevious());
|
if (results.length <= 0) {
|
||||||
|
return $q.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
this.getFirst = () => this.clear()
|
|
||||||
.then(() => this.api.getFirst())
|
|
||||||
.then(events => {
|
|
||||||
this.state.head = 1;
|
this.state.head = 1;
|
||||||
this.state.tail = 1;
|
this.state.tail = 1;
|
||||||
|
this.pages[1] = results;
|
||||||
|
|
||||||
return this.pushBack(events, 1);
|
return $q.resolve(results);
|
||||||
})
|
});
|
||||||
.then(() => this.getNext());
|
|
||||||
|
|
||||||
this.isOnLastPage = () => this.api.getLastPageNumber() === this.state.tail;
|
this.trimTail = () => {
|
||||||
this.getRecordCount = () => Object.keys(this.records).length;
|
const { tail, head } = this.state;
|
||||||
this.getTailCounter = () => this.state.tail;
|
let popCount = 0;
|
||||||
this.getMaxCounter = () => this.api.getMaxCounter();
|
|
||||||
|
for (let i = tail; i > head; i--) {
|
||||||
|
if (!this.isOverCapacity()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pages[i]) {
|
||||||
|
popCount += this.pages[i].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.pages[i];
|
||||||
|
|
||||||
|
this.state.tail--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return popCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.trimHead = () => {
|
||||||
|
const { head, tail } = this.state;
|
||||||
|
let popCount = 0;
|
||||||
|
|
||||||
|
for (let i = head; i < tail; i++) {
|
||||||
|
if (!this.isOverCapacity()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pages[i]) {
|
||||||
|
popCount += this.pages[i].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.pages[i];
|
||||||
|
|
||||||
|
this.state.head++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return popCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.isOverCapacity = () => this.state.tail - this.state.head > OUTPUT_PAGE_LIMIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
PageService.$inject = ['$q'];
|
PageService.$inject = ['$q'];
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import Entities from 'html-entities';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
EVENT_START_PLAY,
|
EVENT_START_PLAY,
|
||||||
|
EVENT_START_PLAYBOOK,
|
||||||
EVENT_STATS_PLAY,
|
EVENT_STATS_PLAY,
|
||||||
EVENT_START_TASK,
|
EVENT_START_TASK,
|
||||||
|
OUTPUT_ANSI_COLORMAP,
|
||||||
OUTPUT_ELEMENT_TBODY,
|
OUTPUT_ELEMENT_TBODY,
|
||||||
|
OUTPUT_EVENT_LIMIT,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
const EVENT_GROUPS = [
|
const EVENT_GROUPS = [
|
||||||
@@ -19,7 +22,7 @@ const TIME_EVENTS = [
|
|||||||
EVENT_STATS_PLAY,
|
EVENT_STATS_PLAY,
|
||||||
];
|
];
|
||||||
|
|
||||||
const ansi = new Ansi();
|
const ansi = new Ansi({ stream: true, colors: OUTPUT_ANSI_COLORMAP });
|
||||||
const entities = new Entities.AllHtmlEntities();
|
const entities = new Entities.AllHtmlEntities();
|
||||||
|
|
||||||
// https://github.com/chalk/ansi-regex
|
// https://github.com/chalk/ansi-regex
|
||||||
@@ -33,98 +36,243 @@ const hasAnsi = input => re.test(input);
|
|||||||
|
|
||||||
function JobRenderService ($q, $sce, $window) {
|
function JobRenderService ($q, $sce, $window) {
|
||||||
this.init = ({ compile, toggles }) => {
|
this.init = ({ compile, toggles }) => {
|
||||||
this.parent = null;
|
|
||||||
this.record = {};
|
|
||||||
this.el = $(OUTPUT_ELEMENT_TBODY);
|
|
||||||
this.hooks = { compile };
|
this.hooks = { compile };
|
||||||
|
this.el = $(OUTPUT_ELEMENT_TBODY);
|
||||||
|
this.parent = null;
|
||||||
|
|
||||||
this.createToggles = toggles;
|
this.state = {
|
||||||
|
head: 0,
|
||||||
|
tail: 0,
|
||||||
|
collapseAll: false,
|
||||||
|
toggleMode: toggles,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sortByLineNumber = (a, b) => {
|
this.records = {};
|
||||||
if (a.start_line > b.start_line) {
|
this.uuids = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setCollapseAll = value => {
|
||||||
|
this.state.collapseAll = value;
|
||||||
|
Object.keys(this.records).forEach(key => {
|
||||||
|
this.records[key].isCollapsed = value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sortByCounter = (a, b) => {
|
||||||
|
if (a.counter > b.counter) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.start_line < b.start_line) {
|
if (a.counter < b.counter) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.transformEventGroup = events => {
|
//
|
||||||
|
// Event Data Transformation / HTML Building
|
||||||
|
//
|
||||||
|
|
||||||
|
this.appendEventGroup = events => {
|
||||||
let lines = 0;
|
let lines = 0;
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
events.sort(this.sortByLineNumber);
|
events.sort(this.sortByCounter);
|
||||||
|
|
||||||
for (let i = 0; i < events.length; ++i) {
|
for (let i = 0; i <= events.length - 1; i++) {
|
||||||
const line = this.transformEvent(events[i]);
|
const current = events[i];
|
||||||
html += line.html;
|
|
||||||
lines += line.count;
|
if (this.state.tail && current.counter !== this.state.tail + 1) {
|
||||||
|
const missing = this.appendMissingEventGroup(current);
|
||||||
|
|
||||||
|
html += missing.html;
|
||||||
|
lines += missing.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventLines = this.transformEvent(current);
|
||||||
|
|
||||||
|
html += eventLines.html;
|
||||||
|
lines += eventLines.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { html, lines };
|
return { html, lines };
|
||||||
};
|
};
|
||||||
|
|
||||||
this.transformEvent = event => {
|
this.appendMissingEventGroup = event => {
|
||||||
if (this.record[event.uuid]) {
|
const tailUUID = this.uuids[this.state.tail];
|
||||||
|
const tailRecord = this.records[tailUUID];
|
||||||
|
|
||||||
|
if (!tailRecord) {
|
||||||
return { html: '', count: 0 };
|
return { html: '', count: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!event || !event.stdout) {
|
let uuid;
|
||||||
|
|
||||||
|
if (tailRecord.isMissing) {
|
||||||
|
uuid = tailUUID;
|
||||||
|
} else {
|
||||||
|
uuid = `${event.counter}-${tailUUID}`;
|
||||||
|
this.records[uuid] = { uuid, counters: [], lineCount: 1, isMissing: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = this.state.tail + 1; i < event.counter; i++) {
|
||||||
|
this.records[uuid].counters.push(i);
|
||||||
|
this.uuids[i] = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tailRecord.isMissing) {
|
||||||
|
return { html: '', count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tailRecord.end === event.start_line) {
|
||||||
|
return { html: '', count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = this.buildRowHTML(this.records[uuid]);
|
||||||
|
const count = 1;
|
||||||
|
|
||||||
|
return { html, count };
|
||||||
|
};
|
||||||
|
|
||||||
|
this.prependEventGroup = events => {
|
||||||
|
let lines = 0;
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
events.sort(this.sortByCounter);
|
||||||
|
|
||||||
|
for (let i = events.length - 1; i >= 0; i--) {
|
||||||
|
const current = events[i];
|
||||||
|
|
||||||
|
if (this.state.head && current.counter !== this.state.head - 1) {
|
||||||
|
const missing = this.prependMissingEventGroup(current);
|
||||||
|
|
||||||
|
html = missing.html + html;
|
||||||
|
lines += missing.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventLines = this.transformEvent(current);
|
||||||
|
|
||||||
|
html = eventLines.html + html;
|
||||||
|
lines += eventLines.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { html, lines };
|
||||||
|
};
|
||||||
|
|
||||||
|
this.prependMissingEventGroup = event => {
|
||||||
|
const headUUID = this.uuids[this.state.head];
|
||||||
|
const headRecord = this.records[headUUID];
|
||||||
|
|
||||||
|
if (!headRecord) {
|
||||||
|
return { html: '', count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
let uuid;
|
||||||
|
|
||||||
|
if (headRecord.isMissing) {
|
||||||
|
uuid = headUUID;
|
||||||
|
} else {
|
||||||
|
uuid = `${headUUID}-${event.counter}`;
|
||||||
|
this.records[uuid] = { uuid, counters: [], lineCount: 1, isMissing: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = this.state.head - 1; i > event.counter; i--) {
|
||||||
|
this.records[uuid].counters.unshift(i);
|
||||||
|
this.uuids[i] = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headRecord.isMissing) {
|
||||||
|
return { html: '', count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.end_line === headRecord.start) {
|
||||||
|
return { html: '', count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = this.buildRowHTML(this.records[uuid]);
|
||||||
|
const count = 1;
|
||||||
|
|
||||||
|
return { html, count };
|
||||||
|
};
|
||||||
|
|
||||||
|
this.transformEvent = event => {
|
||||||
|
if (!event || event.stdout === null || event.stdout === undefined) {
|
||||||
|
return { html: '', count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.uuid && this.records[event.uuid]) {
|
||||||
return { html: '', count: 0 };
|
return { html: '', count: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const stdout = this.sanitize(event.stdout);
|
const stdout = this.sanitize(event.stdout);
|
||||||
const lines = stdout.split('\r\n');
|
const lines = stdout.split('\r\n');
|
||||||
|
const record = this.createRecord(event, lines);
|
||||||
|
|
||||||
|
if (event.event === EVENT_START_PLAYBOOK) {
|
||||||
|
return { html: '', count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
let count = lines.length;
|
let count = lines.length;
|
||||||
let ln = event.start_line;
|
let ln = event.start_line;
|
||||||
|
|
||||||
const current = this.createRecord(ln, lines, event);
|
for (let i = 0; i <= lines.length - 1; i++) {
|
||||||
|
|
||||||
const html = lines.reduce((concat, line, i) => {
|
|
||||||
ln++;
|
ln++;
|
||||||
|
|
||||||
|
const line = lines[i];
|
||||||
const isLastLine = i === lines.length - 1;
|
const isLastLine = i === lines.length - 1;
|
||||||
|
|
||||||
let row = this.createRow(current, ln, line);
|
let row = this.buildRowHTML(record, ln, line);
|
||||||
|
|
||||||
if (current && current.isTruncated && isLastLine) {
|
if (record && record.isTruncated && isLastLine) {
|
||||||
row += this.createRow(current);
|
row += this.buildRowHTML(record);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${concat}${row}`;
|
html += row;
|
||||||
}, '');
|
}
|
||||||
|
|
||||||
|
if (this.records[event.uuid]) {
|
||||||
|
this.records[event.uuid].lineCount = count;
|
||||||
|
}
|
||||||
|
|
||||||
return { html, count };
|
return { html, count };
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isHostEvent = (event) => {
|
this.createRecord = (event, lines) => {
|
||||||
if (typeof event.host === 'number') {
|
if (!event.counter) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === 'project_update_event' &&
|
|
||||||
event.event !== 'runner_on_skipped' &&
|
|
||||||
event.event_data.host) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.createRecord = (ln, lines, event) => {
|
|
||||||
if (!event.uuid) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = {
|
if (!this.state.head || event.counter < this.state.head) {
|
||||||
|
this.state.head = event.counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.tail || event.counter > this.state.tail) {
|
||||||
|
this.state.tail = event.counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.uuid) {
|
||||||
|
this.uuids[event.counter] = event.counter;
|
||||||
|
this.records[event.counter] = { counters: [event.counter], lineCount: lines.length };
|
||||||
|
|
||||||
|
return this.records[event.counter];
|
||||||
|
}
|
||||||
|
|
||||||
|
let isHost = false;
|
||||||
|
if (typeof event.host === 'number') {
|
||||||
|
isHost = true;
|
||||||
|
} else if (event.type === 'project_update_event' &&
|
||||||
|
event.event !== 'runner_on_skipped' &&
|
||||||
|
event.event_data.host) {
|
||||||
|
isHost = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
isHost,
|
||||||
id: event.id,
|
id: event.id,
|
||||||
line: ln + 1,
|
line: event.start_line + 1,
|
||||||
name: event.event,
|
name: event.event,
|
||||||
uuid: event.uuid,
|
uuid: event.uuid,
|
||||||
level: event.event_level,
|
level: event.event_level,
|
||||||
@@ -132,50 +280,49 @@ function JobRenderService ($q, $sce, $window) {
|
|||||||
end: event.end_line,
|
end: event.end_line,
|
||||||
isTruncated: (event.end_line - event.start_line) > lines.length,
|
isTruncated: (event.end_line - event.start_line) > lines.length,
|
||||||
lineCount: lines.length,
|
lineCount: lines.length,
|
||||||
isHost: this.isHostEvent(event),
|
isCollapsed: this.state.collapseAll,
|
||||||
|
counters: [event.counter],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (event.parent_uuid) {
|
if (event.parent_uuid) {
|
||||||
info.parents = this.getParentEvents(event.parent_uuid);
|
record.parents = this.getParentEvents(event.parent_uuid);
|
||||||
|
if (this.records[event.parent_uuid]) {
|
||||||
|
record.isCollapsed = this.records[event.parent_uuid].isCollapsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.isTruncated) {
|
if (record.isTruncated) {
|
||||||
info.truncatedAt = event.start_line + lines.length;
|
record.truncatedAt = event.start_line + lines.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EVENT_GROUPS.includes(event.event)) {
|
if (EVENT_GROUPS.includes(event.event)) {
|
||||||
info.isParent = true;
|
record.isParent = true;
|
||||||
|
|
||||||
if (event.event_level === 1) {
|
if (event.event_level === 1) {
|
||||||
this.parent = event.uuid;
|
this.parent = event.uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.parent_uuid) {
|
if (event.parent_uuid) {
|
||||||
if (this.record[event.parent_uuid]) {
|
if (this.records[event.parent_uuid]) {
|
||||||
if (this.record[event.parent_uuid].children &&
|
if (this.records[event.parent_uuid].children &&
|
||||||
!this.record[event.parent_uuid].children.includes(event.uuid)) {
|
!this.records[event.parent_uuid].children.includes(event.uuid)) {
|
||||||
this.record[event.parent_uuid].children.push(event.uuid);
|
this.records[event.parent_uuid].children.push(event.uuid);
|
||||||
} else {
|
} else {
|
||||||
this.record[event.parent_uuid].children = [event.uuid];
|
this.records[event.parent_uuid].children = [event.uuid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TIME_EVENTS.includes(event.event)) {
|
if (TIME_EVENTS.includes(event.event)) {
|
||||||
info.time = this.getTimestamp(event.created);
|
record.time = this.getTimestamp(event.created);
|
||||||
info.line++;
|
record.line++;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.record[event.uuid] = info;
|
this.records[event.uuid] = record;
|
||||||
|
this.uuids[event.counter] = event.uuid;
|
||||||
|
|
||||||
return info;
|
return record;
|
||||||
};
|
|
||||||
|
|
||||||
this.getRecord = uuid => this.record[uuid];
|
|
||||||
|
|
||||||
this.deleteRecord = uuid => {
|
|
||||||
delete this.record[uuid];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getParentEvents = (uuid, list) => {
|
this.getParentEvents = (uuid, list) => {
|
||||||
@@ -183,42 +330,56 @@ function JobRenderService ($q, $sce, $window) {
|
|||||||
// always push its parent if exists
|
// always push its parent if exists
|
||||||
list.push(uuid);
|
list.push(uuid);
|
||||||
// if we can get grandparent in current visible lines, we also push it
|
// if we can get grandparent in current visible lines, we also push it
|
||||||
if (this.record[uuid] && this.record[uuid].parents) {
|
if (this.records[uuid] && this.records[uuid].parents) {
|
||||||
list = list.concat(this.record[uuid].parents);
|
list = list.concat(this.records[uuid].parents);
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.createRow = (current, ln, content) => {
|
this.buildRowHTML = (record, ln, content) => {
|
||||||
let id = '';
|
let id = '';
|
||||||
|
let icon = '';
|
||||||
let timestamp = '';
|
let timestamp = '';
|
||||||
let tdToggle = '';
|
let tdToggle = '';
|
||||||
let tdEvent = '';
|
let tdEvent = '';
|
||||||
let classList = '';
|
let classList = '';
|
||||||
|
|
||||||
|
if (record.isMissing) {
|
||||||
|
return `<div id="${record.uuid}" class="at-Stdout-row">
|
||||||
|
<div class="at-Stdout-toggle"></div>
|
||||||
|
<div class="at-Stdout-line at-Stdout-line--clickable" ng-click="vm.showMissingEvents('${record.uuid}')">...</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
content = content || '';
|
content = content || '';
|
||||||
|
|
||||||
if (hasAnsi(content)) {
|
if (hasAnsi(content)) {
|
||||||
content = ansi.toHtml(content);
|
content = ansi.toHtml(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current) {
|
if (record) {
|
||||||
if (this.createToggles && current.isParent && current.line === ln) {
|
if (this.state.toggleMode && record.isParent && record.line === ln) {
|
||||||
id = current.uuid;
|
id = record.uuid;
|
||||||
tdToggle = `<div class="at-Stdout-toggle" ng-click="vm.toggleLineExpand('${id}')"><i class="fa fa-angle-down can-toggle"></i></div>`;
|
|
||||||
|
if (record.isCollapsed) {
|
||||||
|
icon = 'fa-angle-right';
|
||||||
|
} else {
|
||||||
|
icon = 'fa-angle-down';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.isHost) {
|
tdToggle = `<div class="at-Stdout-toggle" ng-click="vm.toggleCollapse('${id}')"><i class="fa ${icon} can-toggle"></i></div>`;
|
||||||
tdEvent = `<div class="at-Stdout-event--host" ng-click="vm.showHostDetails('${current.id}', '${current.uuid}')"><span ng-non-bindable>${content}</span></div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.time && current.line === ln) {
|
if (record.isHost) {
|
||||||
timestamp = `<span>${current.time}</span>`;
|
tdEvent = `<div class="at-Stdout-event--host" ng-click="vm.showHostDetails('${record.id}', '${record.uuid}')"><span ng-non-bindable>${content}</span></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.parents) {
|
if (record.time && record.line === ln) {
|
||||||
classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
|
timestamp = `<span>${record.time}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.parents) {
|
||||||
|
classList = record.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +395,12 @@ function JobRenderService ($q, $sce, $window) {
|
|||||||
ln = '...';
|
ln = '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (record && record.isCollapsed) {
|
||||||
|
if (record.level === 3 || record.level === 0) {
|
||||||
|
classList += ' hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div id="${id}" class="at-Stdout-row ${classList}">
|
<div id="${id}" class="at-Stdout-row ${classList}">
|
||||||
${tdToggle}
|
${tdToggle}
|
||||||
@@ -252,6 +419,10 @@ function JobRenderService ($q, $sce, $window) {
|
|||||||
return `${hour}:${minute}:${second}`;
|
return `${hour}:${minute}:${second}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Element Operations
|
||||||
|
//
|
||||||
|
|
||||||
this.remove = elements => this.requestAnimationFrame(() => elements.remove());
|
this.remove = elements => this.requestAnimationFrame(() => elements.remove());
|
||||||
|
|
||||||
this.requestAnimationFrame = fn => $q(resolve => {
|
this.requestAnimationFrame = fn => $q(resolve => {
|
||||||
@@ -270,19 +441,25 @@ function JobRenderService ($q, $sce, $window) {
|
|||||||
return this.requestAnimationFrame();
|
return this.requestAnimationFrame();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.clear = () => {
|
this.removeAll = () => {
|
||||||
const elements = this.el.children();
|
const elements = this.el.contents();
|
||||||
return this.remove(elements);
|
return this.remove(elements);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.shift = lines => {
|
this.shift = lines => {
|
||||||
const elements = this.el.children().slice(0, lines);
|
// We multiply by two here under the assumption that one element and one text node
|
||||||
|
// is generated for each line of output.
|
||||||
|
const count = 2 * lines;
|
||||||
|
const elements = this.el.contents().slice(0, count);
|
||||||
|
|
||||||
return this.remove(elements);
|
return this.remove(elements);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pop = lines => {
|
this.pop = lines => {
|
||||||
const elements = this.el.children().slice(-lines);
|
// We multiply by two here under the assumption that one element and one text node
|
||||||
|
// is generated for each line of output.
|
||||||
|
const count = 2 * lines;
|
||||||
|
const elements = this.el.contents().slice(-count);
|
||||||
|
|
||||||
return this.remove(elements);
|
return this.remove(elements);
|
||||||
};
|
};
|
||||||
@@ -292,7 +469,7 @@ function JobRenderService ($q, $sce, $window) {
|
|||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = this.transformEventGroup(events);
|
const result = this.prependEventGroup(events);
|
||||||
const html = this.trustHtml(result.html);
|
const html = this.trustHtml(result.html);
|
||||||
|
|
||||||
const newElements = angular.element(html);
|
const newElements = angular.element(html);
|
||||||
@@ -307,7 +484,7 @@ function JobRenderService ($q, $sce, $window) {
|
|||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = this.transformEventGroup(events);
|
const result = this.appendEventGroup(events);
|
||||||
const html = this.trustHtml(result.html);
|
const html = this.trustHtml(result.html);
|
||||||
|
|
||||||
const newElements = angular.element(html);
|
const newElements = angular.element(html);
|
||||||
@@ -318,8 +495,110 @@ function JobRenderService ($q, $sce, $window) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html));
|
this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html));
|
||||||
|
|
||||||
this.sanitize = html => entities.encode(html);
|
this.sanitize = html => entities.encode(html);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Event Counter Methods - External code should prefer these.
|
||||||
|
//
|
||||||
|
|
||||||
|
this.clear = () => this.removeAll()
|
||||||
|
.then(() => {
|
||||||
|
const head = this.getHeadCounter();
|
||||||
|
const tail = this.getTailCounter();
|
||||||
|
|
||||||
|
for (let i = head; i <= tail; ++i) {
|
||||||
|
const uuid = this.uuids[i];
|
||||||
|
|
||||||
|
if (uuid) {
|
||||||
|
delete this.records[uuid];
|
||||||
|
delete this.uuids[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.head = 0;
|
||||||
|
this.state.tail = 0;
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pushFront = events => {
|
||||||
|
const tail = this.getTailCounter();
|
||||||
|
|
||||||
|
return this.append(events.filter(({ counter }) => counter > tail));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pushBack = events => {
|
||||||
|
const head = this.getHeadCounter();
|
||||||
|
const tail = this.getTailCounter();
|
||||||
|
|
||||||
|
return this.prepend(events.filter(({ counter }) => counter < head || counter > tail));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.popFront = count => {
|
||||||
|
if (!count || count <= 0) {
|
||||||
|
return $q.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const max = this.state.tail;
|
||||||
|
const min = max - count;
|
||||||
|
|
||||||
|
let lines = 0;
|
||||||
|
|
||||||
|
for (let i = max; i >= min; --i) {
|
||||||
|
const uuid = this.uuids[i];
|
||||||
|
|
||||||
|
if (!uuid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.records[uuid].counters.pop();
|
||||||
|
delete this.uuids[i];
|
||||||
|
|
||||||
|
if (this.records[uuid].counters.length === 0) {
|
||||||
|
lines += this.records[uuid].lineCount;
|
||||||
|
|
||||||
|
delete this.records[uuid];
|
||||||
|
this.state.tail--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pop(lines);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.popBack = count => {
|
||||||
|
if (!count || count <= 0) {
|
||||||
|
return $q.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = this.state.head;
|
||||||
|
const max = min + count;
|
||||||
|
|
||||||
|
let lines = 0;
|
||||||
|
|
||||||
|
for (let i = min; i <= max; ++i) {
|
||||||
|
const uuid = this.uuids[i];
|
||||||
|
|
||||||
|
if (!uuid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.records[uuid].counters.shift();
|
||||||
|
delete this.uuids[i];
|
||||||
|
|
||||||
|
if (this.records[uuid].counters.length === 0) {
|
||||||
|
lines += this.records[uuid].lineCount;
|
||||||
|
|
||||||
|
delete this.records[uuid];
|
||||||
|
this.state.head++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.shift(lines);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getHeadCounter = () => this.state.head;
|
||||||
|
this.getTailCounter = () => this.state.tail;
|
||||||
|
this.getCapacity = () => OUTPUT_EVENT_LIMIT - (this.getTailCounter() - this.getHeadCounter());
|
||||||
}
|
}
|
||||||
|
|
||||||
JobRenderService.$inject = ['$q', '$sce', '$window'];
|
JobRenderService.$inject = ['$q', '$sce', '$window'];
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import {
|
|||||||
OUTPUT_SCROLL_THRESHOLD,
|
OUTPUT_SCROLL_THRESHOLD,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
const MAX_THRASH = 20;
|
|
||||||
|
|
||||||
function JobScrollService ($q, $timeout) {
|
function JobScrollService ($q, $timeout) {
|
||||||
this.init = ({ next, previous, onThresholdLeave }) => {
|
this.init = ({ next, previous, onThresholdLeave }) => {
|
||||||
this.el = $(OUTPUT_ELEMENT_CONTAINER);
|
this.el = $(OUTPUT_ELEMENT_CONTAINER);
|
||||||
@@ -33,7 +31,6 @@ function JobScrollService ($q, $timeout) {
|
|||||||
paused: false,
|
paused: false,
|
||||||
locked: false,
|
locked: false,
|
||||||
hover: false,
|
hover: false,
|
||||||
running: true,
|
|
||||||
thrash: 0,
|
thrash: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,13 +41,6 @@ function JobScrollService ($q, $timeout) {
|
|||||||
|
|
||||||
this.onMouseEnter = () => {
|
this.onMouseEnter = () => {
|
||||||
this.state.hover = true;
|
this.state.hover = true;
|
||||||
|
|
||||||
if (this.state.thrash >= MAX_THRASH) {
|
|
||||||
this.state.thrash = MAX_THRASH - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.unlock();
|
|
||||||
this.unhide();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onMouseLeave = () => {
|
this.onMouseLeave = () => {
|
||||||
@@ -62,23 +52,6 @@ function JobScrollService ($q, $timeout) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.thrash > 0) {
|
|
||||||
if (this.isLocked() || this.state.hover) {
|
|
||||||
this.state.thrash--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.hover) {
|
|
||||||
this.state.thrash++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.thrash >= MAX_THRASH) {
|
|
||||||
if (this.isRunning()) {
|
|
||||||
this.lock();
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isLocked()) {
|
if (this.isLocked()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -195,16 +168,6 @@ function JobScrollService ($q, $timeout) {
|
|||||||
this.setScrollPosition(this.getScrollHeight());
|
this.setScrollPosition(this.getScrollHeight());
|
||||||
};
|
};
|
||||||
|
|
||||||
this.start = () => {
|
|
||||||
this.state.running = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.stop = () => {
|
|
||||||
this.unlock();
|
|
||||||
this.unhide();
|
|
||||||
this.state.running = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.lock = () => {
|
this.lock = () => {
|
||||||
this.state.locked = true;
|
this.state.locked = true;
|
||||||
};
|
};
|
||||||
@@ -256,7 +219,6 @@ function JobScrollService ($q, $timeout) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.isPaused = () => this.state.paused;
|
this.isPaused = () => this.state.paused;
|
||||||
this.isRunning = () => this.state.running;
|
|
||||||
this.isLocked = () => this.state.locked;
|
this.isLocked = () => this.state.locked;
|
||||||
this.isMissing = () => $(OUTPUT_ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
this.isMissing = () => $(OUTPUT_ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,12 @@
|
|||||||
/* eslint camelcase: 0 */
|
/* eslint camelcase: 0 */
|
||||||
import {
|
import {
|
||||||
API_MAX_PAGE_SIZE,
|
OUTPUT_MAX_BUFFER_LENGTH,
|
||||||
OUTPUT_EVENT_LIMIT,
|
|
||||||
OUTPUT_PAGE_SIZE,
|
OUTPUT_PAGE_SIZE,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
function getContinuous (events, reverse = false) {
|
|
||||||
const counters = events.map(({ counter }) => counter);
|
|
||||||
|
|
||||||
const min = Math.min(...counters);
|
|
||||||
const max = Math.max(...counters);
|
|
||||||
|
|
||||||
const missing = [];
|
|
||||||
for (let i = min; i <= max; i++) {
|
|
||||||
if (counters.indexOf(i) < 0) {
|
|
||||||
missing.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missing.length === 0) {
|
|
||||||
return events;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reverse) {
|
|
||||||
const threshold = Math.max(...missing);
|
|
||||||
|
|
||||||
return events.filter(({ counter }) => counter > threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
const threshold = Math.min(...missing);
|
|
||||||
|
|
||||||
return events.filter(({ counter }) => counter < threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SlidingWindowService ($q) {
|
function SlidingWindowService ($q) {
|
||||||
this.init = (storage, api, { getScrollHeight }) => {
|
this.init = ({ getRange, getFirst, getLast, getMaxCounter }, storage) => {
|
||||||
const { prepend, append, shift, pop, getRecord, deleteRecord, clear } = storage;
|
const { getHeadCounter, getTailCounter } = storage;
|
||||||
const { getRange, getFirst, getLast, getMaxCounter } = api;
|
|
||||||
|
|
||||||
this.api = {
|
this.api = {
|
||||||
getRange,
|
getRange,
|
||||||
@@ -46,32 +16,20 @@ function SlidingWindowService ($q) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.storage = {
|
this.storage = {
|
||||||
clear,
|
getHeadCounter,
|
||||||
prepend,
|
getTailCounter,
|
||||||
append,
|
|
||||||
shift,
|
|
||||||
pop,
|
|
||||||
getRecord,
|
|
||||||
deleteRecord,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.hooks = {
|
|
||||||
getScrollHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.lines = {};
|
|
||||||
this.uuids = {};
|
|
||||||
this.chain = $q.resolve();
|
|
||||||
|
|
||||||
this.state = { head: null, tail: null };
|
|
||||||
this.cache = { first: null };
|
|
||||||
|
|
||||||
this.buffer = {
|
this.buffer = {
|
||||||
events: [],
|
events: [],
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 0,
|
max: 0,
|
||||||
count: 0,
|
count: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.cache = {
|
||||||
|
first: null
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getBoundedRange = range => {
|
this.getBoundedRange = range => {
|
||||||
@@ -92,273 +50,46 @@ function SlidingWindowService ($q) {
|
|||||||
return this.getBoundedRange([head - 1 - displacement, head - 1]);
|
return this.getBoundedRange([head - 1 - displacement, head - 1]);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.createRecord = ({ counter, uuid, start_line, end_line }) => {
|
|
||||||
this.lines[counter] = end_line - start_line;
|
|
||||||
this.uuids[counter] = uuid;
|
|
||||||
|
|
||||||
if (this.state.tail === null) {
|
|
||||||
this.state.tail = counter;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counter > this.state.tail) {
|
|
||||||
this.state.tail = counter;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.head === null) {
|
|
||||||
this.state.head = counter;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counter < this.state.head) {
|
|
||||||
this.state.head = counter;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.deleteRecord = counter => {
|
|
||||||
this.storage.deleteRecord(this.uuids[counter]);
|
|
||||||
|
|
||||||
delete this.uuids[counter];
|
|
||||||
delete this.lines[counter];
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getLineCount = counter => {
|
|
||||||
const record = this.storage.getRecord(counter);
|
|
||||||
|
|
||||||
if (record && record.lineCount) {
|
|
||||||
return record.lineCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.lines[counter]) {
|
|
||||||
return this.lines[counter];
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pushFront = events => {
|
|
||||||
const tail = this.getTailCounter();
|
|
||||||
const newEvents = events.filter(({ counter }) => counter > tail);
|
|
||||||
|
|
||||||
return this.storage.append(newEvents)
|
|
||||||
.then(() => {
|
|
||||||
newEvents.forEach(event => this.createRecord(event));
|
|
||||||
|
|
||||||
return $q.resolve();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pushBack = events => {
|
|
||||||
const [head, tail] = this.getRange();
|
|
||||||
const newEvents = events
|
|
||||||
.filter(({ counter }) => counter < head || counter > tail);
|
|
||||||
|
|
||||||
return this.storage.prepend(newEvents)
|
|
||||||
.then(() => {
|
|
||||||
newEvents.forEach(event => this.createRecord(event));
|
|
||||||
|
|
||||||
return $q.resolve();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.popFront = count => {
|
|
||||||
if (!count || count <= 0) {
|
|
||||||
return $q.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const max = this.getTailCounter();
|
|
||||||
const min = max - count;
|
|
||||||
|
|
||||||
let lines = 0;
|
|
||||||
|
|
||||||
for (let i = max; i >= min; --i) {
|
|
||||||
lines += this.getLineCount(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.storage.pop(lines)
|
|
||||||
.then(() => {
|
|
||||||
for (let i = max; i >= min; --i) {
|
|
||||||
this.deleteRecord(i);
|
|
||||||
this.state.tail--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $q.resolve();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.popBack = count => {
|
|
||||||
if (!count || count <= 0) {
|
|
||||||
return $q.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const min = this.getHeadCounter();
|
|
||||||
const max = min + count;
|
|
||||||
|
|
||||||
let lines = 0;
|
|
||||||
|
|
||||||
for (let i = min; i <= max; ++i) {
|
|
||||||
lines += this.getLineCount(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.storage.shift(lines)
|
|
||||||
.then(() => {
|
|
||||||
for (let i = min; i <= max; ++i) {
|
|
||||||
this.deleteRecord(i);
|
|
||||||
this.state.head++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $q.resolve();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.clear = () => this.storage.clear()
|
|
||||||
.then(() => {
|
|
||||||
const [head, tail] = this.getRange();
|
|
||||||
|
|
||||||
for (let i = head; i <= tail; ++i) {
|
|
||||||
this.deleteRecord(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.head = null;
|
|
||||||
this.state.tail = null;
|
|
||||||
|
|
||||||
return $q.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.getNext = (displacement = OUTPUT_PAGE_SIZE) => {
|
this.getNext = (displacement = OUTPUT_PAGE_SIZE) => {
|
||||||
const next = this.getNextRange(displacement);
|
const next = this.getNextRange(displacement);
|
||||||
const [head, tail] = this.getRange();
|
|
||||||
|
|
||||||
this.chain = this.chain
|
return this.api.getRange(next);
|
||||||
.then(() => this.api.getRange(next))
|
|
||||||
.then(events => {
|
|
||||||
const results = getContinuous(events);
|
|
||||||
const min = Math.min(...results.map(({ counter }) => counter));
|
|
||||||
|
|
||||||
if (min > tail + 1) {
|
|
||||||
return $q.resolve([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $q.resolve(results);
|
|
||||||
})
|
|
||||||
.then(results => {
|
|
||||||
const count = (tail - head + results.length);
|
|
||||||
const excess = count - OUTPUT_EVENT_LIMIT;
|
|
||||||
|
|
||||||
return this.popBack(excess)
|
|
||||||
.then(() => {
|
|
||||||
const popHeight = this.hooks.getScrollHeight();
|
|
||||||
|
|
||||||
return this.pushFront(results).then(() => $q.resolve(popHeight));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.chain;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getPrevious = (displacement = OUTPUT_PAGE_SIZE) => {
|
this.getPrevious = (displacement = OUTPUT_PAGE_SIZE) => {
|
||||||
const previous = this.getPreviousRange(displacement);
|
const previous = this.getPreviousRange(displacement);
|
||||||
const [head, tail] = this.getRange();
|
|
||||||
|
|
||||||
this.chain = this.chain
|
return this.api.getRange(previous);
|
||||||
.then(() => this.api.getRange(previous))
|
|
||||||
.then(events => {
|
|
||||||
const results = getContinuous(events, true);
|
|
||||||
const max = Math.max(...results.map(({ counter }) => counter));
|
|
||||||
|
|
||||||
if (head > max + 1) {
|
|
||||||
return $q.resolve([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $q.resolve(results);
|
|
||||||
})
|
|
||||||
.then(results => {
|
|
||||||
const count = (tail - head + results.length);
|
|
||||||
const excess = count - OUTPUT_EVENT_LIMIT;
|
|
||||||
|
|
||||||
return this.popFront(excess)
|
|
||||||
.then(() => {
|
|
||||||
const popHeight = this.hooks.getScrollHeight();
|
|
||||||
|
|
||||||
return this.pushBack(results).then(() => $q.resolve(popHeight));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.chain;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getFirst = () => {
|
this.getFirst = () => {
|
||||||
this.chain = this.chain
|
|
||||||
.then(() => this.clear())
|
|
||||||
.then(() => {
|
|
||||||
if (this.cache.first) {
|
if (this.cache.first) {
|
||||||
return $q.resolve(this.cache.first);
|
return $q.resolve(this.cache.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.api.getFirst();
|
return this.api.getFirst()
|
||||||
})
|
|
||||||
.then(events => {
|
.then(events => {
|
||||||
if (events.length === OUTPUT_PAGE_SIZE) {
|
if (events.length === OUTPUT_PAGE_SIZE) {
|
||||||
this.cache.first = events;
|
this.cache.first = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.pushFront(events);
|
return $q.resolve(events);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.chain
|
|
||||||
.then(() => this.getNext());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getLast = () => {
|
this.getLast = () => this.getFrames()
|
||||||
this.chain = this.chain
|
|
||||||
.then(() => this.getFrames())
|
|
||||||
.then(frames => {
|
.then(frames => {
|
||||||
if (frames.length > 0) {
|
if (frames.length > 0) {
|
||||||
return $q.resolve(frames);
|
return $q.resolve(frames);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.api.getLast();
|
return this.api.getLast();
|
||||||
})
|
|
||||||
.then(events => {
|
|
||||||
const min = Math.min(...events.map(({ counter }) => counter));
|
|
||||||
|
|
||||||
if (min <= this.getTailCounter() + 1) {
|
|
||||||
return this.pushFront(events);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.clear()
|
|
||||||
.then(() => this.pushBack(events));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.chain
|
|
||||||
.then(() => this.getPrevious());
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getTailCounter = () => {
|
|
||||||
if (this.state.tail === null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.tail < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.state.tail;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getHeadCounter = () => {
|
|
||||||
if (this.state.head === null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.head < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.state.head;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pushFrames = events => {
|
this.pushFrames = events => {
|
||||||
|
const head = this.getHeadCounter();
|
||||||
|
const tail = this.getTailCounter();
|
||||||
const frames = this.buffer.events.concat(events);
|
const frames = this.buffer.events.concat(events);
|
||||||
const [head, tail] = this.getRange();
|
|
||||||
|
|
||||||
let min;
|
let min;
|
||||||
let max;
|
let max;
|
||||||
@@ -367,7 +98,7 @@ function SlidingWindowService ($q) {
|
|||||||
for (let i = frames.length - 1; i >= 0; i--) {
|
for (let i = frames.length - 1; i >= 0; i--) {
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
if (count > API_MAX_PAGE_SIZE) {
|
if (count > OUTPUT_MAX_BUFFER_LENGTH) {
|
||||||
frames.splice(i, 1);
|
frames.splice(i, 1);
|
||||||
|
|
||||||
count--;
|
count--;
|
||||||
@@ -388,27 +119,41 @@ function SlidingWindowService ($q) {
|
|||||||
this.buffer.max = max;
|
this.buffer.max = max;
|
||||||
this.buffer.count = count;
|
this.buffer.count = count;
|
||||||
|
|
||||||
if (min >= head && min <= tail + 1) {
|
if (tail - head === 0) {
|
||||||
return frames.filter(({ counter }) => counter > tail);
|
return frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return frames.filter(({ counter }) => counter > tail);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.clear = () => {
|
||||||
|
this.buffer.events.length = 0;
|
||||||
|
this.buffer.min = 0;
|
||||||
|
this.buffer.max = 0;
|
||||||
|
this.buffer.count = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getFrames = () => $q.resolve(this.buffer.events);
|
this.getFrames = () => $q.resolve(this.buffer.events);
|
||||||
|
|
||||||
this.getMaxCounter = () => {
|
this.getMaxCounter = () => {
|
||||||
if (this.buffer.min) {
|
if (this.buffer.max && this.buffer.max > 1) {
|
||||||
return this.buffer.min;
|
return this.buffer.max;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.api.getMaxCounter();
|
return this.api.getMaxCounter();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isOnLastPage = () => this.getTailCounter() >= (this.getMaxCounter() - OUTPUT_PAGE_SIZE);
|
this.isOnLastPage = () => {
|
||||||
this.getRange = () => [this.getHeadCounter(), this.getTailCounter()];
|
if (this.buffer.min) {
|
||||||
this.getRecordCount = () => Object.keys(this.lines).length;
|
return this.getTailCounter() >= this.buffer.min - 1;
|
||||||
this.getCapacity = () => OUTPUT_EVENT_LIMIT - this.getRecordCount();
|
}
|
||||||
|
|
||||||
|
return this.getTailCounter() >= this.getMaxCounter() - OUTPUT_PAGE_SIZE;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.isOnFirstPage = () => this.getHeadCounter() === 1;
|
||||||
|
this.getTailCounter = () => this.storage.getTailCounter();
|
||||||
|
this.getHeadCounter = () => this.storage.getHeadCounter();
|
||||||
}
|
}
|
||||||
|
|
||||||
SlidingWindowService.$inject = ['$q'];
|
SlidingWindowService.$inject = ['$q'];
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { OUTPUT_NO_COUNT_JOB_TYPES } from './constants';
|
||||||
|
|
||||||
const templateUrl = require('~features/output/stats.partial.html');
|
const templateUrl = require('~features/output/stats.partial.html');
|
||||||
|
|
||||||
let vm;
|
let vm;
|
||||||
@@ -21,6 +23,7 @@ function JobStatsController (strings, { subscribe }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vm.$onInit = () => {
|
vm.$onInit = () => {
|
||||||
|
vm.hideCounts = OUTPUT_NO_COUNT_JOB_TYPES.includes(vm.resource.model.get('type'));
|
||||||
vm.download = vm.resource.model.get('related.stdout');
|
vm.download = vm.resource.model.get('related.stdout');
|
||||||
vm.tooltips.toggleExpand = vm.expanded ?
|
vm.tooltips.toggleExpand = vm.expanded ?
|
||||||
strings.get('tooltips.COLLAPSE_OUTPUT') :
|
strings.get('tooltips.COLLAPSE_OUTPUT') :
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<!-- todo: styling, markup, css etc. - disposition according to project lib conventions -->
|
<!-- todo: styling, markup, css etc. - disposition according to project lib conventions -->
|
||||||
<div class="at-u-floatRight">
|
<div class="at-u-floatRight">
|
||||||
<span class="at-Panel-label">plays</span>
|
<span ng-show="!vm.hideCounts" class="at-Panel-label">plays</span>
|
||||||
<span ng-show="vm.running" class="at-Panel-headingTitleBadge">...</span>
|
<span ng-show="!vm.hideCounts && vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">...</span>
|
||||||
<span ng-show="!vm.running" class="at-Panel-headingTitleBadge">{{ vm.plays || 0 }}</span>
|
<span ng-show="!vm.hideCounts && !vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">{{ vm.plays || 0 }}</span>
|
||||||
|
|
||||||
<span class="at-Panel-label">tasks</span>
|
<span ng-show="!vm.hideCounts" class="at-Panel-label">tasks</span>
|
||||||
<span ng-show="vm.running" class="at-Panel-headingTitleBadge">...</span>
|
<span ng-show="!vm.hideCounts && vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">...</span>
|
||||||
<span ng-show="!vm.running" class="at-Panel-headingTitleBadge">{{ vm.tasks || 0 }}</span>
|
<span ng-show="!vm.hideCounts && !vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">{{ vm.tasks || 0 }}</span>
|
||||||
|
|
||||||
<span class="at-Panel-label">{{:: vm.strings.get('stats.HOSTS')}}</span>
|
<span ng-show="!vm.hideCounts" class="at-Panel-label">{{:: vm.strings.get('stats.HOSTS')}}</span>
|
||||||
<span ng-show="vm.running" class="at-Panel-headingTitleBadge">...</span>
|
<span ng-show="!vm.hideCounts && vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">...</span>
|
||||||
<span ng-show="!vm.running" class="at-Panel-headingTitleBadge">{{ vm.hosts || 1 }}</span>
|
<span ng-show="!vm.hideCounts && !vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">{{ vm.hosts || 1 }}</span>
|
||||||
|
|
||||||
<span class="at-Panel-label">{{:: vm.strings.get('stats.ELAPSED') }}</span>
|
<span class="at-Panel-label">{{:: vm.strings.get('stats.ELAPSED') }}</span>
|
||||||
<span ng-show="vm.running" class="at-Panel-headingTitleBadge">...</span>
|
<span ng-show="vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">...</span>
|
||||||
<span ng-show="!vm.running" class="at-Panel-headingTitleBadge">
|
<span ng-show="!vm.running" class="at-Panel-headingTitleBadge at-Panel-headingTitleBadge--inline">
|
||||||
{{ (vm.elapsed * 1000 || 0) | duration: "hh:mm:ss"}}
|
{{ (vm.elapsed * 1000 || 0) | duration: "hh:mm:ss"}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,49 @@
|
|||||||
/* eslint camelcase: 0 */
|
/* eslint camelcase: 0 */
|
||||||
import {
|
import {
|
||||||
EVENT_STATS_PLAY,
|
EVENT_STATS_PLAY,
|
||||||
|
OUTPUT_MAX_BUFFER_LENGTH,
|
||||||
OUTPUT_MAX_LAG,
|
OUTPUT_MAX_LAG,
|
||||||
OUTPUT_PAGE_SIZE,
|
OUTPUT_PAGE_SIZE,
|
||||||
|
OUTPUT_EVENT_LIMIT,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
|
const rx = [];
|
||||||
|
|
||||||
function OutputStream ($q) {
|
function OutputStream ($q) {
|
||||||
this.init = ({ bufferAdd, bufferEmpty, onFrames, onStop }) => {
|
this.init = ({ onFrames, onFrameRate, onStop }) => {
|
||||||
this.hooks = {
|
this.hooks = {
|
||||||
bufferAdd,
|
|
||||||
bufferEmpty,
|
|
||||||
onFrames,
|
onFrames,
|
||||||
|
onFrameRate,
|
||||||
onStop,
|
onStop,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.bufferInit();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.bufferInit = () => {
|
||||||
|
rx.length = 0;
|
||||||
|
|
||||||
this.counters = {
|
this.counters = {
|
||||||
used: [],
|
|
||||||
ready: [],
|
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 0,
|
max: -1,
|
||||||
|
ready: -1,
|
||||||
final: null,
|
final: null,
|
||||||
|
used: [],
|
||||||
|
missing: [],
|
||||||
|
total: 0,
|
||||||
|
length: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
ending: false,
|
ending: false,
|
||||||
ended: false,
|
ended: false,
|
||||||
|
overflow: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.lag = 0;
|
this.lag = 0;
|
||||||
this.chain = $q.resolve();
|
this.chain = $q.resolve();
|
||||||
|
|
||||||
this.factors = this.calcFactors(OUTPUT_PAGE_SIZE);
|
this.factors = this.calcFactors(OUTPUT_EVENT_LIMIT);
|
||||||
this.setFramesPerRender();
|
this.setFramesPerRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,6 +66,7 @@ function OutputStream ($q) {
|
|||||||
const boundedIndex = Math.min(this.factors.length - 1, index);
|
const boundedIndex = Math.min(this.factors.length - 1, index);
|
||||||
|
|
||||||
this.framesPerRender = this.factors[boundedIndex];
|
this.framesPerRender = this.factors[boundedIndex];
|
||||||
|
this.hooks.onFrameRate(this.framesPerRender);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setMissingCounterThreshold = counter => {
|
this.setMissingCounterThreshold = counter => {
|
||||||
@@ -61,36 +75,87 @@ function OutputStream ($q) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateCounterState = ({ counter }) => {
|
this.bufferAdd = event => {
|
||||||
this.counters.used.push(counter);
|
const { counter } = event;
|
||||||
|
|
||||||
if (counter > this.counters.max) {
|
if (counter > this.counters.max) {
|
||||||
this.counters.max = counter;
|
this.counters.max = counter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ready;
|
||||||
|
const used = [];
|
||||||
const missing = [];
|
const missing = [];
|
||||||
let minReady;
|
|
||||||
let maxReady;
|
|
||||||
|
|
||||||
for (let i = this.counters.min; i <= this.counters.max; i++) {
|
for (let i = this.counters.min; i <= this.counters.max; i++) {
|
||||||
if (this.counters.used.indexOf(i) === -1) {
|
if (this.counters.used.indexOf(i) === -1) {
|
||||||
|
if (i === counter) {
|
||||||
|
rx.push(event);
|
||||||
|
used.push(i);
|
||||||
|
this.counters.length += 1;
|
||||||
|
} else {
|
||||||
missing.push(i);
|
missing.push(i);
|
||||||
} else if (missing.length === 0) {
|
}
|
||||||
maxReady = i;
|
} else {
|
||||||
|
used.push(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxReady) {
|
const excess = this.counters.length - OUTPUT_MAX_BUFFER_LENGTH;
|
||||||
minReady = this.counters.min;
|
this.state.overflow = (excess > 0);
|
||||||
|
|
||||||
this.counters.min = maxReady + 1;
|
if (missing.length === 0) {
|
||||||
this.counters.used = this.counters.used.filter(c => c > maxReady);
|
ready = this.counters.max;
|
||||||
|
} else if (this.state.overflow) {
|
||||||
|
ready = this.counters.min + this.framesPerRender;
|
||||||
|
} else {
|
||||||
|
ready = missing[0] - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.counters.total += 1;
|
||||||
|
this.counters.ready = ready;
|
||||||
|
this.counters.used = used;
|
||||||
this.counters.missing = missing;
|
this.counters.missing = missing;
|
||||||
this.counters.ready = [minReady, maxReady];
|
};
|
||||||
|
|
||||||
return this.counters.ready;
|
this.bufferEmpty = threshold => {
|
||||||
|
let removed = [];
|
||||||
|
|
||||||
|
for (let i = rx.length - 1; i >= 0; i--) {
|
||||||
|
if (rx[i].counter <= threshold) {
|
||||||
|
removed = removed.concat(rx.splice(i, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.counters.min = threshold + 1;
|
||||||
|
this.counters.used = this.counters.used.filter(c => c > threshold);
|
||||||
|
this.counters.length = rx.length;
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.isReadyToRender = () => {
|
||||||
|
const { total } = this.counters;
|
||||||
|
const readyCount = this.counters.ready - this.counters.min;
|
||||||
|
|
||||||
|
if (readyCount <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.ending) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total % this.framesPerRender === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total < OUTPUT_PAGE_SIZE) {
|
||||||
|
if (readyCount % this.framesPerRender === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pushJobEvent = data => {
|
this.pushJobEvent = data => {
|
||||||
@@ -103,24 +168,24 @@ function OutputStream ($q) {
|
|||||||
this.counters.final = data.counter;
|
this.counters.final = data.counter;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [minReady, maxReady] = this.updateCounterState(data);
|
this.bufferAdd(data);
|
||||||
const count = this.hooks.bufferAdd(data);
|
|
||||||
|
|
||||||
if (count % OUTPUT_PAGE_SIZE === 0) {
|
if (this.counters.total % OUTPUT_PAGE_SIZE === 0) {
|
||||||
this.setFramesPerRender();
|
this.setFramesPerRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
const isReady = maxReady && (this.state.ending ||
|
if (!this.isReadyToRender()) {
|
||||||
(maxReady - minReady) % this.framesPerRender === 0);
|
|
||||||
|
|
||||||
if (!isReady) {
|
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLastFrame = this.state.ending && (maxReady >= this.counters.final);
|
const isLast = this.state.ending && (this.counters.ready >= this.counters.final);
|
||||||
const events = this.hooks.bufferEmpty(minReady, maxReady);
|
const events = this.bufferEmpty(this.counters.ready);
|
||||||
|
|
||||||
return this.emitFrames(events, isLastFrame);
|
if (events.length > 0) {
|
||||||
|
return this.emitFrames(events, isLast);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
})
|
})
|
||||||
.then(() => --this.lag);
|
.then(() => --this.lag);
|
||||||
|
|
||||||
@@ -133,16 +198,20 @@ function OutputStream ($q) {
|
|||||||
this.state.ending = true;
|
this.state.ending = true;
|
||||||
this.counters.final = counter;
|
this.counters.final = counter;
|
||||||
|
|
||||||
if (counter >= this.counters.min) {
|
if (counter > this.counters.ready) {
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const readyCount = this.counters.ready - this.counters.min;
|
||||||
|
|
||||||
let events = [];
|
let events = [];
|
||||||
if (this.counters.ready.length > 0) {
|
if (readyCount > 0) {
|
||||||
events = this.hooks.bufferEmpty(...this.counters.ready);
|
events = this.bufferEmpty(this.counters.ready);
|
||||||
}
|
|
||||||
|
|
||||||
return this.emitFrames(events, true);
|
return this.emitFrames(events, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $q.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.chain;
|
return this.chain;
|
||||||
@@ -157,7 +226,6 @@ function OutputStream ($q) {
|
|||||||
this.hooks.onStop();
|
this.hooks.onStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.counters.ready.length = 0;
|
|
||||||
return $q.resolve();
|
return $q.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
/** @define TokenModal */
|
|
||||||
.TokenModal {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TokenModal-label {
|
|
||||||
font-weight: bold;
|
|
||||||
width: 130px;
|
|
||||||
}
|
|
||||||
@@ -58,30 +58,30 @@ function AddTokensController (
|
|||||||
return postToken
|
return postToken
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
const refreshHTML = data.refresh_token ?
|
const refreshHTML = data.refresh_token ?
|
||||||
`<div class="TokenModal">
|
`<div class="PopupModal">
|
||||||
<div class="TokenModal-label">
|
<div class="PopupModal-label">
|
||||||
${strings.get('add.REFRESH_TOKEN_LABEL')}
|
${strings.get('add.REFRESH_TOKEN_LABEL')}
|
||||||
</div>
|
</div>
|
||||||
<div class="TokenModal-value">
|
<div class="PopupModal-value">
|
||||||
${data.refresh_token}
|
${data.refresh_token}
|
||||||
</div>
|
</div>
|
||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
Alert(strings.get('add.TOKEN_MODAL_HEADER'), `
|
Alert(strings.get('add.TOKEN_MODAL_HEADER'), `
|
||||||
<div class="TokenModal">
|
<div class="PopupModal">
|
||||||
<div class="TokenModal-label">
|
<div class="PopupModal-label">
|
||||||
${strings.get('add.TOKEN_LABEL')}
|
${strings.get('add.TOKEN_LABEL')}
|
||||||
</div>
|
</div>
|
||||||
<div class="TokenModal-value">
|
<div class="PopupModal-value">
|
||||||
${data.token}
|
${data.token}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${refreshHTML}
|
${refreshHTML}
|
||||||
<div class="TokenModal">
|
<div class="PopupModal">
|
||||||
<div class="TokenModal-label">
|
<div class="PopupModal-label">
|
||||||
${strings.get('add.TOKEN_EXPIRES_LABEL')}
|
${strings.get('add.TOKEN_EXPIRES_LABEL')}
|
||||||
</div>
|
</div>
|
||||||
<div class="TokenModal-value">
|
<div class="PopupModal-value">
|
||||||
${$filter('longDate')(data.expires)}
|
${$filter('longDate')(data.expires)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function AtLayoutController ($scope, $http, strings, ProcessErrors, $transitions
|
|||||||
|
|
||||||
if (!vm.isSuperUser) {
|
if (!vm.isSuperUser) {
|
||||||
checkOrgAdmin();
|
checkOrgAdmin();
|
||||||
|
checkNotificationAdmin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -54,6 +55,24 @@ function AtLayoutController ($scope, $http, strings, ProcessErrors, $transitions
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkNotificationAdmin () {
|
||||||
|
const usersPath = `/api/v2/users/${vm.currentUserId}/roles/?role_field=notification_admin_role`;
|
||||||
|
$http.get(usersPath)
|
||||||
|
.then(({ data }) => {
|
||||||
|
if (data.count > 0) {
|
||||||
|
vm.isNotificationAdmin = true;
|
||||||
|
} else {
|
||||||
|
vm.isNotificationAdmin = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(({ data, status }) => {
|
||||||
|
ProcessErrors(null, data, status, null, {
|
||||||
|
hdr: strings.get('error.HEADER'),
|
||||||
|
msg: strings.get('error.CALL', { path: usersPath, action: 'GET', status })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AtLayoutController.$inject = ['$scope', '$http', 'ComponentsStrings', 'ProcessErrors', '$transitions'];
|
AtLayoutController.$inject = ['$scope', '$http', 'ComponentsStrings', 'ProcessErrors', '$transitions'];
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
system-admin-only="true">
|
system-admin-only="true">
|
||||||
</at-side-nav-item>
|
</at-side-nav-item>
|
||||||
<at-side-nav-item icon-class="fa-bell" route="notifications" name="NOTIFICATIONS"
|
<at-side-nav-item icon-class="fa-bell" route="notifications" name="NOTIFICATIONS"
|
||||||
system-admin-only="true">
|
ng-show="$parent.layoutVm.isSuperUser || $parent.layoutVm.isOrgAdmin || $parent.layoutVm.isNotificationAdmin">
|
||||||
</at-side-nav-item>
|
</at-side-nav-item>
|
||||||
<at-side-nav-item icon-class="fa-briefcase" route="managementJobsList" name="MANAGEMENT_JOBS"
|
<at-side-nav-item icon-class="fa-briefcase" route="managementJobsList" name="MANAGEMENT_JOBS"
|
||||||
system-admin-only="true">
|
system-admin-only="true">
|
||||||
|
|||||||
@@ -46,6 +46,11 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
||||||
|
&--inline {
|
||||||
|
margin-right: @at-space-2x;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.at-Panel-headingCustomContent {
|
.at-Panel-headingCustomContent {
|
||||||
@@ -59,6 +64,7 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: normal!important;
|
font-weight: normal!important;
|
||||||
width: 30%;
|
width: 30%;
|
||||||
|
margin: @at-space-2x;
|
||||||
|
|
||||||
@media screen and (max-width: @breakpoint-md) {
|
@media screen and (max-width: @breakpoint-md) {
|
||||||
flex: 2.5 0 auto;
|
flex: 2.5 0 auto;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr
|
|||||||
};
|
};
|
||||||
|
|
||||||
scope.hasSelectedRows = function(){
|
scope.hasSelectedRows = function(){
|
||||||
return _.any(scope.allSelected, (type) => Object.keys(type).length > 0);
|
return _.some(scope.allSelected, (type) => Object.keys(type).length > 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
scope.selectTab = function(selected){
|
scope.selectTab = function(selected){
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ function(scope, $state, i18n, CreateSelect2, Rest, $q, Wait, ProcessErrors) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
scope.showSection2Container = function(){
|
scope.showSection2Container = function(){
|
||||||
return _.any(scope.allSelected, (type) => Object.keys(type).length > 0);
|
return _.some(scope.allSelected, (type) => Object.keys(type).length > 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
scope.showSection2Tab = function(tab){
|
scope.showSection2Tab = function(tab){
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ export default function BuildAnchor($log, $filter) {
|
|||||||
const inventoryId = _.get(obj, 'inventory', '').split('-').reverse()[0];
|
const inventoryId = _.get(obj, 'inventory', '').split('-').reverse()[0];
|
||||||
url += `inventories/inventory/${inventoryId}/inventory_sources/edit/${obj.id}`;
|
url += `inventories/inventory/${inventoryId}/inventory_sources/edit/${obj.id}`;
|
||||||
break;
|
break;
|
||||||
|
case 'o_auth2_application':
|
||||||
|
url += `applications/${obj.id}`;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
url += resource + 's/' + obj.id + '/';
|
url += resource + 's/' + obj.id + '/';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ angular
|
|||||||
$rootScope.$broadcast("RemoveIndicator");
|
$rootScope.$broadcast("RemoveIndicator");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.contains(trans.from().name, 'output') && trans.to().name === 'jobs'){
|
if(_.includes(trans.from().name, 'output') && trans.to().name === 'jobs'){
|
||||||
$state.reload();
|
$state.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -375,7 +375,7 @@ angular
|
|||||||
$rootScope.user_is_system_auditor = Authorization.getUserInfo('is_system_auditor');
|
$rootScope.user_is_system_auditor = Authorization.getUserInfo('is_system_auditor');
|
||||||
|
|
||||||
// state the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket)
|
// state the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket)
|
||||||
if (!_.contains($location.$$url, '/login')) {
|
if (!_.includes($location.$$url, '/login')) {
|
||||||
ConfigService.getConfig().then(function() {
|
ConfigService.getConfig().then(function() {
|
||||||
Timer.init().then(function(timer) {
|
Timer.init().then(function(timer) {
|
||||||
$rootScope.sessionTimer = timer;
|
$rootScope.sessionTimer = timer;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default
|
|||||||
if(streamConfig && streamConfig.activityStream) {
|
if(streamConfig && streamConfig.activityStream) {
|
||||||
if(streamConfig.activityStreamTarget) {
|
if(streamConfig.activityStreamTarget) {
|
||||||
stateGoParams.target = streamConfig.activityStreamTarget;
|
stateGoParams.target = streamConfig.activityStreamTarget;
|
||||||
let isTemplateTarget = _.contains(['template', 'job_template', 'workflow_job_template'], streamConfig.activityStreamTarget);
|
let isTemplateTarget = _.includes(['template', 'job_template', 'workflow_job_template'], streamConfig.activityStreamTarget);
|
||||||
stateGoParams.activity_search = {
|
stateGoParams.activity_search = {
|
||||||
or__object1__in: isTemplateTarget ? 'job_template,workflow_job_template' : streamConfig.activityStreamTarget,
|
or__object1__in: isTemplateTarget ? 'job_template,workflow_job_template' : streamConfig.activityStreamTarget,
|
||||||
or__object2__in: isTemplateTarget ? 'job_template,workflow_job_template' : streamConfig.activityStreamTarget,
|
or__object2__in: isTemplateTarget ? 'job_template,workflow_job_template' : streamConfig.activityStreamTarget,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default
|
|||||||
if(expandedBreadcrumbWidth > availableWidth) {
|
if(expandedBreadcrumbWidth > availableWidth) {
|
||||||
let widthToTrim = expandedBreadcrumbWidth - availableWidth;
|
let widthToTrim = expandedBreadcrumbWidth - availableWidth;
|
||||||
// Sort the crumbs from biggest to smallest
|
// Sort the crumbs from biggest to smallest
|
||||||
let sortedCrumbs = _.sortByOrder(crumbs, ["origWidth"], ["desc"]);
|
let sortedCrumbs = _.orderBy(crumbs, ["origWidth"], ["desc"]);
|
||||||
let maxWidth;
|
let maxWidth;
|
||||||
for(let i=0; i<sortedCrumbs.length; i++) {
|
for(let i=0; i<sortedCrumbs.length; i++) {
|
||||||
if(sortedCrumbs[i+1]) {
|
if(sortedCrumbs[i+1]) {
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export default [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
var forms = _.pluck(authForms, 'formDef');
|
var forms = _.map(authForms, 'formDef');
|
||||||
_.each(forms, function(form) {
|
_.each(forms, function(form) {
|
||||||
var keys = _.keys(form.fields);
|
var keys = _.keys(form.fields);
|
||||||
_.each(keys, function(key) {
|
_.each(keys, function(key) {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default [
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
$('.select2-selection__choice').each(function(i, element){
|
$('.select2-selection__choice').each(function(i, element){
|
||||||
if(!_.contains($scope.$parent.configDataResolve.AD_HOC_COMMANDS.default, element.title)){
|
if(!_.includes($scope.$parent.configDataResolve.AD_HOC_COMMANDS.default, element.title)){
|
||||||
$(`#configuration_jobs_template_AD_HOC_COMMANDS option[value='${element.title}']`).remove();
|
$(`#configuration_jobs_template_AD_HOC_COMMANDS option[value='${element.title}']`).remove();
|
||||||
element.remove();
|
element.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default [
|
|||||||
id: 'system-misc-form'
|
id: 'system-misc-form'
|
||||||
}];
|
}];
|
||||||
|
|
||||||
var forms = _.pluck(systemForms, 'formDef');
|
var forms = _.map(systemForms, 'formDef');
|
||||||
_.each(forms, function(form) {
|
_.each(forms, function(form) {
|
||||||
var keys = _.keys(form.fields);
|
var keys = _.keys(form.fields);
|
||||||
_.each(keys, function(key) {
|
_.each(keys, function(key) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default ['i18n', function(i18n) {
|
|||||||
SESSION_COOKIE_AGE: {
|
SESSION_COOKIE_AGE: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
integer: true,
|
integer: true,
|
||||||
min: 60,
|
min: 61,
|
||||||
reset: 'SESSION_COOKIE_AGE',
|
reset: 'SESSION_COOKIE_AGE',
|
||||||
},
|
},
|
||||||
SESSIONS_PER_USER: {
|
SESSIONS_PER_USER: {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ function CapacityAdjuster (templateUrl, ProcessErrors, Wait, strings) {
|
|||||||
value: scope.state.mem_capacity
|
value: scope.state.mem_capacity
|
||||||
}];
|
}];
|
||||||
|
|
||||||
scope.min_capacity = _.min(adjustment_values, 'value');
|
scope.min_capacity = _.minBy(adjustment_values, 'value');
|
||||||
scope.max_capacity = _.max(adjustment_values, 'value');
|
scope.max_capacity = _.maxBy(adjustment_values, 'value');
|
||||||
|
|
||||||
capacityAdjusterController.init();
|
capacityAdjusterController.init();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default ['Rest', 'Wait', 'NotificationsFormObject',
|
|||||||
init();
|
init();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
Rest.setUrl(GetBasePath('projects'));
|
Rest.setUrl(GetBasePath('notification_templates'));
|
||||||
Rest.options()
|
Rest.options()
|
||||||
.then(({data}) => {
|
.then(({data}) => {
|
||||||
if (!data.actions.POST) {
|
if (!data.actions.POST) {
|
||||||
@@ -205,7 +205,7 @@ export default ['Rest', 'Wait', 'NotificationsFormObject',
|
|||||||
return $scope[i];
|
return $scope[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
params.notification_configuration = _.object(Object.keys(form.fields)
|
params.notification_configuration = _.fromPairs(Object.keys(form.fields)
|
||||||
.filter(i => (form.fields[i].ngShow && form.fields[i].ngShow.indexOf(v) > -1))
|
.filter(i => (form.fields[i].ngShow && form.fields[i].ngShow.indexOf(v) > -1))
|
||||||
.map(i => [i, processValue($scope[i], i, form.fields[i])]));
|
.map(i => [i, processValue($scope[i], i, form.fields[i])]));
|
||||||
|
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ export default ['Rest', 'Wait',
|
|||||||
return $scope[i];
|
return $scope[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
params.notification_configuration = _.object(Object.keys(form.fields)
|
params.notification_configuration = _.fromPairs(Object.keys(form.fields)
|
||||||
.filter(i => (form.fields[i].ngShow && form.fields[i].ngShow.indexOf(v) > -1))
|
.filter(i => (form.fields[i].ngShow && form.fields[i].ngShow.indexOf(v) > -1))
|
||||||
.map(i => [i, processValue($scope[i], i, form.fields[i])]));
|
.map(i => [i, processValue($scope[i], i, form.fields[i])]));
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){
|
|||||||
hover: false,
|
hover: false,
|
||||||
emptyListText: i18n.sprintf(i18n._("This list is populated by notification templates added from the %sNotifications%s section"), " <a ui-sref='notifications.add'>", "</a> "),
|
emptyListText: i18n.sprintf(i18n._("This list is populated by notification templates added from the %sNotifications%s section"), " <a ui-sref='notifications.add'>", "</a> "),
|
||||||
basePath: 'notification_templates',
|
basePath: 'notification_templates',
|
||||||
ngIf: 'current_user.is_superuser || isOrgAdmin',
|
ngIf: 'current_user.is_superuser || isOrgAdmin || isNotificationAdmin',
|
||||||
fields: {
|
fields: {
|
||||||
name: {
|
name: {
|
||||||
key: true,
|
key: true,
|
||||||
@@ -40,6 +40,7 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){
|
|||||||
flag: 'notification_templates_success',
|
flag: 'notification_templates_success',
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
ngClick: "toggleNotification($event, notification.id, \"notification_templates_success\")",
|
ngClick: "toggleNotification($event, notification.id, \"notification_templates_success\")",
|
||||||
|
ngDisabled: "!(current_user.is_superuser || isOrgAdmin)",
|
||||||
awToolTip: "{{ schedule.play_tip }}",
|
awToolTip: "{{ schedule.play_tip }}",
|
||||||
dataTipWatch: "schedule.play_tip",
|
dataTipWatch: "schedule.play_tip",
|
||||||
dataPlacement: "right",
|
dataPlacement: "right",
|
||||||
@@ -51,6 +52,7 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){
|
|||||||
flag: 'notification_templates_error',
|
flag: 'notification_templates_error',
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
ngClick: "toggleNotification($event, notification.id, \"notification_templates_error\")",
|
ngClick: "toggleNotification($event, notification.id, \"notification_templates_error\")",
|
||||||
|
ngDisabled: "!(current_user.is_superuser || isOrgAdmin)",
|
||||||
awToolTip: "{{ schedule.play_tip }}",
|
awToolTip: "{{ schedule.play_tip }}",
|
||||||
dataTipWatch: "schedule.play_tip",
|
dataTipWatch: "schedule.play_tip",
|
||||||
dataPlacement: "right",
|
dataPlacement: "right",
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
export default ['$scope', '$location', '$stateParams', 'OrgAdminLookup',
|
export default ['$scope', '$location', '$stateParams', 'OrgAdminLookup',
|
||||||
'OrganizationForm', 'Rest', 'ProcessErrors', 'Prompt',
|
'OrganizationForm', 'Rest', 'ProcessErrors', 'Prompt', '$rootScope',
|
||||||
'GetBasePath', 'Wait', '$state', 'ToggleNotification', 'CreateSelect2', 'InstanceGroupsService', 'InstanceGroupsData', 'ConfigData',
|
'GetBasePath', 'Wait', '$state', 'ToggleNotification', 'CreateSelect2', 'InstanceGroupsService', 'InstanceGroupsData', 'ConfigData',
|
||||||
function($scope, $location, $stateParams, OrgAdminLookup,
|
function($scope, $location, $stateParams, OrgAdminLookup,
|
||||||
OrganizationForm, Rest, ProcessErrors, Prompt,
|
OrganizationForm, Rest, ProcessErrors, Prompt, $rootScope,
|
||||||
GetBasePath, Wait, $state, ToggleNotification, CreateSelect2, InstanceGroupsService, InstanceGroupsData, ConfigData) {
|
GetBasePath, Wait, $state, ToggleNotification, CreateSelect2, InstanceGroupsService, InstanceGroupsData, ConfigData) {
|
||||||
|
|
||||||
let form = OrganizationForm(),
|
let form = OrganizationForm(),
|
||||||
@@ -26,6 +26,12 @@ export default ['$scope', '$location', '$stateParams', 'OrgAdminLookup',
|
|||||||
$scope.isOrgAdmin = isOrgAdmin;
|
$scope.isOrgAdmin = isOrgAdmin;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Rest.setUrl(GetBasePath('users') + $rootScope.current_user.id + '/roles/?role_field=notification_admin_role');
|
||||||
|
Rest.get()
|
||||||
|
.then(({data}) => {
|
||||||
|
$scope.isNotificationAdmin = (data.count && data.count > 0);
|
||||||
|
});
|
||||||
|
|
||||||
$scope.$watch('organization_obj.summary_fields.user_capabilities.edit', function(val) {
|
$scope.$watch('organization_obj.summary_fields.user_capabilities.edit', function(val) {
|
||||||
if (val === false) {
|
if (val === false) {
|
||||||
$scope.canAdd = false;
|
$scope.canAdd = false;
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
|||||||
}
|
}
|
||||||
switch ($scope.scm_type.value) {
|
switch ($scope.scm_type.value) {
|
||||||
case 'git':
|
case 'git':
|
||||||
$scope.credentialLabel = "SCM Credential";
|
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||||
$scope.urlPopover = '<p>' +
|
$scope.urlPopover = '<p>' +
|
||||||
i18n._('Example URLs for GIT SCM include:') +
|
i18n._('Example URLs for GIT SCM include:') +
|
||||||
'</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
|
'</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
|
||||||
@@ -146,7 +146,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
|||||||
$scope.scmBranchLabel = i18n._('SCM Branch/Tag/Commit');
|
$scope.scmBranchLabel = i18n._('SCM Branch/Tag/Commit');
|
||||||
break;
|
break;
|
||||||
case 'svn':
|
case 'svn':
|
||||||
$scope.credentialLabel = "SCM Credential";
|
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||||
$scope.urlPopover = '<p>' + i18n._('Example URLs for Subversion SCM include:') + '</p>' +
|
$scope.urlPopover = '<p>' + i18n._('Example URLs for Subversion SCM include:') + '</p>' +
|
||||||
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
|
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
|
||||||
'<li>svn+ssh://servername.example.com/path</li></ul>';
|
'<li>svn+ssh://servername.example.com/path</li></ul>';
|
||||||
@@ -155,7 +155,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
|||||||
$scope.scmBranchLabel = i18n._('Revision #');
|
$scope.scmBranchLabel = i18n._('Revision #');
|
||||||
break;
|
break;
|
||||||
case 'hg':
|
case 'hg':
|
||||||
$scope.credentialLabel = "SCM Credential";
|
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||||
$scope.urlPopover = '<p>' + i18n._('Example URLs for Mercurial SCM include:') + '</p>' +
|
$scope.urlPopover = '<p>' + i18n._('Example URLs for Mercurial SCM include:') + '</p>' +
|
||||||
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
|
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
|
||||||
'<li>ssh://server.example.com/path</li></ul>' +
|
'<li>ssh://server.example.com/path</li></ul>' +
|
||||||
@@ -174,7 +174,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
|||||||
$scope.lookupType = 'insights_credential';
|
$scope.lookupType = 'insights_credential';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$scope.credentialLabel = "SCM Credential";
|
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||||
$scope.urlPopover = '<p> ' + i18n._('URL popover text') + '</p>';
|
$scope.urlPopover = '<p> ' + i18n._('URL popover text') + '</p>';
|
||||||
$scope.credRequired = false;
|
$scope.credRequired = false;
|
||||||
$scope.lookupType = 'scm_credential';
|
$scope.lookupType = 'scm_credential';
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
|||||||
}
|
}
|
||||||
switch ($scope.scm_type.value) {
|
switch ($scope.scm_type.value) {
|
||||||
case 'git':
|
case 'git':
|
||||||
$scope.credentialLabel = "SCM Credential";
|
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||||
$scope.urlPopover = '<p>' + i18n._('Example URLs for GIT SCM include:') + '</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
|
$scope.urlPopover = '<p>' + i18n._('Example URLs for GIT SCM include:') + '</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
|
||||||
'<li>git@github.com:ansible/ansible.git</li><li>git://servername.example.com/ansible.git</li></ul>' +
|
'<li>git@github.com:ansible/ansible.git</li><li>git://servername.example.com/ansible.git</li></ul>' +
|
||||||
'<p>' + i18n.sprintf(i18n._('%sNote:%s When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' +
|
'<p>' + i18n.sprintf(i18n._('%sNote:%s When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' +
|
||||||
@@ -281,7 +281,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
|||||||
$scope.scmBranchLabel = i18n._('SCM Branch/Tag/Commit');
|
$scope.scmBranchLabel = i18n._('SCM Branch/Tag/Commit');
|
||||||
break;
|
break;
|
||||||
case 'svn':
|
case 'svn':
|
||||||
$scope.credentialLabel = "SCM Credential";
|
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||||
$scope.urlPopover = '<p>' + i18n._('Example URLs for Subversion SCM include:') + '</p>' +
|
$scope.urlPopover = '<p>' + i18n._('Example URLs for Subversion SCM include:') + '</p>' +
|
||||||
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
|
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
|
||||||
'<li>svn+ssh://servername.example.com/path</li></ul>';
|
'<li>svn+ssh://servername.example.com/path</li></ul>';
|
||||||
@@ -290,7 +290,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
|||||||
$scope.scmBranchLabel = i18n._('Revision #');
|
$scope.scmBranchLabel = i18n._('Revision #');
|
||||||
break;
|
break;
|
||||||
case 'hg':
|
case 'hg':
|
||||||
$scope.credentialLabel = "SCM Credential";
|
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||||
$scope.urlPopover = '<p>' + i18n._('Example URLs for Mercurial SCM include:') + '</p>' +
|
$scope.urlPopover = '<p>' + i18n._('Example URLs for Mercurial SCM include:') + '</p>' +
|
||||||
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
|
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
|
||||||
'<li>ssh://server.example.com/path</li></ul>' +
|
'<li>ssh://server.example.com/path</li></ul>' +
|
||||||
@@ -309,7 +309,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
|||||||
$scope.lookupType = 'insights_credential';
|
$scope.lookupType = 'insights_credential';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$scope.credentialLabel = "SCM Credential";
|
$scope.credentialLabel = "SCM " + i18n._("Credential");
|
||||||
$scope.urlPopover = '<p> ' + i18n._('URL popover text');
|
$scope.urlPopover = '<p> ' + i18n._('URL popover text');
|
||||||
$scope.credRequired = false;
|
$scope.credRequired = false;
|
||||||
$scope.lookupType = 'scm_credential';
|
$scope.lookupType = 'scm_credential';
|
||||||
|
|||||||
@@ -670,6 +670,8 @@ function(ConfigurationUtils, i18n, $rootScope) {
|
|||||||
query += '&role_level=workflow_admin_role';
|
query += '&role_level=workflow_admin_role';
|
||||||
} else if ($state.current.name.includes('projects')) {
|
} else if ($state.current.name.includes('projects')) {
|
||||||
query += '&role_level=project_admin_role';
|
query += '&role_level=project_admin_role';
|
||||||
|
} else if ($state.current.name.includes('notifications')) {
|
||||||
|
query += '&role_level=notification_admin_role';
|
||||||
} else {
|
} else {
|
||||||
query += '&role_level=admin_role';
|
query += '&role_level=admin_role';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ export default ['$scope',
|
|||||||
$scope.selection.selectedItems =
|
$scope.selection.selectedItems =
|
||||||
_items.filter(function(item) {
|
_items.filter(function(item) {
|
||||||
return item.isSelected;
|
return item.isSelected;
|
||||||
}).pluck('value').value();
|
}).map('value').value();
|
||||||
|
|
||||||
$scope.selection.deselectedItems =
|
$scope.selection.deselectedItems =
|
||||||
_items.pluck('value').difference($scope.selection.selectedItems)
|
_items.map('value').difference($scope.selection.selectedItems)
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -310,12 +310,12 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if(defaultParams) {
|
if(defaultParams) {
|
||||||
let stripped =_.pick(params, (value, key) => {
|
let stripped =_.pickBy(params, (value, key) => {
|
||||||
// setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value
|
// setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value
|
||||||
return defaultParams[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaultParams[key] !== null;
|
return defaultParams[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaultParams[key] !== null;
|
||||||
});
|
});
|
||||||
let strippedCopy = _.cloneDeep(stripped);
|
let strippedCopy = _.cloneDeep(stripped);
|
||||||
if(_.keys(_.pick(defaultParams, _.keys(strippedCopy))).length > 0){
|
if(_.keys(_.pickBy(defaultParams, _.keys(strippedCopy))).length > 0){
|
||||||
for (var key in strippedCopy) {
|
for (var key in strippedCopy) {
|
||||||
if (strippedCopy.hasOwnProperty(key)) {
|
if (strippedCopy.hasOwnProperty(key)) {
|
||||||
let value = strippedCopy[key];
|
let value = strippedCopy[key];
|
||||||
@@ -336,7 +336,7 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc
|
|||||||
mergeQueryset (queryset, additional, singleSearchParam) {
|
mergeQueryset (queryset, additional, singleSearchParam) {
|
||||||
const space = '%20and%20';
|
const space = '%20and%20';
|
||||||
|
|
||||||
const merged = _.merge({}, queryset, additional, (objectValue, sourceValue, key, object) => {
|
const merged = _.mergeWith({}, queryset, additional, (objectValue, sourceValue, key, object) => {
|
||||||
if (!(object[key] && object[key] !== sourceValue)) {
|
if (!(object[key] && object[key] !== sourceValue)) {
|
||||||
// // https://lodash.com/docs/3.10.1#each
|
// // https://lodash.com/docs/3.10.1#each
|
||||||
// If this returns undefined merging is handled by default _.merge algorithm
|
// If this returns undefined merging is handled by default _.merge algorithm
|
||||||
@@ -418,7 +418,7 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc
|
|||||||
termParams = searchWithoutKey(term, singleSearchParam);
|
termParams = searchWithoutKey(term, singleSearchParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
params = _.merge(params, termParams, combineSameSearches);
|
params = _.mergeWith(params, termParams, combineSameSearches);
|
||||||
});
|
});
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function SmartSearchController (
|
|||||||
const listName = $scope.list.name;
|
const listName = $scope.list.name;
|
||||||
const baseRelatedTypePath = `models.${listName}.base.${rootField}.type`;
|
const baseRelatedTypePath = `models.${listName}.base.${rootField}.type`;
|
||||||
|
|
||||||
const isRelatedSearchTermField = (_.contains($scope.models[listName].related, rootField));
|
const isRelatedSearchTermField = (_.includes($scope.models[listName].related, rootField));
|
||||||
const isBaseModelRelatedSearchTermField = (_.get($scope, baseRelatedTypePath) === 'field');
|
const isBaseModelRelatedSearchTermField = (_.get($scope, baseRelatedTypePath) === 'field');
|
||||||
|
|
||||||
return (isRelatedSearchTermField || isBaseModelRelatedSearchTermField);
|
return (isRelatedSearchTermField || isBaseModelRelatedSearchTermField);
|
||||||
@@ -254,7 +254,7 @@ function SmartSearchController (
|
|||||||
defaults[key] = queryset[key];
|
defaults[key] = queryset[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const cleared = _(defaults).omit(_.isNull).value();
|
const cleared = _(defaults).omitBy(_.isNull).value();
|
||||||
delete cleared.page;
|
delete cleared.page;
|
||||||
queryset = cleared;
|
queryset = cleared;
|
||||||
|
|
||||||
|
|||||||
@@ -744,11 +744,21 @@ function($injector, $stateExtender, $log, i18n) {
|
|||||||
// search will think they need to be set as search tags.
|
// search will think they need to be set as search tags.
|
||||||
var params;
|
var params;
|
||||||
if(field.sourceModel === "organization"){
|
if(field.sourceModel === "organization"){
|
||||||
|
if (form.name === "notification_template") {
|
||||||
|
// Users with admin_role role level should also have
|
||||||
|
// notification_admin_role so this should handle regular admin
|
||||||
|
// users as well as notification admin users
|
||||||
|
params = {
|
||||||
|
page_size: '5',
|
||||||
|
role_level: 'notification_admin_role'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
params = {
|
params = {
|
||||||
page_size: '5',
|
page_size: '5',
|
||||||
role_level: 'admin_role'
|
role_level: 'admin_role'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if(field.sourceModel === "inventory_script"){
|
else if(field.sourceModel === "inventory_script"){
|
||||||
params = {
|
params = {
|
||||||
page_size: '5',
|
page_size: '5',
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ export default ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Rest',
|
|||||||
$scope.canEdit = me.get('summary_fields.user_capabilities.edit');
|
$scope.canEdit = me.get('summary_fields.user_capabilities.edit');
|
||||||
$scope.isOrgAdmin = me.get('related.admin_of_organizations.count') > 0;
|
$scope.isOrgAdmin = me.get('related.admin_of_organizations.count') > 0;
|
||||||
$scope.team_id = id;
|
$scope.team_id = id;
|
||||||
setScopeFields(data);
|
_.forEach(form.fields, (value, key) => {
|
||||||
|
$scope[key] = data[key];
|
||||||
|
});
|
||||||
$scope.organization_name = data.summary_fields.organization.name;
|
$scope.organization_name = data.summary_fields.organization.name;
|
||||||
|
|
||||||
OrgAdminLookup.checkForAdminAccess({organization: data.organization})
|
OrgAdminLookup.checkForAdminAccess({organization: data.organization})
|
||||||
@@ -36,19 +38,6 @@ export default ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Rest',
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @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
|
// prepares a data payload for a PUT request to the API
|
||||||
function processNewData(fields) {
|
function processNewData(fields) {
|
||||||
var data = {};
|
var data = {};
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="Prompt-previewTags--outer">
|
<div class="Prompt-previewTags--outer">
|
||||||
<div ng-show="promptData.launchConf.defaults.inventory.id && !promptData.prompts.inventory.value.id" class="Prompt-noSelectedItem">{{:: vm.strings.get('prompt.NO_INVENTORY_SELECTED') }}</div>
|
<div ng-show="promptData.launchConf.defaults.inventory.id && !promptData.prompts.inventory.value.id" class="Prompt-noSelectedItem">{{:: vm.strings.get('prompt.NO_INVENTORY_SELECTED') }}</div>
|
||||||
<at-tag tag="promptData.prompts.inventory.value.name" remove-tag="vm.deleteSelectedInventory()" ng-show="!readOnlyPrompts"></at-tag>
|
<at-tag tag="promptData.prompts.inventory.value.name" remove-tag="vm.deleteSelectedInventory()" ng-show="!readOnlyPrompts && promptData.prompts.inventory.value.id"></at-tag>
|
||||||
<at-tag tag="promptData.prompts.inventory.value.name" ng-show="readOnlyPrompts"></at-tag>
|
<at-tag tag="promptData.prompts.inventory.value.name" ng-show="readOnlyPrompts && promptData.prompts.inventory.value.id"></at-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="Prompt-previewTagRevert">
|
<div class="Prompt-previewTagRevert">
|
||||||
<a class="Prompt-revertLink" href="" ng-hide="readOnlyPrompts || promptData.prompts.inventory.value.id === promptData.launchConf.defaults.inventory.id" ng-click="vm.revert()">{{:: vm.strings.get('prompt.REVERT') }}</a>
|
<a class="Prompt-revertLink" href="" ng-hide="readOnlyPrompts || promptData.prompts.inventory.value.id === promptData.launchConf.defaults.inventory.id" ng-click="vm.revert()">{{:: vm.strings.get('prompt.REVERT') }}</a>
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ export default
|
|||||||
scope.maxTextError = false;
|
scope.maxTextError = false;
|
||||||
|
|
||||||
if(scope.type.type==="text"){
|
if(scope.type.type==="text"){
|
||||||
if(scope.default.trim() !== ""){
|
if(scope.default && scope.default.trim() !== ""){
|
||||||
if(scope.default.trim().length < scope.text_min && scope.text_min !== "" ){
|
if(scope.default.trim().length < scope.text_min && scope.text_min !== "" ){
|
||||||
scope.minTextError = true;
|
scope.minTextError = true;
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ export default
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(scope.type.type==="textarea"){
|
if(scope.type.type==="textarea"){
|
||||||
if(scope.default_textarea.trim() !== ""){
|
if(scope.default_textarea && scope.default_textarea.trim() !== ""){
|
||||||
if(scope.default_textarea.trim().length < scope.textarea_min && scope.textarea_min !== "" ){
|
if(scope.default_textarea.trim().length < scope.textarea_min && scope.textarea_min !== "" ){
|
||||||
scope.minTextError = true;
|
scope.minTextError = true;
|
||||||
}
|
}
|
||||||
@@ -283,7 +283,7 @@ export default
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(scope.type.type==="password"){
|
if(scope.type.type==="password"){
|
||||||
if(scope.default_password.trim() !== ""){
|
if(scope.default_password && scope.default_password.trim() !== ""){
|
||||||
if(scope.default_password.trim().length < scope.password_min && scope.password_min !== "" ){
|
if(scope.default_password.trim().length < scope.password_min && scope.password_min !== "" ){
|
||||||
scope.minTextError = true;
|
scope.minTextError = true;
|
||||||
}
|
}
|
||||||
@@ -293,7 +293,7 @@ export default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(scope.type.type==="multiselect" && scope.default_multiselect.trim() !== ""){
|
if(scope.type.type==="multiselect" && scope.default_multiselect && scope.default_multiselect.trim() !== ""){
|
||||||
choiceArray = scope.choices.split(/\n/);
|
choiceArray = scope.choices.split(/\n/);
|
||||||
answerArray = scope.default_multiselect.split(/\n/);
|
answerArray = scope.default_multiselect.split(/\n/);
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ export default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(scope.type.type==="multiplechoice" && scope.default.trim() !== ""){
|
if(scope.type.type==="multiplechoice" && scope.default && scope.default.trim() !== ""){
|
||||||
choiceArray = scope.choices.split(/\n/);
|
choiceArray = scope.choices.split(/\n/);
|
||||||
if($.inArray(scope.default, choiceArray)===-1){
|
if($.inArray(scope.default, choiceArray)===-1){
|
||||||
scope.invalidChoice = true;
|
scope.invalidChoice = true;
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||||
'$state', 'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
|
'$state', 'ProcessErrors', 'CreateSelect2', '$q', 'JobTemplateModel',
|
||||||
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout',
|
'Empty', 'PromptService', 'Rest', 'TemplatesStrings', '$timeout',
|
||||||
|
'i18n',
|
||||||
function($scope, WorkflowService, GetBasePath, TemplatesService,
|
function($scope, WorkflowService, GetBasePath, TemplatesService,
|
||||||
$state, ProcessErrors, CreateSelect2, $q, JobTemplate,
|
$state, ProcessErrors, CreateSelect2, $q, JobTemplate,
|
||||||
Empty, PromptService, Rest, TemplatesStrings, $timeout) {
|
Empty, PromptService, Rest, TemplatesStrings, $timeout, i18n) {
|
||||||
|
|
||||||
let promptWatcher, surveyQuestionWatcher, credentialsWatcher;
|
let promptWatcher, surveyQuestionWatcher, credentialsWatcher;
|
||||||
|
|
||||||
@@ -301,15 +302,15 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
if (!optionsToInclude) {
|
if (!optionsToInclude) {
|
||||||
$scope.edgeTypeOptions = [
|
$scope.edgeTypeOptions = [
|
||||||
{
|
{
|
||||||
label: 'Always',
|
label: i18n._('Always'),
|
||||||
value: 'always'
|
value: 'always'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'On Success',
|
label: i18n._('On Success'),
|
||||||
value: 'success'
|
value: 'success'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'On Failure',
|
label: i18n._('On Failure'),
|
||||||
value: 'failure'
|
value: 'failure'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -641,6 +642,31 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
|
|
||||||
if (!_.isEmpty($scope.nodeBeingEdited.promptData)) {
|
if (!_.isEmpty($scope.nodeBeingEdited.promptData)) {
|
||||||
$scope.promptData = _.cloneDeep($scope.nodeBeingEdited.promptData);
|
$scope.promptData = _.cloneDeep($scope.nodeBeingEdited.promptData);
|
||||||
|
const launchConf = $scope.promptData.launchConf;
|
||||||
|
|
||||||
|
if (!launchConf.survey_enabled &&
|
||||||
|
!launchConf.ask_inventory_on_launch &&
|
||||||
|
!launchConf.ask_credential_on_launch &&
|
||||||
|
!launchConf.ask_verbosity_on_launch &&
|
||||||
|
!launchConf.ask_job_type_on_launch &&
|
||||||
|
!launchConf.ask_limit_on_launch &&
|
||||||
|
!launchConf.ask_tags_on_launch &&
|
||||||
|
!launchConf.ask_skip_tags_on_launch &&
|
||||||
|
!launchConf.ask_diff_mode_on_launch &&
|
||||||
|
!launchConf.credential_needed_to_start &&
|
||||||
|
!launchConf.ask_variables_on_launch &&
|
||||||
|
launchConf.variables_needed_to_start.length === 0) {
|
||||||
|
$scope.showPromptButton = false;
|
||||||
|
$scope.promptModalMissingReqFields = false;
|
||||||
|
} else {
|
||||||
|
$scope.showPromptButton = true;
|
||||||
|
|
||||||
|
if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeBeingEdited.originalNodeObj.summary_fields.inventory')) {
|
||||||
|
$scope.promptModalMissingReqFields = true;
|
||||||
|
} else {
|
||||||
|
$scope.promptModalMissingReqFields = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'job_template' ||
|
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.unified_job_type') === 'job_template' ||
|
||||||
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === 'job_template'
|
_.get($scope, 'nodeBeingEdited.unifiedJobTemplate.type') === 'job_template'
|
||||||
@@ -727,8 +753,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
!launchConf.ask_tags_on_launch &&
|
!launchConf.ask_tags_on_launch &&
|
||||||
!launchConf.ask_skip_tags_on_launch &&
|
!launchConf.ask_skip_tags_on_launch &&
|
||||||
!launchConf.ask_diff_mode_on_launch &&
|
!launchConf.ask_diff_mode_on_launch &&
|
||||||
!launchConf.survey_enabled &&
|
|
||||||
!launchConf.credential_needed_to_start &&
|
!launchConf.credential_needed_to_start &&
|
||||||
|
!launchConf.ask_variables_on_launch &&
|
||||||
launchConf.variables_needed_to_start.length === 0) {
|
launchConf.variables_needed_to_start.length === 0) {
|
||||||
$scope.showPromptButton = false;
|
$scope.showPromptButton = false;
|
||||||
$scope.promptModalMissingReqFields = false;
|
$scope.promptModalMissingReqFields = false;
|
||||||
@@ -839,19 +865,19 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
|
|
||||||
switch($scope.nodeBeingEdited.edgeType) {
|
switch($scope.nodeBeingEdited.edgeType) {
|
||||||
case "always":
|
case "always":
|
||||||
$scope.edgeType = {label: "Always", value: "always"};
|
$scope.edgeType = {label: i18n._("Always"), value: "always"};
|
||||||
if (siblingConnectionTypes.length === 1 && _.includes(siblingConnectionTypes, "always") || $scope.nodeBeingEdited.isRoot) {
|
if (siblingConnectionTypes.length === 1 && _.includes(siblingConnectionTypes, "always") || $scope.nodeBeingEdited.isRoot) {
|
||||||
edgeDropdownOptions = ["always"];
|
edgeDropdownOptions = ["always"];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "success":
|
case "success":
|
||||||
$scope.edgeType = {label: "On Success", value: "success"};
|
$scope.edgeType = {label: i18n._("On Success"), value: "success"};
|
||||||
if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) {
|
if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) {
|
||||||
edgeDropdownOptions = ["success", "failure"];
|
edgeDropdownOptions = ["success", "failure"];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "failure":
|
case "failure":
|
||||||
$scope.edgeType = {label: "On Failure", value: "failure"};
|
$scope.edgeType = {label: i18n._("On Failure"), value: "failure"};
|
||||||
if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) {
|
if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) {
|
||||||
edgeDropdownOptions = ["success", "failure"];
|
edgeDropdownOptions = ["success", "failure"];
|
||||||
}
|
}
|
||||||
@@ -978,7 +1004,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
|
|
||||||
switch($scope.nodeBeingEdited.edgeType) {
|
switch($scope.nodeBeingEdited.edgeType) {
|
||||||
case "always":
|
case "always":
|
||||||
$scope.edgeType = {label: "Always", value: "always"};
|
$scope.edgeType = {label: i18n._("Always"), value: "always"};
|
||||||
if (
|
if (
|
||||||
_.includes(siblingConnectionTypes, "always") &&
|
_.includes(siblingConnectionTypes, "always") &&
|
||||||
!_.includes(siblingConnectionTypes, "success") &&
|
!_.includes(siblingConnectionTypes, "success") &&
|
||||||
@@ -990,7 +1016,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "success":
|
case "success":
|
||||||
$scope.edgeType = {label: "On Success", value: "success"};
|
$scope.edgeType = {label: i18n._("On Success"), value: "success"};
|
||||||
if (
|
if (
|
||||||
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
|
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
|
||||||
!_.includes(siblingConnectionTypes, "always")
|
!_.includes(siblingConnectionTypes, "always")
|
||||||
@@ -1001,7 +1027,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "failure":
|
case "failure":
|
||||||
$scope.edgeType = {label: "On Failure", value: "failure"};
|
$scope.edgeType = {label: i18n._("On Failure"), value: "failure"};
|
||||||
if (
|
if (
|
||||||
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
|
(_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) &&
|
||||||
!_.includes(siblingConnectionTypes, "always")
|
!_.includes(siblingConnectionTypes, "always")
|
||||||
@@ -1071,8 +1097,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
!launchConf.ask_tags_on_launch &&
|
!launchConf.ask_tags_on_launch &&
|
||||||
!launchConf.ask_skip_tags_on_launch &&
|
!launchConf.ask_skip_tags_on_launch &&
|
||||||
!launchConf.ask_diff_mode_on_launch &&
|
!launchConf.ask_diff_mode_on_launch &&
|
||||||
!launchConf.survey_enabled &&
|
|
||||||
!launchConf.credential_needed_to_start &&
|
!launchConf.credential_needed_to_start &&
|
||||||
|
!launchConf.ask_variables_on_launch &&
|
||||||
launchConf.variables_needed_to_start.length === 0) {
|
launchConf.variables_needed_to_start.length === 0) {
|
||||||
$scope.showPromptButton = false;
|
$scope.showPromptButton = false;
|
||||||
$scope.promptModalMissingReqFields = false;
|
$scope.promptModalMissingReqFields = false;
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
|||||||
init();
|
init();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
_.forEach(form.fields, (value, key) => {
|
||||||
|
$scope[key] = user_obj[key];
|
||||||
|
});
|
||||||
|
|
||||||
$scope.canEdit = me.get('summary_fields.user_capabilities.edit');
|
$scope.canEdit = me.get('summary_fields.user_capabilities.edit');
|
||||||
$scope.isOrgAdmin = me.get('related.admin_of_organizations.count') > 0;
|
$scope.isOrgAdmin = me.get('related.admin_of_organizations.count') > 0;
|
||||||
$scope.isCurrentlyLoggedInUser = (parseInt(id) === $rootScope.current_user.id);
|
$scope.isCurrentlyLoggedInUser = (parseInt(id) === $rootScope.current_user.id);
|
||||||
@@ -73,9 +77,6 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
|||||||
$scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) {
|
$scope.$watch('user_obj.summary_fields.user_capabilities.edit', function(val) {
|
||||||
$scope.canAdd = (val === false) ? false : true;
|
$scope.canAdd = (val === false) ? false : true;
|
||||||
});
|
});
|
||||||
|
|
||||||
setScopeFields(user_obj);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function user_type_sync($scope) {
|
function user_type_sync($scope) {
|
||||||
@@ -107,19 +108,6 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function setScopeFields(data) {
|
|
||||||
_(data)
|
|
||||||
.pick(function(value, key) {
|
|
||||||
return form.fields.hasOwnProperty(key) === true;
|
|
||||||
})
|
|
||||||
.forEach(function(value, key) {
|
|
||||||
$scope[key] = value;
|
|
||||||
})
|
|
||||||
.value();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.redirectToResource = function(resource) {
|
$scope.redirectToResource = function(resource) {
|
||||||
let type = resource.summary_fields.resource_type.replace(/ /g , "_");
|
let type = resource.summary_fields.resource_type.replace(/ /g , "_");
|
||||||
var id = resource.related[type].split("/")[4];
|
var id = resource.related[type].split("/")[4];
|
||||||
@@ -152,9 +140,13 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
|||||||
var processNewData = function(fields) {
|
var processNewData = function(fields) {
|
||||||
var data = {};
|
var data = {};
|
||||||
_.forEach(fields, function(value, key) {
|
_.forEach(fields, function(value, key) {
|
||||||
|
if (value.type === 'sensitive') {
|
||||||
if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined) {
|
if ($scope[key] !== '' && $scope[key] !== null && $scope[key] !== undefined) {
|
||||||
data[key] = $scope[key];
|
data[key] = $scope[key];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
data[key] = $scope[key];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
data.is_superuser = $scope.is_superuser;
|
data.is_superuser = $scope.is_superuser;
|
||||||
data.is_system_auditor = $scope.is_system_auditor;
|
data.is_system_auditor = $scope.is_system_auditor;
|
||||||
|
|||||||
39
awx/ui/npm-shrinkwrap.json
generated
39
awx/ui/npm-shrinkwrap.json
generated
@@ -223,6 +223,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "3.8.0",
|
||||||
|
"from": "lodash@>=3.8.0 <3.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.8.0.tgz"
|
||||||
|
},
|
||||||
"rrule": {
|
"rrule": {
|
||||||
"version": "2.2.0-dev",
|
"version": "2.2.0-dev",
|
||||||
"from": "jkbrzt/rrule#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c",
|
"from": "jkbrzt/rrule#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c",
|
||||||
@@ -233,7 +238,7 @@
|
|||||||
"angular-tz-extensions": {
|
"angular-tz-extensions": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"from": "git+https://git@github.com/ansible/angular-tz-extensions.git#v0.5.2",
|
"from": "git+https://git@github.com/ansible/angular-tz-extensions.git#v0.5.2",
|
||||||
"resolved": "git+https://git@github.com/ansible/angular-tz-extensions.git#9cabb05d58079092bfb29ccae721b35b46f28af6",
|
"resolved": "git://github.com/ansible/angular-tz-extensions.git#9cabb05d58079092bfb29ccae721b35b46f28af6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jquery": {
|
"jquery": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
@@ -1496,7 +1501,15 @@
|
|||||||
"version": "0.19.0",
|
"version": "0.19.0",
|
||||||
"from": "cheerio@>=0.19.0 <0.20.0",
|
"from": "cheerio@>=0.19.0 <0.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"from": "lodash@>=3.2.0 <4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"chokidar": {
|
"chokidar": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
@@ -5420,6 +5433,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lodash": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"from": "lodash@>=3.8.0 <4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"source-map": {
|
"source-map": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.7",
|
||||||
"from": "source-map@>=0.5.3 <0.6.0",
|
"from": "source-map@>=0.5.3 <0.6.0",
|
||||||
@@ -5723,6 +5742,12 @@
|
|||||||
"from": "inquirer@>=0.8.2 <0.9.0",
|
"from": "inquirer@>=0.8.2 <0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.8.5.tgz",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"from": "lodash@>=3.6.0 <4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
|
||||||
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5796,9 +5821,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "3.8.0",
|
"version": "4.17.10",
|
||||||
"from": "lodash@>=3.8.0 <3.9.0",
|
"from": "lodash@>=4.17.10 <4.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.8.0.tgz"
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz"
|
||||||
},
|
},
|
||||||
"lodash._arraycopy": {
|
"lodash._arraycopy": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -6317,6 +6342,12 @@
|
|||||||
"from": "glob@>=5.0.0 <6.0.0",
|
"from": "glob@>=5.0.0 <6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"from": "lodash@>=3.0.0 <4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
|
||||||
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"dev": "webpack --config build/webpack.development.js --progress",
|
"dev": "webpack --config build/webpack.development.js --progress",
|
||||||
"watch": "webpack-dev-server --config build/webpack.watch.js --progress --https",
|
"watch": "webpack-dev-server --config build/webpack.watch.js --progress --https",
|
||||||
"production": "webpack --config build/webpack.production.js"
|
"production": "webpack --config build/webpack.production.js",
|
||||||
|
"grab-licenses": "./utils/get_licenses.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"angular-mocks": "~1.6.6",
|
"angular-mocks": "~1.6.6",
|
||||||
@@ -120,7 +121,7 @@
|
|||||||
"jquery-ui": "^1.12.1",
|
"jquery-ui": "^1.12.1",
|
||||||
"js-yaml": "^3.2.7",
|
"js-yaml": "^3.2.7",
|
||||||
"legacy-loader": "0.0.2",
|
"legacy-loader": "0.0.2",
|
||||||
"lodash": "~3.8.0",
|
"lodash": "~4.17.10",
|
||||||
"lr-infinite-scroll": "git+https://git@github.com/lorenzofox3/lrInfiniteScroll",
|
"lr-infinite-scroll": "git+https://git@github.com/lorenzofox3/lrInfiniteScroll",
|
||||||
"moment": "^2.19.4",
|
"moment": "^2.19.4",
|
||||||
"ng-toast": "git+https://git@github.com/ansible/ngToast#v2.1.1",
|
"ng-toast": "git+https://git@github.com/ansible/ngToast#v2.1.1",
|
||||||
|
|||||||
@@ -236,8 +236,8 @@ msgstr ""
|
|||||||
msgid "Add Project"
|
msgid "Add Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1718
|
#: client/src/shared/form-generator.js:1731
|
||||||
#: client/src/templates/job_templates/job-template.form.js:464
|
#: client/src/templates/job_templates/job-template.form.js:468
|
||||||
#: client/src/templates/workflows.form.js:205
|
#: client/src/templates/workflows.form.js:205
|
||||||
msgid "Add Survey"
|
msgid "Add Survey"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -277,12 +277,12 @@ msgstr ""
|
|||||||
#: client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js:115
|
#: client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js:115
|
||||||
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:117
|
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:117
|
||||||
#: client/src/projects/projects.form.js:255
|
#: client/src/projects/projects.form.js:255
|
||||||
#: client/src/templates/job_templates/job-template.form.js:407
|
#: client/src/templates/job_templates/job-template.form.js:411
|
||||||
#: client/src/templates/workflows.form.js:148
|
#: client/src/templates/workflows.form.js:148
|
||||||
msgid "Add a permission"
|
msgid "Add a permission"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1453
|
#: client/src/shared/form-generator.js:1466
|
||||||
msgid "Admin"
|
msgid "Admin"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -315,8 +315,8 @@ msgstr ""
|
|||||||
msgid "All Jobs"
|
msgid "All Jobs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:286
|
#: client/src/templates/job_templates/job-template.form.js:290
|
||||||
#: client/src/templates/job_templates/job-template.form.js:293
|
#: client/src/templates/job_templates/job-template.form.js:297
|
||||||
msgid "Allow Provisioning Callbacks"
|
msgid "Allow Provisioning Callbacks"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -337,8 +337,8 @@ msgstr ""
|
|||||||
#: client/src/organizations/organizations.form.js:52
|
#: client/src/organizations/organizations.form.js:52
|
||||||
#: client/src/projects/projects.form.js:207
|
#: client/src/projects/projects.form.js:207
|
||||||
#: client/src/projects/projects.form.js:212
|
#: client/src/projects/projects.form.js:212
|
||||||
#: client/src/templates/job_templates/job-template.form.js:235
|
#: client/src/templates/job_templates/job-template.form.js:239
|
||||||
#: client/src/templates/job_templates/job-template.form.js:241
|
#: client/src/templates/job_templates/job-template.form.js:245
|
||||||
msgid "Ansible Environment"
|
msgid "Ansible Environment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -434,7 +434,7 @@ msgstr ""
|
|||||||
msgid "Associate this host with a new group"
|
msgid "Associate this host with a new group"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1455
|
#: client/src/shared/form-generator.js:1468
|
||||||
msgid "Auditor"
|
msgid "Auditor"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -703,7 +703,7 @@ msgstr ""
|
|||||||
#: client/src/access/add-rbac-user-team/rbac-user-team.partial.html:188
|
#: client/src/access/add-rbac-user-team/rbac-user-team.partial.html:188
|
||||||
#: client/src/configuration/configuration.controller.js:617
|
#: client/src/configuration/configuration.controller.js:617
|
||||||
#: client/src/scheduler/scheduler.strings.js:56
|
#: client/src/scheduler/scheduler.strings.js:56
|
||||||
#: client/src/shared/form-generator.js:1706
|
#: client/src/shared/form-generator.js:1719
|
||||||
#: client/src/shared/lookup/lookup-modal.partial.html:19
|
#: client/src/shared/lookup/lookup-modal.partial.html:19
|
||||||
#: client/src/workflow-results/workflow-results.controller.js:38
|
#: client/src/workflow-results/workflow-results.controller.js:38
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
@@ -759,7 +759,7 @@ msgstr ""
|
|||||||
msgid "Check"
|
msgid "Check"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1078
|
#: client/src/shared/form-generator.js:1087
|
||||||
msgid "Choose a %s"
|
msgid "Choose a %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -844,7 +844,7 @@ msgid "Client Secret"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/scheduler/scheduler.strings.js:55
|
#: client/src/scheduler/scheduler.strings.js:55
|
||||||
#: client/src/shared/form-generator.js:1710
|
#: client/src/shared/form-generator.js:1723
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -870,7 +870,7 @@ msgstr ""
|
|||||||
#: client/src/inventories-hosts/inventories/related/hosts/related-host.form.js:128
|
#: client/src/inventories-hosts/inventories/related/hosts/related-host.form.js:128
|
||||||
#: client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js:155
|
#: client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js:155
|
||||||
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:172
|
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:172
|
||||||
#: client/src/templates/job_templates/job-template.form.js:439
|
#: client/src/templates/job_templates/job-template.form.js:443
|
||||||
#: client/src/templates/workflows.form.js:180
|
#: client/src/templates/workflows.form.js:180
|
||||||
msgid "Completed Jobs"
|
msgid "Completed Jobs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1338,7 +1338,7 @@ msgstr ""
|
|||||||
#: client/features/output/output.strings.js:34
|
#: client/features/output/output.strings.js:34
|
||||||
#: client/features/users/tokens/tokens.strings.js:14
|
#: client/features/users/tokens/tokens.strings.js:14
|
||||||
#: client/src/license/license.partial.html:5
|
#: client/src/license/license.partial.html:5
|
||||||
#: client/src/shared/form-generator.js:1488
|
#: client/src/shared/form-generator.js:1501
|
||||||
msgid "Details"
|
msgid "Details"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1514,8 +1514,8 @@ msgstr ""
|
|||||||
msgid "Edit Question"
|
msgid "Edit Question"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1722
|
#: client/src/shared/form-generator.js:1735
|
||||||
#: client/src/templates/job_templates/job-template.form.js:471
|
#: client/src/templates/job_templates/job-template.form.js:475
|
||||||
#: client/src/templates/workflows.form.js:212
|
#: client/src/templates/workflows.form.js:212
|
||||||
msgid "Edit Survey"
|
msgid "Edit Survey"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1608,8 +1608,8 @@ msgstr ""
|
|||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:299
|
#: client/src/templates/job_templates/job-template.form.js:303
|
||||||
#: client/src/templates/job_templates/job-template.form.js:304
|
#: client/src/templates/job_templates/job-template.form.js:308
|
||||||
#: client/src/templates/workflows.form.js:100
|
#: client/src/templates/workflows.form.js:100
|
||||||
#: client/src/templates/workflows.form.js:105
|
#: client/src/templates/workflows.form.js:105
|
||||||
msgid "Enable Concurrent Jobs"
|
msgid "Enable Concurrent Jobs"
|
||||||
@@ -1620,8 +1620,8 @@ msgid "Enable External Logging"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:124
|
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:124
|
||||||
#: client/src/templates/job_templates/job-template.form.js:275
|
#: client/src/templates/job_templates/job-template.form.js:279
|
||||||
#: client/src/templates/job_templates/job-template.form.js:280
|
#: client/src/templates/job_templates/job-template.form.js:284
|
||||||
msgid "Enable Privilege Escalation"
|
msgid "Enable Privilege Escalation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1629,7 +1629,7 @@ msgstr ""
|
|||||||
msgid "Enable survey"
|
msgid "Enable survey"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:290
|
#: client/src/templates/job_templates/job-template.form.js:294
|
||||||
msgid "Enables creation of a provisioning callback URL. Using the URL a host can contact {{BRAND_NAME}} and request a configuration update using this job template."
|
msgid "Enables creation of a provisioning callback URL. Using the URL a host can contact {{BRAND_NAME}} and request a configuration update using this job template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1815,8 +1815,8 @@ msgstr ""
|
|||||||
#: client/src/job-submission/job-submission.partial.html:165
|
#: client/src/job-submission/job-submission.partial.html:165
|
||||||
#: client/src/partials/logviewer.html:8
|
#: client/src/partials/logviewer.html:8
|
||||||
#: client/src/scheduler/scheduler.strings.js:53
|
#: client/src/scheduler/scheduler.strings.js:53
|
||||||
#: client/src/templates/job_templates/job-template.form.js:353
|
#: client/src/templates/job_templates/job-template.form.js:357
|
||||||
#: client/src/templates/job_templates/job-template.form.js:360
|
#: client/src/templates/job_templates/job-template.form.js:364
|
||||||
#: client/src/templates/workflows.form.js:83
|
#: client/src/templates/workflows.form.js:83
|
||||||
#: client/src/templates/workflows.form.js:90
|
#: client/src/templates/workflows.form.js:90
|
||||||
#: client/src/workflow-results/workflow-results.controller.js:122
|
#: client/src/workflow-results/workflow-results.controller.js:122
|
||||||
@@ -2091,8 +2091,8 @@ msgstr ""
|
|||||||
msgid "Host (Authentication URL)"
|
msgid "Host (Authentication URL)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:335
|
#: client/src/templates/job_templates/job-template.form.js:339
|
||||||
#: client/src/templates/job_templates/job-template.form.js:344
|
#: client/src/templates/job_templates/job-template.form.js:348
|
||||||
msgid "Host Config Key"
|
msgid "Host Config Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2269,7 +2269,7 @@ msgstr ""
|
|||||||
msgid "If checked, any hosts and groups that were previously present on the external source but are now removed will be removed from the Tower inventory. Hosts and groups that were not managed by the inventory source will be promoted to the next manually created group or if there is no manually created group to promote them into, they will be left in the \"all\" default group for the inventory."
|
msgid "If checked, any hosts and groups that were previously present on the external source but are now removed will be removed from the Tower inventory. Hosts and groups that were not managed by the inventory source will be promoted to the next manually created group or if there is no manually created group to promote them into, they will be left in the \"all\" default group for the inventory."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:278
|
#: client/src/templates/job_templates/job-template.form.js:282
|
||||||
msgid "If enabled, run this playbook as an administrator."
|
msgid "If enabled, run this playbook as an administrator."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2277,11 +2277,11 @@ msgstr ""
|
|||||||
msgid "If enabled, show the changes made by Ansible tasks, where supported. This is equivalent to Ansible’s --diff mode."
|
msgid "If enabled, show the changes made by Ansible tasks, where supported. This is equivalent to Ansible’s --diff mode."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:263
|
#: client/src/templates/job_templates/job-template.form.js:267
|
||||||
msgid "If enabled, show the changes made by Ansible tasks, where supported. This is equivalent to Ansible’s --diff mode."
|
msgid "If enabled, show the changes made by Ansible tasks, where supported. This is equivalent to Ansible’s --diff mode."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:302
|
#: client/src/templates/job_templates/job-template.form.js:306
|
||||||
msgid "If enabled, simultaneous runs of this job template will be allowed."
|
msgid "If enabled, simultaneous runs of this job template will be allowed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2289,7 +2289,7 @@ msgstr ""
|
|||||||
msgid "If enabled, simultaneous runs of this workflow job template will be allowed."
|
msgid "If enabled, simultaneous runs of this workflow job template will be allowed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:313
|
#: client/src/templates/job_templates/job-template.form.js:317
|
||||||
msgid "If enabled, use cached facts if available and store discovered facts in the cache."
|
msgid "If enabled, use cached facts if available and store discovered facts in the cache."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2360,8 +2360,8 @@ msgstr ""
|
|||||||
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:64
|
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:64
|
||||||
#: client/src/organizations/organizations.form.js:38
|
#: client/src/organizations/organizations.form.js:38
|
||||||
#: client/src/organizations/organizations.form.js:41
|
#: client/src/organizations/organizations.form.js:41
|
||||||
#: client/src/templates/job_templates/job-template.form.js:248
|
#: client/src/templates/job_templates/job-template.form.js:252
|
||||||
#: client/src/templates/job_templates/job-template.form.js:251
|
#: client/src/templates/job_templates/job-template.form.js:255
|
||||||
msgid "Instance Groups"
|
msgid "Instance Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2679,7 +2679,7 @@ msgstr ""
|
|||||||
msgid "Last Updated"
|
msgid "Last Updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1714
|
#: client/src/shared/form-generator.js:1727
|
||||||
msgid "Launch"
|
msgid "Launch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2773,7 +2773,7 @@ msgstr ""
|
|||||||
msgid "Live events: error connecting to the server."
|
msgid "Live events: error connecting to the server."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1992
|
#: client/src/shared/form-generator.js:2005
|
||||||
msgid "Loading..."
|
msgid "Loading..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2843,6 +2843,10 @@ msgstr ""
|
|||||||
msgid "Manual projects do not require an SCM update"
|
msgid "Manual projects do not require an SCM update"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: client/src/templates/job_templates/job-template.form.js:234
|
||||||
|
msgid "Max 512 characters per label."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/login/loginModal/loginModal.partial.html:28
|
#: client/src/login/loginModal/loginModal.partial.html:28
|
||||||
msgid "Maximum per-user sessions reached. Please sign in."
|
msgid "Maximum per-user sessions reached. Please sign in."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -3153,7 +3157,7 @@ msgid "No recent notifications."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/inventories-hosts/hosts/hosts.partial.html:36
|
#: client/src/inventories-hosts/hosts/hosts.partial.html:36
|
||||||
#: client/src/shared/form-generator.js:1886
|
#: client/src/shared/form-generator.js:1899
|
||||||
#: client/src/shared/list-generator/list-generator.factory.js:240
|
#: client/src/shared/list-generator/list-generator.factory.js:240
|
||||||
msgid "No records matched your search."
|
msgid "No records matched your search."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -3300,7 +3304,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: client/src/notifications/notificationTemplates.form.js:453
|
#: client/src/notifications/notificationTemplates.form.js:453
|
||||||
#: client/src/partials/logviewer.html:7
|
#: client/src/partials/logviewer.html:7
|
||||||
#: client/src/templates/job_templates/job-template.form.js:271
|
#: client/src/templates/job_templates/job-template.form.js:275
|
||||||
#: client/src/templates/workflows.form.js:96
|
#: client/src/templates/workflows.form.js:96
|
||||||
msgid "Options"
|
msgid "Options"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -3414,7 +3418,7 @@ msgid "PLEASE ADD A SURVEY PROMPT."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/organizations/list/organizations-list.partial.html:37
|
#: client/src/organizations/list/organizations-list.partial.html:37
|
||||||
#: client/src/shared/form-generator.js:1892
|
#: client/src/shared/form-generator.js:1905
|
||||||
#: client/src/shared/list-generator/list-generator.factory.js:248
|
#: client/src/shared/list-generator/list-generator.factory.js:248
|
||||||
msgid "PLEASE ADD ITEMS TO THIS LIST"
|
msgid "PLEASE ADD ITEMS TO THIS LIST"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -3448,7 +3452,7 @@ msgstr ""
|
|||||||
msgid "Pagerduty subdomain"
|
msgid "Pagerduty subdomain"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:359
|
#: client/src/templates/job_templates/job-template.form.js:363
|
||||||
msgid "Pass extra command line variables to the playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Tower documentation for example syntax."
|
msgid "Pass extra command line variables to the playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Tower documentation for example syntax."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3531,7 +3535,7 @@ msgstr ""
|
|||||||
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:106
|
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:106
|
||||||
#: client/src/projects/projects.form.js:247
|
#: client/src/projects/projects.form.js:247
|
||||||
#: client/src/teams/teams.form.js:120
|
#: client/src/teams/teams.form.js:120
|
||||||
#: client/src/templates/job_templates/job-template.form.js:398
|
#: client/src/templates/job_templates/job-template.form.js:402
|
||||||
#: client/src/templates/workflows.form.js:139
|
#: client/src/templates/workflows.form.js:139
|
||||||
#: client/src/users/users.form.js:189
|
#: client/src/users/users.form.js:189
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
@@ -3542,7 +3546,7 @@ msgid "Personal Access Token"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/features/output/output.strings.js:63
|
#: client/features/output/output.strings.js:63
|
||||||
#: client/src/shared/form-generator.js:1076
|
#: client/src/shared/form-generator.js:1085
|
||||||
#: client/src/templates/job_templates/job-template.form.js:107
|
#: client/src/templates/job_templates/job-template.form.js:107
|
||||||
#: client/src/templates/job_templates/job-template.form.js:115
|
#: client/src/templates/job_templates/job-template.form.js:115
|
||||||
msgid "Playbook"
|
msgid "Playbook"
|
||||||
@@ -3598,15 +3602,15 @@ msgstr ""
|
|||||||
msgid "Please enter a URL that begins with ssh, http or https. The URL may not contain the '@' character."
|
msgid "Please enter a URL that begins with ssh, http or https. The URL may not contain the '@' character."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1165
|
#: client/src/shared/form-generator.js:1178
|
||||||
msgid "Please enter a number greater than %d and less than %d."
|
msgid "Please enter a number greater than %d and less than %d."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1167
|
#: client/src/shared/form-generator.js:1180
|
||||||
msgid "Please enter a number greater than %d."
|
msgid "Please enter a number greater than %d."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1159
|
#: client/src/shared/form-generator.js:1172
|
||||||
msgid "Please enter a number."
|
msgid "Please enter a number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3708,7 +3712,7 @@ msgstr ""
|
|||||||
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:102
|
#: client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js:102
|
||||||
#: client/src/projects/projects.form.js:239
|
#: client/src/projects/projects.form.js:239
|
||||||
#: client/src/teams/teams.form.js:116
|
#: client/src/teams/teams.form.js:116
|
||||||
#: client/src/templates/job_templates/job-template.form.js:391
|
#: client/src/templates/job_templates/job-template.form.js:395
|
||||||
#: client/src/templates/workflows.form.js:132
|
#: client/src/templates/workflows.form.js:132
|
||||||
msgid "Please save before assigning permissions."
|
msgid "Please save before assigning permissions."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -3772,11 +3776,11 @@ msgstr ""
|
|||||||
msgid "Please select Users from the list below."
|
msgid "Please select Users from the list below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1200
|
#: client/src/shared/form-generator.js:1213
|
||||||
msgid "Please select a number between"
|
msgid "Please select a number between"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1196
|
#: client/src/shared/form-generator.js:1209
|
||||||
msgid "Please select a number."
|
msgid "Please select a number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3784,10 +3788,10 @@ msgstr ""
|
|||||||
msgid "Please select a value"
|
msgid "Please select a value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1088
|
#: client/src/shared/form-generator.js:1097
|
||||||
#: client/src/shared/form-generator.js:1156
|
#: client/src/shared/form-generator.js:1169
|
||||||
#: client/src/shared/form-generator.js:1277
|
#: client/src/shared/form-generator.js:1290
|
||||||
#: client/src/shared/form-generator.js:1385
|
#: client/src/shared/form-generator.js:1398
|
||||||
msgid "Please select a value."
|
msgid "Please select a value."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3799,7 +3803,7 @@ msgstr ""
|
|||||||
msgid "Please select an organization before editing the host filter."
|
msgid "Please select an organization before editing the host filter."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1193
|
#: client/src/shared/form-generator.js:1206
|
||||||
msgid "Please select at least one value."
|
msgid "Please select at least one value."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3942,8 +3946,8 @@ msgstr ""
|
|||||||
#: client/src/templates/job_templates/job-template.form.js:185
|
#: client/src/templates/job_templates/job-template.form.js:185
|
||||||
#: client/src/templates/job_templates/job-template.form.js:202
|
#: client/src/templates/job_templates/job-template.form.js:202
|
||||||
#: client/src/templates/job_templates/job-template.form.js:219
|
#: client/src/templates/job_templates/job-template.form.js:219
|
||||||
#: client/src/templates/job_templates/job-template.form.js:266
|
#: client/src/templates/job_templates/job-template.form.js:270
|
||||||
#: client/src/templates/job_templates/job-template.form.js:366
|
#: client/src/templates/job_templates/job-template.form.js:370
|
||||||
#: client/src/templates/job_templates/job-template.form.js:60
|
#: client/src/templates/job_templates/job-template.form.js:60
|
||||||
#: client/src/templates/job_templates/job-template.form.js:86
|
#: client/src/templates/job_templates/job-template.form.js:86
|
||||||
msgid "Prompt on launch"
|
msgid "Prompt on launch"
|
||||||
@@ -3982,8 +3986,8 @@ msgstr ""
|
|||||||
msgid "Provide the named URL encoded name or id of the remote Tower inventory to be imported."
|
msgid "Provide the named URL encoded name or id of the remote Tower inventory to be imported."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:322
|
#: client/src/templates/job_templates/job-template.form.js:326
|
||||||
#: client/src/templates/job_templates/job-template.form.js:330
|
#: client/src/templates/job_templates/job-template.form.js:334
|
||||||
msgid "Provisioning Callback URL"
|
msgid "Provisioning Callback URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4492,7 +4496,7 @@ msgstr ""
|
|||||||
#: client/src/access/add-rbac-user-team/rbac-user-team.partial.html:193
|
#: client/src/access/add-rbac-user-team/rbac-user-team.partial.html:193
|
||||||
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:158
|
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:158
|
||||||
#: client/src/scheduler/scheduler.strings.js:57
|
#: client/src/scheduler/scheduler.strings.js:57
|
||||||
#: client/src/shared/form-generator.js:1698
|
#: client/src/shared/form-generator.js:1711
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4543,7 +4547,7 @@ msgstr ""
|
|||||||
#: client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js:35
|
#: client/src/activity-stream/streamDropdownNav/stream-dropdown-nav.directive.js:35
|
||||||
#: client/src/inventories-hosts/inventories/related/sources/sources.form.js:440
|
#: client/src/inventories-hosts/inventories/related/sources/sources.form.js:440
|
||||||
#: client/src/projects/projects.form.js:290
|
#: client/src/projects/projects.form.js:290
|
||||||
#: client/src/templates/job_templates/job-template.form.js:444
|
#: client/src/templates/job_templates/job-template.form.js:448
|
||||||
#: client/src/templates/workflows.form.js:185
|
#: client/src/templates/workflows.form.js:185
|
||||||
msgid "Schedules"
|
msgid "Schedules"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -4566,7 +4570,7 @@ msgstr ""
|
|||||||
msgid "Security Token Service (STS) is a web service that enables you to request temporary, limited-privilege credentials for AWS Identity and Access Management (IAM) users."
|
msgid "Security Token Service (STS) is a web service that enables you to request temporary, limited-privilege credentials for AWS Identity and Access Management (IAM) users."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1702
|
#: client/src/shared/form-generator.js:1715
|
||||||
#: client/src/shared/lookup/lookup-modal.directive.js:59
|
#: client/src/shared/lookup/lookup-modal.directive.js:59
|
||||||
#: client/src/shared/lookup/lookup-modal.partial.html:20
|
#: client/src/shared/lookup/lookup-modal.partial.html:20
|
||||||
msgid "Select"
|
msgid "Select"
|
||||||
@@ -4634,7 +4638,7 @@ msgstr ""
|
|||||||
msgid "Select the Instance Groups for this Inventory to run on. Refer to the Ansible Tower documentation for more detail."
|
msgid "Select the Instance Groups for this Inventory to run on. Refer to the Ansible Tower documentation for more detail."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:250
|
#: client/src/templates/job_templates/job-template.form.js:254
|
||||||
msgid "Select the Instance Groups for this Job Template to run on."
|
msgid "Select the Instance Groups for this Job Template to run on."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4642,7 +4646,7 @@ msgstr ""
|
|||||||
msgid "Select the Instance Groups for this Organization to run on."
|
msgid "Select the Instance Groups for this Organization to run on."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:240
|
#: client/src/templates/job_templates/job-template.form.js:244
|
||||||
msgid "Select the custom Python virtual environment for this job template to run on."
|
msgid "Select the custom Python virtual environment for this job template to run on."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4709,8 +4713,8 @@ msgstr ""
|
|||||||
#: client/features/templates/templates.strings.js:46
|
#: client/features/templates/templates.strings.js:46
|
||||||
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:115
|
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:115
|
||||||
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:118
|
#: client/src/inventories-hosts/inventories/adhoc/adhoc.form.js:118
|
||||||
#: client/src/templates/job_templates/job-template.form.js:257
|
#: client/src/templates/job_templates/job-template.form.js:261
|
||||||
#: client/src/templates/job_templates/job-template.form.js:260
|
#: client/src/templates/job_templates/job-template.form.js:264
|
||||||
msgid "Show Changes"
|
msgid "Show Changes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4757,7 +4761,7 @@ msgstr ""
|
|||||||
#: client/src/inventories-hosts/inventories/inventory.list.js:86
|
#: client/src/inventories-hosts/inventories/inventory.list.js:86
|
||||||
#: client/src/inventories-hosts/inventories/list/inventory-list.controller.js:76
|
#: client/src/inventories-hosts/inventories/list/inventory-list.controller.js:76
|
||||||
#: client/src/organizations/linkout/controllers/organizations-inventories.controller.js:70
|
#: client/src/organizations/linkout/controllers/organizations-inventories.controller.js:70
|
||||||
#: client/src/shared/form-generator.js:1463
|
#: client/src/shared/form-generator.js:1476
|
||||||
msgid "Smart Inventory"
|
msgid "Smart Inventory"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5111,8 +5115,8 @@ msgstr ""
|
|||||||
msgid "Textarea"
|
msgid "Textarea"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1393
|
#: client/src/shared/form-generator.js:1406
|
||||||
#: client/src/shared/form-generator.js:1399
|
#: client/src/shared/form-generator.js:1412
|
||||||
msgid "That value was not found. Please enter or select a valid value."
|
msgid "That value was not found. Please enter or select a valid value."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5564,12 +5568,12 @@ msgstr ""
|
|||||||
|
|
||||||
#: client/src/organizations/organizations.form.js:48
|
#: client/src/organizations/organizations.form.js:48
|
||||||
#: client/src/projects/projects.form.js:209
|
#: client/src/projects/projects.form.js:209
|
||||||
#: client/src/templates/job_templates/job-template.form.js:237
|
#: client/src/templates/job_templates/job-template.form.js:241
|
||||||
msgid "Use Default Environment"
|
msgid "Use Default Environment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/templates/job_templates/job-template.form.js:310
|
#: client/src/templates/job_templates/job-template.form.js:314
|
||||||
#: client/src/templates/job_templates/job-template.form.js:315
|
#: client/src/templates/job_templates/job-template.form.js:319
|
||||||
msgid "Use Fact Cache"
|
msgid "Use Fact Cache"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5764,8 +5768,8 @@ msgstr ""
|
|||||||
msgid "View Project checkout results"
|
msgid "View Project checkout results"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1726
|
#: client/src/shared/form-generator.js:1739
|
||||||
#: client/src/templates/job_templates/job-template.form.js:455
|
#: client/src/templates/job_templates/job-template.form.js:459
|
||||||
#: client/src/templates/workflows.form.js:196
|
#: client/src/templates/workflows.form.js:196
|
||||||
msgid "View Survey"
|
msgid "View Survey"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -5946,7 +5950,7 @@ msgstr ""
|
|||||||
msgid "Workflow Templates"
|
msgid "Workflow Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1730
|
#: client/src/shared/form-generator.js:1743
|
||||||
#: client/src/templates/workflows.form.js:222
|
#: client/src/templates/workflows.form.js:222
|
||||||
msgid "Workflow Visualizer"
|
msgid "Workflow Visualizer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6032,7 +6036,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: client/src/inventories-hosts/inventories/related/groups/list/groups-list.partial.html:24
|
#: client/src/inventories-hosts/inventories/related/groups/list/groups-list.partial.html:24
|
||||||
#: client/src/job-submission/job-submission.partial.html:317
|
#: client/src/job-submission/job-submission.partial.html:317
|
||||||
#: client/src/shared/form-generator.js:1200
|
#: client/src/shared/form-generator.js:1213
|
||||||
#: client/src/templates/prompt/steps/survey/prompt-survey.partial.html:42
|
#: client/src/templates/prompt/steps/survey/prompt-survey.partial.html:42
|
||||||
msgid "and"
|
msgid "and"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6144,7 +6148,7 @@ msgstr ""
|
|||||||
msgid "organization"
|
msgid "organization"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: client/src/shared/form-generator.js:1076
|
#: client/src/shared/form-generator.js:1085
|
||||||
msgid "playbook"
|
msgid "playbook"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user