mirror of
https://github.com/ansible/awx.git
synced 2026-02-27 15:58:45 -03:30
Merge branch 'release_3.1.0' into 4673-alert-modal-spacing
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -106,7 +106,6 @@ reports
|
|||||||
*.log.[0-9]
|
*.log.[0-9]
|
||||||
*.results
|
*.results
|
||||||
local/
|
local/
|
||||||
*.mo
|
|
||||||
|
|
||||||
# AWX python libs populated by requirements.txt
|
# AWX python libs populated by requirements.txt
|
||||||
awx/lib/.deps_built
|
awx/lib/.deps_built
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -559,9 +559,12 @@ messages:
|
|||||||
fi; \
|
fi; \
|
||||||
$(PYTHON) manage.py makemessages -l $(LANG) --keep-pot
|
$(PYTHON) manage.py makemessages -l $(LANG) --keep-pot
|
||||||
|
|
||||||
# generate l10n .json .mo
|
# generate l10n .json
|
||||||
languages: $(UI_DEPS_FLAG_FILE) check-po
|
ui-languages: $(UI_DEPS_FLAG_FILE) check-po
|
||||||
$(NPM_BIN) --prefix awx/ui run languages
|
$(NPM_BIN) --prefix awx/ui run languages
|
||||||
|
|
||||||
|
# generate l10n .mo
|
||||||
|
api-languages:
|
||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/tower/bin/activate; \
|
. $(VENV_BASE)/tower/bin/activate; \
|
||||||
fi; \
|
fi; \
|
||||||
@@ -592,8 +595,7 @@ ui-devel: $(UI_DEPS_FLAG_FILE)
|
|||||||
|
|
||||||
ui-release: $(UI_RELEASE_FLAG_FILE)
|
ui-release: $(UI_RELEASE_FLAG_FILE)
|
||||||
|
|
||||||
# todo: include languages target when .po deliverables are added to source control
|
$(UI_RELEASE_FLAG_FILE): ui-languages $(UI_DEPS_FLAG_FILE)
|
||||||
$(UI_RELEASE_FLAG_FILE): $(UI_DEPS_FLAG_FILE)
|
|
||||||
$(NPM_BIN) --prefix awx/ui run build-release
|
$(NPM_BIN) --prefix awx/ui run build-release
|
||||||
touch $(UI_RELEASE_FLAG_FILE)
|
touch $(UI_RELEASE_FLAG_FILE)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework import metadata
|
from rest_framework import metadata
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.relations import RelatedField
|
from rest_framework.relations import RelatedField, ManyRelatedField
|
||||||
from rest_framework.request import clone_request
|
from rest_framework.request import clone_request
|
||||||
|
|
||||||
# Ansible Tower
|
# Ansible Tower
|
||||||
@@ -75,7 +75,7 @@ class Metadata(metadata.SimpleMetadata):
|
|||||||
elif getattr(field, 'fields', None):
|
elif getattr(field, 'fields', None):
|
||||||
field_info['children'] = self.get_serializer_info(field)
|
field_info['children'] = self.get_serializer_info(field)
|
||||||
|
|
||||||
if hasattr(field, 'choices') and not isinstance(field, RelatedField):
|
if not isinstance(field, (RelatedField, ManyRelatedField)) and hasattr(field, 'choices'):
|
||||||
field_info['choices'] = [(choice_value, choice_name) for choice_value, choice_name in field.choices.items()]
|
field_info['choices'] = [(choice_value, choice_name) for choice_value, choice_name in field.choices.items()]
|
||||||
|
|
||||||
# Indicate if a field is write-only.
|
# Indicate if a field is write-only.
|
||||||
|
|||||||
@@ -2370,7 +2370,7 @@ class WorkflowJobTemplateNodeSerializer(WorkflowNodeBaseSerializer):
|
|||||||
if view and view.request:
|
if view and view.request:
|
||||||
request_method = view.request.method
|
request_method = view.request.method
|
||||||
if request_method in ['PATCH']:
|
if request_method in ['PATCH']:
|
||||||
obj = view.get_object()
|
obj = self.instance
|
||||||
char_prompts = copy.copy(obj.char_prompts)
|
char_prompts = copy.copy(obj.char_prompts)
|
||||||
char_prompts.update(self.extract_char_prompts(data))
|
char_prompts.update(self.extract_char_prompts(data))
|
||||||
else:
|
else:
|
||||||
@@ -2709,18 +2709,15 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
|
|||||||
variables_needed_to_start = serializers.ReadOnlyField()
|
variables_needed_to_start = serializers.ReadOnlyField()
|
||||||
survey_enabled = serializers.SerializerMethodField()
|
survey_enabled = serializers.SerializerMethodField()
|
||||||
extra_vars = VerbatimField(required=False, write_only=True)
|
extra_vars = VerbatimField(required=False, write_only=True)
|
||||||
warnings = serializers.SerializerMethodField()
|
|
||||||
workflow_job_template_data = serializers.SerializerMethodField()
|
workflow_job_template_data = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkflowJobTemplate
|
model = WorkflowJobTemplate
|
||||||
fields = ('can_start_without_user_input', 'extra_vars', 'warnings',
|
fields = ('can_start_without_user_input', 'extra_vars',
|
||||||
'survey_enabled', 'variables_needed_to_start',
|
'survey_enabled', 'variables_needed_to_start',
|
||||||
|
'node_templates_missing', 'node_prompts_rejected',
|
||||||
'workflow_job_template_data')
|
'workflow_job_template_data')
|
||||||
|
|
||||||
def get_warnings(self, obj):
|
|
||||||
return obj.get_warnings()
|
|
||||||
|
|
||||||
def get_survey_enabled(self, obj):
|
def get_survey_enabled(self, obj):
|
||||||
if obj:
|
if obj:
|
||||||
return obj.survey_enabled and 'spec' in obj.survey_spec
|
return obj.survey_enabled and 'spec' in obj.survey_spec
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Which will act on data older than 30 days.
|
|||||||
|
|
||||||
For `cleanup_facts`:
|
For `cleanup_facts`:
|
||||||
|
|
||||||
`{"older_than": "4w", `granularity`: "3d"}`
|
`{"older_than": "4w", "granularity": "3d"}`
|
||||||
|
|
||||||
Which will reduce the granularity of scan data to one scan per 3 days when the data is older than 4w.
|
Which will reduce the granularity of scan data to one scan per 3 days when the data is older than 4w.
|
||||||
|
|
||||||
|
|||||||
@@ -2946,7 +2946,10 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
|
|||||||
always_allow_superuser = False
|
always_allow_superuser = False
|
||||||
|
|
||||||
def update_raw_data(self, data):
|
def update_raw_data(self, data):
|
||||||
obj = self.get_object()
|
try:
|
||||||
|
obj = self.get_object()
|
||||||
|
except PermissionDenied:
|
||||||
|
return data
|
||||||
extra_vars = data.pop('extra_vars', None) or {}
|
extra_vars = data.pop('extra_vars', None) or {}
|
||||||
if obj:
|
if obj:
|
||||||
for v in obj.variables_needed_to_start:
|
for v in obj.variables_needed_to_start:
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ from __future__ import (absolute_import, division, print_function)
|
|||||||
|
|
||||||
# Python
|
# Python
|
||||||
import contextlib
|
import contextlib
|
||||||
import copy
|
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@@ -77,41 +75,11 @@ class BaseCallbackModule(CallbackBase):
|
|||||||
super(BaseCallbackModule, self).__init__()
|
super(BaseCallbackModule, self).__init__()
|
||||||
self.task_uuids = set()
|
self.task_uuids = set()
|
||||||
|
|
||||||
def censor_result(self, res, no_log=False):
|
|
||||||
if not isinstance(res, dict):
|
|
||||||
if no_log:
|
|
||||||
return "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
|
|
||||||
return res
|
|
||||||
if res.get('_ansible_no_log', no_log):
|
|
||||||
new_res = {}
|
|
||||||
for k in self.CENSOR_FIELD_WHITELIST:
|
|
||||||
if k in res:
|
|
||||||
new_res[k] = res[k]
|
|
||||||
if k == 'cmd' and k in res:
|
|
||||||
if isinstance(res['cmd'], list):
|
|
||||||
res['cmd'] = ' '.join(res['cmd'])
|
|
||||||
if re.search(r'\s', res['cmd']):
|
|
||||||
new_res['cmd'] = re.sub(r'^(([^\s\\]|\\\s)+).*$',
|
|
||||||
r'\1 <censored>',
|
|
||||||
res['cmd'])
|
|
||||||
new_res['censored'] = "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
|
|
||||||
res = new_res
|
|
||||||
if 'results' in res:
|
|
||||||
if isinstance(res['results'], list):
|
|
||||||
for i in xrange(len(res['results'])):
|
|
||||||
res['results'][i] = self.censor_result(res['results'][i], res.get('_ansible_no_log', no_log))
|
|
||||||
elif res.get('_ansible_no_log', False):
|
|
||||||
res['results'] = "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
|
|
||||||
return res
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def capture_event_data(self, event, **event_data):
|
def capture_event_data(self, event, **event_data):
|
||||||
|
|
||||||
event_data.setdefault('uuid', str(uuid.uuid4()))
|
event_data.setdefault('uuid', str(uuid.uuid4()))
|
||||||
|
|
||||||
if 'res' in event_data:
|
|
||||||
event_data['res'] = self.censor_result(copy.copy(event_data['res']))
|
|
||||||
|
|
||||||
if event not in self.EVENTS_WITHOUT_TASK:
|
if event not in self.EVENTS_WITHOUT_TASK:
|
||||||
task = event_data.pop('task', None)
|
task = event_data.pop('task', None)
|
||||||
else:
|
else:
|
||||||
@@ -258,7 +226,7 @@ class BaseCallbackModule(CallbackBase):
|
|||||||
if task_uuid in self.task_uuids:
|
if task_uuid in self.task_uuids:
|
||||||
# FIXME: When this task UUID repeats, it means the play is using the
|
# FIXME: When this task UUID repeats, it means the play is using the
|
||||||
# free strategy, so different hosts may be running different tasks
|
# free strategy, so different hosts may be running different tasks
|
||||||
# within a play.
|
# within a play.
|
||||||
return
|
return
|
||||||
self.task_uuids.add(task_uuid)
|
self.task_uuids.add(task_uuid)
|
||||||
self.set_task(task)
|
self.set_task(task)
|
||||||
|
|||||||
BIN
awx/locale/en-us/LC_MESSAGES/django.mo
Normal file
BIN
awx/locale/en-us/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
3637
awx/locale/en-us/LC_MESSAGES/django.po
Normal file
3637
awx/locale/en-us/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
awx/locale/fr/LC_MESSAGES/ansible-tower-ui.mo
Normal file
BIN
awx/locale/fr/LC_MESSAGES/ansible-tower-ui.mo
Normal file
Binary file not shown.
2963
awx/locale/fr/LC_MESSAGES/ansible-tower-ui.po
Normal file
2963
awx/locale/fr/LC_MESSAGES/ansible-tower-ui.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
awx/locale/fr/LC_MESSAGES/django.mo
Normal file
BIN
awx/locale/fr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
4303
awx/locale/fr/LC_MESSAGES/django.po
Normal file
4303
awx/locale/fr/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
awx/locale/ja/LC_MESSAGES/ansible-tower-ui.mo
Normal file
BIN
awx/locale/ja/LC_MESSAGES/ansible-tower-ui.mo
Normal file
Binary file not shown.
2803
awx/locale/ja/LC_MESSAGES/ansible-tower-ui.po
Normal file
2803
awx/locale/ja/LC_MESSAGES/ansible-tower-ui.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
awx/locale/ja/LC_MESSAGES/django.mo
Normal file
BIN
awx/locale/ja/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
3984
awx/locale/ja/LC_MESSAGES/django.po
Normal file
3984
awx/locale/ja/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -985,8 +985,6 @@ class ProjectUpdateAccess(BaseAccess):
|
|||||||
|
|
||||||
@check_superuser
|
@check_superuser
|
||||||
def can_cancel(self, obj):
|
def can_cancel(self, obj):
|
||||||
if not obj.can_cancel:
|
|
||||||
return False
|
|
||||||
if self.user == obj.created_by:
|
if self.user == obj.created_by:
|
||||||
return True
|
return True
|
||||||
# Project updates cascade delete with project, admin role descends from org admin
|
# Project updates cascade delete with project, admin role descends from org admin
|
||||||
@@ -1395,7 +1393,8 @@ class WorkflowJobTemplateNodeAccess(BaseAccess):
|
|||||||
qs = self.model.objects.filter(
|
qs = self.model.objects.filter(
|
||||||
workflow_job_template__in=WorkflowJobTemplate.accessible_objects(
|
workflow_job_template__in=WorkflowJobTemplate.accessible_objects(
|
||||||
self.user, 'read_role'))
|
self.user, 'read_role'))
|
||||||
qs = qs.prefetch_related('success_nodes', 'failure_nodes', 'always_nodes')
|
qs = qs.prefetch_related('success_nodes', 'failure_nodes', 'always_nodes',
|
||||||
|
'unified_job_template')
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def can_use_prompted_resources(self, data):
|
def can_use_prompted_resources(self, data):
|
||||||
|
|||||||
@@ -1002,9 +1002,8 @@ class InventorySourceOptions(BaseModel):
|
|||||||
if r not in valid_regions and r not in invalid_regions:
|
if r not in valid_regions and r not in invalid_regions:
|
||||||
invalid_regions.append(r)
|
invalid_regions.append(r)
|
||||||
if invalid_regions:
|
if invalid_regions:
|
||||||
raise ValidationError(_('Invalid %(source)s region%(plural)s: %(region)s') % {
|
raise ValidationError(_('Invalid %(source)s region: %(region)s') % {
|
||||||
'source': self.source, 'plural': '' if len(invalid_regions) == 1 else 's',
|
'source': self.source, 'region': ', '.join(invalid_regions)})
|
||||||
'region': ', '.join(invalid_regions)})
|
|
||||||
return ','.join(regions)
|
return ','.join(regions)
|
||||||
|
|
||||||
source_vars_dict = VarsDictProperty('source_vars')
|
source_vars_dict = VarsDictProperty('source_vars')
|
||||||
@@ -1028,9 +1027,8 @@ class InventorySourceOptions(BaseModel):
|
|||||||
if instance_filter_name not in self.INSTANCE_FILTER_NAMES:
|
if instance_filter_name not in self.INSTANCE_FILTER_NAMES:
|
||||||
invalid_filters.append(instance_filter)
|
invalid_filters.append(instance_filter)
|
||||||
if invalid_filters:
|
if invalid_filters:
|
||||||
raise ValidationError(_('Invalid filter expression%(plural)s: %(filter)s') %
|
raise ValidationError(_('Invalid filter expression: %(filter)s') %
|
||||||
{'plural': '' if len(invalid_filters) == 1 else 's',
|
{'filter': ', '.join(invalid_filters)})
|
||||||
'filter': ', '.join(invalid_filters)})
|
|
||||||
return instance_filters
|
return instance_filters
|
||||||
|
|
||||||
def clean_group_by(self):
|
def clean_group_by(self):
|
||||||
@@ -1047,9 +1045,8 @@ class InventorySourceOptions(BaseModel):
|
|||||||
if c not in valid_choices and c not in invalid_choices:
|
if c not in valid_choices and c not in invalid_choices:
|
||||||
invalid_choices.append(c)
|
invalid_choices.append(c)
|
||||||
if invalid_choices:
|
if invalid_choices:
|
||||||
raise ValidationError(_('Invalid group by choice%(plural)s: %(choice)s') %
|
raise ValidationError(_('Invalid group by choice: %(choice)s') %
|
||||||
{'plural': '' if len(invalid_choices) == 1 else 's',
|
{'choice': ', '.join(invalid_choices)})
|
||||||
'choice': ', '.join(invalid_choices)})
|
|
||||||
return ','.join(choices)
|
return ','.join(choices)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class WorkflowNodeBase(CreatedModifiedModel):
|
|||||||
scan_errors = ujt_obj._extra_job_type_errors(accepted_fields)
|
scan_errors = ujt_obj._extra_job_type_errors(accepted_fields)
|
||||||
ignored_dict.update(scan_errors)
|
ignored_dict.update(scan_errors)
|
||||||
for fd in ['inventory', 'credential']:
|
for fd in ['inventory', 'credential']:
|
||||||
if getattr(ujt_obj, fd) is None and not (ask_for_vars_dict.get(fd, False) and fd in prompts_dict):
|
if getattr(ujt_obj, "{}_id".format(fd)) is None and not (ask_for_vars_dict.get(fd, False) and fd in prompts_dict):
|
||||||
missing_dict[fd] = 'Job Template does not have this field and workflow node does not provide it'
|
missing_dict[fd] = 'Job Template does not have this field and workflow node does not provide it'
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
@@ -421,18 +421,22 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
|||||||
|
|
||||||
def can_start_without_user_input(self):
|
def can_start_without_user_input(self):
|
||||||
'''Return whether WFJT can be launched without survey passwords.'''
|
'''Return whether WFJT can be launched without survey passwords.'''
|
||||||
return not bool(self.variables_needed_to_start)
|
return not bool(
|
||||||
|
self.variables_needed_to_start or
|
||||||
|
self.node_templates_missing() or
|
||||||
|
self.node_prompts_rejected())
|
||||||
|
|
||||||
def get_warnings(self):
|
def node_templates_missing(self):
|
||||||
warning_data = {}
|
return [node.pk for node in self.workflow_job_template_nodes.filter(
|
||||||
for node in self.workflow_job_template_nodes.all():
|
unified_job_template__isnull=True).all()]
|
||||||
if node.unified_job_template is None:
|
|
||||||
warning_data[node.pk] = 'Node is missing a linked unified_job_template'
|
def node_prompts_rejected(self):
|
||||||
continue
|
node_list = []
|
||||||
|
for node in self.workflow_job_template_nodes.select_related('unified_job_template').all():
|
||||||
node_prompts_warnings = node.get_prompts_warnings()
|
node_prompts_warnings = node.get_prompts_warnings()
|
||||||
if node_prompts_warnings:
|
if node_prompts_warnings:
|
||||||
warning_data[node.pk] = node_prompts_warnings
|
node_list.append(node.pk)
|
||||||
return warning_data
|
return node_list
|
||||||
|
|
||||||
def user_copy(self, user):
|
def user_copy(self, user):
|
||||||
new_wfjt = self.copy_unified_jt()
|
new_wfjt = self.copy_unified_jt()
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ class TestWorkflowJobTemplateNodeSerializerCharPrompts():
|
|||||||
serializer = WorkflowJobTemplateNodeSerializer()
|
serializer = WorkflowJobTemplateNodeSerializer()
|
||||||
node = WorkflowJobTemplateNode(pk=1)
|
node = WorkflowJobTemplateNode(pk=1)
|
||||||
node.char_prompts = {'limit': 'webservers'}
|
node.char_prompts = {'limit': 'webservers'}
|
||||||
|
serializer.instance = node
|
||||||
view = FakeView(node)
|
view = FakeView(node)
|
||||||
view.request = FakeRequest()
|
view.request = FakeRequest()
|
||||||
view.request.method = "PATCH"
|
view.request.method = "PATCH"
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ STDOUT_MAX_BYTES_DISPLAY = 1048576
|
|||||||
|
|
||||||
# Returned in the header on event api lists as a recommendation to the UI
|
# Returned in the header on event api lists as a recommendation to the UI
|
||||||
# on how many events to display before truncating/hiding
|
# on how many events to display before truncating/hiding
|
||||||
RECOMMENDED_MAX_EVENTS_DISPLAY_HEADER = 10000
|
RECOMMENDED_MAX_EVENTS_DISPLAY_HEADER = 4000
|
||||||
|
|
||||||
# The maximum size of the ansible callback event's res data structure
|
# The maximum size of the ansible callback event's res data structure
|
||||||
# beyond this limit and the value will be removed
|
# beyond this limit and the value will be removed
|
||||||
@@ -169,6 +169,10 @@ SESSION_COOKIE_SECURE = True
|
|||||||
# Disallow sending csrf cookies over insecure connections
|
# Disallow sending csrf cookies over insecure connections
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
|
|
||||||
|
# Limit CSRF cookies to browser sessions
|
||||||
|
CSRF_COOKIE_AGE = None
|
||||||
|
|
||||||
|
|
||||||
TEMPLATE_CONTEXT_PROCESSORS = ( # NOQA
|
TEMPLATE_CONTEXT_PROCESSORS = ( # NOQA
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.core.context_processors.debug',
|
'django.core.context_processors.debug',
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 footer-copyright">
|
<div class="col-sm-6 footer-copyright">
|
||||||
Copyright © 2016 <a href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc. All Rights Reserved.
|
Copyright © 2017 <a href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc. All Rights Reserved.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1720,8 +1720,7 @@ tr td button i {
|
|||||||
|
|
||||||
|
|
||||||
/* PW progress bar */
|
/* PW progress bar */
|
||||||
.pw-progress {
|
.pw-progress { margin-top: 10px;
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
li {
|
li {
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
|
|||||||
@@ -11,19 +11,19 @@
|
|||||||
<!-- Don't indent this properly, you'll break the cow -->
|
<!-- Don't indent this properly, you'll break the cow -->
|
||||||
<pre class="About-cowsay--code">
|
<pre class="About-cowsay--code">
|
||||||
________________
|
________________
|
||||||
/ Tower {{version_str}} \\
|
/ Tower {{version_str}} \
|
||||||
\\<span>{{version}}</span>/
|
\<span>{{version}}</span>/
|
||||||
----------------
|
----------------
|
||||||
\\ ^__^
|
\ ^__^
|
||||||
\\ (oo)\\_______
|
\ (oo)\_______
|
||||||
(__) A )\\/\\
|
(__) A )\/\
|
||||||
||----w |
|
||----w |
|
||||||
|| ||
|
|| ||
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="About-modal--footer">
|
<div class="About-modal--footer">
|
||||||
<img class="About-brand--redhat img-responsive" src="/static/assets/tower-logo-login.svg" />
|
<img class="About-brand--redhat img-responsive" src="/static/assets/tower-logo-login.svg" />
|
||||||
<p class="text-right">Copyright © 2016 Red Hat, Inc. <br>
|
<p class="text-right">Copyright © 2017 Red Hat, Inc. <br>
|
||||||
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
|
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
36
awx/ui/client/src/credentials/ownerList.block.less
Normal file
36
awx/ui/client/src/credentials/ownerList.block.less
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/** @define OwnerList */
|
||||||
|
@import "./client/src/shared/branding/colors.default.less";
|
||||||
|
|
||||||
|
.OwnerList {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OwnerList-seeBase {
|
||||||
|
display: flex;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
color: @default-link;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 2px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OwnerList-seeBase:hover {
|
||||||
|
color: @default-link-hov;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OwnerList-seeLess {
|
||||||
|
.OwnerList-seeBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OwnerList-seeMore {
|
||||||
|
.OwnerList-seeBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OwnerList-Container {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
<div ng-repeat="owner in owners_list">
|
<div class="OwnerList" ng-init="ownersLimit = 5; ownersLimitConst = 5; ">
|
||||||
<a ng-if="owner.type === 'organization'" ui-sref="organizations.edit({ organization_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
|
<div class="OwnerList-Container" ng-repeat="owner in owners_list | limitTo:ownersLimit">
|
||||||
<a ng-if="owner.type === 'user'" ui-sref="users.edit({ user_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
|
<a ng-if="owner.type === 'organization'" ui-sref="organizations.edit({ organization_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
|
||||||
<a ng-if="owner.type === 'team'" ui-sref="teams.edit({ team_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
|
<a ng-if="owner.type === 'user'" ui-sref="users.edit({ user_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
|
||||||
</div>
|
<a ng-if="owner.type === 'team'" ui-sref="teams.edit({ team_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="OwnerList-seeMore" ng-show="owners_list.length > ownersLimitConst && ownersLimit == ownersLimitConst"
|
||||||
|
ng-click="ownersLimit = owners_list.length">View More</div>
|
||||||
|
<div class="OwnerList-seeLess" ng-show="owners_list.length > ownersLimitConst && ownersLimit != ownersLimitConst"
|
||||||
|
ng-click="ownersLimit = ownersLimitConst">View Less</div>
|
||||||
|
</div>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<footer class='Footer'>
|
<footer class='Footer'>
|
||||||
<div class="Footer-copyright" ng-class="{'is-loggedOut' : !current_user || !current_user.username}">Copyright © 2016 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc.</div>
|
<div class="Footer-copyright" ng-class="{'is-loggedOut' : !current_user || !current_user.username}">Copyright © 2017 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc.</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ export default
|
|||||||
var langUrl = langInfo.replace('-', '_');
|
var langUrl = langInfo.replace('-', '_');
|
||||||
//gettextCatalog.debug = true;
|
//gettextCatalog.debug = true;
|
||||||
gettextCatalog.setCurrentLanguage(langInfo);
|
gettextCatalog.setCurrentLanguage(langInfo);
|
||||||
// TODO: the line below is commented out temporarily until
|
gettextCatalog.loadRemote('/static/languages/' + langUrl + '.json');
|
||||||
// the .po files are received from the i18n team, in order to avoid
|
|
||||||
// 404 file not found console errors in dev
|
|
||||||
// gettextCatalog.loadRemote('/static/languages/' + langUrl + '.json');
|
|
||||||
};
|
};
|
||||||
}])
|
}])
|
||||||
.factory('i18n', ['gettextCatalog',
|
.factory('i18n', ['gettextCatalog',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
$scope.item = group;
|
$scope.item = group;
|
||||||
$scope.submitMode = $stateParams.groups === undefined ? 'move' : 'copy';
|
$scope.submitMode = $stateParams.groups === undefined ? 'move' : 'copy';
|
||||||
$scope['toggle_'+ list.iterator] = function(id){
|
$scope.toggle_row = function(id){
|
||||||
// toggle off anything else currently selected
|
// toggle off anything else currently selected
|
||||||
_.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;});
|
_.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;});
|
||||||
// yoink the currently selected thing
|
// yoink the currently selected thing
|
||||||
@@ -60,9 +60,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
function init(){
|
function init(){
|
||||||
var url = GetBasePath('inventory') + $stateParams.inventory_id + '/groups/';
|
|
||||||
url += $stateParams.group ? '?not__id__in=' + group.id + ',' + _.last($stateParams.group) : '?not__id=' + group.id;
|
|
||||||
list.basePath = url;
|
|
||||||
$scope.atRootLevel = $stateParams.group ? false : true;
|
$scope.atRootLevel = $stateParams.group ? false : true;
|
||||||
|
|
||||||
// search init
|
// search init
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
['$scope', '$state', '$stateParams', 'generateList', 'HostManageService', 'GetBasePath', 'CopyMoveGroupList', 'host', 'Dataset',
|
['$scope', '$state', '$stateParams', 'generateList', 'HostManageService', 'GetBasePath', 'CopyMoveGroupList', 'host', 'Dataset',
|
||||||
function($scope, $state, $stateParams, GenerateList, HostManageService, GetBasePath, CopyMoveGroupList, host, Dataset){
|
function($scope, $state, $stateParams, GenerateList, HostManageService, GetBasePath, CopyMoveGroupList, host, Dataset){
|
||||||
var list = CopyMoveGroupList;
|
var list = CopyMoveGroupList;
|
||||||
|
|
||||||
$scope.item = host;
|
$scope.item = host;
|
||||||
$scope.submitMode = 'copy';
|
$scope.submitMode = 'copy';
|
||||||
$scope['toggle_'+ list.iterator] = function(id){
|
$scope.toggle_row = function(id){
|
||||||
// toggle off anything else currently selected
|
// toggle off anything else currently selected
|
||||||
_.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;});
|
_.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;});
|
||||||
// yoink the currently selected thing
|
// yoink the currently selected thing
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ var copyMoveGroupRoute = {
|
|||||||
resolve: {
|
resolve: {
|
||||||
Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath', 'group',
|
Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath', 'group',
|
||||||
function(list, qs, $stateParams, GetBasePath, group) {
|
function(list, qs, $stateParams, GetBasePath, group) {
|
||||||
$stateParams.copy_search.not__id__in = ($stateParams.group.length > 0 ? group.id + ',' + _.last($stateParams.group) : group.id);
|
$stateParams.copy_search.not__id__in = ($stateParams.group && $stateParams.group.length > 0 ? group.id + ',' + _.last($stateParams.group) : group.id.toString());
|
||||||
let path = GetBasePath(list.name);
|
let path = GetBasePath('inventory') + $stateParams.inventory_id + '/groups/';
|
||||||
return qs.search(path, $stateParams.copy_search);
|
return qs.search(path, $stateParams.copy_search);
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -66,7 +66,7 @@ var copyMoveHostRoute = {
|
|||||||
resolve: {
|
resolve: {
|
||||||
Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath',
|
Dataset: ['CopyMoveGroupList', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||||
function(list, qs, $stateParams, GetBasePath) {
|
function(list, qs, $stateParams, GetBasePath) {
|
||||||
let path = GetBasePath(list.name);
|
let path = GetBasePath('inventory') + $stateParams.inventory_id + '/hosts/';
|
||||||
return qs.search(path, $stateParams.copy_search);
|
return qs.search(path, $stateParams.copy_search);
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -80,7 +80,9 @@ var copyMoveHostRoute = {
|
|||||||
controller: CopyMoveHostsController,
|
controller: CopyMoveHostsController,
|
||||||
},
|
},
|
||||||
'copyMoveList@inventoryManage.copyMoveHost': {
|
'copyMoveList@inventoryManage.copyMoveHost': {
|
||||||
templateProvider: function(CopyMoveGroupList, generateList) {
|
templateProvider: function(CopyMoveGroupList, generateList, $stateParams, GetBasePath) {
|
||||||
|
let list = CopyMoveGroupList;
|
||||||
|
list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/hosts/';
|
||||||
let html = generateList.build({
|
let html = generateList.build({
|
||||||
list: CopyMoveGroupList,
|
list: CopyMoveGroupList,
|
||||||
mode: 'lookup',
|
mode: 'lookup',
|
||||||
|
|||||||
@@ -98,9 +98,17 @@ export default {
|
|||||||
},
|
},
|
||||||
// target ui-views with name@inventoryManage state
|
// target ui-views with name@inventoryManage state
|
||||||
'groupsList@inventoryManage': {
|
'groupsList@inventoryManage': {
|
||||||
templateProvider: function(InventoryGroups, generateList, $templateRequest) {
|
templateProvider: function(InventoryGroups, generateList, $templateRequest, $stateParams, GetBasePath) {
|
||||||
|
let list = _.cloneDeep(InventoryGroups);
|
||||||
|
if($stateParams && $stateParams.group) {
|
||||||
|
list.basePath = GetBasePath('groups') + _.last($stateParams.group) + '/children';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//reaches here if the user is on the root level group
|
||||||
|
list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/root_groups';
|
||||||
|
}
|
||||||
let html = generateList.build({
|
let html = generateList.build({
|
||||||
list: InventoryGroups,
|
list: list,
|
||||||
mode: 'edit'
|
mode: 'edit'
|
||||||
});
|
});
|
||||||
html = generateList.wrapPanel(html);
|
html = generateList.wrapPanel(html);
|
||||||
@@ -112,9 +120,17 @@ export default {
|
|||||||
controller: GroupsListController
|
controller: GroupsListController
|
||||||
},
|
},
|
||||||
'hostsList@inventoryManage': {
|
'hostsList@inventoryManage': {
|
||||||
templateProvider: function(InventoryHosts, generateList) {
|
templateProvider: function(InventoryHosts, generateList, $stateParams, GetBasePath) {
|
||||||
|
let list = _.cloneDeep(InventoryHosts);
|
||||||
|
if($stateParams && $stateParams.group) {
|
||||||
|
list.basePath = GetBasePath('groups') + _.last($stateParams.group) + '/all_hosts';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//reaches here if the user is on the root level group
|
||||||
|
list.basePath = GetBasePath('inventory') + $stateParams.inventory_id + '/hosts';
|
||||||
|
}
|
||||||
let html = generateList.build({
|
let html = generateList.build({
|
||||||
list: InventoryHosts,
|
list: list,
|
||||||
mode: 'edit'
|
mode: 'edit'
|
||||||
});
|
});
|
||||||
return generateList.wrapPanel(html);
|
return generateList.wrapPanel(html);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default [ 'templateUrl',
|
|||||||
link: function(scope) {
|
link: function(scope) {
|
||||||
// as count is changed by event data coming in,
|
// as count is changed by event data coming in,
|
||||||
// update the host status bar
|
// update the host status bar
|
||||||
scope.$watch('count', function(val) {
|
var toDestroy = scope.$watch('count', function(val) {
|
||||||
if (val) {
|
if (val) {
|
||||||
Object.keys(val).forEach(key => {
|
Object.keys(val).forEach(key => {
|
||||||
// reposition the hosts status bar by setting
|
// reposition the hosts status bar by setting
|
||||||
@@ -38,6 +38,10 @@ export default [ 'templateUrl',
|
|||||||
.filter(key => (val[key] > 0)).length > 0);
|
.filter(key => (val[key] > 0)).length > 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
scope.$on('$destroy', function(){
|
||||||
|
toDestroy();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}];
|
}];
|
||||||
|
|||||||
@@ -162,6 +162,7 @@
|
|||||||
|
|
||||||
.JobResultsStdOut-stdoutColumn {
|
.JobResultsStdOut-stdoutColumn {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
color: @default-interface-txt;
|
color: @default-interface-txt;
|
||||||
@@ -171,6 +172,12 @@
|
|||||||
width:100%;
|
width:100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.JobResultsStdOut-stdoutColumn--tooMany {
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: @default-err;
|
||||||
|
}
|
||||||
|
|
||||||
.JobResultsStdOut-stdoutColumn {
|
.JobResultsStdOut-stdoutColumn {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -216,6 +223,11 @@
|
|||||||
color: @default-interface-txt;
|
color: @default-interface-txt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.JobResultsStdOut-cappedLine {
|
||||||
|
color: @b7grey;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: @breakpoint-md) {
|
@media (max-width: @breakpoint-md) {
|
||||||
.JobResultsStdOut-numberColumnPreload {
|
.JobResultsStdOut-numberColumnPreload {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
|||||||
templateUrl: templateUrl('job-results/job-results-stdout/job-results-stdout'),
|
templateUrl: templateUrl('job-results/job-results-stdout/job-results-stdout'),
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
link: function(scope, element) {
|
link: function(scope, element) {
|
||||||
|
var toDestroy = [],
|
||||||
|
resizer,
|
||||||
|
scrollWatcher;
|
||||||
|
|
||||||
|
scope.$on('$destroy', function(){
|
||||||
|
$(window).off("resize", resizer);
|
||||||
|
$(window).off("scroll", scrollWatcher);
|
||||||
|
$(".JobResultsStdOut-stdoutContainer").off('scroll',
|
||||||
|
scrollWatcher);
|
||||||
|
toDestroy.forEach(closureFunc => closureFunc());
|
||||||
|
});
|
||||||
|
|
||||||
scope.stdoutContainerAvailable.resolve("container available");
|
scope.stdoutContainerAvailable.resolve("container available");
|
||||||
// utility function used to find the top visible line and
|
// utility function used to find the top visible line and
|
||||||
// parent header in the pane
|
// parent header in the pane
|
||||||
@@ -115,9 +127,15 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
|||||||
// stop iterating over the standard out
|
// stop iterating over the standard out
|
||||||
// lines once the first one has been
|
// lines once the first one has been
|
||||||
// found
|
// found
|
||||||
|
|
||||||
|
$this = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
$this = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
$container = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
visLine: visItem,
|
visLine: visItem,
|
||||||
@@ -131,22 +149,24 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
|||||||
} else {
|
} else {
|
||||||
scope.isMobile = false;
|
scope.isMobile = false;
|
||||||
}
|
}
|
||||||
// watch changes to the window size
|
|
||||||
$(window).resize(function() {
|
resizer = function() {
|
||||||
// and update the isMobile var accordingly
|
// and update the isMobile var accordingly
|
||||||
if (window.innerWidth <= 1200 && !scope.isMobile) {
|
if (window.innerWidth <= 1200 && !scope.isMobile) {
|
||||||
scope.isMobile = true;
|
scope.isMobile = true;
|
||||||
} else if (window.innerWidth > 1200 & scope.isMobile) {
|
} else if (window.innerWidth > 1200 & scope.isMobile) {
|
||||||
scope.isMobile = false;
|
scope.isMobile = false;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
// watch changes to the window size
|
||||||
|
$(window).resize(resizer);
|
||||||
|
|
||||||
var lastScrollTop;
|
var lastScrollTop;
|
||||||
|
|
||||||
var initScrollTop = function() {
|
var initScrollTop = function() {
|
||||||
lastScrollTop = 0;
|
lastScrollTop = 0;
|
||||||
};
|
};
|
||||||
var scrollWatcher = function() {
|
scrollWatcher = function() {
|
||||||
var st = $(this).scrollTop();
|
var st = $(this).scrollTop();
|
||||||
var netScroll = st + $(this).innerHeight();
|
var netScroll = st + $(this).innerHeight();
|
||||||
var fullHeight;
|
var fullHeight;
|
||||||
@@ -178,11 +198,15 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastScrollTop = st;
|
lastScrollTop = st;
|
||||||
|
|
||||||
|
st = null;
|
||||||
|
netScroll = null;
|
||||||
|
fullHeight = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// update scroll watchers when isMobile changes based on
|
// update scroll watchers when isMobile changes based on
|
||||||
// window resize
|
// window resize
|
||||||
scope.$watch('isMobile', function(val) {
|
toDestroy.push(scope.$watch('isMobile', function(val) {
|
||||||
if (val === true) {
|
if (val === true) {
|
||||||
// make sure ^ TOP always shown for mobile
|
// make sure ^ TOP always shown for mobile
|
||||||
scope.stdoutOverflowed = true;
|
scope.stdoutOverflowed = true;
|
||||||
@@ -204,7 +228,7 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
|||||||
$(".JobResultsStdOut-stdoutContainer").on('scroll',
|
$(".JobResultsStdOut-stdoutContainer").on('scroll',
|
||||||
scrollWatcher);
|
scrollWatcher);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
// called to scroll to follow anchor
|
// called to scroll to follow anchor
|
||||||
scope.followScroll = function() {
|
scope.followScroll = function() {
|
||||||
@@ -237,7 +261,7 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
|||||||
|
|
||||||
// if following becomes active, go ahead and get to the bottom
|
// if following becomes active, go ahead and get to the bottom
|
||||||
// of the standard out pane
|
// of the standard out pane
|
||||||
scope.$watch('followEngaged', function(val) {
|
toDestroy.push(scope.$watch('followEngaged', function(val) {
|
||||||
// scroll to follow point if followEngaged is true
|
// scroll to follow point if followEngaged is true
|
||||||
if (val) {
|
if (val) {
|
||||||
scope.followScroll();
|
scope.followScroll();
|
||||||
@@ -251,7 +275,7 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
|||||||
scope.followTooltip = "Click to follow standard out as it comes in.";
|
scope.followTooltip = "Click to follow standard out as it comes in.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
// follow button ng-click function
|
// follow button ng-click function
|
||||||
scope.followToggleClicked = function() {
|
scope.followToggleClicked = function() {
|
||||||
|
|||||||
@@ -34,6 +34,13 @@
|
|||||||
<div id="topAnchor" class="JobResultsStdOut-topAnchor"></div>
|
<div id="topAnchor" class="JobResultsStdOut-topAnchor"></div>
|
||||||
<div class="JobResultsStdOut-numberColumnPreload"></div>
|
<div class="JobResultsStdOut-numberColumnPreload"></div>
|
||||||
<div id='lineAnchor' class="JobResultsStdOut-lineAnchor"></div>
|
<div id='lineAnchor' class="JobResultsStdOut-lineAnchor"></div>
|
||||||
|
<div class="JobResultsStdOut-aLineOfStdOut"
|
||||||
|
ng-show="tooManyEvents">
|
||||||
|
<div class="JobResultsStdOut-lineNumberColumn">
|
||||||
|
<span class="JobResultsStdOut-lineExpander"> </span>
|
||||||
|
</div>
|
||||||
|
<div class="JobResultsStdOut-stdoutColumn JobResultsStdOut-stdoutColumn--tooMany">The standard output is too large to display. Please specify additional filters to narrow the standard out.</div>
|
||||||
|
</div>
|
||||||
<div id="followAnchor"
|
<div id="followAnchor"
|
||||||
class="JobResultsStdOut-followAnchor">
|
class="JobResultsStdOut-followAnchor">
|
||||||
<div class="JobResultsStdOut-toTop"
|
<div class="JobResultsStdOut-toTop"
|
||||||
|
|||||||
@@ -148,6 +148,7 @@
|
|||||||
background-color: @default-bg;
|
background-color: @default-bg;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
color: @default-interface-txt;
|
color: @default-interface-txt;
|
||||||
|
margin-right: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.JobResults-panelRight {
|
.JobResults-panelRight {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', 'eventQueue', '$compile', '$log', 'Dataset', '$q', 'Rest', '$state', 'QuerySet', '$rootScope', 'moment',
|
export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', 'eventQueue', '$compile', '$log', 'Dataset', '$q', 'Rest', '$state', 'QuerySet', '$rootScope', 'moment',
|
||||||
function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile, $log, Dataset, $q, Rest, $state, QuerySet, $rootScope, moment) {
|
function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile, $log, Dataset, $q, Rest, $state, QuerySet, $rootScope, moment) {
|
||||||
|
var toDestroy = [];
|
||||||
|
var cancelRequests = false;
|
||||||
|
|
||||||
|
// this allows you to manage the timing of rest-call based events as
|
||||||
|
// filters are updated. see processPage for more info
|
||||||
|
var currentContext = 1;
|
||||||
|
|
||||||
// used for tag search
|
// used for tag search
|
||||||
$scope.job_event_dataset = Dataset.data;
|
$scope.job_event_dataset = Dataset.data;
|
||||||
@@ -66,14 +72,14 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
|
|||||||
|
|
||||||
// update label in left pane and tooltip in right pane when the job_status
|
// update label in left pane and tooltip in right pane when the job_status
|
||||||
// changes
|
// changes
|
||||||
$scope.$watch('job_status', function(status) {
|
toDestroy.push($scope.$watch('job_status', function(status) {
|
||||||
if (status) {
|
if (status) {
|
||||||
$scope.status_label = $scope.jobOptions.status.choices
|
$scope.status_label = $scope.jobOptions.status.choices
|
||||||
.filter(val => val[0] === status)
|
.filter(val => val[0] === status)
|
||||||
.map(val => val[1])[0];
|
.map(val => val[1])[0];
|
||||||
$scope.status_tooltip = "Job " + $scope.status_label;
|
$scope.status_tooltip = "Job " + $scope.status_label;
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
// update the job_status value. Use the cached rootScope value if there
|
// update the job_status value. Use the cached rootScope value if there
|
||||||
// is one. This is a workaround when the rest call for the jobData is
|
// is one. This is a workaround when the rest call for the jobData is
|
||||||
@@ -185,7 +191,12 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
|
|||||||
|
|
||||||
// This is where the async updates to the UI actually happen.
|
// This is where the async updates to the UI actually happen.
|
||||||
// Flow is event queue munging in the service -> $scope setting in here
|
// Flow is event queue munging in the service -> $scope setting in here
|
||||||
var processEvent = function(event) {
|
var processEvent = function(event, context) {
|
||||||
|
// only care about filter context checking when the event comes
|
||||||
|
// as a rest call
|
||||||
|
if (context && context !== currentContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// put the event in the queue
|
// put the event in the queue
|
||||||
var mungedEvent = eventQueue.populate(event);
|
var mungedEvent = eventQueue.populate(event);
|
||||||
|
|
||||||
@@ -278,6 +289,9 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
|
|||||||
.stdout)($scope.events[mungedEvent
|
.stdout)($scope.events[mungedEvent
|
||||||
.counter]));
|
.counter]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
classList = null;
|
||||||
|
putIn = null;
|
||||||
} else {
|
} else {
|
||||||
// this is a header or recap line, so just
|
// this is a header or recap line, so just
|
||||||
// append to the bottom
|
// append to the bottom
|
||||||
@@ -357,99 +371,113 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
|
|||||||
getSkeleton(jobData.related.job_events + "?order_by=id&or__event__in=playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats");
|
getSkeleton(jobData.related.job_events + "?order_by=id&or__event__in=playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var getEvents;
|
||||||
|
|
||||||
|
var processPage = function(events, context) {
|
||||||
|
// currentContext is the context of the filter when this request
|
||||||
|
// to processPage was made
|
||||||
|
//
|
||||||
|
// currentContext is the context of the filter currently
|
||||||
|
//
|
||||||
|
// if they are not the same, make sure to stop process events/
|
||||||
|
// making rest calls for next pages/etc. (you can see context is
|
||||||
|
// also passed into getEvents and processEvent and similar checks
|
||||||
|
// exist in these functions)
|
||||||
|
//
|
||||||
|
// also, if the page doesn't contain results (i.e.: the response
|
||||||
|
// returns an error), don't process the page
|
||||||
|
if (context !== currentContext || events === undefined ||
|
||||||
|
events.results === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
events.results.forEach(event => {
|
||||||
|
// get the name in the same format as the data
|
||||||
|
// coming over the websocket
|
||||||
|
event.event_name = event.event;
|
||||||
|
delete event.event;
|
||||||
|
|
||||||
|
processEvent(event, context);
|
||||||
|
});
|
||||||
|
if (events.next && !cancelRequests) {
|
||||||
|
getEvents(events.next, context);
|
||||||
|
} else {
|
||||||
|
// put those paused events into the pane
|
||||||
|
$scope.gotPreviouslyRanEvents.resolve("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// grab non-header recap lines
|
// grab non-header recap lines
|
||||||
var getEvents = function(url) {
|
getEvents = function(url, context) {
|
||||||
|
if (context !== currentContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
jobResultsService.getEvents(url)
|
jobResultsService.getEvents(url)
|
||||||
.then(events => {
|
.then(events => {
|
||||||
events.results.forEach(event => {
|
processPage(events, context);
|
||||||
// get the name in the same format as the data
|
|
||||||
// coming over the websocket
|
|
||||||
event.event_name = event.event;
|
|
||||||
delete event.event;
|
|
||||||
processEvent(event);
|
|
||||||
});
|
|
||||||
if (events.next) {
|
|
||||||
getEvents(events.next);
|
|
||||||
} else {
|
|
||||||
// put those paused events into the pane
|
|
||||||
$scope.gotPreviouslyRanEvents.resolve("");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// grab non-header recap lines
|
// grab non-header recap lines
|
||||||
$scope.$watch('job_event_dataset', function(val) {
|
toDestroy.push($scope.$watch('job_event_dataset', function(val) {
|
||||||
|
eventQueue.initialize();
|
||||||
|
|
||||||
|
Object.keys($scope.events)
|
||||||
|
.forEach(v => {
|
||||||
|
// dont destroy scope events for skeleton lines
|
||||||
|
let name = $scope.events[v].event.name;
|
||||||
|
|
||||||
|
if (!(name === "playbook_on_play_start" ||
|
||||||
|
name === "playbook_on_task_start" ||
|
||||||
|
name === "playbook_on_stats")) {
|
||||||
|
$scope.events[v].$destroy();
|
||||||
|
$scope.events[v] = null;
|
||||||
|
delete $scope.events[v];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// pause websocket events from coming in to the pane
|
// pause websocket events from coming in to the pane
|
||||||
$scope.gotPreviouslyRanEvents = $q.defer();
|
$scope.gotPreviouslyRanEvents = $q.defer();
|
||||||
|
currentContext += 1;
|
||||||
|
|
||||||
|
let context = currentContext;
|
||||||
|
|
||||||
$( ".JobResultsStdOut-aLineOfStdOut.not_skeleton" ).remove();
|
$( ".JobResultsStdOut-aLineOfStdOut.not_skeleton" ).remove();
|
||||||
$scope.hasSkeleton.promise.then(() => {
|
$scope.hasSkeleton.promise.then(() => {
|
||||||
val.results.forEach(event => {
|
if (val.count > parseInt(val.maxEvents)) {
|
||||||
// get the name in the same format as the data
|
$(".header_task").hide();
|
||||||
// coming over the websocket
|
$(".header_play").hide();
|
||||||
event.event_name = event.event;
|
$scope.tooManyEvents = true;
|
||||||
delete event.event;
|
|
||||||
processEvent(event);
|
|
||||||
});
|
|
||||||
if (val.next) {
|
|
||||||
getEvents(val.next);
|
|
||||||
} else {
|
} else {
|
||||||
// put those paused events into the pane
|
$(".header_task").show();
|
||||||
$scope.gotPreviouslyRanEvents.resolve("");
|
$(".header_play").show();
|
||||||
|
$scope.tooManyEvents = false;
|
||||||
|
processPage(val, context);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Processing of job_events messages from the websocket
|
// Processing of job_events messages from the websocket
|
||||||
$scope.$on(`ws-job_events-${$scope.job.id}`, function(e, data) {
|
toDestroy.push($scope.$on(`ws-job_events-${$scope.job.id}`, function(e, data) {
|
||||||
$q.all([$scope.gotPreviouslyRanEvents.promise,
|
$q.all([$scope.gotPreviouslyRanEvents.promise,
|
||||||
$scope.hasSkeleton.promise]).then(() => {
|
$scope.hasSkeleton.promise]).then(() => {
|
||||||
var url = Dataset
|
// put the line in the
|
||||||
.config.url.split("?")[0] +
|
// standard out pane (and increment play and task
|
||||||
QuerySet.encodeQueryset($state.params.job_event_search);
|
// count if applicable)
|
||||||
var noFilter = (url.split("&")
|
if (data.event_name === "playbook_on_play_start") {
|
||||||
.filter(v => v.indexOf("page=") !== 0 &&
|
$scope.playCount++;
|
||||||
v.indexOf("/api/v1") !== 0 &&
|
} else if (data.event_name === "playbook_on_task_start") {
|
||||||
v.indexOf("order_by=id") !== 0 &&
|
$scope.taskCount++;
|
||||||
v.indexOf("not__event__in=playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats") !== 0).length === 0);
|
|
||||||
|
|
||||||
if(data.event_name === "playbook_on_start" ||
|
|
||||||
data.event_name === "playbook_on_play_start" ||
|
|
||||||
data.event_name === "playbook_on_task_start" ||
|
|
||||||
data.event_name === "playbook_on_stats" ||
|
|
||||||
noFilter) {
|
|
||||||
// for header and recap lines, as well as if no filters
|
|
||||||
// were added by the user, just put the line in the
|
|
||||||
// standard out pane (and increment play and task
|
|
||||||
// count)
|
|
||||||
if (data.event_name === "playbook_on_play_start") {
|
|
||||||
$scope.playCount++;
|
|
||||||
} else if (data.event_name === "playbook_on_task_start") {
|
|
||||||
$scope.taskCount++;
|
|
||||||
}
|
|
||||||
processEvent(data);
|
|
||||||
} else {
|
|
||||||
// to make sure host event/verbose lines go through a
|
|
||||||
// user defined filter, appent the id to the url, and
|
|
||||||
// make a request to the job_events endpoint with the
|
|
||||||
// id of the incoming event appended. If the event,
|
|
||||||
// is returned, put the line in the standard out pane
|
|
||||||
Rest.setUrl(`${url}&id=${data.id}`);
|
|
||||||
Rest.get()
|
|
||||||
.success(function(isHere) {
|
|
||||||
if (isHere.count) {
|
|
||||||
processEvent(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
processEvent(data);
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
// Processing of job-status messages from the websocket
|
// Processing of job-status messages from the websocket
|
||||||
$scope.$on(`ws-jobs`, function(e, data) {
|
toDestroy.push($scope.$on(`ws-jobs`, function(e, data) {
|
||||||
if (parseInt(data.unified_job_id, 10) ===
|
if (parseInt(data.unified_job_id, 10) ===
|
||||||
parseInt($scope.job.id,10)) {
|
parseInt($scope.job.id,10)) {
|
||||||
// controller is defined, so set the job_status
|
// controller is defined, so set the job_status
|
||||||
@@ -477,5 +505,19 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
|
|||||||
// for this job. cache the socket status on root scope
|
// for this job. cache the socket status on root scope
|
||||||
$rootScope['lastSocketStatus' + data.unified_job_id] = data.status;
|
$rootScope['lastSocketStatus' + data.unified_job_id] = data.status;
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function(){
|
||||||
|
$( ".JobResultsStdOut-aLineOfStdOut" ).remove();
|
||||||
|
cancelRequests = true;
|
||||||
|
eventQueue.initialize();
|
||||||
|
Object.keys($scope.events)
|
||||||
|
.forEach(v => {
|
||||||
|
$scope.events[v].$destroy();
|
||||||
|
$scope.events[v] = null;
|
||||||
|
});
|
||||||
|
$scope.events = {};
|
||||||
|
clearInterval(elapsedInterval);
|
||||||
|
toDestroy.forEach(closureFunc => closureFunc());
|
||||||
});
|
});
|
||||||
}];
|
}];
|
||||||
|
|||||||
@@ -36,9 +36,9 @@
|
|||||||
<button class="List-actionButton
|
<button class="List-actionButton
|
||||||
List-actionButton--delete"
|
List-actionButton--delete"
|
||||||
data-placement="top"
|
data-placement="top"
|
||||||
ng-click="deleteJob()"
|
ng-click="cancelJob()"
|
||||||
ng-show="job_status.status == 'running' ||
|
ng-show="job_status == 'running' ||
|
||||||
job_status.status=='pending' "
|
job_status=='pending' "
|
||||||
aw-tool-tip="Cancel"
|
aw-tool-tip="Cancel"
|
||||||
data-original-title="" title="">
|
data-original-title="" title="">
|
||||||
<i class="fa fa-minus-circle"></i>
|
<i class="fa fa-minus-circle"></i>
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
List-actionButton--delete"
|
List-actionButton--delete"
|
||||||
data-placement="top"
|
data-placement="top"
|
||||||
ng-click="deleteJob()"
|
ng-click="deleteJob()"
|
||||||
ng-hide="job_status.status == 'running' ||
|
ng-hide="job_status == 'running' ||
|
||||||
job_status.status == 'pending' "
|
job_status == 'pending' "
|
||||||
aw-tool-tip="Delete"
|
aw-tool-tip="Delete"
|
||||||
data-original-title=""
|
data-original-title=""
|
||||||
title="">
|
title="">
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default {
|
|||||||
params: {
|
params: {
|
||||||
job_event_search: {
|
job_event_search: {
|
||||||
value: {
|
value: {
|
||||||
|
page_size: 100,
|
||||||
order_by: 'id',
|
order_by: 'id',
|
||||||
not__event__in: 'playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats'
|
not__event__in: 'playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -171,22 +171,8 @@ function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors, InitiatePlaybo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Rest.destroy()
|
|
||||||
.success(function() {
|
|
||||||
Wait('stop');
|
|
||||||
$('#prompt-modal').modal('hide');
|
|
||||||
})
|
|
||||||
.error(function(obj, status) {
|
|
||||||
Wait('stop');
|
|
||||||
$('#prompt-modal').modal('hide');
|
|
||||||
ProcessErrors(null, obj, status, null, {
|
|
||||||
hdr: 'Error!',
|
|
||||||
msg: `Could not cancel job.
|
|
||||||
Returned status: ${status}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
actionText: 'CANCEL'
|
actionText: 'PROCEED'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
relaunchJob: function(scope) {
|
relaunchJob: function(scope) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default ['$log', 'moment', function($log, moment){
|
|||||||
line = line.replace(/u001b/g, '');
|
line = line.replace(/u001b/g, '');
|
||||||
|
|
||||||
// ansi classes
|
// ansi classes
|
||||||
|
line = line.replace(/\[1;im/g, '<span class="JobResultsStdOut-cappedLine">');
|
||||||
line = line.replace(/\[1;31m/g, '<span class="ansi1 ansi31">');
|
line = line.replace(/\[1;31m/g, '<span class="ansi1 ansi31">');
|
||||||
line = line.replace(/\[0;31m/g, '<span class="ansi1 ansi31">');
|
line = line.replace(/\[0;31m/g, '<span class="ansi1 ansi31">');
|
||||||
line = line.replace(/\[0;32m/g, '<span class="ansi32">');
|
line = line.replace(/\[0;32m/g, '<span class="ansi32">');
|
||||||
@@ -185,7 +186,6 @@ export default ['$log', 'moment', function($log, moment){
|
|||||||
data-uuid="${clickClass}">
|
data-uuid="${clickClass}">
|
||||||
</i>
|
</i>
|
||||||
</span>`;
|
</span>`;
|
||||||
// console.log(expandDom);
|
|
||||||
return expandDom;
|
return expandDom;
|
||||||
} else {
|
} else {
|
||||||
// non-header lines don't get an expander
|
// non-header lines don't get an expander
|
||||||
@@ -193,10 +193,22 @@ export default ['$log', 'moment', function($log, moment){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getLineArr: function(event) {
|
getLineArr: function(event) {
|
||||||
return _
|
let lineNums = _.range(event.start_line + 1,
|
||||||
.zip(_.range(event.start_line + 1,
|
event.end_line + 1);
|
||||||
event.end_line + 1),
|
|
||||||
event.stdout.replace("\t", " ").split("\r\n").slice(0, -1));
|
let lines = event.stdout
|
||||||
|
.replace("\t", " ")
|
||||||
|
.split("\r\n");
|
||||||
|
|
||||||
|
if (lineNums.length > lines.length) {
|
||||||
|
let padBy = lineNums.length - lines.length;
|
||||||
|
|
||||||
|
for (let i = 0; i <= padBy; i++) {
|
||||||
|
lines.push("[1;imLine capped.[0m");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.zip(lineNums, lines).slice(0, -1);
|
||||||
},
|
},
|
||||||
// public function that provides the parsed stdout line, given a
|
// public function that provides the parsed stdout line, given a
|
||||||
// job_event
|
// job_event
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ export default
|
|||||||
key: true,
|
key: true,
|
||||||
label: 'Target Group Name'
|
label: 'Target Group Name'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
basePath: 'api/v1/inventories/{{$stateParams.inventory_id}}/groups'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ export default
|
|||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
name: {
|
name: {
|
||||||
key: true,
|
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
columnClass: 'col-md-11'
|
ngBind: 'inventory_source.summary_fields.group.name',
|
||||||
|
columnClass: 'col-md-11',
|
||||||
|
simpleTip: {
|
||||||
|
awToolTip: "Inventory: {{inventory_source.summary_fields.inventory.name}}",
|
||||||
|
dataPlacement: "top"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<aw-smart-status jobs="template.summary_fields.recent_jobs"></aw-smart-status>
|
<aw-smart-status jobs="template.summary_fields.recent_jobs" template-type="template.type"></aw-smart-status>
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
|
|||||||
function applyValidation(viewValue) {
|
function applyValidation(viewValue) {
|
||||||
basePath = GetBasePath(elm.attr('data-basePath')) || elm.attr('data-basePath');
|
basePath = GetBasePath(elm.attr('data-basePath')) || elm.attr('data-basePath');
|
||||||
query = elm.attr('data-query');
|
query = elm.attr('data-query');
|
||||||
query = query.replace(/\:value/, encodeURI(viewValue));
|
query = query.replace(/\:value/, encodeURIComponent(viewValue));
|
||||||
Rest.setUrl(`${basePath}${query}`);
|
Rest.setUrl(`${basePath}${query}`);
|
||||||
// https://github.com/ansible/ansible-tower/issues/3549
|
// https://github.com/ansible/ansible-tower/issues/3549
|
||||||
// capturing both success/failure conditions in .then() promise
|
// capturing both success/failure conditions in .then() promise
|
||||||
|
|||||||
@@ -1830,7 +1830,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
// smart-search directive
|
// smart-search directive
|
||||||
html += `
|
html += `
|
||||||
<div
|
<div
|
||||||
ng-hide="${itm}.length === 0 && (${collection.iterator}_searchTags | isEmpty)">
|
ng-hide="${itm}.length === 0 && (searchTags | isEmpty)">
|
||||||
<smart-search
|
<smart-search
|
||||||
django-model="${itm}"
|
django-model="${itm}"
|
||||||
search-size="${width}"
|
search-size="${width}"
|
||||||
@@ -1855,7 +1855,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
html += `
|
html += `
|
||||||
<div
|
<div
|
||||||
class="row"
|
class="row"
|
||||||
ng-show="${itm}.length === 0 && !(${collection.iterator}_searchTags | isEmpty)">
|
ng-show="${itm}.length === 0 && !(searchTags | isEmpty)">
|
||||||
<div class="col-lg-12 List-searchNoResults">
|
<div class="col-lg-12 List-searchNoResults">
|
||||||
No records matched your search.
|
No records matched your search.
|
||||||
</div>
|
</div>
|
||||||
@@ -1865,7 +1865,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
// Show the "no items" box when loading is done and the user isn't actively searching and there are no results
|
// Show the "no items" box when loading is done and the user isn't actively searching and there are no results
|
||||||
var emptyListText = (collection.emptyListText) ? collection.emptyListText : i18n._("PLEASE ADD ITEMS TO THIS LIST");
|
var emptyListText = (collection.emptyListText) ? collection.emptyListText : i18n._("PLEASE ADD ITEMS TO THIS LIST");
|
||||||
html += `<div ng-hide="is_superuser">`;
|
html += `<div ng-hide="is_superuser">`;
|
||||||
html += `<div class="List-noItems" ng-show="${itm}.length === 0 && (${collection.iterator}_searchTags | isEmpty)"> ${emptyListText} </div>`;
|
html += `<div class="List-noItems" ng-show="${itm}.length === 0 && (searchTags | isEmpty)"> ${emptyListText} </div>`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
|
|||||||
@@ -591,6 +591,9 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if(field.simpleTip) {
|
||||||
|
html += `<span aw-tool-tip="${field.simpleTip.awToolTip}" data-placement=${field.simpleTip.dataPlacement}>`;
|
||||||
|
}
|
||||||
// Add icon:
|
// Add icon:
|
||||||
if (field.ngShowIcon) {
|
if (field.ngShowIcon) {
|
||||||
html += "<i ng-show=\"" + field.ngShowIcon + "\" class=\"" + field.icon + "\"></i> ";
|
html += "<i ng-show=\"" + field.ngShowIcon + "\" class=\"" + field.icon + "\"></i> ";
|
||||||
@@ -615,6 +618,9 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
|||||||
if (field.text) {
|
if (field.text) {
|
||||||
html += field.text;
|
html += field.text;
|
||||||
}
|
}
|
||||||
|
if(field.simpleTip) {
|
||||||
|
html += `</span>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (list.name === 'hosts' || list.name === 'groups') {
|
if (list.name === 'hosts' || list.name === 'groups') {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q
|
|||||||
return `1 - ${pageSize}`;
|
return `1 - ${pageSize}`;
|
||||||
} else {
|
} else {
|
||||||
let floor = (($scope.current() - 1) * parseInt(pageSize)) + 1;
|
let floor = (($scope.current() - 1) * parseInt(pageSize)) + 1;
|
||||||
let ceil = floor + parseInt(pageSize);
|
let ceil = floor + parseInt(pageSize) < $scope.dataset.count ? floor + parseInt(pageSize) : $scope.dataset.count;
|
||||||
return `${floor} - ${ceil}`;
|
return `${floor} - ${ceil}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import directive from './smart-search.directive';
|
|||||||
import controller from './smart-search.controller';
|
import controller from './smart-search.controller';
|
||||||
import service from './queryset.service';
|
import service from './queryset.service';
|
||||||
import DjangoSearchModel from './django-search-model.class';
|
import DjangoSearchModel from './django-search-model.class';
|
||||||
|
import smartSearchService from './smart-search.service';
|
||||||
|
|
||||||
export default
|
export default
|
||||||
angular.module('SmartSearchModule', [])
|
angular.module('SmartSearchModule', [])
|
||||||
.directive('smartSearch', directive)
|
.directive('smartSearch', directive)
|
||||||
.controller('SmartSearchController', controller)
|
.controller('SmartSearchController', controller)
|
||||||
.service('QuerySet', service)
|
.service('QuerySet', service)
|
||||||
|
.service('SmartSearchService', smartSearchService)
|
||||||
.constant('DjangoSearchModel', DjangoSearchModel);
|
.constant('DjangoSearchModel', DjangoSearchModel);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory',
|
export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory', 'SmartSearchService',
|
||||||
function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory) {
|
function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory, SmartSearchService) {
|
||||||
return {
|
return {
|
||||||
// kick off building a model for a specific endpoint
|
// kick off building a model for a specific endpoint
|
||||||
// this is usually a list's basePath
|
// this is usually a list's basePath
|
||||||
@@ -67,29 +67,124 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear
|
|||||||
return angular.isObject(params) ? `?${queryset}` : '';
|
return angular.isObject(params) ? `?${queryset}` : '';
|
||||||
|
|
||||||
function encodeTerm(value, key){
|
function encodeTerm(value, key){
|
||||||
|
|
||||||
|
key = key.replace(/__icontains_DEFAULT/g, "__icontains");
|
||||||
|
key = key.replace(/__search_DEFAULT/g, "__search");
|
||||||
|
|
||||||
if (Array.isArray(value)){
|
if (Array.isArray(value)){
|
||||||
return _.map(value, (item) => `${key}=${item}`).join('&') + '&';
|
let concated = '';
|
||||||
|
angular.forEach(value, function(item){
|
||||||
|
if(item && typeof item === 'string') {
|
||||||
|
item = item.replace(/"|'/g, "");
|
||||||
|
}
|
||||||
|
concated += `${key}=${item}&`;
|
||||||
|
});
|
||||||
|
return concated;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if(value && typeof value === 'string') {
|
||||||
|
value = value.replace(/"|'/g, "");
|
||||||
|
}
|
||||||
return `${key}=${value}&`;
|
return `${key}=${value}&`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// encodes a ui smart-search param to a django-friendly param
|
// encodes a ui smart-search param to a django-friendly param
|
||||||
// operand:key:comparator:value => {operand__key__comparator: value}
|
// operand:key:comparator:value => {operand__key__comparator: value}
|
||||||
encodeParam(param){
|
encodeParam(params){
|
||||||
let split = param.split(':');
|
// Assumption here is that we have a key and a value so the length
|
||||||
return {[split.slice(0,split.length -1).join('__')] : split[split.length-1]};
|
// of the paramParts array will be 2. [0] is the key and [1] the value
|
||||||
|
let paramParts = SmartSearchService.splitTermIntoParts(params.term);
|
||||||
|
let keySplit = paramParts[0].split('.');
|
||||||
|
let exclude = false;
|
||||||
|
let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false;
|
||||||
|
if(keySplit[0].match(/^-/g)) {
|
||||||
|
exclude = true;
|
||||||
|
keySplit[0] = keySplit[0].replace(/^-/, '');
|
||||||
|
}
|
||||||
|
let paramString = exclude ? "not__" : "";
|
||||||
|
let valueString = paramParts[1];
|
||||||
|
if(keySplit.length === 1) {
|
||||||
|
if(params.searchTerm && !lessThanGreaterThan) {
|
||||||
|
paramString += keySplit[0] + '__icontains_DEFAULT';
|
||||||
|
}
|
||||||
|
else if(params.relatedSearchTerm) {
|
||||||
|
paramString += keySplit[0] + '__search_DEFAULT';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
paramString += keySplit[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
paramString += keySplit.join('__');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(lessThanGreaterThan) {
|
||||||
|
if(paramParts[1].match(/^>=.*$/)) {
|
||||||
|
paramString += '__gte';
|
||||||
|
valueString = valueString.replace(/^(>=)/,"");
|
||||||
|
}
|
||||||
|
else if(paramParts[1].match(/^<=.*$/)) {
|
||||||
|
paramString += '__lte';
|
||||||
|
valueString = valueString.replace(/^(<=)/,"");
|
||||||
|
}
|
||||||
|
else if(paramParts[1].match(/^<.*$/)) {
|
||||||
|
paramString += '__lt';
|
||||||
|
valueString = valueString.replace(/^(<)/,"");
|
||||||
|
}
|
||||||
|
else if(paramParts[1].match(/^>.*$/)) {
|
||||||
|
paramString += '__gt';
|
||||||
|
valueString = valueString.replace(/^(>)/,"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {[paramString] : valueString};
|
||||||
},
|
},
|
||||||
// decodes a django queryset param into a ui smart-search tag or set of tags
|
// decodes a django queryset param into a ui smart-search tag or set of tags
|
||||||
decodeParam(value, key){
|
decodeParam(value, key){
|
||||||
|
|
||||||
|
let decodeParamString = function(searchString) {
|
||||||
|
if(key === 'search') {
|
||||||
|
// Don't include 'search:' in the search tag
|
||||||
|
return decodeURIComponent(`${searchString}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
key = key.replace(/__icontains_DEFAULT/g, "");
|
||||||
|
key = key.replace(/__search_DEFAULT/g, "");
|
||||||
|
let split = key.split('__');
|
||||||
|
let decodedParam = searchString;
|
||||||
|
let exclude = false;
|
||||||
|
if(key.startsWith('not__')) {
|
||||||
|
exclude = true;
|
||||||
|
split = split.splice(1, split.length);
|
||||||
|
}
|
||||||
|
if(key.endsWith('__gt')) {
|
||||||
|
decodedParam = '>' + decodedParam;
|
||||||
|
split = split.splice(0, split.length-1);
|
||||||
|
}
|
||||||
|
else if(key.endsWith('__lt')) {
|
||||||
|
decodedParam = '<' + decodedParam;
|
||||||
|
split = split.splice(0, split.length-1);
|
||||||
|
}
|
||||||
|
else if(key.endsWith('__gte')) {
|
||||||
|
decodedParam = '>=' + decodedParam;
|
||||||
|
split = split.splice(0, split.length-1);
|
||||||
|
}
|
||||||
|
else if(key.endsWith('__lte')) {
|
||||||
|
decodedParam = '<=' + decodedParam;
|
||||||
|
split = split.splice(0, split.length-1);
|
||||||
|
}
|
||||||
|
return exclude ? `-${split.join('.')}:${decodedParam}` : `${split.join('.')}:${decodedParam}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (Array.isArray(value)){
|
if (Array.isArray(value)){
|
||||||
return _.map(value, (item) => {
|
return _.map(value, (item) => {
|
||||||
return `${key.split('__').join(':')}:${item}`;
|
return decodeParamString(item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return `${key.split('__').join(':')}:${value}`;
|
return decodeParamString(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -147,20 +242,33 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear
|
|||||||
Wait('start');
|
Wait('start');
|
||||||
this.url = `${endpoint}${this.encodeQueryset(params)}`;
|
this.url = `${endpoint}${this.encodeQueryset(params)}`;
|
||||||
Rest.setUrl(this.url);
|
Rest.setUrl(this.url);
|
||||||
|
|
||||||
return Rest.get()
|
return Rest.get()
|
||||||
.success(this.success.bind(this))
|
.then(function(response) {
|
||||||
.error(this.error.bind(this))
|
Wait('stop');
|
||||||
.finally(Wait('stop'));
|
|
||||||
|
if (response
|
||||||
|
.headers('X-UI-Max-Events') !== null) {
|
||||||
|
response.data.maxEvents = response.
|
||||||
|
headers('X-UI-Max-Events');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(function(response) {
|
||||||
|
Wait('stop');
|
||||||
|
|
||||||
|
this.error(response.data, response.status);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}.bind(this));
|
||||||
},
|
},
|
||||||
error(data, status) {
|
error(data, status) {
|
||||||
ProcessErrors($rootScope, data, status, null, {
|
ProcessErrors($rootScope, data, status, null, {
|
||||||
hdr: 'Error!',
|
hdr: 'Error!',
|
||||||
msg: 'Call to ' + this.url + '. GET returned: ' + status
|
msg: 'Call to ' + this.url + '. GET returned: ' + status
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
success(data) {
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -186,8 +186,6 @@
|
|||||||
.SmartSearch-keyPane {
|
.SmartSearch-keyPane {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin: 0px 0px 20px 0px;
|
margin: 0px 0px 20px 0px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -197,29 +195,11 @@
|
|||||||
border: 1px solid @login-notice-border;
|
border: 1px solid @login-notice-border;
|
||||||
background-color: @login-notice-bg;
|
background-color: @login-notice-bg;
|
||||||
color: @login-notice-text;
|
color: @login-notice-text;
|
||||||
}
|
position: relative;
|
||||||
|
|
||||||
.SmartSearch-relations{
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.SmartSearch-keyRow {
|
.SmartSearch-keyRow {
|
||||||
width: 33%;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
padding-right: 50px;
|
|
||||||
}
|
|
||||||
// 100% rows in a modal
|
|
||||||
.modal-body .SmartSearch-keyRow{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
// `.${list.name}List` class can be used to set add custom class overrides
|
|
||||||
.groupsList .SmartSearch-keyRow, .hostsList .SmartSearch-keyRow, .PortalMode .SmartSearch-keyRow{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.SmartSearch-keyRow:nth-child(3){
|
|
||||||
padding-right: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.SmartSearch-keyName {
|
.SmartSearch-keyName {
|
||||||
@@ -232,3 +212,30 @@
|
|||||||
.SmartSearch-keyComparators {
|
.SmartSearch-keyComparators {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.SmartSearch-keyPane--exitHolder {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SmartSearch-keyPane--exit {
|
||||||
|
background-color: @login-notice-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SmartSearch-examples {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SmartSearch-examples--title {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SmartSearch-examples--search {
|
||||||
|
color: @default-err;
|
||||||
|
background-color: @default-bg;
|
||||||
|
border: 1px solid @default-border;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0px 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet',
|
export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet', 'SmartSearchService',
|
||||||
function($stateParams, $scope, $state, QuerySet, GetBasePath, qs) {
|
function($stateParams, $scope, $state, QuerySet, GetBasePath, qs, SmartSearchService) {
|
||||||
|
|
||||||
let path, relations,
|
let path, relations,
|
||||||
// steps through the current tree of $state configurations, grabs default search params
|
// steps through the current tree of $state configurations, grabs default search params
|
||||||
@@ -17,6 +17,7 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
|
|||||||
$scope.searchTags = stripDefaultParams($state.params[`${$scope.iterator}_search`]);
|
$scope.searchTags = stripDefaultParams($state.params[`${$scope.iterator}_search`]);
|
||||||
qs.initFieldset(path, $scope.djangoModel, relations).then((data) => {
|
qs.initFieldset(path, $scope.djangoModel, relations).then((data) => {
|
||||||
$scope.models = data.models;
|
$scope.models = data.models;
|
||||||
|
$scope.options = data.options.data;
|
||||||
$scope.$emit(`${$scope.list.iterator}_options`, data.options);
|
$scope.$emit(`${$scope.list.iterator}_options`, data.options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,16 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
|
|||||||
return flat;
|
return flat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setDefaults(term) {
|
||||||
|
if ($scope.list.defaultSearchParams) {
|
||||||
|
return $scope.list.defaultSearchParams(term);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
search: encodeURIComponent(term)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$scope.toggleKeyPane = function() {
|
$scope.toggleKeyPane = function() {
|
||||||
$scope.showKeyPane = !$scope.showKeyPane;
|
$scope.showKeyPane = !$scope.showKeyPane;
|
||||||
};
|
};
|
||||||
@@ -69,10 +80,37 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
|
|||||||
|
|
||||||
// remove tag, merge new queryset, $state.go
|
// remove tag, merge new queryset, $state.go
|
||||||
$scope.remove = function(index) {
|
$scope.remove = function(index) {
|
||||||
let removed = qs.encodeParam($scope.searchTags.splice(index, 1)[0]);
|
let tagToRemove = $scope.searchTags.splice(index, 1)[0];
|
||||||
|
let termParts = SmartSearchService.splitTermIntoParts(tagToRemove);
|
||||||
|
let removed;
|
||||||
|
if (termParts.length === 1) {
|
||||||
|
removed = setDefaults(tagToRemove);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let root = termParts[0].split(".")[0].replace(/^-/, '');
|
||||||
|
let encodeParams = {
|
||||||
|
term: tagToRemove
|
||||||
|
};
|
||||||
|
if(_.has($scope.options.actions.GET, root)) {
|
||||||
|
if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') {
|
||||||
|
encodeParams.relatedSearchTerm = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
encodeParams.searchTerm = true;
|
||||||
|
}
|
||||||
|
removed = qs.encodeParam(encodeParams);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
removed = setDefaults(tagToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
_.each(removed, (value, key) => {
|
_.each(removed, (value, key) => {
|
||||||
if (Array.isArray(queryset[key])){
|
if (Array.isArray(queryset[key])){
|
||||||
_.remove(queryset[key], (item) => item === value);
|
_.remove(queryset[key], (item) => item === value);
|
||||||
|
// If the array is now empty, remove that key
|
||||||
|
if(queryset[key].length === 0) {
|
||||||
|
delete queryset[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
delete queryset[key];
|
delete queryset[key];
|
||||||
@@ -92,26 +130,46 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
|
|||||||
let params = {},
|
let params = {},
|
||||||
origQueryset = _.clone(queryset);
|
origQueryset = _.clone(queryset);
|
||||||
|
|
||||||
function setDefaults(term) {
|
// Remove leading/trailing whitespace if there is any
|
||||||
// "name" and "description" are sane defaults for MOST models, but not ALL!
|
terms = terms.trim();
|
||||||
// defaults may be configured in ListDefinition.defaultSearchParams
|
|
||||||
if ($scope.list.defaultSearchParams) {
|
|
||||||
return $scope.list.defaultSearchParams(term);
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
or__name__icontains: term,
|
|
||||||
or__description__icontains: term
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(terms && terms !== '') {
|
if(terms && terms !== '') {
|
||||||
_.forEach(terms.split(' '), (term) => {
|
// Split the terms up
|
||||||
|
let splitTerms = SmartSearchService.splitSearchIntoTerms(terms);
|
||||||
|
_.forEach(splitTerms, (term) => {
|
||||||
|
|
||||||
|
let termParts = SmartSearchService.splitTermIntoParts(term);
|
||||||
|
|
||||||
|
function combineSameSearches(a,b){
|
||||||
|
if (_.isArray(a)) {
|
||||||
|
return a.concat(b);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(a) {
|
||||||
|
return [a,b];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if only a value is provided, search using default keys
|
// if only a value is provided, search using default keys
|
||||||
if (term.split(':').length === 1) {
|
if (termParts.length === 1) {
|
||||||
params = _.merge(params, setDefaults(term));
|
params = _.merge(params, setDefaults(term), combineSameSearches);
|
||||||
} else {
|
} else {
|
||||||
params = _.merge(params, qs.encodeParam(term));
|
// Figure out if this is a search term
|
||||||
|
let root = termParts[0].split(".")[0].replace(/^-/, '');
|
||||||
|
if(_.has($scope.options.actions.GET, root)) {
|
||||||
|
if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') {
|
||||||
|
params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
params = _.merge(params, qs.encodeParam({term: term, searchTerm: true}), combineSameSearches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Its not a search term or a related search term - treat it as a string
|
||||||
|
else {
|
||||||
|
params = _.merge(params, setDefaults(term), combineSameSearches);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,25 +33,29 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- hint key -->
|
<!-- hint key -->
|
||||||
<div class="SmartSearch-keyPane row" ng-repeat="model in models" ng-show="showKeyPane">
|
<div class="SmartSearch-keyPane row" ng-repeat="model in models" ng-show="showKeyPane">
|
||||||
<div class="SmartSearch-keyPane--exit">
|
<div class="SmartSearch-keyPane--exitHolder">
|
||||||
<button class="Form-exit" ng-click="toggleKeyPane()">
|
<button class="Form-exit SmartSearch-keyPane--exit" ng-click="toggleKeyPane()">
|
||||||
<i class="fa fa-times-circle"></i>
|
<i class="fa fa-times-circle"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="SmartSearch-keyRow" ng-repeat="(key,value) in model.base">
|
<div class="SmartSearch-keyRow">
|
||||||
<div class="SmartSearch-keyName">
|
<div class="SmartSearch-examples">
|
||||||
{{ key }}
|
<div class="SmartSearch-examples--title">
|
||||||
</div>
|
<b>EXAMPLES:</b>
|
||||||
<div class="SmartSearch-keyInfo">
|
|
||||||
<div>Type: {{ value.type }}</div>
|
|
||||||
<div>Description: {{value.help_text}}</div>
|
|
||||||
<div ng-if="value.choices">
|
|
||||||
Enumerated: <span ng-repeat="choice in value.choices"> {{ choice[0] }} </span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="SmartSearch-examples--search">name:foo</div>
|
||||||
|
<div class="SmartSearch-examples--search">organization.name:Default</div>
|
||||||
|
<div class="SmartSearch-examples--search">id:>10</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="SmartSearch-keyRow SmartSearch-relations">
|
<div class="SmartSearch-keyRow">
|
||||||
<b>Searchable relationships:</b> <span ng-repeat="relation in model.related track by $index">{{ relation }}<span ng-if="$index !== Object.keys(model.related).length -1">, </span></span>
|
<b>FIELDS:</b> <span ng-repeat="(key,value) in model.base">{{ key }}<span ng-if="!$last">, </span></span>
|
||||||
|
</div>
|
||||||
|
<div class="SmartSearch-keyRow">
|
||||||
|
<b>RELATED FIELDS:</b> <span ng-repeat="relation in model.related">{{ relation }}<span ng-if="!$last">, </span></span>
|
||||||
|
</div>
|
||||||
|
<div class="SmartSearch-keyRow">
|
||||||
|
<b>ADDITIONAL INFORMATION:</b> <span>For additional information on advanced search search syntax please see the Ansible Tower documentation.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export default [function() {
|
||||||
|
return {
|
||||||
|
splitSearchIntoTerms(searchString) {
|
||||||
|
return searchString.match(/(?:[^\s("')]+|"[^"]*"|'[^']*')+/g);
|
||||||
|
},
|
||||||
|
splitTermIntoParts(searchTerm) {
|
||||||
|
let breakOnColon = searchTerm.match(/(?:[^:"]+|"[^"]*")+/g);
|
||||||
|
|
||||||
|
if(breakOnColon.length > 2) {
|
||||||
|
// concat all the strings after the first one together
|
||||||
|
let stringsToJoin = breakOnColon.slice(1,breakOnColon.length);
|
||||||
|
return [breakOnColon[0], stringsToJoin.join(':')];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return breakOnColon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}];
|
||||||
@@ -15,9 +15,21 @@ export default ['$scope', '$filter',
|
|||||||
var singleJobStatus = true;
|
var singleJobStatus = true;
|
||||||
var firstJobStatus;
|
var firstJobStatus;
|
||||||
var recentJobs = $scope.jobs;
|
var recentJobs = $scope.jobs;
|
||||||
|
var detailsBaseUrl;
|
||||||
|
|
||||||
if(!recentJobs){
|
if(!recentJobs){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unless we explicitly define a value for the template-type attribute when invoking the
|
||||||
|
// directive, assume the status icons are for a regular (non-workflow) job when building
|
||||||
|
// the details url path
|
||||||
|
if (typeof $scope.templateType !== 'undefined' && $scope.templateType === 'workflow_job_template') {
|
||||||
|
detailsBaseUrl = '/#/workflows/';
|
||||||
|
} else {
|
||||||
|
detailsBaseUrl = '/#/jobs/';
|
||||||
|
}
|
||||||
|
|
||||||
var sparkData =
|
var sparkData =
|
||||||
_.sortBy(recentJobs.map(function(job) {
|
_.sortBy(recentJobs.map(function(job) {
|
||||||
|
|
||||||
@@ -38,6 +50,7 @@ export default ['$scope', '$filter',
|
|||||||
data.sortDate = job.finished || "running" + data.jobId;
|
data.sortDate = job.finished || "running" + data.jobId;
|
||||||
data.finished = $filter('longDate')(job.finished) || job.status+"";
|
data.finished = $filter('longDate')(job.finished) || job.status+"";
|
||||||
data.status_tip = "JOB ID: " + data.jobId + "<br>STATUS: " + data.smartStatus + "<br>FINISHED: " + data.finished;
|
data.status_tip = "JOB ID: " + data.jobId + "<br>STATUS: " + data.smartStatus + "<br>FINISHED: " + data.finished;
|
||||||
|
data.detailsUrl = detailsBaseUrl + data.jobId;
|
||||||
|
|
||||||
// If we've already determined that there are both failed and successful jobs OR if the current job in the loop is
|
// If we've already determined that there are both failed and successful jobs OR if the current job in the loop is
|
||||||
// pending/waiting/running then we don't worry about checking for a single job status
|
// pending/waiting/running then we don't worry about checking for a single job status
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export default [ 'templateUrl',
|
|||||||
function(templateUrl) {
|
function(templateUrl) {
|
||||||
return {
|
return {
|
||||||
scope: {
|
scope: {
|
||||||
jobs: '='
|
jobs: '=',
|
||||||
|
templateType: '=?',
|
||||||
},
|
},
|
||||||
templateUrl: templateUrl('smart-status/smart-status'),
|
templateUrl: templateUrl('smart-status/smart-status'),
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="SmartStatus-container">
|
<div class="SmartStatus-container">
|
||||||
<div ng-repeat="job in sparkArray track by $index" class='SmartStatus-iconContainer'>
|
<div ng-repeat="job in sparkArray track by $index" class='SmartStatus-iconContainer'>
|
||||||
<a href="#/jobs/{{ job.jobId }}"
|
<a ng-href="{{job.detailsUrl}}"
|
||||||
aw-tool-tip="{{job.status_tip}}"
|
aw-tool-tip="{{job.status_tip}}"
|
||||||
data-tip-watch="job.status_tip"
|
data-tip-watch="job.status_tip"
|
||||||
aw-tip-placement="left"
|
aw-tip-placement="left"
|
||||||
|
|||||||
@@ -294,7 +294,7 @@
|
|||||||
${data.related.callback}
|
${data.related.callback}
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="break">The host configuration key is:
|
<p>The host configuration key is:
|
||||||
<strong>
|
<strong>
|
||||||
${$filter('sanitize')(data.host_config_key)}
|
${$filter('sanitize')(data.host_config_key)}
|
||||||
</strong>
|
</strong>
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ export default
|
|||||||
${data.related.callback}
|
${data.related.callback}
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="break">The host configuration key is:
|
<p>The host configuration key is:
|
||||||
<strong>
|
<strong>
|
||||||
${$filter('sanitize')(data.host_config_key)}
|
${$filter('sanitize')(data.host_config_key)}
|
||||||
</strong>
|
</strong>
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesA
|
|||||||
},
|
},
|
||||||
inventory_source_search: {
|
inventory_source_search: {
|
||||||
value: {
|
value: {
|
||||||
page_size: '5'
|
page_size: '5',
|
||||||
|
not__source: ''
|
||||||
},
|
},
|
||||||
squash: true,
|
squash: true,
|
||||||
dynamic: true
|
dynamic: true
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default
|
|||||||
|
|
||||||
var scope = params.scope,
|
var scope = params.scope,
|
||||||
id = params.id,
|
id = params.id,
|
||||||
|
templateType = params.templateType,
|
||||||
url;
|
url;
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +36,8 @@ export default
|
|||||||
scope.$emit("SurveyDeleted");
|
scope.$emit("SurveyDeleted");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
url = GetBasePath('job_templates')+ id + '/survey_spec/';
|
let basePath = templateType === 'workflow_job_template' ? GetBasePath('workflow_job_templates') : GetBasePath('job_templates');
|
||||||
|
url = basePath + id + '/survey_spec/';
|
||||||
|
|
||||||
Rest.setUrl(url);
|
Rest.setUrl(url);
|
||||||
Rest.destroy()
|
Rest.destroy()
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ export default
|
|||||||
// and closing the modal after success
|
// and closing the modal after success
|
||||||
DeleteSurvey({
|
DeleteSurvey({
|
||||||
scope: scope,
|
scope: scope,
|
||||||
id: id
|
id: id,
|
||||||
|
templateType: templateType
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2963
awx/ui/po/fr.po
Normal file
2963
awx/ui/po/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
2803
awx/ui/po/ja.po
Normal file
2803
awx/ui/po/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -31,10 +31,14 @@ describe('parseStdoutService', () => {
|
|||||||
unstyledLine = 'ok: [host-00]';
|
unstyledLine = 'ok: [host-00]';
|
||||||
expect(parseStdoutService.prettify(line, unstyled)).toBe(unstyledLine);
|
expect(parseStdoutService.prettify(line, unstyled)).toBe(unstyledLine);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can return empty strings', () => {
|
||||||
|
expect(parseStdoutService.prettify("")).toBe("");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getLineClasses()', () => {
|
describe('getLineClasses()', () => {
|
||||||
xit('creates a string that is used as a class', () => {
|
it('creates a string that is used as a class', () => {
|
||||||
let headerEvent = {
|
let headerEvent = {
|
||||||
event_name: 'playbook_on_task_start',
|
event_name: 'playbook_on_task_start',
|
||||||
event_data: {
|
event_data: {
|
||||||
@@ -44,12 +48,15 @@ describe('parseStdoutService', () => {
|
|||||||
};
|
};
|
||||||
let lineNum = 3;
|
let lineNum = 3;
|
||||||
let line = "TASK [setup] *******************************************************************";
|
let line = "TASK [setup] *******************************************************************";
|
||||||
let styledLine = " header_task header_task_80dd087c-268b-45e8-9aab-1083bcfd9364 play_0f667a23-d9ab-4128-a735-80566bcdbca0 line_num_3";
|
let styledLine = " header_task header_task_80dd087c-268b-45e8-9aab-1083bcfd9364 actual_header play_0f667a23-d9ab-4128-a735-80566bcdbca0 line_num_3";
|
||||||
expect(parseStdoutService.getLineClasses(headerEvent, line, lineNum)).toBe(styledLine);
|
expect(parseStdoutService.getLineClasses(headerEvent, line, lineNum)).toBe(styledLine);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getStartTime()', () => {
|
describe('getStartTime()', () => {
|
||||||
|
// TODO: the problem is that the date here calls moment, and thus
|
||||||
|
// the date will be timezone'd in the string (this could be
|
||||||
|
// different based on where you are)
|
||||||
xit('creates returns a badge with the start time of the event', () => {
|
xit('creates returns a badge with the start time of the event', () => {
|
||||||
let headerEvent = {
|
let headerEvent = {
|
||||||
event_name: 'playbook_on_play_start',
|
event_name: 'playbook_on_play_start',
|
||||||
@@ -115,6 +122,19 @@ describe('parseStdoutService', () => {
|
|||||||
|
|
||||||
expect(returnedEvent).toEqual(expectedReturn);
|
expect(returnedEvent).toEqual(expectedReturn);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('deals correctly with capped lines', () => {
|
||||||
|
let mockEvent = {
|
||||||
|
start_line: 7,
|
||||||
|
end_line: 11,
|
||||||
|
stdout: "a\r\nb\r\nc..."
|
||||||
|
};
|
||||||
|
let expectedReturn = [[8, "a"],[9, "b"], [10,"c..."], [11, "[1;imLine capped.[0m"]];
|
||||||
|
|
||||||
|
let returnedEvent = parseStdoutService.getLineArr(mockEvent);
|
||||||
|
|
||||||
|
expect(returnedEvent).toEqual(expectedReturn);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseStdout()', () => {
|
describe('parseStdout()', () => {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
describe('Service: QuerySet', () => {
|
describe('Service: QuerySet', () => {
|
||||||
let $httpBackend,
|
let $httpBackend,
|
||||||
QuerySet,
|
QuerySet,
|
||||||
Authorization;
|
Authorization,
|
||||||
|
SmartSearchService;
|
||||||
|
|
||||||
beforeEach(angular.mock.module('Tower', ($provide) =>{
|
beforeEach(angular.mock.module('Tower', ($provide) =>{
|
||||||
// @todo: improve app source / write testing utilities for interim
|
// @todo: improve app source / write testing utilities for interim
|
||||||
@@ -17,9 +18,10 @@ describe('Service: QuerySet', () => {
|
|||||||
}));
|
}));
|
||||||
beforeEach(angular.mock.module('RestServices'));
|
beforeEach(angular.mock.module('RestServices'));
|
||||||
|
|
||||||
beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_) => {
|
beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_, _SmartSearchService_) => {
|
||||||
$httpBackend = _$httpBackend_;
|
$httpBackend = _$httpBackend_;
|
||||||
QuerySet = _QuerySet_;
|
QuerySet = _QuerySet_;
|
||||||
|
SmartSearchService = _SmartSearchService_;
|
||||||
|
|
||||||
// @todo: improve app source
|
// @todo: improve app source
|
||||||
// config.js / local_settings emit $http requests in the app's run block
|
// config.js / local_settings emit $http requests in the app's run block
|
||||||
@@ -33,24 +35,27 @@ describe('Service: QuerySet', () => {
|
|||||||
.respond(200, '');
|
.respond(200, '');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('fn encodeQuery', () => {
|
describe('fn encodeParam', () => {
|
||||||
xit('null/undefined params should return an empty string', () => {
|
it('should encode parameters properly', () =>{
|
||||||
expect(QuerySet.encodeQuery(null)).toEqual('');
|
expect(QuerySet.encodeParam({term: "name:foo", searchTerm: true})).toEqual({"name__icontains_DEFAULT" : "foo"});
|
||||||
expect(QuerySet.encodeQuery(undefined)).toEqual('');
|
expect(QuerySet.encodeParam({term: "-name:foo", searchTerm: true})).toEqual({"not__name__icontains_DEFAULT" : "foo"});
|
||||||
|
expect(QuerySet.encodeParam({term: "name:'foo bar'", searchTerm: true})).toEqual({"name__icontains_DEFAULT" : "'foo bar'"});
|
||||||
|
expect(QuerySet.encodeParam({term: "-name:'foo bar'", searchTerm: true})).toEqual({"not__name__icontains_DEFAULT" : "'foo bar'"});
|
||||||
|
expect(QuerySet.encodeParam({term: "organization:foo", relatedSearchTerm: true})).toEqual({"organization__search_DEFAULT" : "foo"});
|
||||||
|
expect(QuerySet.encodeParam({term: "-organization:foo", relatedSearchTerm: true})).toEqual({"not__organization__search_DEFAULT" : "foo"});
|
||||||
|
expect(QuerySet.encodeParam({term: "organization.name:foo", relatedSearchTerm: true})).toEqual({"organization__name" : "foo"});
|
||||||
|
expect(QuerySet.encodeParam({term: "-organization.name:foo", relatedSearchTerm: true})).toEqual({"not__organization__name" : "foo"});
|
||||||
|
expect(QuerySet.encodeParam({term: "id:11", searchTerm: true})).toEqual({"id__icontains_DEFAULT" : "11"});
|
||||||
|
expect(QuerySet.encodeParam({term: "-id:11", searchTerm: true})).toEqual({"not__id__icontains_DEFAULT" : "11"});
|
||||||
|
expect(QuerySet.encodeParam({term: "id:>11", searchTerm: true})).toEqual({"id__gt" : "11"});
|
||||||
|
expect(QuerySet.encodeParam({term: "-id:>11", searchTerm: true})).toEqual({"not__id__gt" : "11"});
|
||||||
|
expect(QuerySet.encodeParam({term: "id:>=11", searchTerm: true})).toEqual({"id__gte" : "11"});
|
||||||
|
expect(QuerySet.encodeParam({term: "-id:>=11", searchTerm: true})).toEqual({"not__id__gte" : "11"});
|
||||||
|
expect(QuerySet.encodeParam({term: "id:<11", searchTerm: true})).toEqual({"id__lt" : "11"});
|
||||||
|
expect(QuerySet.encodeParam({term: "-id:<11", searchTerm: true})).toEqual({"not__id__lt" : "11"});
|
||||||
|
expect(QuerySet.encodeParam({term: "id:<=11", searchTerm: true})).toEqual({"id__lte" : "11"});
|
||||||
|
expect(QuerySet.encodeParam({term: "-id:<=11", searchTerm: true})).toEqual({"not__id__lte" : "11"});
|
||||||
});
|
});
|
||||||
xit('should encode params to a string', () => {
|
|
||||||
let params = {
|
|
||||||
or__created_by: 'Jenkins',
|
|
||||||
or__modified_by: 'Jenkins',
|
|
||||||
and__not__status: 'success',
|
|
||||||
},
|
|
||||||
result = '?or__created_by=Jenkins&or__modified_by=Jenkins&and__not__status=success';
|
|
||||||
expect(QuerySet.encodeQuery(params)).toEqual(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
xdescribe('fn decodeQuery', () => {
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
43
awx/ui/tests/spec/smart-search/smart-search.service-test.js
Normal file
43
awx/ui/tests/spec/smart-search/smart-search.service-test.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('Service: SmartSearch', () => {
|
||||||
|
let SmartSearchService;
|
||||||
|
|
||||||
|
beforeEach(angular.mock.module('Tower'));
|
||||||
|
|
||||||
|
beforeEach(angular.mock.module('SmartSearchModule'));
|
||||||
|
|
||||||
|
beforeEach(angular.mock.inject((_SmartSearchService_) => {
|
||||||
|
SmartSearchService = _SmartSearchService_;
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('fn splitSearchIntoTerms', () => {
|
||||||
|
it('should convert the search string to an array tag strings', () =>{
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('foo')).toEqual(["foo"]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('foo bar')).toEqual(["foo", "bar"]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('name:foo bar')).toEqual(["name:foo", "bar"]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('name:foo description:bar')).toEqual(["name:foo", "description:bar"]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar"')).toEqual(["name:\"foo bar\""]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar" description:"bar foo"')).toEqual(["name:\"foo bar\"", "description:\"bar foo\""]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar" description:"bar foo"')).toEqual(["name:\"foo bar\"", "description:\"bar foo\""]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\'')).toEqual(["name:\'foo bar\'"]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\' description:\'bar foo\'')).toEqual(["name:\'foo bar\'", "description:\'bar foo\'"]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\' description:\'bar foo\'')).toEqual(["name:\'foo bar\'", "description:\'bar foo\'"]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('name:\"foo bar\" description:\'bar foo\'')).toEqual(["name:\"foo bar\"", "description:\'bar foo\'"]);
|
||||||
|
expect(SmartSearchService.splitSearchIntoTerms('name:\"foo bar\" foo')).toEqual(["name:\"foo bar\"", "foo"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fn splitTermIntoParts', () => {
|
||||||
|
it('should convert the search term to a key and value', () =>{
|
||||||
|
expect(SmartSearchService.splitTermIntoParts('foo')).toEqual(["foo"]);
|
||||||
|
expect(SmartSearchService.splitTermIntoParts('foo:bar')).toEqual(["foo", "bar"]);
|
||||||
|
expect(SmartSearchService.splitTermIntoParts('foo:bar:foobar')).toEqual(["foo", "bar:foobar"]);
|
||||||
|
expect(SmartSearchService.splitTermIntoParts('name:\"foo bar\"')).toEqual(["name", "\"foo bar\""]);
|
||||||
|
expect(SmartSearchService.splitTermIntoParts('name:\"foo:bar\"')).toEqual(["name", "\"foo:bar\""]);
|
||||||
|
expect(SmartSearchService.splitTermIntoParts('name:\'foo bar\'')).toEqual(["name", "\'foo bar\'"]);
|
||||||
|
expect(SmartSearchService.splitTermIntoParts('name:\'foo:bar\'')).toEqual(["name", "\'foo:bar\'"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -84,6 +84,10 @@ describe('Controller: WorkflowAdd', () => {
|
|||||||
.whenGET('/api')
|
.whenGET('/api')
|
||||||
.respond(200, '');
|
.respond(200, '');
|
||||||
|
|
||||||
|
$httpBackend
|
||||||
|
.whenGET(/\/static\/*/)
|
||||||
|
.respond(200, {});
|
||||||
|
|
||||||
TemplatesService.getLabelOptions = jasmine.createSpy('getLabelOptions').and.returnValue(getLabelsDeferred.promise);
|
TemplatesService.getLabelOptions = jasmine.createSpy('getLabelOptions').and.returnValue(getLabelsDeferred.promise);
|
||||||
TemplatesService.createWorkflowJobTemplate = jasmine.createSpy('createWorkflowJobTemplate').and.returnValue(createWorkflowJobTemplateDeferred.promise);
|
TemplatesService.createWorkflowJobTemplate = jasmine.createSpy('createWorkflowJobTemplate').and.returnValue(createWorkflowJobTemplateDeferred.promise);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2015 Ansible, Inc.
|
# Copyright (c) 2016 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
import sos
|
import sos
|
||||||
@@ -8,7 +8,8 @@ SOSREPORT_TOWER_COMMANDS = [
|
|||||||
"ansible --version", # ansible core version
|
"ansible --version", # ansible core version
|
||||||
"tower-manage --version", # tower version
|
"tower-manage --version", # tower version
|
||||||
"supervisorctl status", # tower process status
|
"supervisorctl status", # tower process status
|
||||||
"pip freeze", # pip package list
|
"/var/lib/awx/venv/tower/bin/pip freeze", # pip package list
|
||||||
|
"/var/lib/awx/venv/ansible/bin/pip freeze", # pip package list
|
||||||
"tree -d /var/lib/awx", # show me the dirs
|
"tree -d /var/lib/awx", # show me the dirs
|
||||||
"ls -ll /var/lib/awx", # check permissions
|
"ls -ll /var/lib/awx", # check permissions
|
||||||
"ls -ll /etc/tower",
|
"ls -ll /etc/tower",
|
||||||
@@ -19,9 +20,8 @@ SOSREPORT_TOWER_DIRS = [
|
|||||||
"/etc/tower/",
|
"/etc/tower/",
|
||||||
"/etc/ansible/",
|
"/etc/ansible/",
|
||||||
"/var/log/tower",
|
"/var/log/tower",
|
||||||
"/var/log/httpd",
|
"/var/log/nginx",
|
||||||
"/var/log/apache2",
|
"/var/log/rabbitmq",
|
||||||
"/var/log/redis",
|
|
||||||
"/var/log/supervisor",
|
"/var/log/supervisor",
|
||||||
"/var/log/syslog",
|
"/var/log/syslog",
|
||||||
"/var/log/udev",
|
"/var/log/udev",
|
||||||
|
|||||||
Reference in New Issue
Block a user