mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 18:40:01 -03:30
Merge branch 'release_3.1.0' into 4673-alert-modal-spacing
This commit is contained in:
commit
c1a6f7b008
1
.gitignore
vendored
1
.gitignore
vendored
@ -106,7 +106,6 @@ reports
|
||||
*.log.[0-9]
|
||||
*.results
|
||||
local/
|
||||
*.mo
|
||||
|
||||
# AWX python libs populated by requirements.txt
|
||||
awx/lib/.deps_built
|
||||
|
||||
10
Makefile
10
Makefile
@ -559,9 +559,12 @@ messages:
|
||||
fi; \
|
||||
$(PYTHON) manage.py makemessages -l $(LANG) --keep-pot
|
||||
|
||||
# generate l10n .json .mo
|
||||
languages: $(UI_DEPS_FLAG_FILE) check-po
|
||||
# generate l10n .json
|
||||
ui-languages: $(UI_DEPS_FLAG_FILE) check-po
|
||||
$(NPM_BIN) --prefix awx/ui run languages
|
||||
|
||||
# generate l10n .mo
|
||||
api-languages:
|
||||
@if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/tower/bin/activate; \
|
||||
fi; \
|
||||
@ -592,8 +595,7 @@ ui-devel: $(UI_DEPS_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_DEPS_FLAG_FILE)
|
||||
$(UI_RELEASE_FLAG_FILE): ui-languages $(UI_DEPS_FLAG_FILE)
|
||||
$(NPM_BIN) --prefix awx/ui run build-release
|
||||
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 metadata
|
||||
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
|
||||
|
||||
# Ansible Tower
|
||||
@ -75,7 +75,7 @@ class Metadata(metadata.SimpleMetadata):
|
||||
elif getattr(field, 'fields', None):
|
||||
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()]
|
||||
|
||||
# Indicate if a field is write-only.
|
||||
|
||||
@ -2370,7 +2370,7 @@ class WorkflowJobTemplateNodeSerializer(WorkflowNodeBaseSerializer):
|
||||
if view and view.request:
|
||||
request_method = view.request.method
|
||||
if request_method in ['PATCH']:
|
||||
obj = view.get_object()
|
||||
obj = self.instance
|
||||
char_prompts = copy.copy(obj.char_prompts)
|
||||
char_prompts.update(self.extract_char_prompts(data))
|
||||
else:
|
||||
@ -2709,18 +2709,15 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
|
||||
variables_needed_to_start = serializers.ReadOnlyField()
|
||||
survey_enabled = serializers.SerializerMethodField()
|
||||
extra_vars = VerbatimField(required=False, write_only=True)
|
||||
warnings = serializers.SerializerMethodField()
|
||||
workflow_job_template_data = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
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',
|
||||
'node_templates_missing', 'node_prompts_rejected',
|
||||
'workflow_job_template_data')
|
||||
|
||||
def get_warnings(self, obj):
|
||||
return obj.get_warnings()
|
||||
|
||||
def get_survey_enabled(self, obj):
|
||||
if obj:
|
||||
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`:
|
||||
|
||||
`{"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.
|
||||
|
||||
|
||||
@ -2946,7 +2946,10 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
|
||||
always_allow_superuser = False
|
||||
|
||||
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 {}
|
||||
if obj:
|
||||
for v in obj.variables_needed_to_start:
|
||||
|
||||
@ -19,8 +19,6 @@ from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
# Python
|
||||
import contextlib
|
||||
import copy
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
@ -77,41 +75,11 @@ class BaseCallbackModule(CallbackBase):
|
||||
super(BaseCallbackModule, self).__init__()
|
||||
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
|
||||
def capture_event_data(self, event, **event_data):
|
||||
|
||||
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:
|
||||
task = event_data.pop('task', None)
|
||||
else:
|
||||
@ -258,7 +226,7 @@ class BaseCallbackModule(CallbackBase):
|
||||
if task_uuid in self.task_uuids:
|
||||
# FIXME: When this task UUID repeats, it means the play is using the
|
||||
# free strategy, so different hosts may be running different tasks
|
||||
# within a play.
|
||||
# within a play.
|
||||
return
|
||||
self.task_uuids.add(task_uuid)
|
||||
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
|
||||
def can_cancel(self, obj):
|
||||
if not obj.can_cancel:
|
||||
return False
|
||||
if self.user == obj.created_by:
|
||||
return True
|
||||
# Project updates cascade delete with project, admin role descends from org admin
|
||||
@ -1395,7 +1393,8 @@ class WorkflowJobTemplateNodeAccess(BaseAccess):
|
||||
qs = self.model.objects.filter(
|
||||
workflow_job_template__in=WorkflowJobTemplate.accessible_objects(
|
||||
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
|
||||
|
||||
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:
|
||||
invalid_regions.append(r)
|
||||
if invalid_regions:
|
||||
raise ValidationError(_('Invalid %(source)s region%(plural)s: %(region)s') % {
|
||||
'source': self.source, 'plural': '' if len(invalid_regions) == 1 else 's',
|
||||
'region': ', '.join(invalid_regions)})
|
||||
raise ValidationError(_('Invalid %(source)s region: %(region)s') % {
|
||||
'source': self.source, 'region': ', '.join(invalid_regions)})
|
||||
return ','.join(regions)
|
||||
|
||||
source_vars_dict = VarsDictProperty('source_vars')
|
||||
@ -1028,9 +1027,8 @@ class InventorySourceOptions(BaseModel):
|
||||
if instance_filter_name not in self.INSTANCE_FILTER_NAMES:
|
||||
invalid_filters.append(instance_filter)
|
||||
if invalid_filters:
|
||||
raise ValidationError(_('Invalid filter expression%(plural)s: %(filter)s') %
|
||||
{'plural': '' if len(invalid_filters) == 1 else 's',
|
||||
'filter': ', '.join(invalid_filters)})
|
||||
raise ValidationError(_('Invalid filter expression: %(filter)s') %
|
||||
{'filter': ', '.join(invalid_filters)})
|
||||
return instance_filters
|
||||
|
||||
def clean_group_by(self):
|
||||
@ -1047,9 +1045,8 @@ class InventorySourceOptions(BaseModel):
|
||||
if c not in valid_choices and c not in invalid_choices:
|
||||
invalid_choices.append(c)
|
||||
if invalid_choices:
|
||||
raise ValidationError(_('Invalid group by choice%(plural)s: %(choice)s') %
|
||||
{'plural': '' if len(invalid_choices) == 1 else 's',
|
||||
'choice': ', '.join(invalid_choices)})
|
||||
raise ValidationError(_('Invalid group by choice: %(choice)s') %
|
||||
{'choice': ', '.join(invalid_choices)})
|
||||
return ','.join(choices)
|
||||
|
||||
|
||||
|
||||
@ -134,7 +134,7 @@ class WorkflowNodeBase(CreatedModifiedModel):
|
||||
scan_errors = ujt_obj._extra_job_type_errors(accepted_fields)
|
||||
ignored_dict.update(scan_errors)
|
||||
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'
|
||||
|
||||
data = {}
|
||||
@ -421,18 +421,22 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
||||
|
||||
def can_start_without_user_input(self):
|
||||
'''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):
|
||||
warning_data = {}
|
||||
for node in self.workflow_job_template_nodes.all():
|
||||
if node.unified_job_template is None:
|
||||
warning_data[node.pk] = 'Node is missing a linked unified_job_template'
|
||||
continue
|
||||
def node_templates_missing(self):
|
||||
return [node.pk for node in self.workflow_job_template_nodes.filter(
|
||||
unified_job_template__isnull=True).all()]
|
||||
|
||||
def node_prompts_rejected(self):
|
||||
node_list = []
|
||||
for node in self.workflow_job_template_nodes.select_related('unified_job_template').all():
|
||||
node_prompts_warnings = node.get_prompts_warnings()
|
||||
if node_prompts_warnings:
|
||||
warning_data[node.pk] = node_prompts_warnings
|
||||
return warning_data
|
||||
node_list.append(node.pk)
|
||||
return node_list
|
||||
|
||||
def user_copy(self, user):
|
||||
new_wfjt = self.copy_unified_jt()
|
||||
|
||||
@ -125,6 +125,7 @@ class TestWorkflowJobTemplateNodeSerializerCharPrompts():
|
||||
serializer = WorkflowJobTemplateNodeSerializer()
|
||||
node = WorkflowJobTemplateNode(pk=1)
|
||||
node.char_prompts = {'limit': 'webservers'}
|
||||
serializer.instance = node
|
||||
view = FakeView(node)
|
||||
view.request = FakeRequest()
|
||||
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
|
||||
# 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
|
||||
# beyond this limit and the value will be removed
|
||||
@ -169,6 +169,10 @@ SESSION_COOKIE_SECURE = True
|
||||
# Disallow sending csrf cookies over insecure connections
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Limit CSRF cookies to browser sessions
|
||||
CSRF_COOKIE_AGE = None
|
||||
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS = ( # NOQA
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.core.context_processors.debug',
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
<div class="col-sm-6">
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@ -1720,8 +1720,7 @@ tr td button i {
|
||||
|
||||
|
||||
/* PW progress bar */
|
||||
.pw-progress {
|
||||
margin-top: 10px;
|
||||
.pw-progress { margin-top: 10px;
|
||||
|
||||
li {
|
||||
line-height: normal;
|
||||
|
||||
@ -11,19 +11,19 @@
|
||||
<!-- Don't indent this properly, you'll break the cow -->
|
||||
<pre class="About-cowsay--code">
|
||||
________________
|
||||
/ Tower {{version_str}} \\
|
||||
\\<span>{{version}}</span>/
|
||||
/ Tower {{version_str}} \
|
||||
\<span>{{version}}</span>/
|
||||
----------------
|
||||
\\ ^__^
|
||||
\\ (oo)\\_______
|
||||
(__) A )\\/\\
|
||||
\ ^__^
|
||||
\ (oo)\_______
|
||||
(__) A )\/\
|
||||
||----w |
|
||||
|| ||
|
||||
</pre>
|
||||
</div>
|
||||
<div class="About-modal--footer">
|
||||
<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>
|
||||
</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">
|
||||
<a ng-if="owner.type === 'organization'" ui-sref="organizations.edit({ organization_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>
|
||||
<a ng-if="owner.type === 'team'" ui-sref="teams.edit({ team_id: owner.id })">{{ owner.name }}{{$last ? '' : ', '}}</a>
|
||||
</div>
|
||||
<div class="OwnerList" ng-init="ownersLimit = 5; ownersLimitConst = 5; ">
|
||||
<div class="OwnerList-Container" ng-repeat="owner in owners_list | limitTo:ownersLimit">
|
||||
<a ng-if="owner.type === 'organization'" ui-sref="organizations.edit({ organization_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>
|
||||
<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'>
|
||||
<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>
|
||||
|
||||
@ -24,10 +24,7 @@ export default
|
||||
var langUrl = langInfo.replace('-', '_');
|
||||
//gettextCatalog.debug = true;
|
||||
gettextCatalog.setCurrentLanguage(langInfo);
|
||||
// TODO: the line below is commented out temporarily until
|
||||
// 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');
|
||||
gettextCatalog.loadRemote('/static/languages/' + langUrl + '.json');
|
||||
};
|
||||
}])
|
||||
.factory('i18n', ['gettextCatalog',
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
$scope.item = group;
|
||||
$scope.submitMode = $stateParams.groups === undefined ? 'move' : 'copy';
|
||||
$scope['toggle_'+ list.iterator] = function(id){
|
||||
$scope.toggle_row = function(id){
|
||||
// toggle off anything else currently selected
|
||||
_.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;});
|
||||
// yoink the currently selected thing
|
||||
@ -60,9 +60,6 @@
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// search init
|
||||
|
||||
@ -8,10 +8,10 @@
|
||||
['$scope', '$state', '$stateParams', 'generateList', 'HostManageService', 'GetBasePath', 'CopyMoveGroupList', 'host', 'Dataset',
|
||||
function($scope, $state, $stateParams, GenerateList, HostManageService, GetBasePath, CopyMoveGroupList, host, Dataset){
|
||||
var list = CopyMoveGroupList;
|
||||
|
||||
|
||||
$scope.item = host;
|
||||
$scope.submitMode = 'copy';
|
||||
$scope['toggle_'+ list.iterator] = function(id){
|
||||
$scope.toggle_row = function(id){
|
||||
// toggle off anything else currently selected
|
||||
_.forEach($scope.groups, (item) => {return item.id === id ? item.checked = 1 : item.checked = null;});
|
||||
// yoink the currently selected thing
|
||||
|
||||
@ -30,8 +30,8 @@ var copyMoveGroupRoute = {
|
||||
resolve: {
|
||||
Dataset: ['CopyMoveGroupList', 'QuerySet', '$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);
|
||||
let path = GetBasePath(list.name);
|
||||
$stateParams.copy_search.not__id__in = ($stateParams.group && $stateParams.group.length > 0 ? group.id + ',' + _.last($stateParams.group) : group.id.toString());
|
||||
let path = GetBasePath('inventory') + $stateParams.inventory_id + '/groups/';
|
||||
return qs.search(path, $stateParams.copy_search);
|
||||
}
|
||||
],
|
||||
@ -66,7 +66,7 @@ var copyMoveHostRoute = {
|
||||
resolve: {
|
||||
Dataset: ['CopyMoveGroupList', 'QuerySet', '$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);
|
||||
}
|
||||
],
|
||||
@ -80,7 +80,9 @@ var copyMoveHostRoute = {
|
||||
controller: CopyMoveHostsController,
|
||||
},
|
||||
'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({
|
||||
list: CopyMoveGroupList,
|
||||
mode: 'lookup',
|
||||
|
||||
@ -98,9 +98,17 @@ export default {
|
||||
},
|
||||
// target ui-views with name@inventoryManage state
|
||||
'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({
|
||||
list: InventoryGroups,
|
||||
list: list,
|
||||
mode: 'edit'
|
||||
});
|
||||
html = generateList.wrapPanel(html);
|
||||
@ -112,9 +120,17 @@ export default {
|
||||
controller: GroupsListController
|
||||
},
|
||||
'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({
|
||||
list: InventoryHosts,
|
||||
list: list,
|
||||
mode: 'edit'
|
||||
});
|
||||
return generateList.wrapPanel(html);
|
||||
|
||||
@ -15,7 +15,7 @@ export default [ 'templateUrl',
|
||||
link: function(scope) {
|
||||
// as count is changed by event data coming in,
|
||||
// update the host status bar
|
||||
scope.$watch('count', function(val) {
|
||||
var toDestroy = scope.$watch('count', function(val) {
|
||||
if (val) {
|
||||
Object.keys(val).forEach(key => {
|
||||
// reposition the hosts status bar by setting
|
||||
@ -38,6 +38,10 @@ export default [ 'templateUrl',
|
||||
.filter(key => (val[key] > 0)).length > 0);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function(){
|
||||
toDestroy();
|
||||
});
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
@ -162,6 +162,7 @@
|
||||
|
||||
.JobResultsStdOut-stdoutColumn {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
color: @default-interface-txt;
|
||||
@ -171,6 +172,12 @@
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-stdoutColumn--tooMany {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: @default-err;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-stdoutColumn {
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -216,6 +223,11 @@
|
||||
color: @default-interface-txt;
|
||||
}
|
||||
|
||||
.JobResultsStdOut-cappedLine {
|
||||
color: @b7grey;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: @breakpoint-md) {
|
||||
.JobResultsStdOut-numberColumnPreload {
|
||||
display: none;
|
||||
|
||||
@ -12,6 +12,18 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
||||
templateUrl: templateUrl('job-results/job-results-stdout/job-results-stdout'),
|
||||
restrict: 'E',
|
||||
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");
|
||||
// utility function used to find the top visible line and
|
||||
// parent header in the pane
|
||||
@ -115,9 +127,15 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
||||
// stop iterating over the standard out
|
||||
// lines once the first one has been
|
||||
// found
|
||||
|
||||
$this = null;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$this = null;
|
||||
});
|
||||
|
||||
$container = null;
|
||||
|
||||
return {
|
||||
visLine: visItem,
|
||||
@ -131,22 +149,24 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
||||
} else {
|
||||
scope.isMobile = false;
|
||||
}
|
||||
// watch changes to the window size
|
||||
$(window).resize(function() {
|
||||
|
||||
resizer = function() {
|
||||
// and update the isMobile var accordingly
|
||||
if (window.innerWidth <= 1200 && !scope.isMobile) {
|
||||
scope.isMobile = true;
|
||||
} else if (window.innerWidth > 1200 & scope.isMobile) {
|
||||
scope.isMobile = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
// watch changes to the window size
|
||||
$(window).resize(resizer);
|
||||
|
||||
var lastScrollTop;
|
||||
|
||||
var initScrollTop = function() {
|
||||
lastScrollTop = 0;
|
||||
};
|
||||
var scrollWatcher = function() {
|
||||
scrollWatcher = function() {
|
||||
var st = $(this).scrollTop();
|
||||
var netScroll = st + $(this).innerHeight();
|
||||
var fullHeight;
|
||||
@ -178,11 +198,15 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
||||
}
|
||||
|
||||
lastScrollTop = st;
|
||||
|
||||
st = null;
|
||||
netScroll = null;
|
||||
fullHeight = null;
|
||||
};
|
||||
|
||||
// update scroll watchers when isMobile changes based on
|
||||
// window resize
|
||||
scope.$watch('isMobile', function(val) {
|
||||
toDestroy.push(scope.$watch('isMobile', function(val) {
|
||||
if (val === true) {
|
||||
// make sure ^ TOP always shown for mobile
|
||||
scope.stdoutOverflowed = true;
|
||||
@ -204,7 +228,7 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
||||
$(".JobResultsStdOut-stdoutContainer").on('scroll',
|
||||
scrollWatcher);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// called to scroll to follow anchor
|
||||
scope.followScroll = function() {
|
||||
@ -237,7 +261,7 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
||||
|
||||
// if following becomes active, go ahead and get to the bottom
|
||||
// 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
|
||||
if (val) {
|
||||
scope.followScroll();
|
||||
@ -251,7 +275,7 @@ export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll',
|
||||
scope.followTooltip = "Click to follow standard out as it comes in.";
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// follow button ng-click function
|
||||
scope.followToggleClicked = function() {
|
||||
|
||||
@ -34,6 +34,13 @@
|
||||
<div id="topAnchor" class="JobResultsStdOut-topAnchor"></div>
|
||||
<div class="JobResultsStdOut-numberColumnPreload"></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"
|
||||
class="JobResultsStdOut-followAnchor">
|
||||
<div class="JobResultsStdOut-toTop"
|
||||
|
||||
@ -148,6 +148,7 @@
|
||||
background-color: @default-bg;
|
||||
border-radius: 5px;
|
||||
color: @default-interface-txt;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
.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',
|
||||
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
|
||||
$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
|
||||
// changes
|
||||
$scope.$watch('job_status', function(status) {
|
||||
toDestroy.push($scope.$watch('job_status', function(status) {
|
||||
if (status) {
|
||||
$scope.status_label = $scope.jobOptions.status.choices
|
||||
.filter(val => val[0] === status)
|
||||
.map(val => val[1])[0];
|
||||
$scope.status_tooltip = "Job " + $scope.status_label;
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 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
|
||||
@ -185,7 +191,12 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
|
||||
|
||||
// This is where the async updates to the UI actually happen.
|
||||
// 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
|
||||
var mungedEvent = eventQueue.populate(event);
|
||||
|
||||
@ -278,6 +289,9 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
|
||||
.stdout)($scope.events[mungedEvent
|
||||
.counter]));
|
||||
}
|
||||
|
||||
classList = null;
|
||||
putIn = null;
|
||||
} else {
|
||||
// this is a header or recap line, so just
|
||||
// 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");
|
||||
});
|
||||
|
||||
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
|
||||
var getEvents = function(url) {
|
||||
getEvents = function(url, context) {
|
||||
if (context !== currentContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
jobResultsService.getEvents(url)
|
||||
.then(events => {
|
||||
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);
|
||||
});
|
||||
if (events.next) {
|
||||
getEvents(events.next);
|
||||
} else {
|
||||
// put those paused events into the pane
|
||||
$scope.gotPreviouslyRanEvents.resolve("");
|
||||
}
|
||||
processPage(events, context);
|
||||
});
|
||||
};
|
||||
|
||||
// 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
|
||||
$scope.gotPreviouslyRanEvents = $q.defer();
|
||||
currentContext += 1;
|
||||
|
||||
let context = currentContext;
|
||||
|
||||
$( ".JobResultsStdOut-aLineOfStdOut.not_skeleton" ).remove();
|
||||
$scope.hasSkeleton.promise.then(() => {
|
||||
val.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);
|
||||
});
|
||||
if (val.next) {
|
||||
getEvents(val.next);
|
||||
if (val.count > parseInt(val.maxEvents)) {
|
||||
$(".header_task").hide();
|
||||
$(".header_play").hide();
|
||||
$scope.tooManyEvents = true;
|
||||
} else {
|
||||
// put those paused events into the pane
|
||||
$scope.gotPreviouslyRanEvents.resolve("");
|
||||
$(".header_task").show();
|
||||
$(".header_play").show();
|
||||
$scope.tooManyEvents = false;
|
||||
processPage(val, context);
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
|
||||
// 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,
|
||||
$scope.hasSkeleton.promise]).then(() => {
|
||||
var url = Dataset
|
||||
.config.url.split("?")[0] +
|
||||
QuerySet.encodeQueryset($state.params.job_event_search);
|
||||
var noFilter = (url.split("&")
|
||||
.filter(v => v.indexOf("page=") !== 0 &&
|
||||
v.indexOf("/api/v1") !== 0 &&
|
||||
v.indexOf("order_by=id") !== 0 &&
|
||||
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);
|
||||
}
|
||||
});
|
||||
// put the line in the
|
||||
// standard out pane (and increment play and task
|
||||
// count if applicable)
|
||||
if (data.event_name === "playbook_on_play_start") {
|
||||
$scope.playCount++;
|
||||
} else if (data.event_name === "playbook_on_task_start") {
|
||||
$scope.taskCount++;
|
||||
}
|
||||
|
||||
processEvent(data);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
// 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) ===
|
||||
parseInt($scope.job.id,10)) {
|
||||
// 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
|
||||
$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
|
||||
List-actionButton--delete"
|
||||
data-placement="top"
|
||||
ng-click="deleteJob()"
|
||||
ng-show="job_status.status == 'running' ||
|
||||
job_status.status=='pending' "
|
||||
ng-click="cancelJob()"
|
||||
ng-show="job_status == 'running' ||
|
||||
job_status=='pending' "
|
||||
aw-tool-tip="Cancel"
|
||||
data-original-title="" title="">
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
@ -49,8 +49,8 @@
|
||||
List-actionButton--delete"
|
||||
data-placement="top"
|
||||
ng-click="deleteJob()"
|
||||
ng-hide="job_status.status == 'running' ||
|
||||
job_status.status == 'pending' "
|
||||
ng-hide="job_status == 'running' ||
|
||||
job_status == 'pending' "
|
||||
aw-tool-tip="Delete"
|
||||
data-original-title=""
|
||||
title="">
|
||||
|
||||
@ -25,6 +25,7 @@ export default {
|
||||
params: {
|
||||
job_event_search: {
|
||||
value: {
|
||||
page_size: 100,
|
||||
order_by: 'id',
|
||||
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) {
|
||||
|
||||
@ -27,6 +27,7 @@ export default ['$log', 'moment', function($log, moment){
|
||||
line = line.replace(/u001b/g, '');
|
||||
|
||||
// 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(/\[0;31m/g, '<span class="ansi1 ansi31">');
|
||||
line = line.replace(/\[0;32m/g, '<span class="ansi32">');
|
||||
@ -185,7 +186,6 @@ export default ['$log', 'moment', function($log, moment){
|
||||
data-uuid="${clickClass}">
|
||||
</i>
|
||||
</span>`;
|
||||
// console.log(expandDom);
|
||||
return expandDom;
|
||||
} else {
|
||||
// non-header lines don't get an expander
|
||||
@ -193,10 +193,22 @@ export default ['$log', 'moment', function($log, moment){
|
||||
}
|
||||
},
|
||||
getLineArr: function(event) {
|
||||
return _
|
||||
.zip(_.range(event.start_line + 1,
|
||||
event.end_line + 1),
|
||||
event.stdout.replace("\t", " ").split("\r\n").slice(0, -1));
|
||||
let lineNums = _.range(event.start_line + 1,
|
||||
event.end_line + 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
|
||||
// job_event
|
||||
|
||||
@ -22,5 +22,6 @@ export default
|
||||
key: true,
|
||||
label: 'Target Group Name'
|
||||
}
|
||||
}
|
||||
},
|
||||
basePath: 'api/v1/inventories/{{$stateParams.inventory_id}}/groups'
|
||||
});
|
||||
|
||||
@ -18,9 +18,13 @@ export default
|
||||
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
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) {
|
||||
basePath = GetBasePath(elm.attr('data-basePath')) || elm.attr('data-basePath');
|
||||
query = elm.attr('data-query');
|
||||
query = query.replace(/\:value/, encodeURI(viewValue));
|
||||
query = query.replace(/\:value/, encodeURIComponent(viewValue));
|
||||
Rest.setUrl(`${basePath}${query}`);
|
||||
// https://github.com/ansible/ansible-tower/issues/3549
|
||||
// capturing both success/failure conditions in .then() promise
|
||||
|
||||
@ -1830,7 +1830,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
// smart-search directive
|
||||
html += `
|
||||
<div
|
||||
ng-hide="${itm}.length === 0 && (${collection.iterator}_searchTags | isEmpty)">
|
||||
ng-hide="${itm}.length === 0 && (searchTags | isEmpty)">
|
||||
<smart-search
|
||||
django-model="${itm}"
|
||||
search-size="${width}"
|
||||
@ -1855,7 +1855,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
html += `
|
||||
<div
|
||||
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">
|
||||
No records matched your search.
|
||||
</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
|
||||
var emptyListText = (collection.emptyListText) ? collection.emptyListText : i18n._("PLEASE ADD ITEMS TO THIS LIST");
|
||||
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 += `
|
||||
|
||||
@ -591,6 +591,9 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(field.simpleTip) {
|
||||
html += `<span aw-tool-tip="${field.simpleTip.awToolTip}" data-placement=${field.simpleTip.dataPlacement}>`;
|
||||
}
|
||||
// Add icon:
|
||||
if (field.ngShowIcon) {
|
||||
html += "<i ng-show=\"" + field.ngShowIcon + "\" class=\"" + field.icon + "\"></i> ";
|
||||
@ -615,6 +618,9 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
if (field.text) {
|
||||
html += field.text;
|
||||
}
|
||||
if(field.simpleTip) {
|
||||
html += `</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (list.name === 'hosts' || list.name === 'groups') {
|
||||
|
||||
@ -59,7 +59,7 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q
|
||||
return `1 - ${pageSize}`;
|
||||
} else {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,12 @@ import directive from './smart-search.directive';
|
||||
import controller from './smart-search.controller';
|
||||
import service from './queryset.service';
|
||||
import DjangoSearchModel from './django-search-model.class';
|
||||
|
||||
import smartSearchService from './smart-search.service';
|
||||
|
||||
export default
|
||||
angular.module('SmartSearchModule', [])
|
||||
.directive('smartSearch', directive)
|
||||
.controller('SmartSearchController', controller)
|
||||
.service('QuerySet', service)
|
||||
.service('SmartSearchService', smartSearchService)
|
||||
.constant('DjangoSearchModel', DjangoSearchModel);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory',
|
||||
function($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, SmartSearchService) {
|
||||
return {
|
||||
// kick off building a model for a specific endpoint
|
||||
// this is usually a list's basePath
|
||||
@ -67,29 +67,124 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear
|
||||
return angular.isObject(params) ? `?${queryset}` : '';
|
||||
|
||||
function encodeTerm(value, key){
|
||||
|
||||
key = key.replace(/__icontains_DEFAULT/g, "__icontains");
|
||||
key = key.replace(/__search_DEFAULT/g, "__search");
|
||||
|
||||
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 {
|
||||
if(value && typeof value === 'string') {
|
||||
value = value.replace(/"|'/g, "");
|
||||
}
|
||||
return `${key}=${value}&`;
|
||||
}
|
||||
}
|
||||
},
|
||||
// encodes a ui smart-search param to a django-friendly param
|
||||
// operand:key:comparator:value => {operand__key__comparator: value}
|
||||
encodeParam(param){
|
||||
let split = param.split(':');
|
||||
return {[split.slice(0,split.length -1).join('__')] : split[split.length-1]};
|
||||
encodeParam(params){
|
||||
// Assumption here is that we have a key and a value so the length
|
||||
// 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
|
||||
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)){
|
||||
return _.map(value, (item) => {
|
||||
return `${key.split('__').join(':')}:${item}`;
|
||||
return decodeParamString(item);
|
||||
});
|
||||
}
|
||||
else {
|
||||
return `${key.split('__').join(':')}:${value}`;
|
||||
return decodeParamString(value);
|
||||
}
|
||||
},
|
||||
|
||||
@ -147,20 +242,33 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear
|
||||
Wait('start');
|
||||
this.url = `${endpoint}${this.encodeQueryset(params)}`;
|
||||
Rest.setUrl(this.url);
|
||||
|
||||
return Rest.get()
|
||||
.success(this.success.bind(this))
|
||||
.error(this.error.bind(this))
|
||||
.finally(Wait('stop'));
|
||||
.then(function(response) {
|
||||
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) {
|
||||
ProcessErrors($rootScope, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Call to ' + this.url + '. GET returned: ' + status
|
||||
});
|
||||
},
|
||||
success(data) {
|
||||
return data;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
@ -186,8 +186,6 @@
|
||||
.SmartSearch-keyPane {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0px 0px 20px 0px;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
@ -197,29 +195,11 @@
|
||||
border: 1px solid @login-notice-border;
|
||||
background-color: @login-notice-bg;
|
||||
color: @login-notice-text;
|
||||
}
|
||||
|
||||
.SmartSearch-relations{
|
||||
margin-top: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.SmartSearch-keyRow {
|
||||
width: 33%;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
@ -232,3 +212,30 @@
|
||||
.SmartSearch-keyComparators {
|
||||
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',
|
||||
function($stateParams, $scope, $state, QuerySet, GetBasePath, qs) {
|
||||
export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet', 'SmartSearchService',
|
||||
function($stateParams, $scope, $state, QuerySet, GetBasePath, qs, SmartSearchService) {
|
||||
|
||||
let path, relations,
|
||||
// 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`]);
|
||||
qs.initFieldset(path, $scope.djangoModel, relations).then((data) => {
|
||||
$scope.models = data.models;
|
||||
$scope.options = data.options.data;
|
||||
$scope.$emit(`${$scope.list.iterator}_options`, data.options);
|
||||
});
|
||||
}
|
||||
@ -51,6 +52,16 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
|
||||
return flat;
|
||||
}
|
||||
|
||||
function setDefaults(term) {
|
||||
if ($scope.list.defaultSearchParams) {
|
||||
return $scope.list.defaultSearchParams(term);
|
||||
} else {
|
||||
return {
|
||||
search: encodeURIComponent(term)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$scope.toggleKeyPane = function() {
|
||||
$scope.showKeyPane = !$scope.showKeyPane;
|
||||
};
|
||||
@ -69,10 +80,37 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
|
||||
|
||||
// remove tag, merge new queryset, $state.go
|
||||
$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) => {
|
||||
if (Array.isArray(queryset[key])){
|
||||
_.remove(queryset[key], (item) => item === value);
|
||||
// If the array is now empty, remove that key
|
||||
if(queryset[key].length === 0) {
|
||||
delete queryset[key];
|
||||
}
|
||||
}
|
||||
else {
|
||||
delete queryset[key];
|
||||
@ -92,26 +130,46 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
|
||||
let params = {},
|
||||
origQueryset = _.clone(queryset);
|
||||
|
||||
function setDefaults(term) {
|
||||
// "name" and "description" are sane defaults for MOST models, but not ALL!
|
||||
// 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
|
||||
};
|
||||
}
|
||||
}
|
||||
// Remove leading/trailing whitespace if there is any
|
||||
terms = terms.trim();
|
||||
|
||||
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 (term.split(':').length === 1) {
|
||||
params = _.merge(params, setDefaults(term));
|
||||
if (termParts.length === 1) {
|
||||
params = _.merge(params, setDefaults(term), combineSameSearches);
|
||||
} 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>
|
||||
<!-- hint key -->
|
||||
<div class="SmartSearch-keyPane row" ng-repeat="model in models" ng-show="showKeyPane">
|
||||
<div class="SmartSearch-keyPane--exit">
|
||||
<button class="Form-exit" ng-click="toggleKeyPane()">
|
||||
<div class="SmartSearch-keyPane--exitHolder">
|
||||
<button class="Form-exit SmartSearch-keyPane--exit" ng-click="toggleKeyPane()">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="SmartSearch-keyRow" ng-repeat="(key,value) in model.base">
|
||||
<div class="SmartSearch-keyName">
|
||||
{{ key }}
|
||||
</div>
|
||||
<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 class="SmartSearch-keyRow">
|
||||
<div class="SmartSearch-examples">
|
||||
<div class="SmartSearch-examples--title">
|
||||
<b>EXAMPLES:</b>
|
||||
</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 class="SmartSearch-keyRow SmartSearch-relations">
|
||||
<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>
|
||||
<div class="SmartSearch-keyRow">
|
||||
<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>
|
||||
|
||||
@ -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 firstJobStatus;
|
||||
var recentJobs = $scope.jobs;
|
||||
var detailsBaseUrl;
|
||||
|
||||
if(!recentJobs){
|
||||
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 =
|
||||
_.sortBy(recentJobs.map(function(job) {
|
||||
|
||||
@ -38,6 +50,7 @@ export default ['$scope', '$filter',
|
||||
data.sortDate = job.finished || "running" + data.jobId;
|
||||
data.finished = $filter('longDate')(job.finished) || job.status+"";
|
||||
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
|
||||
// pending/waiting/running then we don't worry about checking for a single job status
|
||||
|
||||
@ -9,7 +9,8 @@ export default [ 'templateUrl',
|
||||
function(templateUrl) {
|
||||
return {
|
||||
scope: {
|
||||
jobs: '='
|
||||
jobs: '=',
|
||||
templateType: '=?',
|
||||
},
|
||||
templateUrl: templateUrl('smart-status/smart-status'),
|
||||
restrict: 'E',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="SmartStatus-container">
|
||||
<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}}"
|
||||
data-tip-watch="job.status_tip"
|
||||
aw-tip-placement="left"
|
||||
|
||||
@ -294,7 +294,7 @@
|
||||
${data.related.callback}
|
||||
</strong>
|
||||
</p>
|
||||
<p class="break">The host configuration key is:
|
||||
<p>The host configuration key is:
|
||||
<strong>
|
||||
${$filter('sanitize')(data.host_config_key)}
|
||||
</strong>
|
||||
|
||||
@ -430,7 +430,7 @@ export default
|
||||
${data.related.callback}
|
||||
</strong>
|
||||
</p>
|
||||
<p class="break">The host configuration key is:
|
||||
<p>The host configuration key is:
|
||||
<strong>
|
||||
${$filter('sanitize')(data.host_config_key)}
|
||||
</strong>
|
||||
|
||||
@ -103,7 +103,8 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplatesA
|
||||
},
|
||||
inventory_source_search: {
|
||||
value: {
|
||||
page_size: '5'
|
||||
page_size: '5',
|
||||
not__source: ''
|
||||
},
|
||||
squash: true,
|
||||
dynamic: true
|
||||
|
||||
@ -13,6 +13,7 @@ export default
|
||||
|
||||
var scope = params.scope,
|
||||
id = params.id,
|
||||
templateType = params.templateType,
|
||||
url;
|
||||
|
||||
|
||||
@ -35,7 +36,8 @@ export default
|
||||
scope.$emit("SurveyDeleted");
|
||||
|
||||
} 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.destroy()
|
||||
|
||||
@ -52,7 +52,8 @@ export default
|
||||
// and closing the modal after success
|
||||
DeleteSurvey({
|
||||
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]';
|
||||
expect(parseStdoutService.prettify(line, unstyled)).toBe(unstyledLine);
|
||||
});
|
||||
|
||||
it('can return empty strings', () => {
|
||||
expect(parseStdoutService.prettify("")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLineClasses()', () => {
|
||||
xit('creates a string that is used as a class', () => {
|
||||
it('creates a string that is used as a class', () => {
|
||||
let headerEvent = {
|
||||
event_name: 'playbook_on_task_start',
|
||||
event_data: {
|
||||
@ -44,12 +48,15 @@ describe('parseStdoutService', () => {
|
||||
};
|
||||
let lineNum = 3;
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
let headerEvent = {
|
||||
event_name: 'playbook_on_play_start',
|
||||
@ -115,6 +122,19 @@ describe('parseStdoutService', () => {
|
||||
|
||||
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()', () => {
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
describe('Service: QuerySet', () => {
|
||||
let $httpBackend,
|
||||
QuerySet,
|
||||
Authorization;
|
||||
Authorization,
|
||||
SmartSearchService;
|
||||
|
||||
beforeEach(angular.mock.module('Tower', ($provide) =>{
|
||||
// @todo: improve app source / write testing utilities for interim
|
||||
@ -17,9 +18,10 @@ describe('Service: QuerySet', () => {
|
||||
}));
|
||||
beforeEach(angular.mock.module('RestServices'));
|
||||
|
||||
beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_) => {
|
||||
beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_, _SmartSearchService_) => {
|
||||
$httpBackend = _$httpBackend_;
|
||||
QuerySet = _QuerySet_;
|
||||
SmartSearchService = _SmartSearchService_;
|
||||
|
||||
// @todo: improve app source
|
||||
// config.js / local_settings emit $http requests in the app's run block
|
||||
@ -33,24 +35,27 @@ describe('Service: QuerySet', () => {
|
||||
.respond(200, '');
|
||||
}));
|
||||
|
||||
describe('fn encodeQuery', () => {
|
||||
xit('null/undefined params should return an empty string', () => {
|
||||
expect(QuerySet.encodeQuery(null)).toEqual('');
|
||||
expect(QuerySet.encodeQuery(undefined)).toEqual('');
|
||||
describe('fn encodeParam', () => {
|
||||
it('should encode parameters properly', () =>{
|
||||
expect(QuerySet.encodeParam({term: "name:foo", searchTerm: true})).toEqual({"name__icontains_DEFAULT" : "foo"});
|
||||
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')
|
||||
.respond(200, '');
|
||||
|
||||
$httpBackend
|
||||
.whenGET(/\/static\/*/)
|
||||
.respond(200, {});
|
||||
|
||||
TemplatesService.getLabelOptions = jasmine.createSpy('getLabelOptions').and.returnValue(getLabelsDeferred.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.
|
||||
|
||||
import sos
|
||||
@ -8,7 +8,8 @@ SOSREPORT_TOWER_COMMANDS = [
|
||||
"ansible --version", # ansible core version
|
||||
"tower-manage --version", # tower version
|
||||
"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
|
||||
"ls -ll /var/lib/awx", # check permissions
|
||||
"ls -ll /etc/tower",
|
||||
@ -19,9 +20,8 @@ SOSREPORT_TOWER_DIRS = [
|
||||
"/etc/tower/",
|
||||
"/etc/ansible/",
|
||||
"/var/log/tower",
|
||||
"/var/log/httpd",
|
||||
"/var/log/apache2",
|
||||
"/var/log/redis",
|
||||
"/var/log/nginx",
|
||||
"/var/log/rabbitmq",
|
||||
"/var/log/supervisor",
|
||||
"/var/log/syslog",
|
||||
"/var/log/udev",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user