mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 19:10:07 -03:30
Merge branch 'release_3.0.0' of github.com:ansible/ansible-tower into JobDetailHostEvents
This commit is contained in:
commit
7f9174f4ea
@ -23,6 +23,7 @@ from django.db import models
|
||||
# from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.text import capfirst
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@ -1766,7 +1767,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||
notification_templates_any = reverse('api:job_template_notification_templates_any_list', args=(obj.pk,)),
|
||||
notification_templates_success = reverse('api:job_template_notification_templates_success_list', args=(obj.pk,)),
|
||||
notification_templates_error = reverse('api:job_template_notification_templates_error_list', args=(obj.pk,)),
|
||||
access_list = reverse('api:job_template_access_list', args=(obj.pk,)),
|
||||
access_list = reverse('api:job_template_access_list', args=(obj.pk,)),
|
||||
survey_spec = reverse('api:job_template_survey_spec', args=(obj.pk,)),
|
||||
labels = reverse('api:job_template_label_list', args=(obj.pk,)),
|
||||
roles = reverse('api:job_template_roles_list', args=(obj.pk,)),
|
||||
@ -1783,19 +1784,21 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||
if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec):
|
||||
d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description'])
|
||||
request = self.context.get('request', None)
|
||||
if request is not None and request.user is not None and obj.inventory is not None and obj.project is not None:
|
||||
d['can_copy'] = request.user.can_access(JobTemplate, 'add',
|
||||
{'inventory': obj.inventory.pk,
|
||||
'project': obj.project.pk})
|
||||
d['can_edit'] = request.user.can_access(JobTemplate, 'change', obj,
|
||||
{'inventory': obj.inventory.pk,
|
||||
'project': obj.project.pk})
|
||||
elif request is not None and request.user is not None and request.user.is_superuser:
|
||||
d['can_copy'] = True
|
||||
d['can_edit'] = True
|
||||
else:
|
||||
|
||||
# Check for conditions that would create a validation error if coppied
|
||||
validation_errors, resources_needed_to_start = obj.resource_validation_data()
|
||||
|
||||
if request is None or request.user is None:
|
||||
d['can_copy'] = False
|
||||
d['can_edit'] = False
|
||||
elif request.user.is_superuser:
|
||||
d['can_copy'] = not validation_errors
|
||||
d['can_edit'] = True
|
||||
else:
|
||||
jt_data = model_to_dict(obj)
|
||||
d['can_copy'] = (not validation_errors) and request.user.can_access(JobTemplate, 'add', jt_data)
|
||||
d['can_edit'] = request.user.can_access(JobTemplate, 'change', obj, jt_data)
|
||||
|
||||
d['recent_jobs'] = self._recent_jobs(obj)
|
||||
return d
|
||||
|
||||
@ -2259,12 +2262,14 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
obj = self.context.get('obj')
|
||||
data = self.context.get('data')
|
||||
|
||||
for field in obj.resources_needed_to_start:
|
||||
if not (field in attrs and obj._ask_for_vars_dict().get(field, False)):
|
||||
errors[field] = "Job Template '%s' is missing or undefined." % field
|
||||
|
||||
if (not obj.ask_credential_on_launch) or (not attrs.get('credential', None)):
|
||||
credential = obj.credential
|
||||
else:
|
||||
credential = attrs.get('credential', None)
|
||||
if not credential:
|
||||
errors['credential'] = 'Credential not provided'
|
||||
|
||||
# fill passwords dict with request data passwords
|
||||
if credential and credential.passwords_needed:
|
||||
@ -2295,11 +2300,6 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
if validation_errors:
|
||||
errors['variables_needed_to_start'] = validation_errors
|
||||
|
||||
if obj.job_type != PERM_INVENTORY_SCAN and (obj.project is None):
|
||||
errors['project'] = 'Job Template Project is missing or undefined.'
|
||||
if (obj.inventory is None) and not attrs.get('inventory', None):
|
||||
errors['inventory'] = 'Job Template Inventory is missing or undefined.'
|
||||
|
||||
# Special prohibited cases for scan jobs
|
||||
if 'job_type' in data and obj.ask_job_type_on_launch:
|
||||
if ((obj.job_type == PERM_INVENTORY_SCAN and not data['job_type'] == PERM_INVENTORY_SCAN) or
|
||||
|
||||
@ -817,17 +817,76 @@ class JobTemplateAccess(BaseAccess):
|
||||
if self.user not in obj.admin_role:
|
||||
return False
|
||||
if data is not None:
|
||||
data_for_change = dict(data)
|
||||
data = dict(data)
|
||||
|
||||
if self.changes_are_non_sensitive(obj, data):
|
||||
if 'job_type' in data and obj.job_type != data['job_type'] and data['job_type'] == PERM_INVENTORY_SCAN:
|
||||
self.check_license(feature='system_tracking')
|
||||
|
||||
if 'survey_enabled' in data and obj.survey_enabled != data['survey_enabled'] and data['survey_enabled']:
|
||||
self.check_license(feature='surveys')
|
||||
return True
|
||||
|
||||
for required_field in ('credential', 'cloud_credential', 'inventory', 'project'):
|
||||
required_obj = getattr(obj, required_field, None)
|
||||
if required_field not in data_for_change and required_obj is not None:
|
||||
data_for_change[required_field] = required_obj.pk
|
||||
return self.can_read(obj) and self.can_add(data_for_change)
|
||||
|
||||
def changes_are_non_sensitive(self, obj, data):
|
||||
'''
|
||||
Returne true if the changes being made are considered nonsensitive, and
|
||||
thus can be made by a job template administrator which may not have access
|
||||
to the any inventory, project, or credentials associated with the template.
|
||||
'''
|
||||
# We are white listing fields that can
|
||||
field_whitelist = [
|
||||
'name', 'description', 'forks', 'limit', 'verbosity', 'extra_vars',
|
||||
'job_tags', 'force_handlers', 'skip_tags', 'ask_variables_on_launch',
|
||||
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_inventory_on_launch',
|
||||
'ask_credential_on_launch', 'survey_enabled'
|
||||
]
|
||||
|
||||
for k, v in data.items():
|
||||
if hasattr(obj, k) and getattr(obj, k) != v:
|
||||
if k not in field_whitelist:
|
||||
return False
|
||||
return True
|
||||
|
||||
def can_update_sensitive_fields(self, obj, data):
|
||||
project_id = data.get('project', obj.project.id if obj.project else None)
|
||||
inventory_id = data.get('inventory', obj.inventory.id if obj.inventory else None)
|
||||
credential_id = data.get('credential', obj.credential.id if obj.credential else None)
|
||||
cloud_credential_id = data.get('cloud_credential', obj.cloud_credential.id if obj.cloud_credential else None)
|
||||
network_credential_id = data.get('network_credential', obj.network_credential.id if obj.network_credential else None)
|
||||
|
||||
if project_id and self.user not in Project.objects.get(pk=project_id).use_role:
|
||||
return False
|
||||
if inventory_id and self.user not in Inventory.objects.get(pk=inventory_id).use_role:
|
||||
return False
|
||||
if credential_id and self.user not in Credential.objects.get(pk=credential_id).use_role:
|
||||
return False
|
||||
if cloud_credential_id and self.user not in Credential.objects.get(pk=cloud_credential_id).use_role:
|
||||
return False
|
||||
if network_credential_id and self.user not in Credential.objects.get(pk=network_credential_id).use_role:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def can_delete(self, obj):
|
||||
return self.user in obj.admin_role
|
||||
|
||||
class JobAccess(BaseAccess):
|
||||
'''
|
||||
I can see jobs when:
|
||||
- I am a superuser.
|
||||
- I can see its job template
|
||||
- I am an admin or auditor of the organization which contains its inventory
|
||||
- I am an admin or auditor of the organization which contains its project
|
||||
I can delete jobs when:
|
||||
- I am an admin of the organization which contains its inventory
|
||||
- I am an admin of the organization which contains its project
|
||||
'''
|
||||
|
||||
model = Job
|
||||
|
||||
@ -839,10 +898,20 @@ class JobAccess(BaseAccess):
|
||||
if self.user.is_superuser:
|
||||
return qs.all()
|
||||
|
||||
return qs.filter(
|
||||
qs_jt = qs.filter(
|
||||
job_template__in=JobTemplate.accessible_objects(self.user, 'read_role')
|
||||
)
|
||||
|
||||
org_access_qs = Organization.objects.filter(
|
||||
Q(admin_role__members=self.user) | Q(auditor_role__members=self.user))
|
||||
if not org_access_qs.exists():
|
||||
return qs_jt
|
||||
|
||||
return qs.filter(
|
||||
Q(job_template__in=JobTemplate.accessible_objects(self.user, 'read_role')) |
|
||||
Q(inventory__organization__in=org_access_qs) |
|
||||
Q(project__organization__in=org_access_qs)).distinct()
|
||||
|
||||
def can_add(self, data):
|
||||
if not data or '_method' in data: # So the browseable API will work?
|
||||
return True
|
||||
@ -871,7 +940,11 @@ class JobAccess(BaseAccess):
|
||||
|
||||
@check_superuser
|
||||
def can_delete(self, obj):
|
||||
return self.user in obj.inventory.admin_role
|
||||
if obj.inventory is not None and self.user in obj.inventory.organization.admin_role:
|
||||
return True
|
||||
if obj.project is not None and self.user in obj.project.organization.admin_role:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_start(self, obj):
|
||||
self.check_license()
|
||||
|
||||
@ -242,15 +242,44 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
||||
'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled',
|
||||
'labels',]
|
||||
|
||||
def resource_validation_data(self):
|
||||
'''
|
||||
Process consistency errors and need-for-launch related fields.
|
||||
'''
|
||||
resources_needed_to_start = []
|
||||
validation_errors = {}
|
||||
|
||||
# Inventory and Credential related checks
|
||||
if self.inventory is None:
|
||||
resources_needed_to_start.append('inventory')
|
||||
if not self.ask_inventory_on_launch:
|
||||
validation_errors['inventory'] = ["Job Template must provide 'inventory' or allow prompting for it.",]
|
||||
if self.credential is None:
|
||||
resources_needed_to_start.append('credential')
|
||||
if not self.ask_credential_on_launch:
|
||||
validation_errors['credential'] = ["Job Template must provide 'credential' or allow prompting for it.",]
|
||||
|
||||
# Job type dependent checks
|
||||
if self.job_type == 'scan':
|
||||
if self.inventory is None or self.ask_inventory_on_launch:
|
||||
validation_errors['inventory'] = ["Scan jobs must be assigned a fixed inventory.",]
|
||||
elif self.project is None:
|
||||
resources_needed_to_start.append('project')
|
||||
validation_errors['project'] = ["Job types 'run' and 'check' must have assigned a project.",]
|
||||
|
||||
return (validation_errors, resources_needed_to_start)
|
||||
|
||||
def clean(self):
|
||||
if self.job_type == 'scan' and (self.inventory is None or self.ask_inventory_on_launch):
|
||||
raise ValidationError({"inventory": ["Scan jobs must be assigned a fixed inventory.",]})
|
||||
if (not self.ask_inventory_on_launch) and self.inventory is None:
|
||||
raise ValidationError({"inventory": ["Job Template must provide 'inventory' or allow prompting for it.",]})
|
||||
if (not self.ask_credential_on_launch) and self.credential is None:
|
||||
raise ValidationError({"credential": ["Job Template must provide 'credential' or allow prompting for it.",]})
|
||||
validation_errors, resources_needed_to_start = self.resource_validation_data()
|
||||
if validation_errors:
|
||||
raise ValidationError(validation_errors)
|
||||
return super(JobTemplate, self).clean()
|
||||
|
||||
@property
|
||||
def resources_needed_to_start(self):
|
||||
validation_errors, resources_needed_to_start = self.resource_validation_data()
|
||||
return resources_needed_to_start
|
||||
|
||||
def create_job(self, **kwargs):
|
||||
'''
|
||||
Create a new job based on this template.
|
||||
@ -265,9 +294,9 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
||||
Return whether job template can be used to start a new job without
|
||||
requiring any user input.
|
||||
'''
|
||||
return bool(self.credential and not len(self.passwords_needed_to_start) and
|
||||
not len(self.variables_needed_to_start) and
|
||||
self.inventory)
|
||||
return (not self.resources_needed_to_start and
|
||||
not self.passwords_needed_to_start and
|
||||
not self.variables_needed_to_start)
|
||||
|
||||
@property
|
||||
def variables_needed_to_start(self):
|
||||
|
||||
@ -379,11 +379,16 @@ def activity_stream_associate(sender, instance, **kwargs):
|
||||
obj1 = instance
|
||||
object1=camelcase_to_underscore(obj1.__class__.__name__)
|
||||
obj_rel = sender.__module__ + "." + sender.__name__
|
||||
|
||||
for entity_acted in kwargs['pk_set']:
|
||||
obj2 = kwargs['model']
|
||||
obj2_id = entity_acted
|
||||
obj2_actual = obj2.objects.get(id=obj2_id)
|
||||
object2 = camelcase_to_underscore(obj2.__name__)
|
||||
if isinstance(obj2_actual, Role) and obj2_actual.content_object is not None:
|
||||
obj2_actual = obj2_actual.content_object
|
||||
object2 = camelcase_to_underscore(obj2_actual.__class__.__name__)
|
||||
else:
|
||||
object2 = camelcase_to_underscore(obj2.__name__)
|
||||
# Skip recording any inventory source, or system job template changes here.
|
||||
if isinstance(obj1, InventorySource) or isinstance(obj2_actual, InventorySource):
|
||||
continue
|
||||
@ -409,7 +414,7 @@ def activity_stream_associate(sender, instance, **kwargs):
|
||||
# If the m2m is from the User side we need to
|
||||
# set the content_object of the Role for our entry.
|
||||
if type(instance) == User and role.content_object is not None:
|
||||
getattr(activity_entry, role.content_type.name).add(role.content_object)
|
||||
getattr(activity_entry, role.content_type.name.replace(' ', '_')).add(role.content_object)
|
||||
|
||||
activity_entry.role.add(role)
|
||||
activity_entry.object_relationship_type = obj_rel
|
||||
|
||||
@ -107,7 +107,7 @@ def mk_job_template(name, job_type='run',
|
||||
organization=None, inventory=None,
|
||||
credential=None, persisted=True,
|
||||
project=None):
|
||||
jt = JobTemplate(name=name, job_type=job_type)
|
||||
jt = JobTemplate(name=name, job_type=job_type, playbook='mocked')
|
||||
|
||||
jt.inventory = inventory
|
||||
if jt.inventory is None:
|
||||
|
||||
@ -27,6 +27,17 @@ from .fixtures import (
|
||||
from .exc import NotUnique
|
||||
|
||||
|
||||
def generate_objects(artifacts, kwargs):
|
||||
'''generate_objects takes a list of artifacts that are supported by
|
||||
a create function and compares it to the kwargs passed in to the create
|
||||
function. If a kwarg is found that is not in the artifacts list a RuntimeError
|
||||
is raised.
|
||||
'''
|
||||
for k in kwargs.keys():
|
||||
if k not in artifacts:
|
||||
raise RuntimeError('{} is not a valid argument'.format(k))
|
||||
return namedtuple("Objects", ",".join(artifacts))
|
||||
|
||||
def generate_role_objects(objects):
|
||||
'''generate_role_objects assembles a dictionary of all possible objects by name.
|
||||
It will raise an exception if any of the objects share a name due to the fact that
|
||||
@ -167,15 +178,19 @@ class _Mapped(object):
|
||||
# or encapsulated by specific factory fixtures in a conftest
|
||||
#
|
||||
|
||||
def create_job_template(name, **kwargs):
|
||||
Objects = namedtuple("Objects", "job_template, inventory, project, credential, job_type")
|
||||
def create_job_template(name, roles=None, persisted=True, **kwargs):
|
||||
Objects = generate_objects(["job_template",
|
||||
"organization",
|
||||
"inventory",
|
||||
"project",
|
||||
"credential",
|
||||
"job_type",], kwargs)
|
||||
|
||||
org = None
|
||||
proj = None
|
||||
inv = None
|
||||
cred = None
|
||||
job_type = kwargs.get('job_type', 'run')
|
||||
persisted = kwargs.get('persisted', True)
|
||||
|
||||
if 'organization' in kwargs:
|
||||
org = kwargs['organization']
|
||||
@ -202,24 +217,38 @@ def create_job_template(name, **kwargs):
|
||||
job_type=job_type, persisted=persisted)
|
||||
|
||||
role_objects = generate_role_objects([org, proj, inv, cred])
|
||||
apply_roles(kwargs.get('roles'), role_objects, persisted)
|
||||
apply_roles(roles, role_objects, persisted)
|
||||
|
||||
return Objects(job_template=jt,
|
||||
project=proj,
|
||||
inventory=inv,
|
||||
credential=cred,
|
||||
job_type=job_type)
|
||||
job_type=job_type,
|
||||
organization=org,)
|
||||
|
||||
def create_organization(name, **kwargs):
|
||||
Objects = namedtuple("Objects", "organization,teams,users,superusers,projects,labels,notification_templates")
|
||||
def create_organization(name, roles=None, persisted=True, **kwargs):
|
||||
Objects = generate_objects(["organization",
|
||||
"teams", "users",
|
||||
"superusers",
|
||||
"projects",
|
||||
"labels",
|
||||
"notification_templates",
|
||||
"inventories",], kwargs)
|
||||
|
||||
projects = {}
|
||||
inventories = {}
|
||||
labels = {}
|
||||
notification_templates = {}
|
||||
persisted = kwargs.get('persisted', True)
|
||||
|
||||
org = mk_organization(name, '%s-desc'.format(name), persisted=persisted)
|
||||
|
||||
if 'inventories' in kwargs:
|
||||
for i in kwargs['inventories']:
|
||||
if type(i) is Inventory:
|
||||
inventories[i.name] = i
|
||||
else:
|
||||
inventories[i] = mk_inventory(i, organization=org, persisted=persisted)
|
||||
|
||||
if 'projects' in kwargs:
|
||||
for p in kwargs['projects']:
|
||||
if type(p) is Project:
|
||||
@ -246,20 +275,24 @@ def create_organization(name, **kwargs):
|
||||
notification_templates[nt] = mk_notification_template(nt, organization=org, persisted=persisted)
|
||||
|
||||
role_objects = generate_role_objects([org, superusers, users, teams, projects, labels, notification_templates])
|
||||
apply_roles(kwargs.get('roles'), role_objects, persisted)
|
||||
apply_roles(roles, role_objects, persisted)
|
||||
return Objects(organization=org,
|
||||
superusers=_Mapped(superusers),
|
||||
users=_Mapped(users),
|
||||
teams=_Mapped(teams),
|
||||
projects=_Mapped(projects),
|
||||
labels=_Mapped(labels),
|
||||
notification_templates=_Mapped(notification_templates))
|
||||
notification_templates=_Mapped(notification_templates),
|
||||
inventories=_Mapped(inventories))
|
||||
|
||||
def create_notification_template(name, **kwargs):
|
||||
Objects = namedtuple("Objects", "notification_template,organization,users,superusers,teams")
|
||||
def create_notification_template(name, roles=None, persisted=True, **kwargs):
|
||||
Objects = generate_objects(["notification_template",
|
||||
"organization",
|
||||
"users",
|
||||
"superusers",
|
||||
"teams",], kwargs)
|
||||
|
||||
organization = None
|
||||
persisted = kwargs.get('persisted', True)
|
||||
|
||||
if 'organization' in kwargs:
|
||||
org = kwargs['organization']
|
||||
@ -272,7 +305,7 @@ def create_notification_template(name, **kwargs):
|
||||
users = generate_users(organization, teams, False, persisted, users=kwargs.get('users'))
|
||||
|
||||
role_objects = generate_role_objects([organization, notification_template])
|
||||
apply_roles(kwargs.get('roles'), role_objects, persisted)
|
||||
apply_roles(roles, role_objects, persisted)
|
||||
return Objects(notification_template=notification_template,
|
||||
organization=organization,
|
||||
users=_Mapped(users),
|
||||
|
||||
@ -131,3 +131,24 @@ def test_stream_queryset_hides_shows_items(
|
||||
assert queryset.filter(host__pk=host.pk, operation='create').count() == 1
|
||||
assert queryset.filter(team__pk=team.pk, operation='create').count() == 1
|
||||
assert queryset.filter(notification_template__pk=notification_template.pk, operation='create').count() == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||
def test_stream_user_direct_role_updates(get, post, organization_factory):
|
||||
objects = organization_factory('test_org',
|
||||
superusers=['admin'],
|
||||
users=['test'],
|
||||
inventories=['inv1'])
|
||||
|
||||
url = reverse('api:user_roles_list', args=(objects.users.test.pk,))
|
||||
post(url, dict(id=objects.inventories.inv1.read_role.pk), objects.superusers.admin)
|
||||
|
||||
activity_stream = ActivityStream.objects.filter(
|
||||
inventory__pk=objects.inventories.inv1.pk,
|
||||
user__pk=objects.users.test.pk,
|
||||
role__pk=objects.inventories.inv1.read_role.pk).first()
|
||||
url = reverse('api:activity_stream_detail', args=(activity_stream.pk,))
|
||||
response = get(url, objects.users.test)
|
||||
|
||||
assert response.data['object1'] == 'user'
|
||||
assert response.data['object2'] == 'inventory'
|
||||
|
||||
@ -178,7 +178,7 @@ def test_job_launch_fails_without_inventory(deploy_jobtemplate, post, user):
|
||||
args=[deploy_jobtemplate.pk]), {}, user('admin', True))
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.data['inventory'] == ['Job Template Inventory is missing or undefined.']
|
||||
assert response.data['inventory'] == ["Job Template 'inventory' is missing or undefined."]
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.job_runtime_vars
|
||||
|
||||
179
awx/main/tests/functional/api/test_job_template.py
Normal file
179
awx/main/tests/functional/api/test_job_template.py
Normal file
@ -0,0 +1,179 @@
|
||||
import pytest
|
||||
import mock
|
||||
|
||||
# AWX
|
||||
from awx.api.serializers import JobTemplateSerializer
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from awx.main.models.projects import ProjectOptions
|
||||
|
||||
# Django
|
||||
from django.test.client import RequestFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def jt_copy_edit(job_template_factory, project):
|
||||
objects = job_template_factory(
|
||||
'copy-edit-job-template',
|
||||
project=project)
|
||||
return objects.job_template
|
||||
|
||||
@property
|
||||
def project_playbooks(self):
|
||||
return ['mocked', 'mocked.yml', 'alt-mocked.yml']
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_template_role_user(post, organization_factory, job_template_factory):
|
||||
objects = organization_factory("org",
|
||||
superusers=['admin'],
|
||||
users=['test'])
|
||||
|
||||
jt_objects = job_template_factory("jt",
|
||||
organization=objects.organization,
|
||||
inventory='test_inv',
|
||||
project='test_proj')
|
||||
|
||||
url = reverse('api:user_roles_list', args=(objects.users.test.pk,))
|
||||
response = post(url, dict(id=jt_objects.job_template.execute_role.pk), objects.superusers.admin)
|
||||
assert response.status_code == 204
|
||||
|
||||
# Test protection against limited set of validation problems
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_bad_data_copy_edit(admin_user, project):
|
||||
"""
|
||||
If a required resource (inventory here) was deleted, copying not allowed
|
||||
because doing so would caues a validation error
|
||||
"""
|
||||
|
||||
jt_res = JobTemplate.objects.create(
|
||||
job_type='run',
|
||||
project=project,
|
||||
inventory=None, ask_inventory_on_launch=False, # not allowed
|
||||
credential=None, ask_credential_on_launch=True,
|
||||
name='deploy-job-template'
|
||||
)
|
||||
serializer = JobTemplateSerializer(jt_res)
|
||||
request = RequestFactory().get('/api/v1/job_templates/12/')
|
||||
request.user = admin_user
|
||||
serializer.context['request'] = request
|
||||
response = serializer.to_representation(jt_res)
|
||||
assert not response['summary_fields']['can_copy']
|
||||
assert response['summary_fields']['can_edit']
|
||||
|
||||
# Tests for correspondence between view info and actual access
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_admin_copy_edit(jt_copy_edit, admin_user):
|
||||
"Absent a validation error, system admins can do everything"
|
||||
|
||||
# Serializer can_copy/can_edit fields
|
||||
serializer = JobTemplateSerializer(jt_copy_edit)
|
||||
request = RequestFactory().get('/api/v1/job_templates/12/')
|
||||
request.user = admin_user
|
||||
serializer.context['request'] = request
|
||||
response = serializer.to_representation(jt_copy_edit)
|
||||
assert response['summary_fields']['can_copy']
|
||||
assert response['summary_fields']['can_edit']
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_admin_copy_edit(jt_copy_edit, org_admin):
|
||||
"Organization admins SHOULD be able to copy a JT firmly in their org"
|
||||
|
||||
# Serializer can_copy/can_edit fields
|
||||
serializer = JobTemplateSerializer(jt_copy_edit)
|
||||
request = RequestFactory().get('/api/v1/job_templates/12/')
|
||||
request.user = org_admin
|
||||
serializer.context['request'] = request
|
||||
response = serializer.to_representation(jt_copy_edit)
|
||||
assert response['summary_fields']['can_copy']
|
||||
assert response['summary_fields']['can_edit']
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_admin_foreign_cred_no_copy_edit(jt_copy_edit, org_admin, machine_credential):
|
||||
"""
|
||||
Organization admins without access to the 3 related resources:
|
||||
SHOULD NOT be able to copy JT
|
||||
SHOULD NOT be able to edit that job template
|
||||
"""
|
||||
|
||||
# Attach credential to JT that org admin can not use
|
||||
jt_copy_edit.credential = machine_credential
|
||||
jt_copy_edit.save()
|
||||
|
||||
# Serializer can_copy/can_edit fields
|
||||
serializer = JobTemplateSerializer(jt_copy_edit)
|
||||
request = RequestFactory().get('/api/v1/job_templates/12/')
|
||||
request.user = org_admin
|
||||
serializer.context['request'] = request
|
||||
response = serializer.to_representation(jt_copy_edit)
|
||||
assert not response['summary_fields']['can_copy']
|
||||
assert not response['summary_fields']['can_edit']
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_jt_admin_copy_edit(jt_copy_edit, rando):
|
||||
"JT admins wihout access to associated resources SHOULD NOT be able to copy"
|
||||
|
||||
# random user given JT admin access only
|
||||
jt_copy_edit.admin_role.members.add(rando)
|
||||
jt_copy_edit.save()
|
||||
|
||||
# Serializer can_copy/can_edit fields
|
||||
serializer = JobTemplateSerializer(jt_copy_edit)
|
||||
request = RequestFactory().get('/api/v1/job_templates/12/')
|
||||
request.user = rando
|
||||
serializer.context['request'] = request
|
||||
response = serializer.to_representation(jt_copy_edit)
|
||||
assert not response['summary_fields']['can_copy']
|
||||
assert not response['summary_fields']['can_edit']
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_proj_jt_admin_copy_edit(jt_copy_edit, rando):
|
||||
"JT admins with access to associated resources SHOULD be able to copy"
|
||||
|
||||
# random user given JT and project admin abilities
|
||||
jt_copy_edit.admin_role.members.add(rando)
|
||||
jt_copy_edit.save()
|
||||
jt_copy_edit.project.admin_role.members.add(rando)
|
||||
jt_copy_edit.project.save()
|
||||
|
||||
# Serializer can_copy/can_edit fields
|
||||
serializer = JobTemplateSerializer(jt_copy_edit)
|
||||
request = RequestFactory().get('/api/v1/job_templates/12/')
|
||||
request.user = rando
|
||||
serializer.context['request'] = request
|
||||
response = serializer.to_representation(jt_copy_edit)
|
||||
assert response['summary_fields']['can_copy']
|
||||
assert response['summary_fields']['can_edit']
|
||||
|
||||
# Functional tests - create new JT with all returned fields, as the UI does
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
|
||||
def test_org_admin_copy_edit_functional(jt_copy_edit, org_admin, get, post):
|
||||
get_response = get(reverse('api:job_template_detail', args=[jt_copy_edit.pk]), user=org_admin)
|
||||
assert get_response.status_code == 200
|
||||
assert get_response.data['summary_fields']['can_copy']
|
||||
|
||||
post_data = get_response.data
|
||||
post_data['name'] = '%s @ 12:19:47 pm' % post_data['name']
|
||||
post_response = post(reverse('api:job_template_list', args=[]), user=org_admin, data=post_data)
|
||||
assert post_response.status_code == 201
|
||||
assert post_response.data['name'] == 'copy-edit-job-template @ 12:19:47 pm'
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
|
||||
def test_jt_admin_copy_edit_functional(jt_copy_edit, rando, get, post):
|
||||
|
||||
# Grant random user JT admin access only
|
||||
jt_copy_edit.admin_role.members.add(rando)
|
||||
jt_copy_edit.save()
|
||||
|
||||
get_response = get(reverse('api:job_template_detail', args=[jt_copy_edit.pk]), user=rando)
|
||||
assert get_response.status_code == 200
|
||||
assert not get_response.data['summary_fields']['can_copy']
|
||||
|
||||
post_data = get_response.data
|
||||
post_data['name'] = '%s @ 12:19:47 pm' % post_data['name']
|
||||
post_response = post(reverse('api:job_template_list', args=[]), user=rando, data=post_data)
|
||||
assert post_response.status_code == 403
|
||||
109
awx/main/tests/functional/api/test_job_templates.py
Normal file
109
awx/main/tests/functional/api/test_job_templates.py
Normal file
@ -0,0 +1,109 @@
|
||||
import mock # noqa
|
||||
import pytest
|
||||
from awx.main.models.projects import ProjectOptions
|
||||
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
@property
|
||||
def project_playbooks(self):
|
||||
return ['mocked', 'mocked.yml', 'alt-mocked.yml']
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
|
||||
@pytest.mark.parametrize(
|
||||
"grant_project, grant_credential, grant_inventory, expect", [
|
||||
(True, True, True, 201),
|
||||
(True, True, False, 403),
|
||||
(True, False, True, 403),
|
||||
(False, True, True, 403),
|
||||
]
|
||||
)
|
||||
def test_create(post, project, machine_credential, inventory, alice, grant_project, grant_credential, grant_inventory, expect):
|
||||
if grant_project:
|
||||
project.use_role.members.add(alice)
|
||||
if grant_credential:
|
||||
machine_credential.use_role.members.add(alice)
|
||||
if grant_inventory:
|
||||
inventory.use_role.members.add(alice)
|
||||
|
||||
post(reverse('api:job_template_list'), {
|
||||
'name': 'Some name',
|
||||
'project': project.id,
|
||||
'credential': machine_credential.id,
|
||||
'inventory': inventory.id,
|
||||
'playbook': 'mocked.yml',
|
||||
}, alice, expect=expect)
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
|
||||
@pytest.mark.parametrize(
|
||||
"grant_project, grant_credential, grant_inventory, expect", [
|
||||
(True, True, True, 200),
|
||||
(True, True, False, 403),
|
||||
(True, False, True, 403),
|
||||
(False, True, True, 403),
|
||||
]
|
||||
)
|
||||
def test_edit_sensitive_fields(patch, job_template_factory, alice, grant_project, grant_credential, grant_inventory, expect):
|
||||
objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred')
|
||||
objs.job_template.admin_role.members.add(alice)
|
||||
|
||||
if grant_project:
|
||||
objs.project.use_role.members.add(alice)
|
||||
if grant_credential:
|
||||
objs.credential.use_role.members.add(alice)
|
||||
if grant_inventory:
|
||||
objs.inventory.use_role.members.add(alice)
|
||||
|
||||
patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), {
|
||||
'name': 'Some name',
|
||||
'project': objs.project.id,
|
||||
'credential': objs.credential.id,
|
||||
'inventory': objs.inventory.id,
|
||||
'playbook': 'alt-mocked.yml',
|
||||
}, alice, expect=expect)
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
|
||||
def test_edit_playbook(patch, job_template_factory, alice):
|
||||
objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred')
|
||||
objs.job_template.admin_role.members.add(alice)
|
||||
objs.project.use_role.members.add(alice)
|
||||
objs.credential.use_role.members.add(alice)
|
||||
objs.inventory.use_role.members.add(alice)
|
||||
|
||||
patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), {
|
||||
'playbook': 'alt-mocked.yml',
|
||||
}, alice, expect=200)
|
||||
|
||||
objs.inventory.use_role.members.remove(alice)
|
||||
patch(reverse('api:job_template_detail', args=(objs.job_template.id,)), {
|
||||
'playbook': 'mocked.yml',
|
||||
}, alice, expect=403)
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch.object(ProjectOptions, "playbooks", project_playbooks)
|
||||
def test_edit_nonsenstive(patch, job_template_factory, alice):
|
||||
objs = job_template_factory('jt', organization='org1', project='prj', inventory='inv', credential='cred')
|
||||
jt = objs.job_template
|
||||
jt.admin_role.members.add(alice)
|
||||
|
||||
res = patch(reverse('api:job_template_detail', args=(jt.id,)), {
|
||||
'name': 'updated',
|
||||
'description': 'bar',
|
||||
'forks': 14,
|
||||
'limit': 'something',
|
||||
'verbosity': 5,
|
||||
'extra_vars': '--',
|
||||
'job_tags': 'sometags',
|
||||
'force_handlers': True,
|
||||
'skip_tags': True,
|
||||
'ask_variables_on_launch':True,
|
||||
'ask_tags_on_launch':True,
|
||||
'ask_job_type_on_launch':True,
|
||||
'ask_inventory_on_launch':True,
|
||||
'ask_credential_on_launch': True,
|
||||
}, alice, expect=200)
|
||||
print(res.data)
|
||||
assert res.data['name'] == 'updated'
|
||||
@ -215,6 +215,13 @@ def org_admin(user, organization):
|
||||
organization.member_role.members.add(ret)
|
||||
return ret
|
||||
|
||||
@pytest.fixture
|
||||
def org_auditor(user, organization):
|
||||
ret = user('org-auditor', False)
|
||||
organization.auditor_role.members.add(ret)
|
||||
organization.member_role.members.add(ret)
|
||||
return ret
|
||||
|
||||
@pytest.fixture
|
||||
def org_member(user, organization):
|
||||
ret = user('org-member', False)
|
||||
|
||||
72
awx/main/tests/functional/test_rbac_job.py
Normal file
72
awx/main/tests/functional/test_rbac_job.py
Normal file
@ -0,0 +1,72 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.access import JobAccess
|
||||
from awx.main.models import Job
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def normal_job(deploy_jobtemplate):
|
||||
return Job.objects.create(
|
||||
job_template=deploy_jobtemplate,
|
||||
project=deploy_jobtemplate.project,
|
||||
inventory=deploy_jobtemplate.inventory
|
||||
)
|
||||
|
||||
# Read permissions testing
|
||||
@pytest.mark.django_db
|
||||
def test_superuser_sees_orphans(normal_job, admin_user):
|
||||
normal_job.job_template = None
|
||||
access = JobAccess(admin_user)
|
||||
assert access.can_read(normal_job)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_member_does_not_see_orphans(normal_job, org_member, project):
|
||||
normal_job.job_template = None
|
||||
# Check that privledged access to project still does not grant access
|
||||
project.admin_role.members.add(org_member)
|
||||
access = JobAccess(org_member)
|
||||
assert not access.can_read(normal_job)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_admin_sees_orphans(normal_job, org_admin):
|
||||
normal_job.job_template = None
|
||||
access = JobAccess(org_admin)
|
||||
assert access.can_read(normal_job)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_auditor_sees_orphans(normal_job, org_auditor):
|
||||
normal_job.job_template = None
|
||||
access = JobAccess(org_auditor)
|
||||
assert access.can_read(normal_job)
|
||||
|
||||
# Delete permissions testing
|
||||
@pytest.mark.django_db
|
||||
def test_JT_admin_delete_denied(normal_job, rando):
|
||||
normal_job.job_template.admin_role.members.add(rando)
|
||||
access = JobAccess(rando)
|
||||
assert not access.can_delete(normal_job)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_admin_delete_denied(normal_job, rando):
|
||||
normal_job.job_template.inventory.admin_role.members.add(rando)
|
||||
access = JobAccess(rando)
|
||||
assert not access.can_delete(normal_job)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_null_related_delete_denied(normal_job, rando):
|
||||
normal_job.project = None
|
||||
normal_job.inventory = None
|
||||
access = JobAccess(rando)
|
||||
assert not access.can_delete(normal_job)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_org_admin_delete_allowed(normal_job, org_admin):
|
||||
normal_job.project = None # do this so we test job->inventory->org->admin connection
|
||||
access = JobAccess(org_admin)
|
||||
assert access.can_delete(normal_job)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_project_org_admin_delete_allowed(normal_job, org_admin):
|
||||
normal_job.inventory = None # do this so we test job->project->org->admin connection
|
||||
access = JobAccess(org_admin)
|
||||
assert access.can_delete(normal_job)
|
||||
@ -9,9 +9,14 @@ from awx.main.models import Label, Job
|
||||
#DRF
|
||||
from rest_framework import serializers
|
||||
|
||||
def mock_JT_resource_data():
|
||||
return ({}, [])
|
||||
|
||||
@pytest.fixture
|
||||
def job_template(mocker):
|
||||
return mocker.MagicMock(pk=5)
|
||||
mock_jt = mocker.MagicMock(pk=5)
|
||||
mock_jt.resource_validation_data = mock_JT_resource_data
|
||||
return mock_jt
|
||||
|
||||
@pytest.fixture
|
||||
def job(mocker, job_template):
|
||||
|
||||
35
awx/main/tests/unit/models/test_job_template_unit.py
Normal file
35
awx/main/tests/unit/models/test_job_template_unit.py
Normal file
@ -0,0 +1,35 @@
|
||||
from awx.main.tests.factories import create_job_template
|
||||
|
||||
|
||||
def test_missing_project_error():
|
||||
objects = create_job_template(
|
||||
'missing-project-jt',
|
||||
organization='org1',
|
||||
inventory='inventory1',
|
||||
credential='cred1',
|
||||
persisted=False)
|
||||
obj = objects.job_template
|
||||
assert 'project' in obj.resources_needed_to_start
|
||||
validation_errors, resources_needed_to_start = obj.resource_validation_data()
|
||||
assert 'project' in validation_errors
|
||||
|
||||
def test_inventory_credential_need_to_start():
|
||||
objects = create_job_template(
|
||||
'job-template-few-resources',
|
||||
project='project1',
|
||||
persisted=False)
|
||||
obj = objects.job_template
|
||||
assert 'inventory' in obj.resources_needed_to_start
|
||||
assert 'credential' in obj.resources_needed_to_start
|
||||
|
||||
def test_inventory_credential_contradictions():
|
||||
objects = create_job_template(
|
||||
'job-template-paradox',
|
||||
project='project1',
|
||||
persisted=False)
|
||||
obj = objects.job_template
|
||||
obj.ask_inventory_on_launch = False
|
||||
obj.ask_credential_on_launch = False
|
||||
validation_errors, resources_needed_to_start = obj.resource_validation_data()
|
||||
assert 'inventory' in validation_errors
|
||||
assert 'credential' in validation_errors
|
||||
@ -37,13 +37,13 @@ body .navbar {
|
||||
border-color: #E8E8E8;
|
||||
}
|
||||
body .navbar .navbar-brand {
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
body .navbar .navbar-brand:focus,
|
||||
body .navbar .navbar-brand:hover {
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
}
|
||||
body .navbar .navbar-brand img {
|
||||
display: inline-block;
|
||||
@ -60,7 +60,7 @@ body .navbar .navbar-brand > span {
|
||||
body .navbar .navbar-title {
|
||||
float: left;
|
||||
height: 50px;
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
@ -74,19 +74,19 @@ body.show-title .navbar .navbar-title {
|
||||
display: inline-block;
|
||||
}
|
||||
body .navbar .navbar-nav > li > a {
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
body .navbar .navbar-nav > li > a:focus,
|
||||
body .navbar .navbar-nav > li > a:hover {
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
}
|
||||
body .navbar .navbar-nav > li > a > span.glyphicon {
|
||||
font-size: 20px;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
color: #B7B7B7;
|
||||
color: #848992;
|
||||
}
|
||||
|
||||
body .page-header {
|
||||
@ -110,7 +110,7 @@ body .description .hide-description span.glyphicon {
|
||||
font-size: 20px;
|
||||
}
|
||||
body .description .hide-description:hover span.glyphicon {
|
||||
color: #B7B7B7;
|
||||
color: #848992;
|
||||
}
|
||||
body ul.breadcrumb,
|
||||
body .description,
|
||||
@ -167,7 +167,7 @@ body .form-actions button {
|
||||
body .form-horizontal .control-label {
|
||||
text-transform: uppercase;
|
||||
font-weight: normal;
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
}
|
||||
body textarea.form-control {
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
@ -182,22 +182,22 @@ body .description pre {
|
||||
}
|
||||
|
||||
body .tooltip.bottom .tooltip-arrow {
|
||||
border-bottom-color: #848992;
|
||||
border-bottom-color: #707070;
|
||||
}
|
||||
body .tooltip.top .tooltip-arrow {
|
||||
border-top-color: #848992;
|
||||
border-top-color: #707070;
|
||||
}
|
||||
body .tooltip.left .tooltip-arrow {
|
||||
border-left-color: #848992;
|
||||
border-left-color: #707070;
|
||||
}
|
||||
body .tooltip.right .tooltip-arrow {
|
||||
border-right-color: #848992;
|
||||
border-right-color: #707070;
|
||||
}
|
||||
body .tooltip.in {
|
||||
opacity: 1;
|
||||
}
|
||||
body .tooltip-inner {
|
||||
background-color: #848992;
|
||||
background-color: #707070;
|
||||
}
|
||||
|
||||
body .btn {
|
||||
@ -205,7 +205,7 @@ body .btn {
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #FFFFFF;
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
border: 1px solid #E8E8E8;
|
||||
}
|
||||
.btn-primary:hover,
|
||||
@ -224,7 +224,7 @@ body .btn {
|
||||
.open>.dropdown-toggle.btn-primary:hover,
|
||||
.open>.dropdown-toggle.btn-primary {
|
||||
background-color: #FAFAFA;
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
border: 1px solid #E8E8E8;
|
||||
}
|
||||
|
||||
@ -283,7 +283,7 @@ body #footer {
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
height: 40px;
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
}
|
||||
body #footer .footer-logo {
|
||||
text-align: left;
|
||||
@ -302,7 +302,7 @@ body #footer .footer-copyright {
|
||||
padding-top: 10px;
|
||||
}
|
||||
body #footer .footer-copyright a {
|
||||
color: #848992;
|
||||
color: #707070;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
@ -329,7 +329,7 @@ body #footer .footer-copyright a {
|
||||
border-color: #E8E8E8;
|
||||
}
|
||||
body .navbar .navbar-toggle .icon-bar {
|
||||
background-color: #B7B7B7;
|
||||
background-color: #848992;
|
||||
}
|
||||
body .navbar .tooltip {
|
||||
visibility: hidden;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 15 KiB |
@ -962,6 +962,10 @@ input[type="checkbox"].checkbox-no-label {
|
||||
.checkbox-inline, .radio-inline {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.checkbox-inline.stack-inline {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-options {
|
||||
|
||||
@ -178,7 +178,14 @@
|
||||
|
||||
.Form-formGroup--checkbox{
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.Form-textUneditable {
|
||||
.Form-textInput {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.Form-subForm {
|
||||
@ -324,6 +331,12 @@
|
||||
|
||||
.select2-dropdown{
|
||||
border:1px solid @field-border;
|
||||
|
||||
}
|
||||
|
||||
.select2-container--open .select2-dropdown--below {
|
||||
margin-top: -1px;
|
||||
border-top: 1px solid @field-border;
|
||||
}
|
||||
|
||||
.Form-dropDown:focus{
|
||||
@ -427,6 +440,10 @@ input[type='radio']:checked:before {
|
||||
outline:none;
|
||||
}
|
||||
|
||||
.Form-inputLabelContainer {
|
||||
width: 100%;
|
||||
display: block !important;
|
||||
}
|
||||
.Form-inputLabel{
|
||||
text-transform: uppercase;
|
||||
color: @default-interface-txt;
|
||||
@ -437,6 +454,16 @@ input[type='radio']:checked:before {
|
||||
.noselect;
|
||||
}
|
||||
|
||||
.Form-labelAction {
|
||||
text-transform: uppercase;
|
||||
font-weight: normal;
|
||||
font-size: 0.8em;
|
||||
padding-left:5px;
|
||||
float: right;
|
||||
margin-top: 3px;
|
||||
.noselect;
|
||||
}
|
||||
|
||||
.Form-buttons{
|
||||
height: 30px;
|
||||
display: flex;
|
||||
|
||||
@ -214,7 +214,7 @@
|
||||
}
|
||||
|
||||
#job-detail-container {
|
||||
|
||||
|
||||
.well {
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -276,6 +276,8 @@
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: @white;
|
||||
min-height: 40px;
|
||||
|
||||
.row {
|
||||
border-top: 1px solid @grey;
|
||||
}
|
||||
@ -318,7 +320,7 @@
|
||||
|
||||
#play-section {
|
||||
.table-detail {
|
||||
height: 150px;
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -421,7 +423,6 @@
|
||||
table-layout: fixed;
|
||||
}
|
||||
#hosts-table-detail {
|
||||
height: 150px;
|
||||
background-color: @white;
|
||||
}
|
||||
#hosts-table-detail table {
|
||||
|
||||
32
awx/ui/client/lib/lrInfiniteScroll/.bower.json
Normal file
32
awx/ui/client/lib/lrInfiniteScroll/.bower.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "lrInfiniteScroll",
|
||||
"main": "lrInfiniteScroll.js",
|
||||
"version": "1.0.0",
|
||||
"homepage": "https://github.com/lorenzofox3/lrInfiniteScroll",
|
||||
"authors": [
|
||||
"lorenzofox3 <laurent34azerty@gmail.com>"
|
||||
],
|
||||
"description": "angular directive to handle element scroll",
|
||||
"keywords": [
|
||||
"angular",
|
||||
"scroll",
|
||||
"inifinite"
|
||||
],
|
||||
"license": "MIT",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
"_release": "1.0.0",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "1.0.0",
|
||||
"commit": "c833e9d8ff56d6c66e2a21ed7f27ad840f159a8b"
|
||||
},
|
||||
"_source": "https://github.com/lorenzofox3/lrInfiniteScroll.git",
|
||||
"_target": "~1.0.0",
|
||||
"_originalSource": "lrInfiniteScroll"
|
||||
}
|
||||
2
awx/ui/client/lib/lrInfiniteScroll/index.js
Normal file
2
awx/ui/client/lib/lrInfiniteScroll/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
require('./lrInfiniteScroll');
|
||||
module.exports = 'lrInfiniteScroll';
|
||||
@ -2,13 +2,12 @@
|
||||
'use strict';
|
||||
var module = ng.module('lrInfiniteScroll', []);
|
||||
|
||||
module.directive('lrInfiniteScroll', ['$log', '$timeout', function ($log, timeout) {
|
||||
module.directive('lrInfiniteScroll', ['$timeout', function (timeout) {
|
||||
return{
|
||||
link: function (scope, element, attr) {
|
||||
var
|
||||
lengthThreshold = attr.scrollThreshold || 50,
|
||||
timeThreshold = attr.timeThreshold || 400,
|
||||
direction = attr.direction || 'down',
|
||||
handler = scope.$eval(attr.lrInfiniteScroll),
|
||||
promise = null,
|
||||
lastRemaining = 9999;
|
||||
@ -20,14 +19,14 @@
|
||||
handler = ng.noop;
|
||||
}
|
||||
|
||||
$log.debug('lrInfiniteScroll: ' + attr.lrInfiniteScroll);
|
||||
|
||||
element.bind('scroll', function () {
|
||||
var remaining = (direction === 'down') ? element[0].scrollHeight - (element[0].clientHeight + element[0].scrollTop) : element[0].scrollTop;
|
||||
// if we have reached the threshold and we scroll down
|
||||
if ((direction === 'down' && remaining < lengthThreshold && (remaining - lastRemaining) < 0) ||
|
||||
direction === 'up' && remaining < lengthThreshold) {
|
||||
//if there is already a timer running which has not expired yet we have to cancel it and restart the timer
|
||||
var
|
||||
remaining = element[0].scrollHeight - (element[0].clientHeight + element[0].scrollTop);
|
||||
|
||||
//if we have reached the threshold and we scroll down
|
||||
if (remaining < lengthThreshold && (remaining - lastRemaining) < 0) {
|
||||
|
||||
//if there is already a timer running which has no expired yet we have to cancel it and restart the timer
|
||||
if (promise !== null) {
|
||||
timeout.cancel(promise);
|
||||
}
|
||||
|
||||
@ -15,13 +15,13 @@
|
||||
multiSelectExtended: true,
|
||||
index: false,
|
||||
hover: true,
|
||||
|
||||
emptyListText : 'No Teams exist',
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
label: 'name'
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
multiSelectExtended: true,
|
||||
index: false,
|
||||
hover: true,
|
||||
emptyListText : 'No Users exist',
|
||||
|
||||
fields: {
|
||||
first_name: {
|
||||
|
||||
@ -23,7 +23,7 @@ export default
|
||||
return i.role;
|
||||
}))
|
||||
.filter((role) => {
|
||||
return !!attrs.teamRoleList == !!role.team_id;
|
||||
return Boolean(attrs.teamRoleList) === Boolean(role.team_id);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.name
|
||||
|
||||
@ -79,6 +79,7 @@ __deferLoadIfEnabled();
|
||||
|
||||
var tower = angular.module('Tower', [
|
||||
//'ngAnimate',
|
||||
'lrInfiniteScroll',
|
||||
'ngSanitize',
|
||||
'ngCookies',
|
||||
about.name,
|
||||
@ -269,7 +270,7 @@ var tower = angular.module('Tower', [
|
||||
}).
|
||||
|
||||
state('projects', {
|
||||
url: '/projects',
|
||||
url: '/projects?{status}',
|
||||
templateUrl: urlPrefix + 'partials/projects.html',
|
||||
controller: ProjectsList,
|
||||
data: {
|
||||
@ -297,6 +298,10 @@ var tower = angular.module('Tower', [
|
||||
controller: ProjectsEdit,
|
||||
data: {
|
||||
activityStreamId: 'id'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'projects',
|
||||
label: 'EDIT PROJECT'
|
||||
}
|
||||
}).
|
||||
state('projectOrganizations', {
|
||||
@ -340,6 +345,10 @@ var tower = angular.module('Tower', [
|
||||
controller: TeamsEdit,
|
||||
data: {
|
||||
activityStreamId: 'team_id'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: "teams",
|
||||
label: "EDIT TEAM"
|
||||
}
|
||||
}).
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ export default
|
||||
var licenseInfo = FeaturesService.getLicenseInfo();
|
||||
scope.licenseType = licenseInfo ? licenseInfo.license_type : null;
|
||||
if (!licenseInfo) {
|
||||
console.warn("License info not loaded correctly");
|
||||
console.warn("License info not loaded correctly"); // jshint ignore:line
|
||||
}
|
||||
})
|
||||
.catch(function (response) {
|
||||
|
||||
@ -22,7 +22,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $stateParams,
|
||||
Wait('start');
|
||||
|
||||
var list = ProjectList,
|
||||
defaultUrl = GetBasePath('projects'),
|
||||
defaultUrl = GetBasePath('projects') + ($stateParams.status ? '?status=' + $stateParams.status : ''),
|
||||
view = GenerateList,
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
mode = (base === 'projects') ? 'edit' : 'select',
|
||||
@ -495,6 +495,35 @@ export function ProjectsAdd(Refresh, $scope, $rootScope, $compile, $location, $l
|
||||
$scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false;
|
||||
$scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch';
|
||||
}
|
||||
|
||||
// Dynamically update popover values
|
||||
if($scope.scm_type.value) {
|
||||
switch ($scope.scm_type.value) {
|
||||
case 'git':
|
||||
$scope.urlPopover = '<p>Example URLs for GIT SCM include:</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
|
||||
'<li>git@github.com:ansible/ansible.git</li><li>git://servername.example.com/ansible.git</li></ul>' +
|
||||
'<p><strong>Note:</strong> When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' +
|
||||
'do not enter a username (other than git). Additionally, GitHub and Bitbucket do not support password authentication when using ' +
|
||||
'SSH. GIT read only protocol (git://) does not use username or password information.';
|
||||
break;
|
||||
case 'svn':
|
||||
$scope.urlPopover = '<p>Example URLs for Subversion SCM include:</p>' +
|
||||
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
|
||||
'<li>svn+ssh://servername.example.com/path</li></ul>';
|
||||
break;
|
||||
case 'hg':
|
||||
$scope.urlPopover = '<p>Example URLs for Mercurial SCM include:</p>' +
|
||||
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
|
||||
'<li>ssh://server.example.com/path</li></ul>' +
|
||||
'<p><strong>Note:</strong> Mercurial does not support password authentication for SSH. ' +
|
||||
'Do not put the username and key in the URL. ' +
|
||||
'If using Bitbucket and SSH, do not supply your Bitbucket username.';
|
||||
break;
|
||||
default:
|
||||
$scope.urlPopover = '<p> URL popover text';
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
$scope.formCancel = function () {
|
||||
|
||||
@ -162,7 +162,7 @@ export function UsersAdd($scope, $rootScope, $compile, $location, $log,
|
||||
generator.reset();
|
||||
|
||||
$scope.user_type_options = user_type_options;
|
||||
$scope.user_type = user_type_options[0]
|
||||
$scope.user_type = user_type_options[0];
|
||||
$scope.$watch('user_type', user_type_sync($scope));
|
||||
|
||||
CreateSelect2({
|
||||
@ -271,7 +271,7 @@ export function UsersEdit($scope, $rootScope, $location,
|
||||
generator.reset();
|
||||
|
||||
$scope.user_type_options = user_type_options;
|
||||
$scope.user_type = user_type_options[0]
|
||||
$scope.user_type = user_type_options[0];
|
||||
$scope.$watch('user_type', user_type_sync($scope));
|
||||
|
||||
var setScopeFields = function(data){
|
||||
|
||||
@ -49,7 +49,7 @@ export default
|
||||
label: "Inventories",
|
||||
},
|
||||
{
|
||||
url: "/#/inventories/?inventory_sources_with_failures",
|
||||
url: "/#/inventories?status=sync-failed",
|
||||
number: scope.data.inventories.inventory_failed,
|
||||
label: "Inventory Sync Failures",
|
||||
isFailureCount: true
|
||||
@ -60,7 +60,7 @@ export default
|
||||
label: "Projects"
|
||||
},
|
||||
{
|
||||
url: "/#/projects/?status=failed",
|
||||
url: "/#/projects?status=failed",
|
||||
number: scope.data.projects.failed,
|
||||
label: "Project Sync Failures",
|
||||
isFailureCount: true
|
||||
|
||||
@ -118,6 +118,7 @@
|
||||
top: auto;
|
||||
box-shadow: none;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.DashboardGraphs-periodDropdown,
|
||||
|
||||
@ -246,7 +246,7 @@ export default
|
||||
rows: 10,
|
||||
awPopOver: "SSH key description",
|
||||
awPopOverWatch: "key_description",
|
||||
dataTitle: 'Help',
|
||||
dataTitle: 'Private Key',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
subForm: "credentialSubForm"
|
||||
|
||||
@ -81,6 +81,11 @@ export default
|
||||
},
|
||||
project: {
|
||||
label: 'Project',
|
||||
labelAction: {
|
||||
label: 'RESET',
|
||||
ngClick: 'resetProjectToDefault()',
|
||||
'class': "{{!(job_type.value === 'scan' && project_name !== 'Default') ? 'hidden' : ''}}",
|
||||
},
|
||||
type: 'lookup',
|
||||
sourceModel: 'project',
|
||||
sourceField: 'name',
|
||||
@ -99,6 +104,7 @@ export default
|
||||
label: 'Playbook',
|
||||
type:'select',
|
||||
ngOptions: 'book for book in playbook_options track by book',
|
||||
ngDisabled: "job_type.value === 'scan' && project_name === 'Default'",
|
||||
id: 'playbook-select',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "playbookrequired",
|
||||
@ -110,12 +116,6 @@ export default
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
},
|
||||
default_scan: {
|
||||
type: 'custom',
|
||||
column: 1,
|
||||
ngShow: 'job_type.value === "scan" && project_name !== "Default"',
|
||||
control: '<a href="" ng-click="toggleScanInfo()">Reset to default project and playbook</a>'
|
||||
},
|
||||
credential: {
|
||||
label: 'Machine Credential',
|
||||
type: 'lookup',
|
||||
@ -204,7 +204,7 @@ export default
|
||||
},
|
||||
job_tags: {
|
||||
label: 'Job Tags',
|
||||
type: 'textarea',
|
||||
type: 'text',
|
||||
rows: 1,
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
|
||||
@ -64,11 +64,10 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
ngChange: 'scmChange()',
|
||||
addRequired: true,
|
||||
editRequired: true,
|
||||
hasSubForm: true
|
||||
hasSubForm: true,
|
||||
},
|
||||
missing_path_alert: {
|
||||
type: 'alertblock',
|
||||
"class": 'alert-info',
|
||||
ngShow: "showMissingPlaybooksAlert && scm_type.value == 'manual'",
|
||||
alertTxt: '<p class=\"text-justify\"><strong>WARNING:</strong> There are no available playbook directories in {{ base_dir }}. ' +
|
||||
'Either that directory is empty, or all of the contents are already assigned to other projects. ' +
|
||||
@ -79,7 +78,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
base_dir: {
|
||||
label: 'Project Base Path',
|
||||
type: 'text',
|
||||
//"class": 'col-lg-6',
|
||||
class: 'Form-textUneditable',
|
||||
showonly: true,
|
||||
ngShow: "scm_type.value == 'manual' " ,
|
||||
awPopOver: '<p>Base path used for locating playbooks. Directories found inside this path will be listed in the playbook directory drop-down. ' +
|
||||
@ -115,30 +114,12 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
init: false
|
||||
},
|
||||
subForm: 'sourceSubForm',
|
||||
helpCollapse: [{
|
||||
hdr: 'GIT URLs',
|
||||
content: '<p>Example URLs for GIT SCM include:</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
|
||||
'<li>git@github.com:ansible/ansible.git</li><li>git://servername.example.com/ansible.git</li></ul>' +
|
||||
'<p><strong>Note:</strong> When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' +
|
||||
'do not enter a username (other than git). Additionally, GitHub and Bitbucket do not support password authentication when using ' +
|
||||
'SSH. GIT read only protocol (git://) does not use username or password information.',
|
||||
show: "scm_type.value == 'git'"
|
||||
}, {
|
||||
hdr: 'SVN URLs',
|
||||
content: '<p>Example URLs for Subversion SCM include:</p>' +
|
||||
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
|
||||
'<li>svn+ssh://servername.example.com/path</li></ul>',
|
||||
show: "scm_type.value == 'svn'"
|
||||
}, {
|
||||
hdr: 'Mercurial URLs',
|
||||
content: '<p>Example URLs for Mercurial SCM include:</p>' +
|
||||
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
|
||||
'<li>ssh://server.example.com/path</li></ul>' +
|
||||
'<p><strong>Note:</strong> Mercurial does not support password authentication for SSH. ' +
|
||||
'Do not put the username and key in the URL. ' +
|
||||
'If using Bitbucket and SSH, do not supply your Bitbucket username.',
|
||||
show: "scm_type.value == 'hg'"
|
||||
}],
|
||||
hideSubForm: "scm_type.value === 'manual'",
|
||||
awPopOverWatch: "urlPopover",
|
||||
awPopOver: "set in controllers/projects",
|
||||
dataTitle: 'SCM URL',
|
||||
dataContainer: 'body',
|
||||
dataPlacement: 'right'
|
||||
},
|
||||
scm_branch: {
|
||||
labelBind: "scmBranchLabel",
|
||||
@ -174,7 +155,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
dataTitle: 'SCM Clean',
|
||||
dataContainer: 'body',
|
||||
dataPlacement: 'right',
|
||||
labelClass: 'checkbox-options'
|
||||
labelClass: 'checkbox-options stack-inline'
|
||||
}, {
|
||||
name: 'scm_delete_on_update',
|
||||
label: 'Delete on Update',
|
||||
@ -186,7 +167,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
dataTitle: 'SCM Delete',
|
||||
dataContainer: 'body',
|
||||
dataPlacement: 'right',
|
||||
labelClass: 'checkbox-options'
|
||||
labelClass: 'checkbox-options stack-inline'
|
||||
}, {
|
||||
name: 'scm_update_on_launch',
|
||||
label: 'Update on Launch',
|
||||
@ -197,7 +178,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
dataTitle: 'SCM Update',
|
||||
dataContainer: 'body',
|
||||
dataPlacement: 'right',
|
||||
labelClass: 'checkbox-options'
|
||||
labelClass: 'checkbox-options stack-inline'
|
||||
}]
|
||||
},
|
||||
scm_update_cache_timeout: {
|
||||
@ -273,7 +254,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
||||
}
|
||||
},
|
||||
notifications: {
|
||||
include: "NotificationsList"
|
||||
include: "NotificationsList",
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -132,9 +132,10 @@ export default
|
||||
"delete": {
|
||||
label: 'Remove',
|
||||
ngClick: 'deletePermissionFromTeam(team_id, team_obj.name, role.name, role.summary_fields.resource_name, role.related.teams)',
|
||||
class: "List-actionButton--delete",
|
||||
'class': "List-actionButton--delete",
|
||||
iconClass: 'fa fa-times',
|
||||
awToolTip: 'Dissasociate permission from team'
|
||||
awToolTip: 'Dissasociate permission from team',
|
||||
dataPlacement: 'top'
|
||||
}
|
||||
},
|
||||
hideOnSuperuser: true
|
||||
|
||||
@ -138,7 +138,6 @@ export default
|
||||
iterator: 'team',
|
||||
open: false,
|
||||
index: false,
|
||||
|
||||
actions: {},
|
||||
|
||||
fields: {
|
||||
|
||||
@ -784,7 +784,6 @@ export default
|
||||
url, play;
|
||||
|
||||
scope.tasks = [];
|
||||
|
||||
if (scope.selectedPlay) {
|
||||
url = scope.job.url + 'job_tasks/?event_id=' + scope.selectedPlay;
|
||||
url += (scope.search_task_name) ? '&task__icontains=' + scope.search_task_name : '';
|
||||
@ -912,16 +911,25 @@ export default
|
||||
scope.tasks[idx].taskActiveClass = '';
|
||||
}
|
||||
});
|
||||
params = {
|
||||
parent: scope.selectedTask,
|
||||
event__startswith: 'runner',
|
||||
page_size: scope.hostResultsMaxRows,
|
||||
order: 'host_name,counter',
|
||||
};
|
||||
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
|
||||
scope.hostResults = JobDetailService.processHostEvents(res.results);
|
||||
if (scope.selectedTask !== null){
|
||||
params = {
|
||||
parent: scope.selectedTask,
|
||||
event__startswith: 'runner',
|
||||
page_size: scope.hostResultsMaxRows,
|
||||
order: 'host_name,counter',
|
||||
};
|
||||
if (scope.search_host_status === 'failed'){
|
||||
params.failed = true;
|
||||
}
|
||||
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
|
||||
scope.hostResults = JobDetailService.processHostEvents(res.results);
|
||||
scope.hostResultsLoading = false;
|
||||
});
|
||||
}
|
||||
else{
|
||||
scope.hostResults = [];
|
||||
scope.hostResultsLoading = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
|
||||
@ -934,7 +942,7 @@ export default
|
||||
graph_data.push({
|
||||
label: 'OK',
|
||||
value: count.ok.length,
|
||||
color: '#60D66F'
|
||||
color: '#5CB85C'
|
||||
});
|
||||
}
|
||||
if (count.changed.length > 0) {
|
||||
@ -979,18 +987,19 @@ export default
|
||||
job_detail_chart = nv.models.pieChart()
|
||||
.margin({bottom: 15})
|
||||
.x(function(d) {
|
||||
return d.label +': '+ Math.round((d.value/total)*100) + "%";
|
||||
return d.label +': '+ Math.floor((d.value/total)*100) + "%";
|
||||
})
|
||||
.y(function(d) { return d.value; })
|
||||
.showLabels(true)
|
||||
.showLegend(false)
|
||||
.showLabels(false)
|
||||
.showLegend(true)
|
||||
.growOnHover(false)
|
||||
.labelThreshold(0.01)
|
||||
.tooltipContent(function(x, y) {
|
||||
return '<p>'+x+'</p>'+ '<p>' + Math.floor(y.replace(',','')) + ' HOSTS ' + '</p>';
|
||||
})
|
||||
.color(colors);
|
||||
|
||||
job_detail_chart.legend.rightAlign(false);
|
||||
job_detail_chart.legend.margin({top: 5, right: 450, left:0, bottom: 0});
|
||||
d3.select(element.find('svg')[0])
|
||||
.datum(dataset)
|
||||
.transition().duration(350)
|
||||
@ -1000,19 +1009,15 @@ export default
|
||||
"font-style": "normal",
|
||||
"font-weight":400,
|
||||
"src": "url(/static/assets/OpenSans-Regular.ttf)",
|
||||
"width": 500,
|
||||
"width": 600,
|
||||
"height": 300,
|
||||
"color": '#848992'
|
||||
});
|
||||
|
||||
d3.select(element.find(".nv-label text")[0])
|
||||
.attr("class", "HostSummary-graph--successful")
|
||||
d3.select(element.find(".nv-noData")[0])
|
||||
.style({
|
||||
"font-family": 'Open Sans',
|
||||
"font-size": "16px",
|
||||
"text-transform" : "uppercase",
|
||||
"fill" : colors[0],
|
||||
"src": "url(/static/assets/OpenSans-Regular.ttf)"
|
||||
"text-anchor": 'start'
|
||||
});
|
||||
/*
|
||||
d3.select(element.find(".nv-label text")[1])
|
||||
.attr("class", "HostSummary-graph--changed")
|
||||
.style({
|
||||
@ -1040,6 +1045,7 @@ export default
|
||||
"fill" : colors[3],
|
||||
"src": "url(/static/assets/OpenSans-Regular.ttf)"
|
||||
});
|
||||
*/
|
||||
return job_detail_chart;
|
||||
};
|
||||
}])
|
||||
|
||||
@ -177,7 +177,7 @@ angular.module('JobTemplatesHelper', ['Utilities'])
|
||||
});
|
||||
|
||||
|
||||
if(scope.project === "" && scope.playbook === ""){
|
||||
if (scope.project === "" && scope.playbook === "") {
|
||||
scope.toggleScanInfo();
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ export default
|
||||
angular.module('SchedulesHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', 'SearchHelper', 'PaginationHelpers', listGenerator.name, 'ModalDialog',
|
||||
'GeneratorHelpers'])
|
||||
|
||||
.factory('EditSchedule', ['SchedulerInit', '$rootScope', 'Wait', 'Rest',
|
||||
.factory('EditSchedule', ['SchedulerInit', '$rootScope', 'Wait', 'Rest',
|
||||
'ProcessErrors', 'GetBasePath', 'SchedulePost', '$state',
|
||||
function(SchedulerInit, $rootScope, Wait, Rest, ProcessErrors,
|
||||
GetBasePath, SchedulePost, $state) {
|
||||
@ -176,7 +176,7 @@ export default
|
||||
};
|
||||
}])
|
||||
|
||||
.factory('AddSchedule', ['$location', '$rootScope', '$stateParams',
|
||||
.factory('AddSchedule', ['$location', '$rootScope', '$stateParams',
|
||||
'SchedulerInit', 'Wait', 'GetBasePath', 'Empty', 'SchedulePost', '$state', 'Rest', 'ProcessErrors',
|
||||
function($location, $rootScope, $stateParams, SchedulerInit,
|
||||
Wait, GetBasePath, Empty, SchedulePost, $state, Rest,
|
||||
@ -295,8 +295,7 @@ export default
|
||||
}])
|
||||
|
||||
.factory('SchedulePost', ['Rest', 'ProcessErrors', 'RRuleToAPI', 'Wait',
|
||||
'ToJSON',
|
||||
function(Rest, ProcessErrors, RRuleToAPI, Wait, ToJSON) {
|
||||
function(Rest, ProcessErrors, RRuleToAPI, Wait) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
url = params.url,
|
||||
@ -326,8 +325,8 @@ export default
|
||||
schedule.extra_data = JSON.stringify(extra_vars);
|
||||
}
|
||||
else if(scope.extraVars){
|
||||
schedule.extra_data = scope.parseType === 'yaml' ?
|
||||
(scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars;
|
||||
schedule.extra_data = scope.parseType === 'yaml' ?
|
||||
(scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars;
|
||||
}
|
||||
Rest.setUrl(url);
|
||||
if (mode === 'add') {
|
||||
|
||||
@ -17,7 +17,7 @@ function InventoriesList($scope, $rootScope, $location, $log,
|
||||
Find, Empty, $state) {
|
||||
|
||||
var list = InventoryList,
|
||||
defaultUrl = GetBasePath('inventory'),
|
||||
defaultUrl = GetBasePath('inventory') + ($stateParams.status === 'sync-failed' ? '?not__inventory_sources_with_failures=0' : ''),
|
||||
view = generateList,
|
||||
paths = $location.path().replace(/^\//, '').split('/'),
|
||||
mode = (paths[0] === 'inventories') ? 'edit' : 'select';
|
||||
|
||||
@ -9,7 +9,7 @@ import InventoriesList from './inventory-list.controller';
|
||||
|
||||
export default {
|
||||
name: 'inventories',
|
||||
route: '/inventories',
|
||||
route: '/inventories?{status}',
|
||||
templateUrl: templateUrl('inventories/inventories'),
|
||||
controller: InventoriesList,
|
||||
data: {
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
group_id: id
|
||||
});
|
||||
};
|
||||
$scope.showFailedHosts = function(x, y, z){
|
||||
$scope.showFailedHosts = function() {
|
||||
$state.go('inventoryManage', {failed: true}, {reload: true});
|
||||
};
|
||||
$scope.scheduleGroup = function(id) {
|
||||
@ -91,7 +91,7 @@
|
||||
$scope.$parent.groupsSelected = selection.length > 0 ? true : false;
|
||||
$scope.$parent.groupsSelectedItems = selection.selectedItems;
|
||||
});
|
||||
$scope.$on('PostRefresh', () =>{
|
||||
$scope.$on('PostRefresh', () => {
|
||||
$scope.groups.forEach( (group, index) => {
|
||||
var group_status, hosts_status;
|
||||
group_status = GetSyncStatusMsg({
|
||||
|
||||
@ -8,7 +8,7 @@ import {ManageHostsAdd, ManageHostsEdit} from './hosts.route';
|
||||
|
||||
export default
|
||||
angular.module('manageHosts', [])
|
||||
.run(['$stateExtender', '$state', function($stateExtender, $state){
|
||||
.run(['$stateExtender', function($stateExtender){
|
||||
$stateExtender.addState(ManageHostsAdd);
|
||||
$stateExtender.addState(ManageHostsEdit);
|
||||
}]);
|
||||
|
||||
@ -61,7 +61,7 @@ export default
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
}
|
||||
}
|
||||
$scope.canEdit = data['script'] !== null;
|
||||
$scope.canEdit = data.script !== null;
|
||||
if (!$scope.canEdit) {
|
||||
$scope.script = "Script contents hidden";
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
|
||||
<div id="hosts-summary-section" class="section">
|
||||
<div class="JobDetail-instructions"><span class="badge">4</span> Please select a host below to view a summary of all associated tasks.</div>
|
||||
<div class="JobDetail-instructions" ng-hide="hosts.length == 0"><span class="badge">4</span> Please select a host below to view a summary of all associated tasks.</div>
|
||||
<div class="JobDetail-searchHeaderRow" ng-hide="hosts.length == 0">
|
||||
<div class="JobDetail-searchContainer form-group">
|
||||
<div class="search-name">
|
||||
@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<div class="JobDetail-tableToggleContainer form-group">
|
||||
<div class="btn-group" >
|
||||
<button
|
||||
<button
|
||||
ng-click="setFilter('all')"
|
||||
class="JobDetail-tableToggle btn btn-xs" ng-class="{'btn-default': filter === 'failed', 'btn-primary': filter === 'all'}">All</button>
|
||||
<button ng-click="setFilter('failed')"
|
||||
@ -34,7 +34,7 @@
|
||||
</div>
|
||||
|
||||
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="getNextPage" scroll-threshold="10" time-threshold="500">
|
||||
<table class="table">
|
||||
<table class="table" ng-class="{'JobDetails-table--noResults': hosts.length === 0}">
|
||||
<tbody>
|
||||
<tr class="List-tableRow" ng-repeat="host in hosts track by $index" id="{{ host.id }}" ng-class-even="'List-tableRow--evenRow'" ng-class-odd="'List-tableRow--oddRow'">
|
||||
<td class="List-tableCell name col-lg-6 col-md-6 col-sm-6 col-xs-6">
|
||||
|
||||
@ -13,9 +13,15 @@
|
||||
.JobDetail-instructions{
|
||||
color: @default-interface-txt;
|
||||
margin: 10px 0 10px 0;
|
||||
|
||||
.badge {
|
||||
background-color: @default-list-header-bg;
|
||||
color: @default-interface-txt;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
}
|
||||
.JobDetail{
|
||||
.OnePlusOne-container(100%, @breakpoint-md);
|
||||
.OnePlusOne-container(100%, @breakpoint-md);
|
||||
}
|
||||
|
||||
.JobDetail-leftSide{
|
||||
@ -151,8 +157,25 @@
|
||||
background-color: @default-link;
|
||||
border: 1px solid @default-link;
|
||||
color: @default-bg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @default-link-hov;
|
||||
}
|
||||
}
|
||||
.JobDetail .nvd3.nv-noData{
|
||||
color: @default-interface-txt;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
.JobDetail .nv-series{
|
||||
padding-right: 30px;
|
||||
display: block;
|
||||
}
|
||||
.JobDetail-instructions .badge{
|
||||
background-color: @default-list-header-bg;
|
||||
color: @default-interface-txt;
|
||||
}
|
||||
.JobDetail-tableToggle--left{
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
@ -185,6 +208,12 @@
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.JobDetails-table--noResults {
|
||||
tr > td {
|
||||
border-top: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.JobDetail-statusIcon--results{
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
@ -196,7 +225,13 @@
|
||||
}
|
||||
|
||||
.JobDetail-stdoutActionButton--active{
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
flex:none;
|
||||
width:0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.JobDetail-leftSide.JobDetail-stdoutActionButton--active {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
@ -673,21 +673,9 @@ export default
|
||||
|
||||
scope.lessStatus = false; // close the view more status option
|
||||
|
||||
// Detail table height adjusting. First, put page height back to 'normal'.
|
||||
$('#plays-table-detail').height(80);
|
||||
//$('#plays-table-detail').mCustomScrollbar("update");
|
||||
// $('#tasks-table-detail').height(120);
|
||||
//$('#tasks-table-detail').mCustomScrollbar("update");
|
||||
$('#hosts-table-detail').height(150);
|
||||
//$('#hosts-table-detail').mCustomScrollbar("update");
|
||||
|
||||
height = $(window).height() - $('#main-menu-container .navbar').outerHeight() -
|
||||
$('#job-detail-container').outerHeight() - 20;
|
||||
if (height > 15) {
|
||||
// there's a bunch of white space at the bottom, let's use it
|
||||
$('#plays-table-detail').height(80 + (height * 0.10));
|
||||
$('#tasks-table-detail').height(120 + (height * 0.20));
|
||||
$('#hosts-table-detail').height(150 + (height * 0.10));
|
||||
}
|
||||
scope.$emit('RefreshCompleted');
|
||||
};
|
||||
|
||||
@ -776,6 +764,15 @@ export default
|
||||
}
|
||||
};
|
||||
|
||||
scope.filterTaskStatus = function() {
|
||||
scope.search_task_status = (scope.search_task_status === 'all') ? 'failed' : 'all';
|
||||
if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
|
||||
LoadTasks({
|
||||
scope: scope
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
scope.filterPlayStatus = function() {
|
||||
scope.search_play_status = (scope.search_play_status === 'all') ? 'failed' : 'all';
|
||||
if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
|
||||
@ -785,6 +782,26 @@ export default
|
||||
}
|
||||
};
|
||||
|
||||
scope.filterHostStatus = function(){
|
||||
scope.search_host_status = (scope.search_host_status === 'all') ? 'failed' : 'all';
|
||||
if (!scope.liveEventProcessing || scope.pauseLiveEvents){
|
||||
if (scope.selectedTask !== null && scope.selectedPlay !== null){
|
||||
var params = {
|
||||
parent: scope.selectedTask,
|
||||
page_size: scope.hostResultsMaxRows,
|
||||
order: 'host_name,counter',
|
||||
};
|
||||
if (scope.search_host_status === 'failed'){
|
||||
params.failed = true;
|
||||
}
|
||||
JobDetailService.getRelatedJobEvents(scope.job.id, params).success(function(res){
|
||||
scope.hostResults = JobDetailService.processHostEvents(res.results);
|
||||
scope.hostResultsLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scope.searchPlays = function() {
|
||||
if (scope.search_play_name) {
|
||||
scope.searchPlaysEnabled = false;
|
||||
|
||||
@ -189,7 +189,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div id="plays-table-header" class="table-header">
|
||||
<div id="plays-table-header" class="table-header" ng-show="plays.length !== 0">
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -202,7 +202,7 @@
|
||||
</div>
|
||||
<div id="plays-table-detail" class="table-detail" lr-infinite-scroll="playsScrollDown"
|
||||
scroll-threshold="10" time-threshold="500">
|
||||
<table class="table">
|
||||
<table class="table" ng-class="{'JobDetails-table--noResults': plays.length === 0}">
|
||||
<tbody>
|
||||
<tr class="List-tableRow cursor-pointer" ng-repeat="play in plays" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-class="play.playActiveClass" ng-click="selectPlay(play.id, $event)">
|
||||
<td class="List-tableCell col-lg-7 col-md-6 col-sm-6 col-xs-4 status-column" aw-tool-tip="{{ play.status_tip }}" data-tip-watch="play.status_tip" data-placement="top"><i class="JobDetail-statusIcon fa icon-job-{{ play.status }}"></i>{{ play.name }}</td>
|
||||
@ -248,7 +248,7 @@
|
||||
</div>
|
||||
|
||||
<div class="table-header">
|
||||
<table id="tasks-table-header" class="table table-condensed">
|
||||
<table id="tasks-table-header" class="table table-condensed" ng-show="taskList.length !== 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="List-tableHeader col-lg-3 col-md-3 col-sm-6 col-xs-4">Tasks</th>
|
||||
@ -261,7 +261,7 @@
|
||||
</div>
|
||||
<div id="tasks-table-detail" class="table-detail" lr-infinite-scroll="tasksScrollDown"
|
||||
scroll-threshold="10" time-threshold="500">
|
||||
<table class="table">
|
||||
<table class="table" ng-class="{'JobDetails-table--noResults': taskList.length === 0}">
|
||||
<tbody>
|
||||
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="task in taskList = (tasks) track by $index" ng-class="task.taskActiveClass" ng-click="selectTask(task.id)">
|
||||
<td class="List-tableCell col-lg-3 col-md-3 col-sm-6 col-xs-4 status-column" aw-tool-tip="{{ task.status_tip }}"
|
||||
@ -331,7 +331,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-header" id="hosts-table-header">
|
||||
<table class="table table-condensed">
|
||||
<table class="table table-condensed" ng-show="results.length !== 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="List-tableHeader col-lg-4 col-md-3 col-sm-3 col-xs-3">Hosts</th>
|
||||
@ -343,7 +343,7 @@
|
||||
</div>
|
||||
|
||||
<div id="hosts-table-detail" class="table-detail" lr-infinite-scroll="hostResultsScrollDown" scroll-threshold="10" time-threshold="500">
|
||||
<table class="table">
|
||||
<table class="table" ng-class="{'JobDetails-table--noResults': results.length === 0}">
|
||||
<tbody>
|
||||
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="result in results = (hostResults) track by $index">
|
||||
<td class="List-tableCell col-lg-4 col-md-3 col-sm-3 col-xs-3 status-column">
|
||||
@ -404,7 +404,7 @@
|
||||
<button class="StandardOut-actionButton" aw-tool-tip="Toggle Output" data-placement="top" ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}" ng-click="toggleStdoutFullscreen()">
|
||||
<i class="fa fa-arrows-alt"></i>
|
||||
</button>
|
||||
<a ng-show="job_status.status === ('failed' || 'successful')" href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">
|
||||
<a ng-show="job_status.status === 'failed' || job_status.status === 'successful' || job_status.status === 'canceled'" href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download&token={{ token }}">
|
||||
<button class="StandardOut-actionButton" aw-tool-tip="Download Output" data-placement="top">
|
||||
<i class="fa fa-download"></i>
|
||||
</button>
|
||||
|
||||
@ -72,6 +72,7 @@
|
||||
jQuery.extend(true, CloudCredentialList, CredentialList);
|
||||
CloudCredentialList.name = 'cloudcredentials';
|
||||
CloudCredentialList.iterator = 'cloudcredential';
|
||||
CloudCredentialList.basePath = '/api/v1/credentials?cloud=true';
|
||||
|
||||
SurveyControllerInit({
|
||||
scope: $scope,
|
||||
@ -196,12 +197,20 @@
|
||||
});
|
||||
});
|
||||
|
||||
function sync_playbook_select2() {
|
||||
CreateSelect2({
|
||||
element:'#playbook-select',
|
||||
multiple: false
|
||||
});
|
||||
}
|
||||
|
||||
// Update playbook select whenever project value changes
|
||||
selectPlaybook = function (oldValue, newValue) {
|
||||
var url;
|
||||
if($scope.job_type.value === 'scan' && $scope.project_name === "Default"){
|
||||
$scope.playbook_options = ['Default'];
|
||||
$scope.playbook = 'Default';
|
||||
sync_playbook_select2();
|
||||
Wait('stop');
|
||||
}
|
||||
else if (oldValue !== newValue) {
|
||||
@ -216,6 +225,7 @@
|
||||
opts.push(data[i]);
|
||||
}
|
||||
$scope.playbook_options = opts;
|
||||
sync_playbook_select2();
|
||||
Wait('stop');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
@ -226,32 +236,37 @@
|
||||
}
|
||||
};
|
||||
|
||||
$scope.jobTypeChange = function(){
|
||||
if($scope.job_type){
|
||||
if($scope.job_type.value === 'scan'){
|
||||
// If the job_type is 'scan' then we don't want the user to be
|
||||
// able to prompt for job type or inventory
|
||||
$scope.ask_job_type_on_launch = false;
|
||||
$scope.ask_inventory_on_launch = false;
|
||||
$scope.toggleScanInfo();
|
||||
}
|
||||
else if($scope.project_name === "Default"){
|
||||
$scope.project_name = null;
|
||||
$scope.playbook_options = [];
|
||||
// $scope.playbook = 'null';
|
||||
$scope.job_templates_form.playbook.$setPristine();
|
||||
}
|
||||
}
|
||||
let last_non_scan_project_name = null;
|
||||
let last_non_scan_playbook = "";
|
||||
let last_non_scan_playbook_options = [];
|
||||
$scope.jobTypeChange = function() {
|
||||
if ($scope.job_type) {
|
||||
if ($scope.job_type.value === 'scan') {
|
||||
if ($scope.project_name !== "Default") {
|
||||
last_non_scan_project_name = $scope.project_name;
|
||||
last_non_scan_playbook = $scope.playbook;
|
||||
last_non_scan_playbook_options = $scope.playbook_options;
|
||||
}
|
||||
// If the job_type is 'scan' then we don't want the user to be
|
||||
// able to prompt for job type or inventory
|
||||
$scope.ask_job_type_on_launch = false;
|
||||
$scope.ask_inventory_on_launch = false;
|
||||
$scope.resetProjectToDefault();
|
||||
}
|
||||
else if ($scope.project_name === "Default") {
|
||||
$scope.project_name = last_non_scan_project_name;
|
||||
$scope.playbook_options = last_non_scan_playbook_options;
|
||||
$scope.playbook = last_non_scan_playbook;
|
||||
$scope.job_templates_form.playbook.$setPristine();
|
||||
}
|
||||
}
|
||||
sync_playbook_select2();
|
||||
};
|
||||
|
||||
$scope.toggleScanInfo = function() {
|
||||
$scope.resetProjectToDefault = function() {
|
||||
$scope.project_name = 'Default';
|
||||
if($scope.project === null){
|
||||
selectPlaybook();
|
||||
}
|
||||
else {
|
||||
$scope.project = null;
|
||||
}
|
||||
$scope.project = null;
|
||||
selectPlaybook('force_load');
|
||||
};
|
||||
|
||||
// Detect and alert user to potential SCM status issues
|
||||
|
||||
@ -76,6 +76,13 @@ export default
|
||||
$scope.playbook = null;
|
||||
generator.reset();
|
||||
|
||||
function sync_playbook_select2() {
|
||||
CreateSelect2({
|
||||
element:'#playbook-select',
|
||||
multiple: false
|
||||
});
|
||||
}
|
||||
|
||||
getPlaybooks = function (project) {
|
||||
var url;
|
||||
if ($scope.playbook) {
|
||||
@ -85,6 +92,7 @@ export default
|
||||
if($scope.job_type.value === 'scan' && $scope.project_name === "Default"){
|
||||
$scope.playbook_options = ['Default'];
|
||||
$scope.playbook = 'Default';
|
||||
sync_playbook_select2();
|
||||
Wait('stop');
|
||||
}
|
||||
else if (!Empty(project)) {
|
||||
@ -93,14 +101,14 @@ export default
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
var i;
|
||||
$scope.playbook_options = [];
|
||||
for (i = 0; i < data.length; i++) {
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
$scope.playbook_options.push(data[i]);
|
||||
if (data[i] === $scope.playbook) {
|
||||
$scope.job_templates_form.playbook.$setValidity('required', true);
|
||||
}
|
||||
}
|
||||
sync_playbook_select2();
|
||||
if ($scope.playbook) {
|
||||
$scope.$emit('jobTemplateLoadFinished');
|
||||
} else {
|
||||
@ -108,7 +116,7 @@ export default
|
||||
}
|
||||
})
|
||||
.error(function (ret,status_code) {
|
||||
if (status_code == 403) {
|
||||
if (status_code === 403) {
|
||||
/* user doesn't have access to see the project, no big deal. */
|
||||
} else {
|
||||
Alert('Missing Playbooks', 'Unable to retrieve the list of playbooks for this project. Choose a different ' +
|
||||
@ -122,23 +130,31 @@ export default
|
||||
}
|
||||
};
|
||||
|
||||
$scope.jobTypeChange = function(){
|
||||
if($scope.job_type){
|
||||
if($scope.job_type.value === 'scan'){
|
||||
// If the job_type is 'scan' then we don't want the user to be
|
||||
// able to prompt for job type or inventory
|
||||
$scope.ask_job_type_on_launch = false;
|
||||
$scope.ask_inventory_on_launch = false;
|
||||
$scope.toggleScanInfo();
|
||||
}
|
||||
else if($scope.project_name === "Default"){
|
||||
$scope.project_name = null;
|
||||
$scope.playbook_options = [];
|
||||
// $scope.playbook = 'null';
|
||||
$scope.job_templates_form.playbook.$setPristine();
|
||||
}
|
||||
|
||||
}
|
||||
let last_non_scan_project_name = null;
|
||||
let last_non_scan_playbook = "";
|
||||
let last_non_scan_playbook_options = [];
|
||||
$scope.jobTypeChange = function() {
|
||||
if ($scope.job_type) {
|
||||
if ($scope.job_type.value === 'scan') {
|
||||
if ($scope.project_name !== "Default") {
|
||||
last_non_scan_project_name = $scope.project_name;
|
||||
last_non_scan_playbook = $scope.playbook;
|
||||
last_non_scan_playbook_options = $scope.playbook_options;
|
||||
}
|
||||
// If the job_type is 'scan' then we don't want the user to be
|
||||
// able to prompt for job type or inventory
|
||||
$scope.ask_job_type_on_launch = false;
|
||||
$scope.ask_inventory_on_launch = false;
|
||||
$scope.resetProjectToDefault();
|
||||
}
|
||||
else if ($scope.project_name === "Default") {
|
||||
$scope.project_name = last_non_scan_project_name;
|
||||
$scope.playbook_options = last_non_scan_playbook_options;
|
||||
$scope.playbook = last_non_scan_playbook;
|
||||
$scope.job_templates_form.playbook.$setPristine();
|
||||
}
|
||||
}
|
||||
sync_playbook_select2();
|
||||
};
|
||||
|
||||
$scope.toggleNotification = function(event, notifier_id, column) {
|
||||
@ -159,14 +175,10 @@ export default
|
||||
});
|
||||
};
|
||||
|
||||
$scope.toggleScanInfo = function() {
|
||||
$scope.resetProjectToDefault = function() {
|
||||
$scope.project_name = 'Default';
|
||||
if($scope.project === null){
|
||||
getPlaybooks();
|
||||
}
|
||||
else {
|
||||
$scope.project = null;
|
||||
}
|
||||
$scope.project = null;
|
||||
getPlaybooks();
|
||||
};
|
||||
|
||||
// Detect and alert user to potential SCM status issues
|
||||
@ -198,7 +210,7 @@ export default
|
||||
}
|
||||
})
|
||||
.error(function (data, status) {
|
||||
if (status == 403) {
|
||||
if (status === 403) {
|
||||
/* User doesn't have read access to the project, no problem. */
|
||||
} else {
|
||||
ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to get project ' + $scope.project +
|
||||
@ -288,6 +300,7 @@ export default
|
||||
jQuery.extend(true, CloudCredentialList, CredentialList);
|
||||
CloudCredentialList.name = 'cloudcredentials';
|
||||
CloudCredentialList.iterator = 'cloudcredential';
|
||||
CloudCredentialList.basePath = '/api/v1/credentials?cloud=true';
|
||||
LookUpInit({
|
||||
url: GetBasePath('credentials') + '?cloud=true',
|
||||
scope: $scope,
|
||||
|
||||
@ -20,6 +20,7 @@ export default
|
||||
index: false,
|
||||
hover: true,
|
||||
well: false,
|
||||
emptyListText: 'No completed jobs',
|
||||
|
||||
fields: {
|
||||
status: {
|
||||
|
||||
@ -15,6 +15,7 @@ export default
|
||||
index: true,
|
||||
hover: true,
|
||||
well: false,
|
||||
emptyListText: 'No schedules exist',
|
||||
|
||||
fields: {
|
||||
status: {
|
||||
|
||||
@ -157,11 +157,11 @@
|
||||
ng-href="/#/portal"
|
||||
ng-if="!licenseMissing"
|
||||
ng-class="{'is-currentRoute' : isCurrentState('portalMode'), 'is-loggedOut' : !$root.current_user.username}"
|
||||
aw-tool-tip="Portal Mode"
|
||||
aw-tool-tip="My View"
|
||||
data-placement="bottom"
|
||||
data-trigger="hover"
|
||||
data-container="body">
|
||||
<i class="MainMenu-itemImage MainMenu-itemImage--settings fa fa-columns"
|
||||
<i class="MainMenu-itemImage MainMenu-itemImage--settings fa fa-tasks"
|
||||
alt="Portal Mode">
|
||||
</i>
|
||||
</a>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
<div ui-view></div>
|
||||
<div class="tab-pane Panel" id="management_jobs">
|
||||
<div class="List-title">
|
||||
<div class="List-titleText">
|
||||
@ -7,7 +8,6 @@
|
||||
{{ mgmtCards.length }}
|
||||
</span>
|
||||
</div>
|
||||
<div ui-view></div>
|
||||
<div class="MgmtCards">
|
||||
<div class="MgmtCards-card"
|
||||
ng-repeat="card in mgmtCards track by card.id">
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
|
||||
.MgmtCards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.MgmtCards-card {
|
||||
@ -11,15 +12,14 @@
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid @default-border;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
margin-top: 20px;
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
.MgmtCards-card--selected {
|
||||
padding-left: 16px;
|
||||
border-left: 5px solid #337AB7;
|
||||
border-left: 5px solid @default-link;
|
||||
}
|
||||
|
||||
.MgmtCards-card--promptElements{
|
||||
@ -44,7 +44,6 @@
|
||||
font-weight: bold;
|
||||
color: @default-interface-txt;
|
||||
margin-bottom: 25px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@ -86,46 +85,19 @@
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 1179px) {
|
||||
.MgmtCards-card {
|
||||
width: ~"calc(25% - 15px)";
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.MgmtCards-card:nth-child(4n+4) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 901px) and (max-width: 1178px) {
|
||||
.MgmtCards-card {
|
||||
width: ~"calc(33% - 11px)";
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.MgmtCards-card:nth-child(3n+3) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 616px) and (max-width: 900px) {
|
||||
.MgmtCards-card {
|
||||
width: ~"calc(50% - 10px)";
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.MgmtCards-card:nth-child(2n+2) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 615px) {
|
||||
@media (max-width: 840px) {
|
||||
.MgmtCards-card {
|
||||
width: 100%;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 840px) and (max-width: 1240px) {
|
||||
.MgmtCards-card {
|
||||
width: ~"calc(50% - 10px)";
|
||||
}
|
||||
}
|
||||
|
||||
#prompt-for-days-facts, #prompt-for-days {
|
||||
overflow-x: hidden;
|
||||
font-family: "Open Sans";
|
||||
|
||||
@ -141,7 +141,23 @@ export default function() {
|
||||
reqExpression: "channel_required",
|
||||
init: "false"
|
||||
},
|
||||
ngShow: "notification_type.value == 'slack' || notification_type.value == 'hipchat'",
|
||||
ngShow: "notification_type.value == 'slack'",
|
||||
subForm: 'typeSubForm'
|
||||
},
|
||||
rooms: {
|
||||
label: 'Destination Channels',
|
||||
type: 'textarea',
|
||||
rows: 3,
|
||||
awPopOver: '<p>Type an option on each line. The pound symbol (#) is not required.</p>'+
|
||||
'<p>For example:<br>engineering<br>\n #support<br>\n',
|
||||
dataTitle: 'Destination Channels',
|
||||
dataPlacement: 'right',
|
||||
dataContainer: "body",
|
||||
awRequiredWhen: {
|
||||
reqExpression: "room_required",
|
||||
init: "false"
|
||||
},
|
||||
ngShow: "notification_type.value == 'hipchat'",
|
||||
subForm: 'typeSubForm'
|
||||
},
|
||||
token: {
|
||||
@ -243,8 +259,9 @@ export default function() {
|
||||
subForm: 'typeSubForm'
|
||||
},
|
||||
api_url: {
|
||||
label: 'API URL (e.g: https://mycompany.hiptchat.com)',
|
||||
label: 'API URL',
|
||||
type: 'text',
|
||||
placeholder: 'https://mycompany.hipchat.com',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "hipchat_required",
|
||||
init: "false"
|
||||
@ -264,11 +281,7 @@ export default function() {
|
||||
},
|
||||
notify: {
|
||||
label: 'Notify Channel',
|
||||
type: 'text',
|
||||
awRequiredWhen: {
|
||||
reqExpression: "hipchat_required",
|
||||
init: "false"
|
||||
},
|
||||
type: 'checkbox',
|
||||
ngShow: "notification_type.value == 'hipchat' ",
|
||||
subForm: 'typeSubForm'
|
||||
},
|
||||
|
||||
@ -13,6 +13,7 @@ export default function(){
|
||||
iterator: 'notification_template',
|
||||
index: false,
|
||||
hover: false,
|
||||
emptyListText: 'No notifications exist',
|
||||
|
||||
fields: {
|
||||
status: {
|
||||
|
||||
@ -14,6 +14,7 @@ export default function(){
|
||||
iterator: 'notification',
|
||||
index: false,
|
||||
hover: false,
|
||||
emptyListText: 'No Notifications exist',
|
||||
basePath: 'notifications',
|
||||
fields: {
|
||||
name: {
|
||||
|
||||
@ -39,7 +39,7 @@ function () {
|
||||
case 'hipchat':
|
||||
obj.tokenLabel = ' Token';
|
||||
obj.hipchat_required = true;
|
||||
obj.channel_required = true;
|
||||
obj.room_required = true;
|
||||
obj.token_required = true;
|
||||
break;
|
||||
case 'twilio':
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<div id="scheduled_jobs_link" class="Form-tab"
|
||||
ng-class="{'is-selected': schedulesSelected }"
|
||||
ng-click="toggleTab('scheduled')">
|
||||
Schedule
|
||||
Schedules
|
||||
</div>
|
||||
</div>
|
||||
<div id="jobs-tab-content" class="Form-tabSection"
|
||||
|
||||
@ -8,7 +8,7 @@ export default {
|
||||
name: 'portalMode',
|
||||
url: '/portal',
|
||||
ncyBreadcrumb: {
|
||||
label: "PORTAL MODE"
|
||||
label: "MY VIEW"
|
||||
},
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
@ -30,4 +30,4 @@ export default {
|
||||
controller: PortalModeJobsController
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -37,19 +37,28 @@ export default
|
||||
name: 'projectSchedules',
|
||||
route: '/projects/:id/schedules',
|
||||
templateUrl: templateUrl("scheduler/scheduler"),
|
||||
controller: 'schedulerController'
|
||||
controller: 'schedulerController',
|
||||
ncyBreadcrumb: {
|
||||
label: 'PROJECT SCHEDULES'
|
||||
}
|
||||
});
|
||||
$stateExtender.addState({
|
||||
name: 'projectSchedules.add',
|
||||
route: '/add',
|
||||
templateUrl: templateUrl("scheduler/schedulerForm"),
|
||||
controller: 'schedulerAddController'
|
||||
controller: 'schedulerAddController',
|
||||
ncyBreadcrumb: {
|
||||
label: 'PROJECT SCHEDULES ADD'
|
||||
}
|
||||
});
|
||||
$stateExtender.addState({
|
||||
name: 'projectSchedules.edit',
|
||||
route: '/:schedule_id',
|
||||
templateUrl: templateUrl("scheduler/schedulerForm"),
|
||||
controller: 'schedulerEditController'
|
||||
controller: 'schedulerEditController',
|
||||
ncyBreadcrumb: {
|
||||
label: 'PROJECT SCHEDULES EDIT'
|
||||
}
|
||||
});
|
||||
$stateExtender.addState({
|
||||
name: 'inventoryManage.schedules',
|
||||
|
||||
@ -509,7 +509,7 @@
|
||||
Please provide a valid date.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group SchedulerForm-formGroup"
|
||||
<div class="form-group SchedulerForm-formGroup"
|
||||
ng-if="schedulerEnd && schedulerEnd.value == 'on'">
|
||||
<label class="Form-inputLabel">
|
||||
<span class="red-text">*</span>
|
||||
@ -583,7 +583,7 @@
|
||||
<div class="SchedulerFormDetail-container"
|
||||
ng-show="schedulerIsValid">
|
||||
<label class="SchedulerFormDetail-label">
|
||||
Description
|
||||
Schedule Description
|
||||
</label>
|
||||
<div class="SchedulerFormDetail-nlp">
|
||||
{{ rrule_nlp_description }}
|
||||
@ -651,7 +651,7 @@
|
||||
<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>"
|
||||
data-placement="right" data-container="body" over-title="Extra Variables" class="help-link" data-original-title="" title="" tabindex="-1">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
<div class="parse-selection">
|
||||
<input type="radio" ng-model="parseType" ng-change="parseTypeChange()" value="yaml"><span class="parse-label">YAML</span>
|
||||
|
||||
@ -31,7 +31,6 @@
|
||||
align-items: center;
|
||||
max-height: 400px;
|
||||
width: 120px;
|
||||
overflow-y: scroll;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ export default {
|
||||
name: 'setup',
|
||||
route: '/setup',
|
||||
ncyBreadcrumb: {
|
||||
label: "SETUP"
|
||||
label: "SETTINGS"
|
||||
},
|
||||
templateUrl: templateUrl('setup-menu/setup-menu')
|
||||
};
|
||||
|
||||
@ -59,7 +59,7 @@ angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
||||
callback = params.callback,
|
||||
beforeDestroy = params.beforeDestroy,
|
||||
closeOnEscape = (params.closeOnEscape === undefined) ? false : params.closeOnEscape,
|
||||
resizable = (params.resizable === undefined) ? true : params.resizable,
|
||||
resizable = (params.resizable === undefined) ? false : params.resizable,
|
||||
draggable = (params.draggable === undefined) ? true : params.draggable,
|
||||
dialogClass = params.dialogClass,
|
||||
forms = _.chain([params.form]).flatten().compact().value(),
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
|
||||
// buttons
|
||||
@btn-bg: @default-bg;
|
||||
@btn-bord: @default-border;
|
||||
@btn-bord: @d7grey;
|
||||
@btn-txt: @default-interface-txt;
|
||||
@btn-bg-hov: @default-tertiary-bg;
|
||||
@btn-bg-sel: @default-icon-hov;
|
||||
|
||||
@ -714,13 +714,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
function label() {
|
||||
var html = '';
|
||||
if (field.label || field.labelBind) {
|
||||
html += "<label ";
|
||||
if (horizontal || field.labelClass) {
|
||||
html += "class=\"";
|
||||
html += (field.labelClass) ? field.labelClass : "";
|
||||
html += (horizontal) ? " " + getLabelWidth() : "";
|
||||
html += "\" ";
|
||||
}
|
||||
html += "<label class=\"";
|
||||
html += (field.labelClass) ? field.labelClass : "";
|
||||
html += (horizontal) ? " " + getLabelWidth() : "Form-inputLabelContainer ";
|
||||
html += "\" ";
|
||||
html += (field.labelNGClass) ? "ng-class=\"" + field.labelNGClass + "\" " : "";
|
||||
html += "for=\"" + fld + '">\n';
|
||||
html += (field.icon) ? Icon(field.icon) : "";
|
||||
@ -742,6 +739,14 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
html += "\" value=\"json\" ng-change=\"parseTypeChange()\"> <span class=\"parse-label\">JSON</span>\n";
|
||||
html += "</div>\n";
|
||||
}
|
||||
|
||||
if (field.labelAction) {
|
||||
let action = field.labelAction;
|
||||
let href = action.href || "";
|
||||
let ngClick = action.ngClick || "";
|
||||
let cls = action["class"] || "";
|
||||
html += `<a class="Form-labelAction ${cls}" href="${href}" ng-click="${ngClick}">${action.label}</a>`;
|
||||
}
|
||||
html += "\n\t</label>\n";
|
||||
}
|
||||
return html;
|
||||
@ -787,7 +792,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
|
||||
if ((!field.readonly) || (field.readonly && options.mode === 'edit')) {
|
||||
|
||||
if((field.excludeMode === undefined || field.excludeMode !== options.mode)) {
|
||||
if((field.excludeMode === undefined || field.excludeMode !== options.mode) && field.type !== 'alertblock') {
|
||||
|
||||
|
||||
html += "<div class='form-group Form-formGroup ";
|
||||
@ -1612,7 +1617,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
currentSubForm = field.subForm;
|
||||
var subFormTitle = this.form.subFormTitles[field.subForm];
|
||||
|
||||
html += '<div class="Form-subForm '+ currentSubForm + '" ng-hide="'+ hasSubFormField + '.value === undefined"> ';
|
||||
html += '<div class="Form-subForm '+ currentSubForm + '" ng-hide="'+ hasSubFormField + '.value === undefined || ' + field.hideSubForm + '"> ';
|
||||
html += '<span class="Form-subForm--title">'+ subFormTitle +'</span>';
|
||||
}
|
||||
else if (!field.subForm && currentSubForm !== undefined) {
|
||||
@ -1838,7 +1843,12 @@ 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
|
||||
html += "<div class=\"List-noItems\" ng-show=\"" + collection.iterator + "Loading == false && " + collection.iterator + "_active_search == false && " + collection.iterator + "_total_rows < 1\">PLEASE ADD ITEMS TO THIS LIST</div>";
|
||||
// Allow for the suppression of the empty list text to avoid duplication between form generator and list generator
|
||||
var emptyListText = (collection.emptyListText) ? collection.emptyListText : "PLEASE ADD ITEMS TO THIS LIST";
|
||||
html += '<div ng-hide="is_superuser">';
|
||||
html += "<div class=\"List-noItems\" ng-hide=\"is_superuser\" ng-show=\"" + collection.iterator + "Loading == false && " + collection.iterator + "_active_search == false && " + collection.iterator + "_total_rows < 1\">" + emptyListText + "</div>";
|
||||
html += '</div>';
|
||||
//}
|
||||
|
||||
html += `
|
||||
<div class=\"List-noItems\" ng-show=\"is_superuser\">
|
||||
|
||||
@ -8,7 +8,6 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
|
||||
function ($log, $rootScope, $scope, $state, $stateParams, ProcessErrors, Rest, Wait) {
|
||||
|
||||
var api_complete = false,
|
||||
stdout_url,
|
||||
current_range,
|
||||
loaded_sections = [],
|
||||
event_queue = 0,
|
||||
@ -87,7 +86,7 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
|
||||
});
|
||||
|
||||
function loadStdout() {
|
||||
Rest.setUrl($scope.stdoutEndpoint + '?format=json&start_line=-' + page_size);
|
||||
Rest.setUrl($scope.stdoutEndpoint + '?format=json&start_line=0&end_line=' + page_size);
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
Wait('stop');
|
||||
@ -145,38 +144,17 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce
|
||||
});
|
||||
}
|
||||
|
||||
$scope.stdOutScrollToTop = function() {
|
||||
// scroll up or back in time toward the beginning of the file
|
||||
var start, end, url;
|
||||
if (loaded_sections.length > 0 && loaded_sections[0].start > 0) {
|
||||
start = (loaded_sections[0].start - page_size > 0) ? loaded_sections[0].start - page_size : 0;
|
||||
end = loaded_sections[0].start - 1;
|
||||
}
|
||||
else if (loaded_sections.length === 0) {
|
||||
start = 0;
|
||||
end = page_size;
|
||||
}
|
||||
if (start !== undefined && end !== undefined) {
|
||||
$('#stdoutMoreRowsTop').fadeIn();
|
||||
url = stdout_url + '?format=json&start_line=' + start + '&end_line=' + end;
|
||||
// lrInfiniteScroll handler
|
||||
// grabs the next stdout section
|
||||
$scope.stdOutGetNextSection = function(){
|
||||
if (current_range.absolute_end > current_range.end){
|
||||
var url = $scope.stdoutEndpoint + '?format=json&start_line=' + current_range.end +
|
||||
'&end_line=' + (current_range.end + page_size);
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success( function(data) {
|
||||
//var currentPos = $('#pre-container').scrollTop();
|
||||
var newSH, oldSH = $('#pre-container').prop('scrollHeight'),
|
||||
st = $('#pre-container').scrollTop();
|
||||
|
||||
$('#pre-container-content').prepend(data.content);
|
||||
|
||||
newSH = $('#pre-container').prop('scrollHeight');
|
||||
$('#pre-container').scrollTop(newSH - oldSH + st);
|
||||
|
||||
loaded_sections.unshift({
|
||||
start: (data.range.start < 0) ? 0 : data.range.start,
|
||||
end: data.range.end
|
||||
});
|
||||
.success(function(data){
|
||||
$('#pre-container-content').append(data.content);
|
||||
current_range = data.range;
|
||||
$('#stdoutMoreRowsTop').fadeOut(400);
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<div class="StandardOut-consoleOutput">
|
||||
<div id="pre-container" class="body_background body_foreground pre mono-space StandardOut-preContainer"
|
||||
lr-infinite-scroll="stdOutScrollToTop" scroll-threshold="300" data-direction="up" time-threshold="500">
|
||||
<div class="StandardOut-consoleOutput" lr-infinite-scroll="stdOutGetNextSection" scroll-threshold="300" time-threshold="500">
|
||||
<div id="pre-container" class="body_background body_foreground pre mono-space StandardOut-preContainer">
|
||||
<div id="pre-container-content" class="StandardOut-preContent"></div>
|
||||
</div>
|
||||
<div class="scroll-spinner" id="stdoutMoreRowsBottom">
|
||||
|
||||
829
npm-shrinkwrap.json
generated
829
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user