mirror of
https://github.com/ansible/awx.git
synced 2026-02-24 06:26:00 -03:30
Merge branch 'release_3.1.0' into jobResultsPerf
This commit is contained in:
@@ -251,6 +251,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
'inventory_update': _('Inventory Sync'),
|
||||
'system_job': _('Management Job'),
|
||||
'workflow_job': _('Workflow Job'),
|
||||
'workflow_job_template': _('Workflow Template'),
|
||||
}
|
||||
choices = []
|
||||
for t in self.get_types():
|
||||
@@ -2708,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
|
||||
|
||||
@@ -1553,12 +1553,14 @@ class InventoryScriptList(ListCreateAPIView):
|
||||
|
||||
model = CustomInventoryScript
|
||||
serializer_class = CustomInventoryScriptSerializer
|
||||
new_in_210 = True
|
||||
|
||||
|
||||
class InventoryScriptDetail(RetrieveUpdateDestroyAPIView):
|
||||
|
||||
model = CustomInventoryScript
|
||||
serializer_class = CustomInventoryScriptSerializer
|
||||
new_in_210 = True
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
@@ -2904,10 +2906,16 @@ class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, GenericAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
data = {}
|
||||
copy_TF, messages = request.user.can_access_with_errors(self.model, 'copy', obj)
|
||||
data['can_copy'] = copy_TF
|
||||
data['warnings'] = messages
|
||||
can_copy, messages = request.user.can_access_with_errors(self.model, 'copy', obj)
|
||||
data = OrderedDict([
|
||||
('can_copy', can_copy), ('can_copy_without_user_input', can_copy),
|
||||
('templates_unable_to_copy', [] if can_copy else ['all']),
|
||||
('credentials_unable_to_copy', [] if can_copy else ['all']),
|
||||
('inventories_unable_to_copy', [] if can_copy else ['all'])
|
||||
])
|
||||
if messages and can_copy:
|
||||
data['can_copy_without_user_input'] = False
|
||||
data.update(messages)
|
||||
return Response(data)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -2938,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:
|
||||
|
||||
@@ -328,7 +328,7 @@ class BaseCallbackModule(CallbackBase):
|
||||
ok=stats.ok,
|
||||
processed=stats.processed,
|
||||
skipped=stats.skipped,
|
||||
artifact_data=stats.custom.get('_run', {})
|
||||
artifact_data=stats.custom.get('_run', {}) if hasattr(stats, 'custom') else {}
|
||||
)
|
||||
|
||||
with self.capture_event_data('playbook_on_stats', **event_data):
|
||||
|
||||
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
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
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
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
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
@@ -1043,7 +1043,7 @@ class JobTemplateAccess(BaseAccess):
|
||||
Project.accessible_objects(self.user, 'use_role').exists() or
|
||||
Inventory.accessible_objects(self.user, 'use_role').exists())
|
||||
|
||||
# if reference_obj is provided, determine if it can be coppied
|
||||
# if reference_obj is provided, determine if it can be copied
|
||||
reference_obj = data.get('reference_obj', None)
|
||||
|
||||
if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN:
|
||||
@@ -1537,22 +1537,28 @@ class WorkflowJobTemplateAccess(BaseAccess):
|
||||
|
||||
def can_copy(self, obj):
|
||||
if self.save_messages:
|
||||
wfjt_errors = {}
|
||||
missing_ujt = []
|
||||
missing_credentials = []
|
||||
missing_inventories = []
|
||||
qs = obj.workflow_job_template_nodes
|
||||
qs.select_related('unified_job_template', 'inventory', 'credential')
|
||||
for node in qs.all():
|
||||
node_errors = {}
|
||||
if node.inventory and self.user not in node.inventory.use_role:
|
||||
node_errors['inventory'] = 'Prompted inventory %s can not be coppied.' % node.inventory.name
|
||||
missing_inventories.append(node.inventory.name)
|
||||
if node.credential and self.user not in node.credential.use_role:
|
||||
node_errors['credential'] = 'Prompted credential %s can not be coppied.' % node.credential.name
|
||||
missing_credentials.append(node.credential.name)
|
||||
ujt = node.unified_job_template
|
||||
if ujt and not self.user.can_access(UnifiedJobTemplate, 'start', ujt, validate_license=False):
|
||||
node_errors['unified_job_template'] = (
|
||||
'Prompted %s %s can not be coppied.' % (ujt._meta.verbose_name_raw, ujt.name))
|
||||
missing_ujt.append(ujt.name)
|
||||
if node_errors:
|
||||
wfjt_errors[node.id] = node_errors
|
||||
self.messages.update(wfjt_errors)
|
||||
if missing_ujt:
|
||||
self.messages['templates_unable_to_copy'] = missing_ujt
|
||||
if missing_credentials:
|
||||
self.messages['credentials_unable_to_copy'] = missing_credentials
|
||||
if missing_inventories:
|
||||
self.messages['inventories_unable_to_copy'] = missing_inventories
|
||||
|
||||
return self.check_related('organization', Organization, {'reference_obj': obj}, mandatory=True)
|
||||
|
||||
|
||||
@@ -96,12 +96,12 @@ class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--older_than',
|
||||
dest='older_than',
|
||||
default=None,
|
||||
help='Specify the relative time to consider facts older than (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y).'),
|
||||
default='30d',
|
||||
help='Specify the relative time to consider facts older than (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y). Defaults to 30d.'),
|
||||
make_option('--granularity',
|
||||
dest='granularity',
|
||||
default=None,
|
||||
help='Window duration to group same hosts by for deletion (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y).'),
|
||||
default='1w',
|
||||
help='Window duration to group same hosts by for deletion (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y). Defaults to 1w.'),
|
||||
make_option('--module',
|
||||
dest='module',
|
||||
default=None,
|
||||
|
||||
@@ -20,7 +20,7 @@ from django.core.urlresolvers import reverse
|
||||
# AWX
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.models.unified_jobs import * # noqa
|
||||
from awx.main.models.notifications import JobNotificationMixin
|
||||
from awx.main.models.notifications import JobNotificationMixin, NotificationTemplate
|
||||
from awx.main.fields import JSONField
|
||||
|
||||
logger = logging.getLogger('awx.main.models.ad_hoc_commands')
|
||||
@@ -157,18 +157,20 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
|
||||
|
||||
@property
|
||||
def notification_templates(self):
|
||||
all_inventory_sources = set()
|
||||
all_orgs = set()
|
||||
for h in self.hosts.all():
|
||||
for invsrc in h.inventory_sources.all():
|
||||
all_inventory_sources.add(invsrc)
|
||||
all_orgs.add(h.inventory.organization)
|
||||
active_templates = dict(error=set(),
|
||||
success=set(),
|
||||
any=set())
|
||||
for invsrc in all_inventory_sources:
|
||||
notifications_dict = invsrc.notification_templates
|
||||
for notification_type in active_templates.keys():
|
||||
for templ in notifications_dict[notification_type]:
|
||||
active_templates[notification_type].add(templ)
|
||||
base_notification_templates = NotificationTemplate.objects
|
||||
for org in all_orgs:
|
||||
for templ in base_notification_templates.filter(organization_notification_templates_for_errors=org):
|
||||
active_templates['error'].add(templ)
|
||||
for templ in base_notification_templates.filter(organization_notification_templates_for_success=org):
|
||||
active_templates['success'].add(templ)
|
||||
for templ in base_notification_templates.filter(organization_notification_templates_for_any=org):
|
||||
active_templates['any'].add(templ)
|
||||
active_templates['error'] = list(active_templates['error'])
|
||||
active_templates['any'] = list(active_templates['any'])
|
||||
active_templates['success'] = list(active_templates['success'])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -114,6 +114,8 @@ class TaskManager():
|
||||
dag = WorkflowDAG(workflow_job)
|
||||
spawn_nodes = dag.bfs_nodes_to_run()
|
||||
for spawn_node in spawn_nodes:
|
||||
if spawn_node.unified_job_template is None:
|
||||
continue
|
||||
kv = spawn_node.get_job_kwargs()
|
||||
job = spawn_node.unified_job_template.create_unified_job(**kv)
|
||||
spawn_node.job = job
|
||||
|
||||
@@ -67,6 +67,8 @@ class WorkflowDAG(SimpleDAG):
|
||||
obj = n['node_object']
|
||||
job = obj.job
|
||||
|
||||
if obj.unified_job_template is None:
|
||||
continue
|
||||
if not job:
|
||||
return False
|
||||
# Job is about to run or is running. Hold our horses and wait for
|
||||
|
||||
@@ -120,13 +120,11 @@ class TestWorkflowJobAccess:
|
||||
access = WorkflowJobTemplateAccess(rando, save_messages=True)
|
||||
assert not access.can_copy(wfjt)
|
||||
warnings = access.messages
|
||||
assert 1 in warnings
|
||||
assert 'inventory' in warnings[1]
|
||||
assert 'inventories_unable_to_copy' in warnings
|
||||
|
||||
def test_workflow_copy_warnings_jt(self, wfjt, rando, job_template):
|
||||
wfjt.workflow_job_template_nodes.create(unified_job_template=job_template)
|
||||
access = WorkflowJobTemplateAccess(rando, save_messages=True)
|
||||
assert not access.can_copy(wfjt)
|
||||
warnings = access.messages
|
||||
assert 1 in warnings
|
||||
assert 'unified_job_template' in warnings[1]
|
||||
assert 'templates_unable_to_copy' in warnings
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
# AWX
|
||||
from awx.main.scheduler.dag_simple import SimpleDAG
|
||||
from awx.main.scheduler.dag_workflow import WorkflowDAG
|
||||
from awx.main.models import Job
|
||||
from awx.main.models import Job, JobTemplate
|
||||
from awx.main.models.workflow import WorkflowJobNode
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ def factory_node():
|
||||
if status:
|
||||
j = Job(status=status)
|
||||
wfn.job = j
|
||||
wfn.unified_job_template = JobTemplate(name='JT{}'.format(id))
|
||||
return wfn
|
||||
return fn
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ DATABASES = {
|
||||
# timezone as the operating system.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
TIME_ZONE = 'America/New_York'
|
||||
TIME_ZONE = None
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
@@ -163,6 +163,12 @@ MAX_EVENT_RES_DATA = 700000
|
||||
# Note: This setting may be overridden by database settings.
|
||||
EVENT_STDOUT_MAX_BYTES_DISPLAY = 1024
|
||||
|
||||
# Disallow sending session cookies over insecure connections
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
# Disallow sending csrf cookies over insecure connections
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS = ( # NOQA
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.core.context_processors.debug',
|
||||
|
||||
@@ -24,11 +24,11 @@ ALLOWED_HOSTS = ['*']
|
||||
mimetypes.add_type("image/svg+xml", ".svg", True)
|
||||
mimetypes.add_type("image/svg+xml", ".svgz", True)
|
||||
|
||||
MONGO_HOST = '127.0.0.1'
|
||||
MONGO_PORT = 27017
|
||||
MONGO_USERNAME = None
|
||||
MONGO_PASSWORD = None
|
||||
MONGO_DB = 'system_tracking_dev'
|
||||
# Disallow sending session cookies over insecure connections
|
||||
SESSION_COOKIE_SECURE = False
|
||||
|
||||
# Disallow sending csrf cookies over insecure connections
|
||||
CSRF_COOKIE_SECURE = False
|
||||
|
||||
# Override django.template.loaders.cached.Loader in defaults.py
|
||||
TEMPLATE_LOADERS = (
|
||||
|
||||
@@ -114,7 +114,7 @@ SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
|
||||
# timezone as the operating system.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
TIME_ZONE = 'America/New_York'
|
||||
TIME_ZONE = None
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
|
||||
@@ -71,7 +71,7 @@ SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
|
||||
# timezone as the operating system.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
TIME_ZONE = 'America/New_York'
|
||||
TIME_ZONE = None
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
|
||||
@@ -44,11 +44,10 @@
|
||||
color: @list-header-txt;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 25px;
|
||||
min-height: 45px;
|
||||
word-break: break-all;
|
||||
max-width: 90%;
|
||||
word-wrap: break-word;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.Form-secondaryTitle{
|
||||
|
||||
@@ -15,7 +15,8 @@ export default ['templateUrl', '$state',
|
||||
usersDataset: '=',
|
||||
teamsDataset: '=',
|
||||
resourceData: '=',
|
||||
withoutTeamPermissions: '@'
|
||||
withoutTeamPermissions: '@',
|
||||
title: '@'
|
||||
},
|
||||
controller: controller,
|
||||
templateUrl: templateUrl('access/add-rbac-resource/rbac-resource'),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="List-titleText ng-binding">
|
||||
{{ object.name || object.username }}
|
||||
<div class="List-titleLockup"></div>
|
||||
Add Permissions
|
||||
{{ title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="Form-exitHolder">
|
||||
|
||||
@@ -11,7 +11,8 @@ export default ['templateUrl',
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
resolve: "="
|
||||
resolve: "=",
|
||||
title: "@",
|
||||
},
|
||||
controller: controller,
|
||||
templateUrl: templateUrl('access/add-rbac-user-team/rbac-user-team'),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="List-titleText ng-binding">
|
||||
{{ owner.name || owner.username }}
|
||||
<div class="List-titleLockup"></div>
|
||||
Add Permissions
|
||||
{{ title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="Form-exitHolder">
|
||||
|
||||
@@ -51,6 +51,10 @@
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.AddPermissions-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.AddPermissions-list .List-searchRow {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,14 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL
|
||||
list.fields.first_name.columnClass = 'col-md-3 col-sm-3 hidden-xs';
|
||||
list.fields.last_name.columnClass = 'col-md-3 col-sm-3 hidden-xs';
|
||||
break;
|
||||
case 'Teams':
|
||||
list.fields = {
|
||||
name: list.fields.name,
|
||||
organization: list.fields.organization,
|
||||
};
|
||||
list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11';
|
||||
list.fields.organization.columnClass = 'col-md-5 col-sm-5 hidden-xs';
|
||||
break;
|
||||
default:
|
||||
list.fields = {
|
||||
name: list.fields.name,
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
reset: 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY'
|
||||
},
|
||||
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET: {
|
||||
type: 'text',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
reset: 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET'
|
||||
},
|
||||
SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP: {
|
||||
@@ -38,8 +39,8 @@
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
|
||||
reset: 'SOCIAL_AUTH_GITHUB_ORG_KEY'
|
||||
},
|
||||
SOCIAL_AUTH_GITHUB_ORG_SECRET: {
|
||||
type: 'text',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
reset: 'SOCIAL_AUTH_GITHUB_ORG_SECRET'
|
||||
},
|
||||
SOCIAL_AUTH_GITHUB_ORG_NAME: {
|
||||
@@ -28,8 +29,8 @@ export default ['i18n', function(i18n) {
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
|
||||
reset: 'SOCIAL_AUTH_GITHUB_TEAM_KEY'
|
||||
},
|
||||
SOCIAL_AUTH_GITHUB_TEAM_SECRET: {
|
||||
type: 'text',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
reset: 'SOCIAL_AUTH_GITHUB_TEAM_SECRET'
|
||||
},
|
||||
SOCIAL_AUTH_GITHUB_TEAM_ID: {
|
||||
@@ -28,8 +29,8 @@ export default ['i18n', function(i18n) {
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
|
||||
reset: 'SOCIAL_AUTH_GITHUB_KEY'
|
||||
},
|
||||
SOCIAL_AUTH_GITHUB_SECRET: {
|
||||
type: 'text',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
reset: 'SOCIAL_AUTH_GITHUB_SECRET'
|
||||
}
|
||||
},
|
||||
@@ -24,8 +25,8 @@ export default ['i18n', function(i18n) {
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
|
||||
reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY'
|
||||
},
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: {
|
||||
type: 'text',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET'
|
||||
},
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: {
|
||||
@@ -36,8 +37,8 @@ export default ['i18n', function(i18n) {
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -21,7 +21,8 @@ export default ['i18n', function(i18n) {
|
||||
reset: 'AUTH_LDAP_BIND_DN'
|
||||
},
|
||||
AUTH_LDAP_BIND_PASSWORD: {
|
||||
type: 'password'
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
},
|
||||
AUTH_LDAP_USER_SEARCH: {
|
||||
type: 'textarea',
|
||||
@@ -84,8 +85,8 @@ export default ['i18n', function(i18n) {
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -21,7 +21,8 @@ export default ['i18n', function(i18n) {
|
||||
reset: 'RADIUS_PORT'
|
||||
},
|
||||
RADIUS_SECRET: {
|
||||
type: 'text',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
reset: 'RADIUS_SECRET'
|
||||
}
|
||||
},
|
||||
@@ -29,8 +30,8 @@ export default ['i18n', function(i18n) {
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -20,7 +20,8 @@ export default ['i18n', function(i18n) {
|
||||
reset: 'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT'
|
||||
},
|
||||
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: {
|
||||
type: 'text',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
reset: 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY'
|
||||
},
|
||||
SOCIAL_AUTH_SAML_ORG_INFO: {
|
||||
@@ -56,8 +57,8 @@ export default ['i18n', function(i18n) {
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -12,6 +12,19 @@
|
||||
float: right
|
||||
}
|
||||
|
||||
.Form-resetAll {
|
||||
border: none;
|
||||
padding: 0;
|
||||
background-color: @white;
|
||||
margin-right: auto;
|
||||
color: @default-link;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
color: @default-link-hov;
|
||||
}
|
||||
}
|
||||
|
||||
.Form-tab {
|
||||
min-width: 77px;
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
reset: 'LOG_AGGREGATOR_USERNAME'
|
||||
},
|
||||
LOG_AGGREGATOR_PASSWORD: {
|
||||
type: 'text',
|
||||
type: 'sensitive',
|
||||
hasShowInputButton: true,
|
||||
reset: 'LOG_AGGREGATOR_PASSWORD'
|
||||
},
|
||||
LOG_AGGREGATOR_LOGGERS: {
|
||||
@@ -48,8 +49,8 @@
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -26,8 +26,8 @@ export default ['i18n', function(i18n) {
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -32,8 +32,8 @@ export default ['i18n', function(i18n) {
|
||||
buttons: {
|
||||
reset: {
|
||||
ngClick: 'vm.resetAllConfirm()',
|
||||
label: i18n._('Reset All'),
|
||||
class: 'Form-button--left Form-cancelButton'
|
||||
label: i18n._('Revert all to default'),
|
||||
class: 'Form-resetAll'
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'vm.formCancel()',
|
||||
|
||||
@@ -283,7 +283,7 @@ CredentialsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location',
|
||||
export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
|
||||
$stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt,
|
||||
GetBasePath, GetChoices, KindChange, Empty, OwnerChange, FormSave, Wait,
|
||||
$state, CreateSelect2, Authorization) {
|
||||
$state, CreateSelect2, Authorization, i18n) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@@ -336,13 +336,14 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
|
||||
});
|
||||
}
|
||||
|
||||
// if the credential is assigned to an organization, allow permission delegation
|
||||
// do NOT use $scope.organization in a view directive to determine if a credential is associated with an org
|
||||
// @todo why not? ^ and what is this type check for a number doing - should this be a type check for undefined?
|
||||
$scope.disablePermissionAssignment = typeof($scope.organization) === 'number' ? false : true;
|
||||
if ($scope.disablePermissionAssignment) {
|
||||
$scope.permissionsTooltip = 'Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.';
|
||||
}
|
||||
$scope.$watch('organization', function(val) {
|
||||
if (val === undefined) {
|
||||
$scope.permissionsTooltip = i18n._('Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.');
|
||||
} else {
|
||||
$scope.permissionsTooltip = '';
|
||||
}
|
||||
});
|
||||
|
||||
setAskCheckboxes();
|
||||
KindChange({
|
||||
scope: $scope,
|
||||
@@ -613,5 +614,5 @@ CredentialsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location',
|
||||
'$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert',
|
||||
'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices',
|
||||
'KindChange', 'Empty', 'OwnerChange',
|
||||
'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization'
|
||||
'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n',
|
||||
];
|
||||
|
||||
@@ -420,9 +420,12 @@ export default
|
||||
|
||||
related: {
|
||||
permissions: {
|
||||
disabled: 'disablePermissionAssignment',
|
||||
disabled: '(organization === undefined ? true : false)',
|
||||
// Do not transition the state if organization is undefined
|
||||
ngClick: `(organization === undefined ? true : false)||$state.go('credentials.edit.permissions')`,
|
||||
awToolTip: '{{permissionsTooltip}}',
|
||||
dataTipWatch: 'permissionsTooltip',
|
||||
awToolTipTabEnabledInEditMode: true,
|
||||
dataPlacement: 'top',
|
||||
basePath: 'api/v1/credentials/{{$stateParams.credential_id}}/access_list/',
|
||||
search: {
|
||||
|
||||
@@ -103,7 +103,7 @@ angular.module('InventoryFormDefinition', ['ScanJobsListDefinition'])
|
||||
add: {
|
||||
label: i18n._('Add'),
|
||||
ngClick: "$state.go('.add')",
|
||||
awToolTip: 'Add a permission',
|
||||
awToolTip: i18n._('Add a permission'),
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ ADD',
|
||||
ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
|
||||
@@ -68,7 +68,7 @@ export default
|
||||
searchType: 'select',
|
||||
actions: {
|
||||
add: {
|
||||
ngClick: "addPermission",
|
||||
ngClick: "$state.go('.add')",
|
||||
label: i18n._('Add'),
|
||||
awToolTip: i18n._('Add a permission'),
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
|
||||
@@ -121,6 +121,7 @@ export default
|
||||
organizations: {
|
||||
awToolTip: i18n._('Please save before assigning to organizations'),
|
||||
basePath: 'api/v1/users/{{$stateParams.user_id}}/organizations',
|
||||
emptyListText: i18n._('Please add user to an Organization.'),
|
||||
search: {
|
||||
page_size: '10'
|
||||
},
|
||||
|
||||
@@ -122,7 +122,7 @@ export default
|
||||
add: {
|
||||
ngClick: "$state.go('.add')",
|
||||
label: i18n._('Add'),
|
||||
awToolTip: 'Add a permission',
|
||||
awToolTip: i18n._('Add a permission'),
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ '+ i18n._('ADD'),
|
||||
ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'
|
||||
|
||||
@@ -66,7 +66,19 @@ angular.module('inventory', [
|
||||
],
|
||||
ParentObject: ['groupData', function(groupData) {
|
||||
return groupData;
|
||||
}]
|
||||
}],
|
||||
UnifiedJobsOptions: ['Rest', 'GetBasePath', '$stateParams', '$q',
|
||||
function(Rest, GetBasePath, $stateParams, $q) {
|
||||
Rest.setUrl(GetBasePath('unified_jobs'));
|
||||
var val = $q.defer();
|
||||
Rest.options()
|
||||
.then(function(data) {
|
||||
val.resolve(data.data);
|
||||
}, function(data) {
|
||||
val.reject(data);
|
||||
});
|
||||
return val.promise;
|
||||
}]
|
||||
},
|
||||
views: {
|
||||
// clear form template when views render in this substate
|
||||
|
||||
@@ -120,7 +120,7 @@ export default
|
||||
Rest.post(job_launch_data)
|
||||
.success(function(data) {
|
||||
Wait('stop');
|
||||
var job = data.job || data.system_job || data.project_update || data.inventory_update || data.ad_hoc_command || data.workflow_job;
|
||||
var job = data.job || data.system_job || data.project_update || data.inventory_update || data.ad_hoc_command;
|
||||
if($rootScope.portalMode===false && Empty(data.system_job) || (base === 'home')){
|
||||
// use $state.go with reload: true option to re-instantiate sockets in
|
||||
|
||||
@@ -131,7 +131,8 @@ export default
|
||||
if(_.has(data, 'job')) {
|
||||
goToJobDetails('jobDetail');
|
||||
}
|
||||
else if(_.has(data, 'workflow_job')) {
|
||||
else if(data.type && data.type === 'workflow_job') {
|
||||
job = data.id;
|
||||
goToJobDetails('workflowResults');
|
||||
}
|
||||
else if(_.has(data, 'ad_hoc_command')) {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
export default
|
||||
angular.module('AllJobsDefinition', ['sanitizeFilter', 'capitalizeFilter'])
|
||||
.value( 'AllJobsList', {
|
||||
.factory('AllJobsList', ['i18n', function(i18n) {
|
||||
return {
|
||||
|
||||
name: 'jobs',
|
||||
basePath: 'unified_jobs',
|
||||
@@ -16,6 +17,7 @@ export default
|
||||
index: false,
|
||||
hover: true,
|
||||
well: false,
|
||||
emptyListText: i18n._('No jobs have yet run.'),
|
||||
title: false,
|
||||
|
||||
fields: {
|
||||
@@ -115,4 +117,5 @@ export default
|
||||
ngShow: "(job.status !== 'running' && job.status !== 'waiting' && job.status !== 'pending') && job.summary_fields.user_capabilities.delete"
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
@@ -39,18 +39,18 @@ export default
|
||||
columnClass: "col-lg-4 col-md-4 col-sm-5 col-xs-7 List-staticColumnAdjacent",
|
||||
modalColumnClass: 'col-md-8'
|
||||
},
|
||||
scm_revision: {
|
||||
label: i18n._('Revision'),
|
||||
excludeModal: true,
|
||||
columnClass: 'col-lg-4 col-md-2 col-sm-3 hidden-xs',
|
||||
class: 'List-staticColumnAdjacent--monospace'
|
||||
},
|
||||
scm_type: {
|
||||
label: i18n._('Type'),
|
||||
ngBind: 'project.type_label',
|
||||
excludeModal: true,
|
||||
columnClass: 'col-lg-2 col-md-2 col-sm-3 hidden-xs'
|
||||
},
|
||||
scm_revision: {
|
||||
label: i18n._('Revision'),
|
||||
excludeModal: true,
|
||||
columnClass: 'col-lg-4 col-md-2 col-sm-3 hidden-xs',
|
||||
class: 'List-staticColumnAdjacent--monospace'
|
||||
},
|
||||
last_updated: {
|
||||
label: i18n._('Last Updated'),
|
||||
filter: "longDate",
|
||||
@@ -61,6 +61,14 @@ export default
|
||||
},
|
||||
|
||||
actions: {
|
||||
refresh: {
|
||||
mode: 'all',
|
||||
awToolTip: i18n._("Refresh the page"),
|
||||
ngClick: "refresh()",
|
||||
ngShow: "socketStatus === 'error'",
|
||||
actionClass: 'btn List-buttonDefault',
|
||||
buttonContent: i18n._('REFRESH')
|
||||
},
|
||||
add: {
|
||||
mode: 'all', // One of: edit, select, all
|
||||
ngClick: 'addProject()',
|
||||
@@ -68,14 +76,6 @@ export default
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ ' + i18n._('ADD'),
|
||||
ngShow: "canAdd"
|
||||
},
|
||||
refresh: {
|
||||
mode: 'all',
|
||||
awToolTip: i18n._("Refresh the page"),
|
||||
ngClick: "refresh()",
|
||||
ngShow: "socketStatus == 'error'",
|
||||
actionClass: 'btn List-buttonDefault',
|
||||
buttonContent: i18n._('REFRESH')
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -323,6 +323,7 @@ export default ['i18n', function(i18n) {
|
||||
headers: {
|
||||
label: i18n._('HTTP Headers'),
|
||||
type: 'textarea',
|
||||
name: 'headers',
|
||||
rows: 5,
|
||||
'class': 'Form-formGroup--fullWidth',
|
||||
awRequiredWhen: {
|
||||
|
||||
@@ -127,6 +127,16 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
|
||||
});
|
||||
};
|
||||
|
||||
function isDeletedOrganizationBeingEdited(deleted_organization_id, editing_organization_id) {
|
||||
if (editing_organization_id === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (deleted_organization_id === editing_organization_id) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$scope.deleteOrganization = function(id, name) {
|
||||
|
||||
var action = function() {
|
||||
@@ -137,7 +147,11 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
|
||||
Rest.destroy()
|
||||
.success(function() {
|
||||
Wait('stop');
|
||||
$state.reload('organizations');
|
||||
if (isDeletedOrganizationBeingEdited(id, parseInt($stateParams.organization_id)) === true) {
|
||||
$state.go('^', null, { reload: true });
|
||||
} else {
|
||||
$state.reload('organizations');
|
||||
}
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
|
||||
@@ -285,9 +285,7 @@ export default
|
||||
}
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'job',
|
||||
activityStreamId: 'id'
|
||||
activityStream: false,
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'jobs',
|
||||
|
||||
@@ -198,6 +198,8 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
|
||||
msg += 'Please contact your system administrator.';
|
||||
}
|
||||
Alert(defaultMsg.hdr, msg);
|
||||
} else if (status === 409) {
|
||||
Alert('Conflict', data.conflict || "Resource currently in use.");
|
||||
} else if (status === 410) {
|
||||
Alert('Deleted Object', 'The requested object was previously deleted and can no longer be accessed.');
|
||||
} else if ((status === 'Token is expired') || (status === 401 && data.detail && data.detail === 'Token is expired') ||
|
||||
|
||||
@@ -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
|
||||
@@ -620,12 +620,10 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
|
||||
|
||||
if (attrs.tipWatch) {
|
||||
// Add dataTipWatch: 'variable_name'
|
||||
scope.$watch(attrs.tipWatch, function(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
// Where did fixTitle come from?:
|
||||
// http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click
|
||||
$(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle');
|
||||
}
|
||||
scope.$watch(attrs.tipWatch, function(newVal) {
|
||||
// Where did fixTitle come from?:
|
||||
// http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click
|
||||
$(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1479,7 +1479,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
html += `<div id="${itm}_tab" `+
|
||||
`class="Form-tab" `;
|
||||
html += (this.form.related[itm].ngClick) ? `ng-click="` + this.form.related[itm].ngClick + `" ` : `ng-click="$state.go('${this.form.stateTree}.edit.${itm}')" `;
|
||||
if (collection.awToolTip){
|
||||
if (collection.awToolTip && collection.awToolTipTabEnabledInEditMode === true) {
|
||||
html += `aw-tool-tip="${collection.awToolTip}" ` +
|
||||
`aw-tip-placement="${collection.dataPlacement}" ` +
|
||||
`data-tip-watch="${collection.dataTipWatch}" `;
|
||||
@@ -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 += `
|
||||
|
||||
@@ -18,7 +18,7 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q
|
||||
return;
|
||||
}
|
||||
path = GetBasePath($scope.basePath) || $scope.basePath;
|
||||
queryset = _.merge($stateParams[`${$scope.iterator}_search`], { page: page });
|
||||
queryset = _.merge($stateParams[`${$scope.iterator}_search`], { page: page.toString() });
|
||||
$state.go('.', {
|
||||
[$scope.iterator + '_search']: queryset
|
||||
});
|
||||
|
||||
@@ -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,120 @@ 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){
|
||||
item = item.replace(/"|'/g, "");
|
||||
concated += `${key}=${item}&`;
|
||||
});
|
||||
return concated;
|
||||
}
|
||||
else {
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -27,7 +28,20 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
|
||||
// setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value
|
||||
return defaults[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaults[key] !== null;
|
||||
});
|
||||
return _(stripped).map(qs.decodeParam).flatten().value();
|
||||
let strippedCopy = _.cloneDeep(stripped);
|
||||
if(_.keys(_.pick(defaults, _.keys(strippedCopy))).length > 0){
|
||||
for (var key in strippedCopy) {
|
||||
if (strippedCopy.hasOwnProperty(key)) {
|
||||
let value = strippedCopy[key];
|
||||
if(_.isArray(value)){
|
||||
let index = _.indexOf(value, defaults[key]);
|
||||
value = value.splice(index, 1)[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
stripped = strippedCopy;
|
||||
}
|
||||
return _(strippedCopy).map(qs.decodeParam).flatten().value();
|
||||
}
|
||||
|
||||
// searchable relationships
|
||||
@@ -38,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;
|
||||
};
|
||||
@@ -56,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];
|
||||
@@ -79,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);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}];
|
||||
@@ -267,7 +267,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
|
||||
},
|
||||
views: {
|
||||
[`modal@${formStateDefinition.name}`]: {
|
||||
template: `<add-rbac-user-team resolve="$resolve"></add-rbac-user-team>`
|
||||
template: `<add-rbac-user-team resolve="$resolve" title="Add Permissions"></add-rbac-user-team>`
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
@@ -332,7 +332,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
|
||||
},
|
||||
views: {
|
||||
[`modal@${formStateDefinition.name}`]: {
|
||||
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" teams-dataset="$resolve.teamsDataset" selected="allSelected" resource-data="$resolve.resourceData"></add-rbac-resource>`
|
||||
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" teams-dataset="$resolve.teamsDataset" selected="allSelected" resource-data="$resolve.resourceData" title="Add Users / Teams"></add-rbac-resource>`
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
@@ -501,7 +501,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
|
||||
},
|
||||
views: {
|
||||
[`modal@${formStateDefinition.name}`]: {
|
||||
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" selected="allSelected" resource-data="$resolve.resourceData" without-team-permissions="true"></add-rbac-resource>`
|
||||
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" selected="allSelected" resource-data="$resolve.resourceData" without-team-permissions="true" title="Add Users"></add-rbac-resource>`
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
|
||||
@@ -287,20 +287,18 @@
|
||||
if (data.related &&
|
||||
data.related.callback) {
|
||||
Alert('Callback URL',
|
||||
`<div>
|
||||
<p>Host callbacks are enabled for this template. The callback URL is:</p>
|
||||
<p style=\"padding: 10px 0;\">
|
||||
<strong>
|
||||
${$scope.callback_server_path}
|
||||
${data.related.callback}
|
||||
</string>
|
||||
</p>
|
||||
<p>The host configuration key is:
|
||||
<strong>
|
||||
${$filter('sanitize')(data.host_config_key)}
|
||||
</string>
|
||||
</p>
|
||||
</div>`,
|
||||
`Host callbacks are enabled for this template. The callback URL is:
|
||||
<p style=\"padding: 10px 0;\">
|
||||
<strong>
|
||||
${$scope.callback_server_path}
|
||||
${data.related.callback}
|
||||
</strong>
|
||||
</p>
|
||||
<p class="break">The host configuration key is:
|
||||
<strong>
|
||||
${$filter('sanitize')(data.host_config_key)}
|
||||
</strong>
|
||||
</p>`,
|
||||
'alert-danger', saveCompleted, null, null,
|
||||
null, true);
|
||||
}
|
||||
|
||||
@@ -423,23 +423,20 @@ export default
|
||||
if (data.related &&
|
||||
data.related.callback) {
|
||||
Alert('Callback URL',
|
||||
`
|
||||
<div>
|
||||
<p>Host callbacks are enabled for this template. The callback URL is:</p>
|
||||
<p style=\"padding: 10px 0;\">
|
||||
<strong>
|
||||
${$scope.callback_server_path}
|
||||
${data.related.callback}
|
||||
</string>
|
||||
</p>
|
||||
<p>The host configuration key is:
|
||||
<strong>
|
||||
${$filter('sanitize')(data.host_config_key)}
|
||||
</string>
|
||||
</p>
|
||||
</div>
|
||||
`Host callbacks are enabled for this template. The callback URL is:
|
||||
<p style=\"padding: 10px 0;\">
|
||||
<strong>
|
||||
${$scope.callback_server_path}
|
||||
${data.related.callback}
|
||||
</strong>
|
||||
</p>
|
||||
<p class="break">The host configuration key is:
|
||||
<strong>
|
||||
${$filter('sanitize')(data.host_config_key)}
|
||||
</strong>
|
||||
</p>
|
||||
`,
|
||||
'alert-info', saveCompleted, null, null,
|
||||
'alert-danger', saveCompleted, null, null,
|
||||
null, true);
|
||||
}
|
||||
var orgDefer = $q.defer();
|
||||
|
||||
@@ -131,7 +131,7 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
|
||||
handleSuccessfulDelete();
|
||||
}, function (data) {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
ProcessErrors($scope, data, data.status, null, { hdr: 'Error!',
|
||||
msg: 'Call to delete workflow job template failed. DELETE returned status: ' + status });
|
||||
});
|
||||
}
|
||||
@@ -141,8 +141,8 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
|
||||
handleSuccessfulDelete();
|
||||
}, function (data) {
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to delete job template failed. DELETE returned status: ' + status });
|
||||
ProcessErrors($scope, data, data.status, null, { hdr: 'Error!',
|
||||
msg: 'Call to delete job template failed. DELETE returned status: ' + data.status });
|
||||
});
|
||||
}
|
||||
else {
|
||||
@@ -220,7 +220,7 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
|
||||
.then(function(result) {
|
||||
|
||||
if(result.data.can_copy) {
|
||||
if(!result.data.warnings || _.isEmpty(result.data.warnings)) {
|
||||
if(result.data.can_copy_without_user_input) {
|
||||
// Go ahead and copy the workflow - the user has full priveleges on all the resources
|
||||
TemplateCopyService.copyWorkflow(template.id)
|
||||
.then(function(result) {
|
||||
@@ -235,18 +235,40 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
|
||||
|
||||
let bodyHtml = `
|
||||
<div class="Prompt-bodyQuery">
|
||||
You may not have access to all resources used by this workflow. Resources that you don\'t have access to will not be copied and may result in an incomplete workflow.
|
||||
You do not have access to all resources used by this workflow. Resources that you don\'t have access to will not be copied and will result in an incomplete workflow.
|
||||
</div>
|
||||
<div class="Prompt-bodyTarget">`;
|
||||
|
||||
// Go and grab all of the warning strings
|
||||
_.forOwn(result.data.warnings, function(warning) {
|
||||
if(warning) {
|
||||
_.forOwn(warning, function(warningString) {
|
||||
bodyHtml += '<div>' + warningString + '</div>';
|
||||
});
|
||||
}
|
||||
} );
|
||||
// List the unified job templates user can not access
|
||||
if (result.data.templates_unable_to_copy.length > 0) {
|
||||
bodyHtml += '<div>Unified Job Templates that can not be copied<ul>';
|
||||
_.forOwn(result.data.templates_unable_to_copy, function(ujt) {
|
||||
if(ujt) {
|
||||
bodyHtml += '<li>' + ujt + '</li>';
|
||||
}
|
||||
});
|
||||
bodyHtml += '</ul></div>';
|
||||
}
|
||||
// List the prompted inventories user can not access
|
||||
if (result.data.inventories_unable_to_copy.length > 0) {
|
||||
bodyHtml += '<div>Node prompted inventories that can not be copied<ul>';
|
||||
_.forOwn(result.data.inventories_unable_to_copy, function(inv) {
|
||||
if(inv) {
|
||||
bodyHtml += '<li>' + inv + '</li>';
|
||||
}
|
||||
});
|
||||
bodyHtml += '</ul></div>';
|
||||
}
|
||||
// List the prompted credentials user can not access
|
||||
if (result.data.credentials_unable_to_copy.length > 0) {
|
||||
bodyHtml += '<div>Node prompted credentials that can not be copied<ul>';
|
||||
_.forOwn(result.data.credentials_unable_to_copy, function(cred) {
|
||||
if(cred) {
|
||||
bodyHtml += '<li>' + cred + '</li>';
|
||||
}
|
||||
});
|
||||
bodyHtml += '</ul></div>';
|
||||
}
|
||||
|
||||
bodyHtml += '</div>';
|
||||
|
||||
|
||||
@@ -174,7 +174,20 @@ export default [ '$state','moment',
|
||||
|
||||
nodeEnter.each(function(d) {
|
||||
let thisNode = d3.select(this);
|
||||
if(d.isStartNode) {
|
||||
if(d.isStartNode && scope.mode === 'details') {
|
||||
// Overwrite the default root height and width and replace it with a small blue square
|
||||
rootW = 25;
|
||||
rootH = 25;
|
||||
thisNode.append("rect")
|
||||
.attr("width", rootW)
|
||||
.attr("height", rootH)
|
||||
.attr("y", 10)
|
||||
.attr("rx", 5)
|
||||
.attr("ry", 5)
|
||||
.attr("fill", "#337ab7")
|
||||
.attr("class", "WorkflowChart-rootNode");
|
||||
}
|
||||
else if(d.isStartNode && scope.mode !== 'details') {
|
||||
thisNode.append("rect")
|
||||
.attr("width", rootW)
|
||||
.attr("height", rootH)
|
||||
@@ -190,7 +203,6 @@ export default [ '$state','moment',
|
||||
.attr("dy", ".35em")
|
||||
.attr("class", "WorkflowChart-startText")
|
||||
.text(function () { return "START"; })
|
||||
.attr("display", function() { return scope.mode === 'details' ? 'none' : null;})
|
||||
.call(add_node);
|
||||
}
|
||||
else {
|
||||
@@ -200,15 +212,15 @@ export default [ '$state','moment',
|
||||
.attr("rx", 5)
|
||||
.attr("ry", 5)
|
||||
.attr('stroke', function(d) {
|
||||
if(d.edgeType) {
|
||||
if(d.edgeType === "failure") {
|
||||
return "#d9534f";
|
||||
}
|
||||
else if(d.edgeType === "success") {
|
||||
if(d.job && d.job.status) {
|
||||
if(d.job.status === "successful"){
|
||||
return "#5cb85c";
|
||||
}
|
||||
else if(d.edgeType === "always"){
|
||||
return "#337ab7";
|
||||
else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") {
|
||||
return "#d9534f";
|
||||
}
|
||||
else {
|
||||
return "#D7D7D7";
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -593,15 +605,15 @@ export default [ '$state','moment',
|
||||
|
||||
t.selectAll(".rect")
|
||||
.attr('stroke', function(d) {
|
||||
if(d.edgeType) {
|
||||
if(d.edgeType === "failure") {
|
||||
return "#d9534f";
|
||||
}
|
||||
else if(d.edgeType === "success") {
|
||||
if(d.job && d.job.status) {
|
||||
if(d.job.status === "successful"){
|
||||
return "#5cb85c";
|
||||
}
|
||||
else if(d.edgeType === "always"){
|
||||
return "#337ab7";
|
||||
else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") {
|
||||
return "#d9534f";
|
||||
}
|
||||
else {
|
||||
return "#D7D7D7";
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -126,7 +126,7 @@ export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErr
|
||||
},
|
||||
relaunchJob: function(scope) {
|
||||
InitiatePlaybookRun({ scope: scope, id: scope.workflow.id,
|
||||
relaunch: true, job_type: 'workflow_job_template' });
|
||||
relaunch: true, job_type: 'workflow_job' });
|
||||
}
|
||||
};
|
||||
return val;
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<div id="alert-modal-msg" class="alert" ng-bind-html="alertBody"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" ng-hide="disableButtons" data-target="#form-modal" data-dismiss="modal" id="alert_ok_btn" class="btn btn-primary">{% trans 'OK' %}</a>
|
||||
<a href="#" ng-hide="disableButtons" data-target="#form-modal" data-dismiss="modal" id="alert_ok_btn" class="btn btn-default">{% trans 'OK' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- modal-content -->
|
||||
|
||||
@@ -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\'"]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user