Merge branch 'devel' of github.com:ansible/ansible-tower into rbac

This commit is contained in:
Akita Noek 2016-03-04 14:02:01 -05:00
commit e9c3d98a44
82 changed files with 21118 additions and 1959 deletions

2
.gitignore vendored
View File

@ -34,7 +34,7 @@ __pycache__
/tar-build
/setup-bundle-build
/dist
*.egg-info
/*.egg-info
*.py[c,o]
# JavaScript

View File

@ -273,9 +273,9 @@ version_file:
# Do any one-time init tasks.
init:
@if [ "$(VIRTUAL_ENV)" ]; then \
$(PYTHON) manage.py register_instance --primary --hostname=127.0.0.1; \
tower-manage register_instance --primary --hostname=127.0.0.1; \
else \
sudo $(PYTHON) manage.py register_instance --primary --hostname=127.0.0.1; \
sudo tower-manage register_instance --primary --hostname=127.0.0.1; \
fi
# Refresh development environment after pulling new code.

View File

@ -558,7 +558,7 @@ class BaseFactSerializer(BaseSerializer):
def get_fields(self):
ret = super(BaseFactSerializer, self).get_fields()
if 'module' in ret and feature_enabled('system_tracking'):
if 'module' in ret:
# TODO: the values_list may pull in a LOT of entries before the distinct is called
modules = Fact.objects.all().values_list('module', flat=True).distinct()
choices = [(o, o.title()) for o in modules]

View File

@ -1289,7 +1289,17 @@ class HostActivityStreamList(SubListAPIView):
qs = self.request.user.get_queryset(self.model)
return qs.filter(Q(host=parent) | Q(inventory=parent.inventory))
class HostFactVersionsList(ListAPIView, ParentMixin):
class SystemTrackingEnforcementMixin(APIView):
'''
Use check_permissions instead of initial() because it's in the OPTION's path as well
'''
def check_permissions(self, request):
if not feature_enabled("system_tracking"):
raise LicenseForbids("Your license does not permit use "
"of system tracking.")
return super(SystemTrackingEnforcementMixin, self).check_permissions(request)
class HostFactVersionsList(ListAPIView, ParentMixin, SystemTrackingEnforcementMixin):
model = Fact
serializer_class = FactVersionSerializer
@ -1297,10 +1307,6 @@ class HostFactVersionsList(ListAPIView, ParentMixin):
new_in_220 = True
def get_queryset(self):
if not feature_enabled("system_tracking"):
raise LicenseForbids("Your license does not permit use "
"of system tracking.")
from_spec = self.request.query_params.get('from', None)
to_spec = self.request.query_params.get('to', None)
module_spec = self.request.query_params.get('module', None)
@ -1318,7 +1324,7 @@ class HostFactVersionsList(ListAPIView, ParentMixin):
queryset = self.get_queryset() or []
return Response(dict(results=self.serializer_class(queryset, many=True).data))
class HostFactCompareView(SubDetailAPIView):
class HostFactCompareView(SubDetailAPIView, SystemTrackingEnforcementMixin):
model = Fact
new_in_220 = True
@ -1326,11 +1332,6 @@ class HostFactCompareView(SubDetailAPIView):
serializer_class = FactSerializer
def retrieve(self, request, *args, **kwargs):
# Sanity check: Does the license allow system tracking?
if not feature_enabled('system_tracking'):
raise LicenseForbids('Your license does not permit use '
'of system tracking.')
datetime_spec = request.query_params.get('datetime', None)
module_spec = request.query_params.get('module', "ansible")
datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now()

View File

@ -1,28 +0,0 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
from django.conf import settings
from mongoengine import connect
from mongoengine.connection import ConnectionError
from pymongo.errors import AutoReconnect
def test_mongo_connection():
# Connect to Mongo
try:
# Sanity check: If we have intentionally invalid settings, then we
# know we cannot connect.
if settings.MONGO_HOST == NotImplemented:
raise ConnectionError
# Attempt to connect to the MongoDB database.
db = connect(settings.MONGO_DB,
host=settings.MONGO_HOST,
port=int(settings.MONGO_PORT),
username=settings.MONGO_USERNAME,
password=settings.MONGO_PASSWORD,
tz_aware=settings.USE_TZ)
db[settings.MONGO_DB].command('ping')
return True
except (ConnectionError, AutoReconnect):
return False

View File

@ -12,7 +12,7 @@ from django.db import transaction
from django.utils.timezone import now
# AWX
from awx.fact.models.fact import * # noqa
from awx.main.models.fact import Fact
from awx.api.license import feature_enabled
OLDER_THAN = 'older_than'
@ -31,7 +31,7 @@ class CleanupFacts(object):
# pivot -= granularity
# group by host
def cleanup(self, older_than_abs, granularity, module=None):
fact_oldest = FactVersion.objects.all().order_by('timestamp').first()
fact_oldest = Fact.objects.all().order_by('timestamp').first()
if not fact_oldest:
return 0
@ -44,7 +44,10 @@ class CleanupFacts(object):
# Special case, granularity=0x where x is d, w, or y
# The intent is to delete all facts < older_than_abs
if granularity == relativedelta():
return FactVersion.objects.filter(**kv).order_by('-timestamp').delete()
qs = Fact.objects.filter(**kv)
count = qs.count()
qs.delete()
return count
total = 0
@ -61,18 +64,17 @@ class CleanupFacts(object):
kv['module'] = module
fact_version_objs = FactVersion.objects.filter(**kv).order_by('-timestamp').limit(1)
if fact_version_objs:
fact_version_obj = fact_version_objs[0]
fact_version_obj = Fact.objects.filter(**kv).order_by('-timestamp').first()
if fact_version_obj:
kv = {
'timestamp__lt': fact_version_obj.timestamp,
'timestamp__gt': date_pivot_next
}
if module:
kv['module'] = module
count = FactVersion.objects.filter(**kv).delete()
# FIXME: These two deletes should be a transaction
count = Fact.objects.filter(**kv).delete()
qs = Fact.objects.filter(**kv)
count = qs.count()
qs.delete()
total += count
date_pivot = date_pivot_next

View File

@ -67,7 +67,7 @@ class FactCacheReceiver(object):
self.timestamp = datetime.fromtimestamp(date_key, None)
# Update existing Fact entry
fact_obj = Fact.get_host_fact(host_obj.id, module_name, self.timestamp)
fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp)
if fact_obj:
fact_obj.facts = facts
fact_obj.save()

View File

@ -53,7 +53,6 @@ from awx.main.task_engine import TaskSerializer, TASK_TIMEOUT_INTERVAL
from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url,
ignore_inventory_computed_fields, emit_websocket_notification,
check_proot_installed, build_proot_temp_dir, wrap_args_with_proot)
from awx.fact.utils.connection import test_mongo_connection
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
'RunAdHocCommand', 'handle_work_error', 'handle_work_success',
@ -959,11 +958,6 @@ class RunJob(BaseTask):
'''
return getattr(tower_settings, 'AWX_PROOT_ENABLED', False)
def pre_run_hook(self, job, **kwargs):
if job.job_type == PERM_INVENTORY_SCAN:
if not test_mongo_connection():
raise RuntimeError("Fact Scan Database is offline")
def post_run_hook(self, job, **kwargs):
'''
Hook for actions to run after job/task has completed.

View File

@ -16,6 +16,9 @@ from django.utils import timezone
def mock_feature_enabled(feature, bypass_database=None):
return True
def mock_feature_disabled(feature, bypass_database=None):
return False
def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), get_params={}, host_count=1):
hosts = hosts(host_count=host_count)
fact_scans(fact_scans=3, timestamp_epoch=epoch)
@ -42,8 +45,33 @@ def check_response_facts(facts_known, response):
assert timestamp_apiformat(fact_known.timestamp) == response.data['results'][i]['timestamp']
check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module)
def check_system_tracking_feature_forbidden(response):
assert 402 == response.status_code
assert 'Your license does not permit use of system tracking.' == response.data['detail']
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_system_tracking_license_get(hosts, get, user):
hosts = hosts(host_count=1)
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
response = get(url, user('admin', True))
check_system_tracking_feature_forbidden(response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_system_tracking_license_options(hosts, options, user):
hosts = hosts(host_count=1)
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
response = options(url, None, user('admin', True))
check_system_tracking_feature_forbidden(response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_no_facts_db(hosts, get, user):
hosts = hosts(host_count=1)
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
@ -72,28 +100,19 @@ def test_basic_fields(hosts, fact_scans, get, user):
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
@pytest.mark.skipif(True, reason="Options fix landed in devel but not here. Enable this after this pr gets merged.")
@pytest.mark.license_feature
def test_basic_options_fields(hosts, fact_scans, options, user):
hosts = hosts(host_count=1)
fact_scans(fact_scans=1)
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
response = options(url, user('admin', True), pk=hosts[0].id)
response = options(url, None, user('admin', True), pk=hosts[0].id)
#import json
#print(json.dumps(response.data))
assert 'related' in response.data
assert 'id' in response.data
assert 'facts' in response.data
assert 'module' in response.data
assert 'host' in response.data
assert isinstance(response.data['host'], int)
assert 'summary_fields' in response.data
assert 'host' in response.data['summary_fields']
assert 'name' in response.data['summary_fields']['host']
assert 'description' in response.data['summary_fields']['host']
assert 'host' in response.data['related']
assert reverse('api:host_detail', args=(hosts[0].pk,)) == response.data['related']['host']
assert 'related' in response.data['actions']['GET']
assert 'module' in response.data['actions']['GET']
assert ("ansible", "Ansible") in response.data['actions']['GET']['module']['choices']
assert ("services", "Services") in response.data['actions']['GET']['module']['choices']
assert ("packages", "Packages") in response.data['actions']['GET']['module']['choices']
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db

View File

@ -1,7 +1,6 @@
import mock
import pytest
import json
import urllib
from awx.main.utils import timestamp_apiformat
from django.core.urlresolvers import reverse
@ -10,6 +9,9 @@ from django.utils import timezone
def mock_feature_enabled(feature, bypass_database=None):
return True
def mock_feature_disabled(feature, bypass_database=None):
return False
# TODO: Consider making the fact_scan() fixture a Class, instead of a function, and move this method into it
def find_fact(facts, host_id, module_name, timestamp):
for f in facts:
@ -27,6 +29,30 @@ def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), module_name
fact_known = find_fact(facts, hosts[0].id, module_name, epoch)
return (fact_known, response)
def check_system_tracking_feature_forbidden(response):
assert 402 == response.status_code
assert 'Your license does not permit use of system tracking.' == response.data['detail']
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_system_tracking_license_get(hosts, get, user):
hosts = hosts(host_count=1)
url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,))
response = get(url, user('admin', True))
check_system_tracking_feature_forbidden(response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_system_tracking_license_options(hosts, options, user):
hosts = hosts(host_count=1)
url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,))
response = options(url, None, user('admin', True))
check_system_tracking_feature_forbidden(response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_no_fact_found(hosts, get, user):

View File

@ -0,0 +1,200 @@
# Copyright (c) 2016 Ansible, Inc.
# All Rights Reserved
# Python
import pytest
import mock
from dateutil.relativedelta import relativedelta
from datetime import timedelta
# Django
from django.utils import timezone
from django.core.management.base import CommandError
# AWX
from awx.main.management.commands.cleanup_facts import CleanupFacts, Command
from awx.main.models.fact import Fact
from awx.main.models.inventory import Host
def mock_feature_enabled(feature, bypass_database=None):
return True
def mock_feature_disabled(feature, bypass_database=None):
return False
@pytest.mark.django_db
def test_cleanup_granularity(fact_scans, hosts):
epoch = timezone.now()
hosts(5)
fact_scans(10, timestamp_epoch=epoch)
fact_newest = Fact.objects.all().order_by('-timestamp').first()
timestamp_future = fact_newest.timestamp + timedelta(days=365)
granularity = relativedelta(days=2)
cleanup_facts = CleanupFacts()
deleted_count = cleanup_facts.cleanup(timestamp_future, granularity)
assert 60 == deleted_count
'''
Delete half of the scans
'''
@pytest.mark.django_db
def test_cleanup_older_than(fact_scans, hosts):
epoch = timezone.now()
hosts(5)
fact_scans(28, timestamp_epoch=epoch)
qs = Fact.objects.all().order_by('-timestamp')
fact_middle = qs[qs.count() / 2]
granularity = relativedelta()
cleanup_facts = CleanupFacts()
deleted_count = cleanup_facts.cleanup(fact_middle.timestamp, granularity)
assert 210 == deleted_count
@pytest.mark.django_db
def test_cleanup_older_than_granularity_module(fact_scans, hosts):
epoch = timezone.now()
hosts(5)
fact_scans(10, timestamp_epoch=epoch)
fact_newest = Fact.objects.all().order_by('-timestamp').first()
timestamp_future = fact_newest.timestamp + timedelta(days=365)
granularity = relativedelta(days=2)
cleanup_facts = CleanupFacts()
deleted_count = cleanup_facts.cleanup(timestamp_future, granularity, module='ansible')
assert 20 == deleted_count
'''
Reduce the granularity of half of the facts scans, by half.
'''
@pytest.mark.django_db
def test_cleanup_logic(fact_scans, hosts):
epoch = timezone.now()
hosts = hosts(5)
fact_scans(60, timestamp_epoch=epoch)
timestamp_middle = epoch + timedelta(days=30)
granularity = relativedelta(days=2)
module = 'ansible'
cleanup_facts = CleanupFacts()
cleanup_facts.cleanup(timestamp_middle, granularity, module=module)
host_ids = Host.objects.all().values_list('id', flat=True)
host_facts = {}
for host_id in host_ids:
facts = Fact.objects.filter(host__id=host_id, module=module, timestamp__lt=timestamp_middle).order_by('-timestamp')
host_facts[host_id] = facts
for host_id, facts in host_facts.iteritems():
assert 15 == len(facts)
timestamp_pivot = timestamp_middle
for fact in facts:
timestamp_pivot -= granularity
assert fact.timestamp == timestamp_pivot
@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_disabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_system_tracking_feature_disabled(mocker):
cmd = Command()
with pytest.raises(CommandError) as err:
cmd.handle(None)
assert 'The System Tracking feature is not enabled for your Tower instance' in err.value
@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_parameters_ok(mocker):
run = mocker.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run')
kv = {
'older_than': '1d',
'granularity': '1d',
'module': None,
}
cmd = Command()
cmd.handle(None, **kv)
run.assert_called_once_with(relativedelta(days=1), relativedelta(days=1), module=None)
@pytest.mark.django_db
def test_string_time_to_timestamp_ok():
kvs = [
{
'time': '2w',
'timestamp': relativedelta(weeks=2),
'msg': '2 weeks',
},
{
'time': '23d',
'timestamp': relativedelta(days=23),
'msg': '23 days',
},
{
'time': '11m',
'timestamp': relativedelta(months=11),
'msg': '11 months',
},
{
'time': '14y',
'timestamp': relativedelta(years=14),
'msg': '14 years',
},
]
for kv in kvs:
cmd = Command()
res = cmd.string_time_to_timestamp(kv['time'])
assert kv['timestamp'] == res
@pytest.mark.django_db
def test_string_time_to_timestamp_invalid():
kvs = [
{
'time': '2weeks',
'msg': 'weeks instead of w',
},
{
'time': '2days',
'msg': 'days instead of d',
},
{
'time': '23',
'msg': 'no unit specified',
},
{
'time': None,
'msg': 'no value specified',
},
{
'time': 'zigzag',
'msg': 'random string specified',
},
]
for kv in kvs:
cmd = Command()
res = cmd.string_time_to_timestamp(kv['time'])
assert res is None
@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_parameters_fail(mocker):
# Mock run() just in case, but it should never get called because an error should be thrown
mocker.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run')
kvs = [
{
'older_than': '1week',
'granularity': '1d',
'msg': '--older_than invalid value "1week"',
},
{
'older_than': '1d',
'granularity': '1year',
'msg': '--granularity invalid value "1year"',
}
]
for kv in kvs:
cmd = Command()
with pytest.raises(CommandError) as err:
cmd.handle(None, older_than=kv['older_than'], granularity=kv['granularity'])
assert kv['msg'] in err.value

View File

@ -1,238 +0,0 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved
# Python
from datetime import datetime
from dateutil.relativedelta import relativedelta
import mock
#Django
from django.core.management.base import CommandError
# AWX
from awx.main.tests.base import BaseTest
from awx.fact.tests.base import MongoDBRequired, FactScanBuilder, TEST_FACT_PACKAGES, TEST_FACT_ANSIBLE, TEST_FACT_SERVICES
from command_base import BaseCommandMixin
from awx.main.management.commands.cleanup_facts import Command, CleanupFacts
from awx.fact.models.fact import * # noqa
__all__ = ['CommandTest','CleanupFactsUnitTest', 'CleanupFactsCommandFunctionalTest']
class CleanupFactsCommandFunctionalTest(BaseCommandMixin, BaseTest, MongoDBRequired):
def setUp(self):
super(CleanupFactsCommandFunctionalTest, self).setUp()
self.create_test_license_file()
self.builder = FactScanBuilder()
self.builder.add_fact('ansible', TEST_FACT_ANSIBLE)
def test_invoke_zero_ok(self):
self.builder.set_epoch(datetime(year=2015, day=2, month=1, microsecond=0))
self.builder.build(scan_count=20, host_count=10)
result, stdout, stderr = self.run_command('cleanup_facts', granularity='2y', older_than='1d')
self.assertEqual(stdout, 'Deleted %s facts.\n' % ((200 / 2)))
def test_invoke_zero_deleted(self):
result, stdout, stderr = self.run_command('cleanup_facts', granularity='1w',older_than='5d')
self.assertEqual(stdout, 'Deleted 0 facts.\n')
def test_invoke_all_deleted(self):
self.builder.build(scan_count=20, host_count=10)
result, stdout, stderr = self.run_command('cleanup_facts', granularity='0d', older_than='0d')
self.assertEqual(stdout, 'Deleted 200 facts.\n')
def test_invoke_params_required(self):
result, stdout, stderr = self.run_command('cleanup_facts')
self.assertIsInstance(result, CommandError)
self.assertEqual(str(result), 'Both --granularity and --older_than are required.')
def test_module(self):
self.builder.add_fact('packages', TEST_FACT_PACKAGES)
self.builder.add_fact('services', TEST_FACT_SERVICES)
self.builder.build(scan_count=5, host_count=5)
result, stdout, stderr = self.run_command('cleanup_facts', granularity='0d', older_than='0d', module='packages')
self.assertEqual(stdout, 'Deleted 25 facts.\n')
class CommandTest(BaseTest):
def setUp(self):
super(CommandTest, self).setUp()
self.create_test_license_file()
@mock.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run')
def test_parameters_ok(self, run):
kv = {
'older_than': '1d',
'granularity': '1d',
'module': None,
}
cmd = Command()
cmd.handle(None, **kv)
run.assert_called_once_with(relativedelta(days=1), relativedelta(days=1), module=None)
def test_string_time_to_timestamp_ok(self):
kvs = [
{
'time': '2w',
'timestamp': relativedelta(weeks=2),
'msg': '2 weeks',
},
{
'time': '23d',
'timestamp': relativedelta(days=23),
'msg': '23 days',
},
{
'time': '11m',
'timestamp': relativedelta(months=11),
'msg': '11 months',
},
{
'time': '14y',
'timestamp': relativedelta(years=14),
'msg': '14 years',
},
]
for kv in kvs:
cmd = Command()
res = cmd.string_time_to_timestamp(kv['time'])
self.assertEqual(kv['timestamp'], res, "%s should convert to %s" % (kv['time'], kv['msg']))
def test_string_time_to_timestamp_invalid(self):
kvs = [
{
'time': '2weeks',
'msg': 'weeks instead of w',
},
{
'time': '2days',
'msg': 'days instead of d',
},
{
'time': '23',
'msg': 'no unit specified',
},
{
'time': None,
'msg': 'no value specified',
},
{
'time': 'zigzag',
'msg': 'random string specified',
},
]
for kv in kvs:
cmd = Command()
res = cmd.string_time_to_timestamp(kv['time'])
self.assertIsNone(res, kv['msg'])
# Mock run() just in case, but it should never get called because an error should be thrown
@mock.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run')
def test_parameters_fail(self, run):
kvs = [
{
'older_than': '1week',
'granularity': '1d',
'msg': 'Invalid older_than param value',
},
{
'older_than': '1d',
'granularity': '1year',
'msg': 'Invalid granularity param value',
}
]
for kv in kvs:
cmd = Command()
with self.assertRaises(CommandError):
cmd.handle(None, older_than=kv['older_than'], granularity=kv['granularity'])
class CleanupFactsUnitTest(BaseCommandMixin, BaseTest, MongoDBRequired):
def setUp(self):
super(CleanupFactsUnitTest, self).setUp()
self.builder = FactScanBuilder()
self.builder.add_fact('ansible', TEST_FACT_ANSIBLE)
self.builder.add_fact('packages', TEST_FACT_PACKAGES)
self.builder.build(scan_count=20, host_count=10)
'''
Create 10 hosts with 40 facts each. After cleanup, there should be 20 facts for each host.
Then ensure the correct facts are deleted.
'''
def test_cleanup_logic(self):
cleanup_facts = CleanupFacts()
fact_oldest = FactVersion.objects.all().order_by('timestamp').first()
granularity = relativedelta(years=2)
deleted_count = cleanup_facts.cleanup(self.builder.get_timestamp(0), granularity)
self.assertEqual(deleted_count, 2 * (self.builder.get_scan_count() * self.builder.get_host_count()) / 2)
# Check the number of facts per host
for host in self.builder.get_hosts():
count = FactVersion.objects.filter(host=host).count()
scan_count = (2 * self.builder.get_scan_count()) / 2
self.assertEqual(count, scan_count)
count = Fact.objects.filter(host=host).count()
self.assertEqual(count, scan_count)
# Ensure that only 2 facts (ansible and packages) exists per granularity time
date_pivot = self.builder.get_timestamp(0)
for host in self.builder.get_hosts():
while date_pivot > fact_oldest.timestamp:
date_pivot_next = date_pivot - granularity
kv = {
'timestamp__lte': date_pivot,
'timestamp__gt': date_pivot_next,
'host': host,
}
count = FactVersion.objects.filter(**kv).count()
self.assertEqual(count, 2, "should only be 2 FactVersion per the 2 year granularity")
count = Fact.objects.filter(**kv).count()
self.assertEqual(count, 2, "should only be 2 Fact per the 2 year granularity")
date_pivot = date_pivot_next
'''
Create 10 hosts with 40 facts each. After cleanup, there should be 30 facts for each host.
Then ensure the correct facts are deleted.
'''
def test_cleanup_module(self):
cleanup_facts = CleanupFacts()
fact_oldest = FactVersion.objects.all().order_by('timestamp').first()
granularity = relativedelta(years=2)
deleted_count = cleanup_facts.cleanup(self.builder.get_timestamp(0), granularity, module='ansible')
self.assertEqual(deleted_count, (self.builder.get_scan_count() * self.builder.get_host_count()) / 2)
# Check the number of facts per host
for host in self.builder.get_hosts():
count = FactVersion.objects.filter(host=host).count()
self.assertEqual(count, 30)
count = Fact.objects.filter(host=host).count()
self.assertEqual(count, 30)
# Ensure that only 1 ansible fact exists per granularity time
date_pivot = self.builder.get_timestamp(0)
for host in self.builder.get_hosts():
while date_pivot > fact_oldest.timestamp:
date_pivot_next = date_pivot - granularity
kv = {
'timestamp__lte': date_pivot,
'timestamp__gt': date_pivot_next,
'host': host,
'module': 'ansible',
}
count = FactVersion.objects.filter(**kv).count()
self.assertEqual(count, 1)
count = Fact.objects.filter(**kv).count()
self.assertEqual(count, 1)
date_pivot = date_pivot_next

View File

@ -1,29 +0,0 @@
<div id="about-dialog-body">
<div class="row">
<div class="col-xs-12 col-sm-5 About-cowsay">
<div style="width: 340px; margin: 0 auto;">
<pre id="cowsay">
________________
/ Tower Version \
\<span id='about-modal-version'></span>/
----------------
\ ^__^
\ (oo)\_______
(__)\ A)\/\
||----w |
|| ||
</pre>
</div>
</div>
<div class="col-xs-12 col-sm-7 text-center">
<img id="about-modal-titlelogo" src="/static/assets/ansible_tower_logo_minimalc.png"><br>
<p>Copyright 2015. All rights reserved.</p>
<p>Ansible and Ansible Tower are registered trademarks of Red Hat, Inc.</p>
<br>
<img class="About-redhat" src="/static/assets/redhat_ansible_lockup.png">
<br>
<p>Visit <a href="http://www.ansible.com" target="_blank">Ansible.com</a> for more information.</p>
<p><span id='about-modal-subscription'></span></p>
</div>
</div>

View File

@ -99,7 +99,9 @@ a:focus {
color: @blue-dark;
text-decoration: none;
}
.btn{
text-transform: uppercase;
}
/* Old style TB default button with grey background */
.btn-grey {
color: #333;
@ -917,15 +919,11 @@ input[type="checkbox"].checkbox-no-label {
/* Display list actions next to search widget */
.list-actions {
text-align: right;
text-align: right;
button {
margin-left: 4px;
}
.fa-lg {
vertical-align: -8%;
}
.fa-lg {
vertical-align: -8%;
}
}
.jqui-accordion {
@ -1950,11 +1948,6 @@ tr td button i {
}
}
button.dropdown-toggle,
.input-group-btn {
z-index: 1;
}
#login-modal-body {
padding-bottom: 5px;
}

View File

@ -166,9 +166,6 @@
.unreachable-hosts-color {
color: @unreachable-hosts-color;
}
.missing-hosts {
color: transparent;
}
.job_well {
padding: 8px;
@ -197,9 +194,6 @@
margin-bottom: 0;
}
#job-detail-tables {
margin-top: 20px;
}
#job_options {
height: 100px;
@ -208,7 +202,6 @@
}
#job_plays, #job_tasks {
height: 150px;
overflow-y: auto;
overflow-x: none;
}
@ -221,10 +214,7 @@
}
#job-detail-container {
position: relative;
padding-left: 15px;
padding-right: 7px;
width: 58.33333333%;
.well {
overflow: hidden;
}
@ -292,9 +282,6 @@
.row:first-child {
border: none;
}
.active {
background-color: @active-color;
}
.loading-info {
padding-top: 5px;
padding-left: 3px;
@ -329,10 +316,6 @@
text-overflow: ellipsis;
}
#tasks-table-detail {
height: 150px;
}
#play-section {
.table-detail {
height: 150px;

View File

@ -32,6 +32,7 @@ table, tbody {
background-color: @list-header-bg;
padding-left: 15px;
padding-right: 15px;
border-bottom-width:0px!important;
}
.List-tableHeader:first-of-type {
@ -69,6 +70,7 @@ table, tbody {
.List-tableCell {
padding-left: 15px;
padding-right: 15px;
border-top:0px!important;
}
.List-actionButtonCell {
@ -141,7 +143,6 @@ table, tbody {
.List-header {
display: flex;
height: 34px;
align-items: center;
}
@ -149,7 +150,7 @@ table, tbody {
align-items: center;
flex: 1 0 auto;
display: flex;
margin-top: -2px;
height: 34px;
}
.List-titleBadge {
@ -170,15 +171,22 @@ table, tbody {
text-transform: uppercase;
}
.List-actions {
.List-actionHolder {
justify-content: flex-end;
display: flex;
height: 34px;
}
.List-actions {
margin-top: -10px;
}
.List-auxAction + .List-actions {
margin-left: 10px;
}
.List-auxAction {
justify-content: flex-end;
align-items: center;
display: flex;
}
@ -186,6 +194,10 @@ table, tbody {
width: 175px;
}
.List-action:not(.ng-hide) ~ .List-action:not(.ng-hide) {
margin-left: 10px;
}
.List-buttonSubmit {
background-color: @submit-button-bg;
color: @submit-button-text;
@ -350,3 +362,25 @@ table, tbody {
display: block;
font-size: 13px;
}
@media (max-width: 991px) {
.List-searchWidget + .List-searchWidget {
margin-top: 20px;
}
}
@media (max-width: 600px) {
.List-header {
flex-direction: column;
align-items: stretch;
}
.List-actionHolder {
justify-content: flex-start;
align-items: center;
flex: 1 0 auto;
margin-top: 12px;
}
.List-well {
margin-top: 20px;
}
}

View File

@ -60,7 +60,7 @@ body {
}
#content-container {
margin-top: 40px;
padding-bottom: 40px;
}
.group-breadcrumbs {

View File

@ -32,6 +32,7 @@
#pre-container {
overflow-x: scroll;
overflow-y: auto;
padding: 10px;
}
}

View File

@ -1,14 +1,42 @@
/** @define About */
.About {
height: 309px !important;
}
@import "awx/ui/client/src/shared/branding/colors.default.less";
.About-cowsay {
margin-top: 30px;
.About-cowsay--container{
width: 340px;
margin: 0 auto;
}
.About-redhat {
max-width: 100%;
margin-top: -61px;
margin-bottom: -33px;
.About-cowsay--code{
background-color: @default-bg;
padding-left: 30px;
border-style: none;
max-width: 340px;
padding-left: 30px;
}
.About .modal-header{
border: none;
padding-bottom: 0px;
}
.About .modal-dialog{
max-width: 500px;
}
.About .modal-body{
padding-top: 0px;
}
.About-brand--redhat{
max-width: 420px;
margin: 0 auto;
margin-top: -50px;
margin-bottom: -30px;
}
.About-brand--ansible{
max-width: 120px;
margin: 0 auto;
}
.About-close{
position: absolute;
top: 15px;
right: 15px;
}
.About p{
color: @default-interface-txt;
}

View File

@ -0,0 +1,31 @@
export default
['$scope', '$state', 'CheckLicense', function($scope, $state, CheckLicense){
var processVersion = function(version){
// prettify version & calculate padding
// e,g 3.0.0-0.git201602191743/ -> 3.0.0
var split = version.split('-')[0]
var spaces = Math.floor((16-split.length)/2),
paddedStr = "";
for(var i=0; i<=spaces; i++){
paddedStr = paddedStr +" ";
}
paddedStr = paddedStr + split;
for(var j = paddedStr.length; j<16; j++){
paddedStr = paddedStr + " ";
}
return paddedStr
}
var init = function(){
CheckLicense.get()
.then(function(res){
$scope.subscription = res.data.license_info.subscription_name;
$scope.version = processVersion(res.data.version);
$('#about-modal').modal('show');
});
};
var back = function(){
$state.go('setup');
}
init();
}
];

View File

@ -0,0 +1,32 @@
<div class="About modal fade" id="about-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<img class="About-brand--ansible img-responsive" src="/static/assets/ansible_tower_logo_minimalc.png" />
<button type="button" class="close About-close" ng-click="back()">
<span class="fa fa-times-circle"></span>
</button>
</div>
<div class="modal-body">
<div class="About-cowsay--container">
<!-- Don't indent this properly, you'll break the cow -->
<pre class="About-cowsay--code">
________________
/ Tower Version \\
\\<span>{{version}}</span>/
----------------
\\ ^__^
\\ (oo)\\_______
(__)\ A)\\/\\
||----w |
|| ||
</pre>
</div>
<img class="About-brand--redhat img-responsive" src="/static/assets/redhat_ansible_lockup.png" />
<p class="text-center">Copyright 2016. All rights reserved.<br>
Ansible and Ansible Tower are registered trademarks of <a href="http://www.redhat.com/" target="_blank">Red Hat, Inc</a>.<br>
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
{{subscription}}</p>
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
import {templateUrl} from '../shared/template-url/template-url.factory';
import controller from './about.controller';
export default {
name: 'setup.about',
route: '/about',
controller: controller,
ncyBreadcrumb: {
label: "ABOUT"
},
templateUrl: templateUrl('about/about')
};

View File

@ -0,0 +1,15 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import controller from './about.controller';
import route from './about.route';
export default
angular.module('aboutTower', [])
.controller('aboutTower', controller)
.run(['$stateExtender', function($stateExtender){
$stateExtender.addState(route);
}]);

View File

@ -15,8 +15,8 @@ export default ['templateUrl', function(templateUrl) {
$scope.streamTarget = ($state.params && $state.params.target) ? $state.params.target : 'dashboard';
$scope.options = [
{label: 'All Activity', value: 'dashboard'},
{label: 'Credentials', value: 'credential'},
{label: 'Dashboard', value: 'dashboard'},
{label: 'Hosts', value: 'host'},
{label: 'Inventories', value: 'inventory'},
{label: 'Inventory Scripts', value: 'inventory_script'},
@ -38,11 +38,11 @@ export default ['templateUrl', function(templateUrl) {
if($scope.streamTarget && $scope.streamTarget == 'dashboard') {
// Just navigate to the base activity stream
$state.go('activityStream', {}, {inherit: false, reload: true});
$state.go('activityStream', {}, {inherit: false});
}
else {
// Attach the taget to the query parameters
$state.go('activityStream', {target: $scope.streamTarget});
$state.go('activityStream', {target: $scope.streamTarget}, {inherit: false});
}
}

View File

@ -1,11 +1,9 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
var urlPrefix;
if ($basePath) {
@ -34,6 +32,8 @@ import managementJobs from './management-jobs/main';
import jobDetail from './job-detail/main';
// modules
import about from './about/main';
import license from './license/main';
import setupMenu from './setup-menu/main';
import mainMenu from './main-menu/main';
import breadCrumb from './bread-crumb/main';
@ -47,7 +47,6 @@ import activityStream from './activity-stream/main';
import standardOut from './standard-out/main';
import lookUpHelper from './lookup/main';
import {JobTemplatesList, JobTemplatesAdd, JobTemplatesEdit} from './controllers/JobTemplates';
import {LicenseController} from './controllers/License';
import {ScheduleEditController} from './controllers/Schedules';
import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects';
import {OrganizationsList, OrganizationsAdd, OrganizationsEdit} from './controllers/Organizations';
@ -80,6 +79,8 @@ var tower = angular.module('Tower', [
// 'ngAnimate',
'ngSanitize',
'ngCookies',
about.name,
license.name,
RestServices.name,
browserData.name,
systemTracking.name,
@ -100,7 +101,6 @@ var tower = angular.module('Tower', [
standardOut.name,
'templates',
'Utilities',
'LicenseHelper',
'OrganizationFormDefinition',
'UserFormDefinition',
'FormGenerator',
@ -181,7 +181,6 @@ var tower = angular.module('Tower', [
'lrInfiniteScroll',
'LoadConfigHelper',
'SocketHelper',
'AboutAnsibleHelpModal',
'PortalJobsListDefinition',
'features',
'longDateFilter',
@ -859,21 +858,6 @@ var tower = angular.module('Tower', [
}
}).
state('license', {
url: '/license',
templateUrl: urlPrefix + 'partials/license.html',
controller: LicenseController,
ncyBreadcrumb: {
parent: 'setup',
label: 'LICENSE'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}]
}
}).
state('sockets', {
url: '/sockets',
templateUrl: urlPrefix + 'partials/sockets.html',
@ -898,12 +882,14 @@ var tower = angular.module('Tower', [
}]);
}])
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
'LoadConfig', 'Store', 'ShowSocketHelp', 'AboutAnsibleHelp', 'pendoService',
function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
LoadConfig, Store, ShowSocketHelp, AboutAnsibleHelp, pendoService) {
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense',
'$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService',
function (
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
LoadConfig, Store, ShowSocketHelp, pendoService)
{
var sock;
function activateTab() {
@ -976,32 +962,28 @@ var tower = angular.module('Tower', [
' status changed to ' + data.status +
' send to ' + $location.$$url);
var urlToCheck = $location.$$url;
if (urlToCheck.indexOf("?") !== -1) {
urlToCheck = urlToCheck.substr(0, urlToCheck.indexOf("?"));
}
// this acts as a router...it emits the proper
// value based on what URL the user is currently
// accessing.
if (urlToCheck === '/jobs') {
if ($state.is('jobs')) {
$rootScope.$emit('JobStatusChange-jobs', data);
} else if (/\/jobs\/(\d)+\/stdout/.test(urlToCheck) ||
/\/ad_hoc_commands\/(\d)+/.test(urlToCheck)) {
// TODO: something will need to change here for stdout
} else if ($state.is('jobDetail') ||
$state.is('adHocJobStdout') ||
$state.is('inventorySyncStdout') ||
$state.is('managementJobStdout') ||
$state.is('scmUpdateStdout')) {
$log.debug("sending status to standard out");
$rootScope.$emit('JobStatusChange-jobStdout', data);
} else if (/\/jobs\/(\d)+/.test(urlToCheck)) {
} if ($state.is('jobDetail')) {
$rootScope.$emit('JobStatusChange-jobDetails', data);
} else if (urlToCheck === '/home') {
} else if ($state.is('dashboard')) {
$rootScope.$emit('JobStatusChange-home', data);
} else if (urlToCheck === '/portal') {
} else if ($state.is('portal')) {
$rootScope.$emit('JobStatusChange-portal', data);
} else if (urlToCheck === '/projects') {
} else if ($state.is('projects')) {
$rootScope.$emit('JobStatusChange-projects', data);
} else if (/\/inventories\/(\d)+\/manage/.test(urlToCheck)) {
} else if ($state.is('inventoryManage')) {
$rootScope.$emit('JobStatusChange-inventory', data);
}
});
@ -1043,7 +1025,6 @@ var tower = angular.module('Tower', [
$rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) {
// this line removes the query params attached to a route
if(prev && prev.$$route &&
prev.$$route.name === 'systemTracking'){
@ -1083,15 +1064,15 @@ var tower = angular.module('Tower', [
if ($rootScope.current_user === undefined || $rootScope.current_user === null) {
Authorization.restoreUserInfo(); //user must have hit browser refresh
}
if (next && next.$$route && (!/^\/(login|logout)/.test(next.$$route.originalPath))) {
// if not headed to /login or /logout, then check the license
CheckLicense.test();
}
}
activateTab();
});
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
// catch license expiration notifications immediately after user logs in, redirect
if (fromState.name == 'signIn'){
CheckLicense.notify();
}
// broadcast event change if editing crud object
if ($location.$$path && $location.$$path.split("/")[3] && $location.$$path.split("/")[3] === "schedules") {
var list = $location.$$path.split("/")[3];
@ -1140,10 +1121,6 @@ var tower = angular.module('Tower', [
activateTab();
$rootScope.viewAboutTower = function(){
AboutAnsibleHelp();
};
$rootScope.viewCurrentUser = function () {
$location.path('/users/' + $rootScope.current_user.id);
};

View File

@ -922,7 +922,7 @@ export function InventoriesManage ($log, $scope, $rootScope, $location,
generateList.inject(InventoryGroups, {
mode: 'edit',
id: 'group-list-container',
searchSize: 'col-lg-6 col-md-6 col-sm-6',
searchSize: 'col-lg-6 col-md-6 col-sm-6 col-xs-12',
scope: $scope
});

View File

@ -1,189 +0,0 @@
/************************************
* Copyright (c) 2015 Ansible, Inc.
*
*
* Organizations.js
*
* Controller functions for Organization model.
*
*/
/**
* @ngdoc function
* @name controllers.function:Organizations
* @description This controller's for the Organizations page
*/
export function LicenseController(ClearScope, $location, $rootScope, $compile, $filter, GenerateForm, Rest, Alert,
GetBasePath, ProcessErrors, FormatDate, Prompt, Empty, LicenseForm, IsAdmin, CreateDialog, CheckLicense,
TextareaResize, $scope, Wait) {
ClearScope();
$scope.getDefaultHTML = function(license_info) {
var fld, html,
self = this,
generator = GenerateForm;
self.form = angular.copy(LicenseForm);
for (fld in self.form.fields) {
if (fld !== 'time_remaining' && fld !== 'license_status' && fld !== 'tower_version') {
if (Empty(license_info[fld])) {
delete self.form.fields[fld];
}
}
}
if (!IsAdmin()) {
delete self.form.fields.license_key;
}
if (license_info.is_aws || Empty(license_info.license_date)) {
delete self.form.fields.license_date;
delete self.form.fields.time_remaining;
}
html = generator.buildHTML(self.form, { mode: 'edit', showButtons: false });
return html;
};
$scope.loadDefaultScope = function(license_info, version) {
var fld, dt, days, license,
self = this;
for (fld in self.form.fields) {
if (!Empty(license_info[fld])) {
$scope[fld] = license_info[fld];
}
}
$scope.tower_version = version;
if ($scope.license_date) {
dt = new Date(parseInt($scope.license_date, 10) * 1000); // expects license_date in seconds
$scope.license_date = FormatDate(dt);
$scope.time_remaining = parseInt($scope.time_remaining,10) * 1000;
if ($scope.time_remaining < 0) {
days = 0;
} else {
days = Math.floor($scope.time_remaining / 86400000);
}
$scope.time_remaining = (days!==1) ? $filter('number')(days, 0) + ' days' : $filter('number')(days, 0) + ' day'; // '1 day' and '0 days/2 days' or more
}
if (parseInt($scope.free_instances) <= 0) {
$scope.free_instances_class = 'field-failure';
} else {
$scope.free_instances_class = 'field-success';
}
license = license_info;
if (license.valid_key === undefined) {
$scope.license_status = 'Missing License Key';
$scope.status_color = 'license-invalid';
} else if (!license.valid_key) {
$scope.license_status = 'Invalid License Key';
$scope.status_color = 'license-invalid';
} else if (license.date_expired !== undefined && license.date_expired) {
$scope.license_status = 'License Expired';
$scope.status_color = 'license-expired';
} else if (license.date_warning !== undefined && license.date_warning) {
$scope.license_status = 'License Expiring Soon';
$scope.status_color = 'license-warning';
} else if (license.free_instances !== undefined && parseInt(license.free_instances) <= 0) {
$scope.license_status = 'No Available Managed Hosts';
$scope.status_color = 'license-invalid';
} else {
$scope.license_status = 'Valid License';
$scope.status_color = 'license-valid';
}
};
$scope.setLicense = function(license_info, version) {
this.license = license_info;
this.version = version;
};
$scope.getLicense = function(){
return this.license;
};
$scope.submitLicenseKey = function() {
CheckLicense.postLicense($scope.license_json, $scope);
};
if ($scope.removeLicenseDataReady) {
$scope.removeLicenseDataReady();
}
$scope.removeLicenseDataReady = $scope.$on('LicenseDataReady', function(e, data) {
var html, version, eula, h;
version = data.version.replace(/-.*$/,'');
$scope.setLicense(data.license_info, version);
html = $scope.getDefaultHTML(data.license_info);
$scope.loadDefaultScope(data.license_info, version);
eula = (data.eula) ? data.eula : "" ;
e = angular.element(document.getElementById('license-modal-dialog'));
e.empty().html(html);
$scope.parseType = 'json';
$scope.license_json = JSON.stringify($scope.license, null, ' ');
$scope.eula = eula;
$scope.eula_agreement = false;
h = CheckLicense.getHTML($scope.getLicense(),true).body;
$('#license-modal-dialog #license_tabs').append("<li><a id=\"update_license_link\" ng-click=\"toggleTab($event, 'update_license_link', 'license_tabs')\" href=\"#update_license\" data-toggle=\"tab\">Update License</a></li>");
$('#license-modal-dialog .tab-content').append("<div class=\"tab-pane\" id=\"update_license\"></div>");
$('#license-modal-dialog #update_license').html(h);
if ($scope.license_status === 'Invalid License Key' || $scope.license_status === 'Missing License Key') {
$('#license_tabs li:eq(1)').hide();
$('#license_tabs li:eq(2) a').tab('show');
}
$('#license_license_json').attr('ng-required' , 'true' );
$('#license_eula_agreement_chbox').attr('ng-required' , 'true' );
$('#license_form_submit_btn').attr('ng-disabled' , "license_form.$invalid" );
e = angular.element(document.getElementById('license-modal-dialog'));
$compile(e)($scope);
if (IsAdmin()) {
setTimeout(function() {
TextareaResize({
scope: $scope,
textareaId: 'license_license_json',
modalId: 'license-modal-dialog',
formId: 'license-notification-body',
fld: 'license_json',
parse: true,
bottom_margin: 90,
onChange: function() { $scope.license_json_api_error = ''; }
});
}, 300);
}
$('#license-ok-button').focus();
$('#update_license_link').on('shown.bs.tab', function() {
if (IsAdmin()) {
TextareaResize({
scope: $scope,
textareaId: 'license_license_json',
modalId: 'license-modal-dialog',
formId: 'license-notification-body',
fld: 'license_json',
bottom_margin: 90,
parse: true,
onChange: function() { $scope.license_json_api_error = ''; }
});
}
});
Wait("stop");
});
CheckLicense.GetLicense('LicenseDataReady', $scope);
}
LicenseController.$inject = ['ClearScope', '$location', '$rootScope', '$compile', '$filter', 'GenerateForm', 'Rest', 'Alert',
'GetBasePath', 'ProcessErrors', 'FormatDate', 'Prompt', 'Empty', 'LicenseForm', 'IsAdmin', 'CreateDialog',
'CheckLicense', 'TextareaResize', '$scope', "Wait"];

View File

@ -6,7 +6,7 @@
color: #848992;
width: 100%;
z-index: 1040;
position: absolute;
position: fixed;
right: 0;
left: 0;
bottom: 0;

View File

@ -7,7 +7,6 @@
import './forms';
import './lists';
import AboutAnsible from "./helpers/AboutAnsible";
import Children from "./helpers/Children";
import Credentials from "./helpers/Credentials";
import EventViewer from "./helpers/EventViewer";
@ -19,7 +18,6 @@ import JobDetail from "./helpers/JobDetail";
import JobSubmission from "./helpers/JobSubmission";
import JobTemplates from "./helpers/JobTemplates";
import Jobs from "./helpers/Jobs";
import License from "./helpers/License";
import LoadConfig from "./helpers/LoadConfig";
import PaginationHelpers from "./helpers/PaginationHelpers";
import Parse from "./helpers/Parse";
@ -43,8 +41,7 @@ import ApiModelHelper from "./helpers/ApiModel";
import ActivityStreamHelper from "./helpers/ActivityStream";
export
{ AboutAnsible,
Children,
{ Children,
Credentials,
EventViewer,
Events,
@ -55,7 +52,6 @@ export
JobSubmission,
JobTemplates,
Jobs,
License,
LoadConfig,
PaginationHelpers,
Parse,

View File

@ -1,93 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc overview
* @name helpers
* @description These are helpers...figure it out :)
*/
/**
* @ngdoc function
* @name helpers.function:AboutAnsible
* @description This is the code for the About Ansible modal window that pops up with cowsay giving company/tower info and copyright information.
*/
export default
angular.module('AboutAnsibleHelpModal', ['RestServices', 'Utilities','ModalDialog'])
.factory('AboutAnsibleHelp', ['$rootScope', '$compile', '$location' , 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'CreateDialog',
function ($rootScope, $compile , $location, Rest, GetBasePath, ProcessErrors, Wait, CreateDialog) {
return function () {
var scope= $rootScope.$new(),
url;
url = GetBasePath('config');
Rest.setUrl(url);
Rest.get()
.success(function (data){
scope.$emit('BuildAboutDialog', data);
})
.error(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get: ' + url + ' GET returned: ' + status });
});
if (scope.removeDialogReady) {
scope.removeDialogReady();
}
scope.removeDialogReady = scope.$on('DialogReady', function() {
// element = angular.element(document.getElementById('about-modal-dialog'));
// $compile(element)(scope);
$('#about-modal-dialog').dialog('open');
});
if (scope.removeBuildAboutDialog) {
scope.removeBuildAboutDialog();
}
scope.removeBuildAboutDialog = scope.$on('BuildAboutDialog', function(e, data) {
var spaces, i, j,
paddedStr = "",
versionParts,
str = data.version,
subscription = data.license_info.subscription_name || "";
versionParts = str.split('-');
spaces = Math.floor((16-versionParts[0].length)/2);
for( i=0; i<=spaces; i++){
paddedStr = paddedStr +" ";
}
paddedStr = paddedStr + versionParts[0];
for( j = paddedStr.length; j<16; j++){
paddedStr = paddedStr + " ";
}
$('#about-modal-version').html(paddedStr);
$('#about-modal-subscription').html(subscription);
scope.modalOK = function(){
$('#about-modal-dialog').dialog('close');
};
CreateDialog({
id: 'about-modal-dialog',
scope: scope,
// buttons: [],
width: 710,
height: 450,
minWidth: 300,
resizable: false,
callback: 'DialogReady',
onOpen: function(){
$('#dialog-ok-button').focus();
$('#about-modal-dialog').scrollTop(0);
$('#about-modal-dialog').css('overflow-x', 'hidden');
$('.ui-widget-overlay').css('width', '100%');
}
});
});
};
}
]);

View File

@ -16,7 +16,7 @@ export default
function () {
return function (target) {
var rtnTitle = 'DASHBOARD';
var rtnTitle = 'ALL ACTIVITY';
switch(target) {
case 'project':
@ -49,6 +49,9 @@ export default
case 'schedule':
rtnTitle = 'SCHEDULES';
break;
case 'host':
rtnTitle = 'HOSTS';
break;
}
return rtnTitle;

View File

@ -229,7 +229,7 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name,
generator = GenerateList;
// Inject the list html
generator.inject(InventoryHosts, { scope: host_scope, mode: 'edit', id: 'host-list-container', searchSize: 'col-lg-6 col-md-6 col-sm-6' });
generator.inject(InventoryHosts, { scope: host_scope, mode: 'edit', id: 'host-list-container', searchSize: 'col-lg-6 col-md-6 col-sm-6 col-xs-12' });
// Load data
HostsReload({ scope: host_scope, group_id: group_id, inventory_id: inventory_id, parent_scope: group_scope, pageSize: pageSize });

View File

@ -235,7 +235,7 @@ export default
}
if (newActivePlay) {
scope.activePlay = newActivePlay;
scope.jobData.plays[scope.activePlay].playActiveClass = 'active';
scope.jobData.plays[scope.activePlay].playActiveClass = 'JobDetail-tableRow--selected';
}
}
};
@ -265,7 +265,7 @@ export default
}
if (newActiveTask) {
scope.activeTask = newActiveTask;
scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'active';
scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'JobDetail-tableRow--selected';
}
}
};
@ -700,12 +700,12 @@ export default
task.missingPct = task.missingPct - diff;
}
}
task.successfulStyle = (task.successfulPct > 0) ? { 'display': 'inline-block', 'width': task.successfulPct + "%" } : { 'display': 'none' };
task.changedStyle = (task.changedPct > 0) ? { 'display': 'inline-block', 'width': task.changedPct + "%" } : { 'display': 'none' };
task.skippedStyle = (task.skippedPct > 0) ? { 'display': 'inline-block', 'width': task.skippedPct + "%" } : { 'display': 'none' };
task.failedStyle = (task.failedPct > 0) ? { 'display': 'inline-block', 'width': task.failedPct + "%" } : { 'display': 'none' };
task.unreachableStyle = (task.unreachablePct > 0) ? { 'display': 'inline-block', 'width': task.unreachablePct + "%" } : { 'display': 'none' };
task.missingStyle = (task.missingPct > 0) ? { 'display': 'inline-block', 'width': task.missingPct + "%" } : { 'display': 'none' };
task.successfulStyle = (task.successfulPct > 0) ? { 'display': 'inline-block' }: { 'display': 'none' };
task.changedStyle = (task.changedPct > 0) ? { 'display': 'inline-block'} : { 'display': 'none' };
task.skippedStyle = (task.skippedPct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' };
task.failedStyle = (task.failedPct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' };
task.unreachableStyle = (task.unreachablePct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' };
task.missingStyle = (task.missingPct > 0) ? { 'display': 'inline-block' } : { 'display': 'none' };
};
}])
@ -793,7 +793,7 @@ export default
scope.selectedPlay = id;
scope.plays.forEach(function(play, idx) {
if (play.id === scope.selectedPlay) {
scope.plays[idx].playActiveClass = 'active';
scope.plays[idx].playActiveClass = 'JobDetail-tableRow--selected';
}
else {
scope.plays[idx].playActiveClass = '';
@ -940,7 +940,7 @@ export default
scope.selectedTask = id;
scope.tasks.forEach(function(task, idx) {
if (task.id === scope.selectedTask) {
scope.tasks[idx].taskActiveClass = 'active';
scope.tasks[idx].taskActiveClass = 'JobDetail-tableRow--selected';
}
else {
scope.tasks[idx].taskActiveClass = '';
@ -1142,8 +1142,7 @@ export default
.factory('DrawGraph', ['DonutChart', function(DonutChart) {
return function(params) {
var scope = params.scope,
resize = params.resize,
width, height, svg_height, svg_width, svg_radius, graph_data = [];
graph_data = [];
// Ready the data
if (scope.host_summary.ok) {
@ -1155,21 +1154,21 @@ export default
}
if (scope.host_summary.changed) {
graph_data.push({
label: 'Changed',
label: 'CHANGED',
value: scope.host_summary.changed,
color: '#FF9900'
});
}
if (scope.host_summary.unreachable) {
graph_data.push({
label: 'Unreachable',
label: 'UNREACHABLE',
value: scope.host_summary.unreachable,
color: '#FF0000'
});
}
if (scope.host_summary.failed) {
graph_data.push({
label: 'Failed',
label: 'FAILED',
value: scope.host_summary.failed,
color: '#ff5850'
});
@ -1180,148 +1179,91 @@ export default
total_count += graph_data[gd_obj].value;
}
scope.total_count_for_graph = total_count;
// Adjust the size
width = $('#job-summary-container .job_well').width();
height = $('#job-summary-container .job_well').height() - $('#summary-well-top-section').height() - $('#graph-section .header').outerHeight() - 80;
svg_radius = Math.min(width, height);
svg_width = width;
svg_height = height;
if (svg_height > 0 && svg_width > 0) {
if (!resize && $('#graph-section svg').length > 0) {
// Donut3D.transition("completedHostsDonut", graph_data, Math.floor(svg_radius * 0.50), Math.floor(svg_radius * 0.25), 18, 0.4);
DonutChart({
target: '#graph-section',
height: height,
width: width,
data: graph_data,
radius: svg_radius
});
}
else {
if ($('#graph-section svg').length > 0) {
$('#graph-section svg').remove();
}
// svg = d3.select("#graph-section").append("svg").attr("width", svg_width).attr("height", svg_height);
// svg.append("g").attr("id","completedHostsDonut");
// Donut3D.draw("completedHostsDonut", graph_data, Math.floor(svg_width / 2), Math.floor(svg_height / 2) - 35, Math.floor(svg_radius * 0.50), Math.floor(svg_radius * 0.25), 18, 0.4);
DonutChart({
target: '#graph-section',
height: height,
width: width,
data: graph_data,
radius: svg_radius
});
$('#graph-section .header .legend').show();
}
}
DonutChart({
data: graph_data
});
};
}])
.factory('DonutChart', [function() {
return function(params) {
var target = params.target,
height = Math.max(params.height, 250),
width = Math.max(params.width, 250),
dataset = params.data,
outerRadius = Math.min(width, height) / 2,
innerRadius = (outerRadius/3),
svg, arc, pie, legend,
tooltip, path,
legendRectSize = 18,
legendSpacing = 4;
var dataset = params.data,
element = $("#graph-section"),
colors, total,job_detail_chart;
svg = d3.select(target)
.append('svg')
.data([dataset])
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
colors = _.map(dataset, function(d){
return d.color;
});
total = d3.sum(dataset.map(function(d) {
return d.value;
}));
job_detail_chart = nv.models.pieChart()
.margin({bottom: 15})
.x(function(d) {
return d.label +': '+ Math.round((d.value/total)*100) + "%";
})
.y(function(d) { return d.value; })
.showLabels(true)
.showLegend(false)
.growOnHover(false)
.labelThreshold(0.01)
.tooltipContent(function(x, y) {
return '<p>'+x+'</p>'+ '<p>' + Math.floor(y.replace(',','')) + ' HOSTS ' + '</p>';
})
.color(colors);
arc = d3.svg.arc()
.innerRadius(outerRadius - innerRadius)
.outerRadius(outerRadius);
pie = d3.layout.pie()
.value(function(d) { return d.value; })
.sort(function() {return null; });
tooltip = d3.select(target)
.append('div')
.attr('class', 'donut-tooltip');
tooltip.append('div')
.attr('class', 'donut-tooltip-inner');
path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d) {
return d.data.color;
d3.select(element.find('svg')[0])
.datum(dataset)
.transition().duration(350)
.call(job_detail_chart)
.style({
"font-family": 'Open Sans',
"font-style": "normal",
"font-weight":400,
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
path.on('mouseenter', function(d) {
var total = d3.sum(dataset.map(function(d) {
return d.value;
}));
var label;
if (d.data.value === 1) {
label = " host ";
} else {
label = " hosts ";
}
var percent = Math.round(1000 * d.data.value / total) / 10;
tooltip.select('.donut-tooltip-inner').html(d.data.value + label + " (" +
percent + "%) " + d.data.label + ".");
//.attr('style', 'color:white;font-family:');
tooltip.style('display', 'block');
});
path.on('mouseleave', function() {
tooltip.style('display', 'none');
});
path.on('mousemove', function() {
// d3.mouse() gives the coordinates of hte mouse, then add
// some offset to provide breathing room for hte tooltip
// based on the dimensions of the donut
tooltip.style('top', (d3.mouse(this)[1] + (height/5) + 'px'))
.style('left', (d3.mouse(this)[0] + (width/3) + 'px'));
});
legend = svg.selectAll('.legend')
.data(pie(dataset))
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * dataset.length / 2;
var horz = -2 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.attr('fill', function(d) {
return d.data.color;
})
.attr('stroke', function(d) {
return d.data.color;
});
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) {
return d.data.label;
});
d3.select(element.find(".nv-label text")[0])
.attr("class", "DashboardGraphs-hostStatusLabel--successful")
.style({
"font-family": 'Open Sans',
"text-anchor": "start",
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : colors[0],
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
d3.select(element.find(".nv-label text")[1])
.attr("class", "DashboardGraphs-hostStatusLabel--failed")
.style({
"font-family": 'Open Sans',
"text-anchor" : "end !imporant",
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : colors[1],
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
d3.select(element.find(".nv-label text")[2])
.attr("class", "DashboardGraphs-hostStatusLabel--successful")
.style({
"font-family": 'Open Sans',
"text-anchor" : "end !imporant",
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : colors[2],
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
d3.select(element.find(".nv-label text")[3])
.attr("class", "DashboardGraphs-hostStatusLabel--failed")
.style({
"font-family": 'Open Sans',
"text-anchor" : "end !imporant",
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : colors[3],
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
return job_detail_chart;
};
}])

View File

@ -1,271 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name helpers.function:License
* @description Routines for checking and reporting license status
* CheckLicense.test() is called in app.js, in line 532, which is when the license is checked. The license information is
* stored in local storage using 'Store()'.
*
*
*
*
*/
import '../forms';
export default
angular.module('LicenseHelper', ['RestServices', 'Utilities', 'LicenseUpdateFormDefinition',
'FormGenerator', 'ParseHelper', 'ModalDialog', 'VariablesHelper', 'LicenseFormDefinition'])
.factory('CheckLicense', ['$q', '$rootScope', '$compile', 'CreateDialog', 'Store',
'LicenseUpdateForm', 'GenerateForm', 'TextareaResize', 'ToJSON', 'GetBasePath',
'Rest', 'ProcessErrors', 'Alert', 'IsAdmin', '$location', 'pendoService',
'Authorization', 'Wait',
function($q, $rootScope, $compile, CreateDialog, Store, LicenseUpdateForm, GenerateForm,
TextareaResize, ToJSON, GetBasePath, Rest, ProcessErrors, Alert, IsAdmin, $location,
pendoService, Authorization, Wait) {
return {
getRemainingDays: function(time_remaining) {
// assumes time_remaining will be in seconds
var tr = parseInt(time_remaining, 10);
return Math.floor(tr / 86400);
},
shouldNotify: function(license) {
if (license && typeof license === 'object' && Object.keys(license).length > 0) {
// we have a license object
if (!license.valid_key) {
// missing valid key
return true;
}
else if (license.free_instances <= 0) {
// host count exceeded
return true;
}
else if (this.getRemainingDays(license.time_remaining) < 15) {
// below 15 days remaining on license
return true;
}
return false;
} else {
// missing license object
return true;
}
},
isAdmin: function() {
return IsAdmin();
},
getHTML: function(license, includeFormButton) {
var title, html,
contact_us = "<a href=\"http://www.ansible.com/contact-us\" target=\"_black\">contact us <i class=\"fa fa-external-link\"></i></a>",
renew = "<a href=\"http://www.ansible.com/renew\" target=\"_blank\">ansible.com/renew <i class=\"fa fa-external-link\"></i></a>",
pricing = "<a href=\"http://www.ansible.com/pricing\" target=\"_blank\">ansible.com/pricing <i class=\"fa fa-external-link\"></i></a>",
license_link = "<a href=\"http://www.ansible.com/license\" target=\"_blank\">click here</a>",
result = {},
license_is_valid=false;
if (license && typeof license === 'object' && Object.keys(license).length > 0 && license.valid_key !== undefined) {
// we have a license
if (!license.valid_key) {
title = "Invalid License";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>The Ansible Tower license is invalid.</p>";
}
else if (this.getRemainingDays(license.time_remaining) <= 0) {
title = "License Expired";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\">\n" +
"<p>Thank you for using Ansible Tower. The Ansible Tower license has expired</p>";
if (parseInt(license.grace_period_remaining,10) > 86400) {
// trial licenses don't get a grace period
if (license.trial) {
html += "<p>Don't worry &mdash; your existing history and content has not been affected, but playbooks will no longer run and new hosts cannot be added. " +
"If you are ready to upgrade, " + contact_us + " or visit " + pricing + " to see all of your license options. Thanks!</p>";
} else {
html += "<p>Don't worry &mdash; your existing history and content has not been affected, but in " + this.getRemainingDays(license.grace_period_remaining) + " days playbooks will no longer " +
"run and new hosts cannot be added. If you are ready to upgrade, " + contact_us + " " +
"or visit <a href=\"http://www.ansible.com/pricing\" target=\"_blank\">ansible.com/pricing <i class=\"fa fa-external-link\"></i></a> to see all of your license options. Thanks!</p>";
}
} else {
html += "<p>Dont worry &mdash; your existing history and content has not been affected, but playbooks will no longer run and new hosts cannot be added. If you are ready to renew or upgrade, contact us " +
"at " + renew + ". Thanks!</p>";
}
}
else if (this.getRemainingDays(license.time_remaining) < 15) {
// Warning: license expiring in less than 15 days
title = "License Warning";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>Thank you for using Ansible Tower. The Ansible Tower license " +
"has " + this.getRemainingDays(license.time_remaining) + " days remaining.</p>";
// trial licenses don't get a grace period
if (license.trial) {
html += "<p>After this license expires, playbooks will no longer run and hosts cannot be added. If you are ready to upgrade, " + contact_us + " or visit " + pricing + " to see all of your license options. Thanks!</p>";
} else {
html += "<p>After this license expires, playbooks will no longer run and hosts cannot be added. If you are ready to renew or upgrade, contact us at " + renew + ". Thanks!</p>";
}
// If there is exactly one day remaining, change "days remaining"
// to "day remaining".
html = html.replace('has 1 days remaining', 'has 1 day remaining');
}
else if (license.free_instances <= 0) {
title = "Host Count Exceeded";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>The Ansible Tower license has reached capacity for the number of managed hosts allowed. No new hosts can be added. Existing " +
"playbooks can still be run against hosts already in inventory.</p>" +
"<p>If you are ready to upgrade, contact us at " + renew + ". Thanks!</p>";
} else {
// license is valid. the following text is displayed in the license viewer
title = "Update License";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>The Ansible Tower license is valid.</p>" +
"<p>If you are ready to upgrade, contact us at " + renew + ". Thanks!</p>";
license_is_valid = true;
}
} else {
// No license
title = "Add Your License";
html = "<div id=\"license-notification-body\"><div style=\"margin-top:5px; margin-bottom:25px;\"><p>Now that youve successfully installed or upgraded Ansible Tower, the next step is to add a license file. " +
"If you dont have a license file yet, " + license_link + " to see all of our free and paid license options.</p>" +
"<p style=\"margin-top:15px; margin-bottom 15px; text-align:center;\"><a href=\"http://ansible.com/license\" target=\"_blank\" class=\"btn btn-danger free-button\">Get a Free Tower Trial License</a></p>";
}
if (IsAdmin()) {
html += "<p>Copy and paste the contents of your license in the field below, agree to the End User License Agreement, and click Submit.</p>";
} else {
html += "<p>A system administrator can install the new license by choosing View License on the Account Menu and clicking on the Update License tab.</p>";
}
html += "</div>";
if (IsAdmin()) {
html += GenerateForm.buildHTML(LicenseUpdateForm, { mode: 'edit', showButtons:((includeFormButton) ? true : false) });
}
html += "</div>";
result.body = html;
result.title = title;
return result;
},
postLicense: function(license_key, in_scope) {
var url = GetBasePath('config'),
self = this,
json_data, scope;
scope = (in_scope) ? in_scope : self.scope;
json_data = ToJSON('json', license_key);
json_data.eula_accepted = scope.eula_agreement;
if (typeof json_data === 'object' && Object.keys(json_data).length > 0) {
Rest.setUrl(url);
Rest.post(json_data)
.success(function (response) {
response.license_info = response;
Alert('License Accepted', 'The Ansible Tower license was updated. To review or update the license, choose View License from the Setup menu.','alert-info');
$rootScope.features = undefined;
Authorization.getLicense()
.success(function (data) {
Authorization.setLicense(data);
pendoService.issuePendoIdentity();
Wait("stop");
$location.path('/home');
})
.error(function () {
Wait('stop');
Alert('Error', 'Failed to access license information. GET returned status: ' + status, 'alert-danger',
$location.path('/logout'));
});
})
.catch(function (response) {
scope.license_json_api_error = "A valid license key in JSON format is required";
ProcessErrors(scope, response.data, response.status, null, { hdr: 'Error!',
msg: 'Failed to update license. POST returned: ' + response.status
});
});
} else {
scope.license_json_api_error = "A valid license key in JSON format is required";
}
},
test: function() {
var license = Store('license'),
self = this,
scope;
var getLicense = function() {
var deferred = $q.defer();
if (license === null) {
Rest.setUrl(GetBasePath('config'));
return Rest.get()
.then(function (data) {
license = data.data.license_info;
deferred.resolve();
return deferred.promise;
}, function () {
deferred.resolve();
return deferred.promise;
});
} else {
deferred.resolve(license);
return deferred.promise;
}
}
var promise = getLicense();
promise.then(function() {
self.scope = $rootScope.$new();
scope = self.scope;
if (license && typeof license === 'object' && Object.keys(license).length > 0) {
if (license.tested) {
return true;
}
license.tested = true;
Store('license',license); //update with tested flag
}
// Don't do anything when the license is valid
if (!self.shouldNotify(license)) {
return true; // if the license is valid it would exit 'test' here, otherwise it moves on to making the modal for the license
}
$location.path('/license');
});
},
GetLicense: function(callback, inScope) {
// Retrieve license detail
var self = this,
scope = (inScope) ? inScope : self.scope,
url = GetBasePath('config');
Rest.setUrl(url);
Rest.get()
.success(function (data) {
if (scope && callback) {
scope.$emit(callback, data);
}
else if (scope) {
scope.$emit('CheckLicenseReady', data);
}
})
.error(function (data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve license. GET status: ' + status
});
});
}
};
}]);

View File

@ -32,14 +32,14 @@ export default
// Which page are we on?
if (Empty(next) && previous) {
// no next page, but there is a previous page
scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/, '')) + 1;
scope[iterator + '_page'] = scope[iterator + '_num_pages'];
} else if (next && Empty(previous)) {
// next page available, but no previous page
scope[iterator + '_page'] = 1;
$('#'+iterator+'-pagination #pagination-links li:eq(1)').attr('class', 'disabled');
} else if (next && previous) {
// we're in between next and previous
scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/, '')) + 1;
scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2;
}
// Calc the range of up to 10 pages to show

View File

@ -3,9 +3,32 @@
@import '../shared/branding/colors.less';
@import '../shared/branding/colors.default.less';
.JobDetail-panelHeader{
height: 50px;
.JobDetail{
display: flex;
flex-direction: row;
}
.JobDetail-leftSide{
flex: 1 0 auto;
width: 50%;
padding-right: 20px;
}
.JobDetail-rightSide{
flex: 1 0 auto;
width: 50%;
}
.JobDetail-panelHeader{
display: flex;
height: 30px;
}
.JobDetail-expandContainer{
flex: 1;
margin: 0px;
line-height: 30px;
white-space: nowrap;
}
.JobDetail-panelHeaderText{
@ -38,6 +61,7 @@
display: flex;
flex-wrap: wrap;
flex-direction: row;
padding-top: 25px;
}
.JobDetail-resultRow{
@ -45,6 +69,10 @@
display: flex;
}
.JobDetail-resultRowLabel{
text-transform: uppercase;
}
.JobDetail-resultRow label{
color: @default-interface-txt;
font-size: 14px;
@ -52,7 +80,103 @@
flex: 1 0 auto;
}
.JobDetail-resultRow--variables{
width: 100%;
display: flex;
flex-direction: column;
padding-left:15px;
}
.JobDetail-extraVars{
text-transform: none;
}
.JobDetail-extraVarsLabel{
margin-left:-15px;
padding-bottom: 15px;
}
.JobDetail-resultRowText{
width: 40%;
flex: 1 0 auto;
padding:0px;
text-transform: none;
}
.JobDetail-searchHeaderRow{
display: flex;
flex-wrap: wrap;
flex-direction: row;
height: 50px;
margin-top: 25px;
}
.JobDetail-searchContainer{
flex: 1 0 auto;
}
.JobDetail-tableToggleContainer{
flex: 1 0 auto;
display: flex;
justify-content: flex-end;
}
.JobDetail-tableToggle{
padding-left:10px;
padding-right: 10px;
border: 1px solid @default-second-border;
}
.JobDetail-tableToggle.active{
background-color: @default-link;
border: 1px solid @default-link;
color: @default-bg;
}
.JobDetail-tableToggle--left{
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
.JobDetail-tableToggle--right{
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.JobDetail-searchInput{
border-radius: 5px !important;
}
.JobDetail-tableHeader:last-of-type{
text-align:justify;
}
.JobDetail-statusIcon{
padding-right: 10px;
padding-left: 10px;
}
.JobDetail-tableRow--selected,
.JobDetail-tableRow--selected > :first-child{
border-left: 5px solid @list-row-select-bord;
}
.JobDetail-tableRow--selected > :first-child > .JobDetail-statusIcon{
margin-left: -5px;
}
.JobDetail-statusIcon--results{
padding-left: 0px;
padding-right: 10px;
}
.JobDetail-graphSection{
height: 320px;
width:100%;
}
.JobDetail-stdoutActionButton--active{
flex:none;
width:0px;
padding-right: 0px;
}

View File

@ -19,7 +19,7 @@ export default
'EventViewer', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer',
'LoadPlays', 'LoadTasks', 'LoadHosts', 'HostsEdit',
'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels',
'EditSchedule',
'EditSchedule', 'ParseTypeChange',
function(
$location, $rootScope, $filter, $scope, $compile, $stateParams,
$log, ClearScope, GetBasePath, Wait, Rest, ProcessErrors,
@ -28,7 +28,7 @@ export default
SetTaskStyles, DigestEvent, UpdateDOM, EventViewer, DeleteJob,
PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts,
HostsEdit, ParseVariableString, GetChoices, fieldChoices,
fieldLabels, EditSchedule
fieldLabels, EditSchedule, ParseTypeChange
) {
ClearScope();
@ -41,8 +41,9 @@ export default
job_type_options;
scope.plays = [];
scope.parseType = 'yaml';
scope.previousTaskFailed = false;
$scope.stdoutFullScreen = false;
scope.$watch('job_status', function(job_status) {
if (job_status && job_status.explanation && job_status.explanation.split(":")[0] === "Previous Task Failed") {
@ -201,6 +202,8 @@ export default
scope.haltEventQueue = false;
scope.processing = false;
scope.lessStatus = false;
scope.lessDetail = false;
scope.lessEvents = true;
scope.host_summary = {};
scope.host_summary.ok = 0;
@ -555,7 +558,7 @@ export default
});
});
if (scope.activeTask && scope.jobData.plays[scope.activePlay] && scope.jobData.plays[scope.activePlay].tasks[scope.activeTask]) {
scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'active';
scope.jobData.plays[scope.activePlay].tasks[scope.activeTask].taskActiveClass = 'JobDetail-tableRow--selected';
}
scope.$emit('LoadHosts');
})
@ -675,7 +678,7 @@ export default
scope.host_summary.failed;
});
if (scope.activePlay && scope.jobData.plays[scope.activePlay]) {
scope.jobData.plays[scope.activePlay].playActiveClass = 'active';
scope.jobData.plays[scope.activePlay].playActiveClass = 'JobDetail-tableRow--selected';
}
scope.$emit('LoadTasks', events_url);
})
@ -804,6 +807,7 @@ export default
return true;
});
//scope.setSearchAll('host');
ParseTypeChange({ scope: scope, field_id: 'pre-formatted-variables' });
scope.$emit('LoadPlays', data.related.job_events);
})
.error(function(data, status) {
@ -839,7 +843,6 @@ export default
$('.overlay').hide();
$('#summary-button').hide();
$('#hide-summary-button').hide();
$('#job-detail-container').css({ "width": "58.33333333%", "padding-right": "7px" });
$('#job-summary-container .job_well').css({
'box-shadow': 'none',
'height': 'auto'
@ -859,12 +862,12 @@ export default
// 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').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() - $('#job-detail-footer').outerHeight() - 20;
$('#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));
@ -872,10 +875,9 @@ export default
$('#hosts-table-detail').height(150 + (height * 0.70));
}
// Summary table height adjusting.
height = ($('#job-detail-container').height() / 2) - $('#hosts-summary-section .header').outerHeight() -
$('#hosts-summary-section .table-header').outerHeight() -
$('#summary-search-section').outerHeight() - 20;
$('#hosts-summary-table').height(height);
height = ($('#job-detail-container').height() / 2) - $('#hosts-summary-section .JobDetail-searchHeaderRow').outerHeight() -
$('#hosts-summary-section .table-header').outerHeight() - 20;
// $('#hosts-summary-table').height(height);
//$('#hosts-summary-table').mCustomScrollbar("update");
scope.$emit('RefreshCompleted');
};
@ -980,15 +982,38 @@ export default
scope.toggleLessStatus = function() {
if (!scope.lessStatus) {
$('#job-status-form .toggle-show').slideUp(200);
$('#job-status-form').slideUp(200);
scope.lessStatus = true;
}
else {
$('#job-status-form .toggle-show').slideDown(200);
$('#job-status-form').slideDown(200);
scope.lessStatus = false;
}
};
scope.toggleLessDetail = function() {
if (!scope.lessDetail) {
$('#job-detail-details').slideUp(200);
scope.lessDetail = true;
}
else {
$('#job-detail-details').slideDown(200);
scope.lessDetail = false;
}
};
scope.toggleLessEvents = function() {
if (!scope.lessEvents) {
$('#events-summary').slideUp(200);
scope.lessEvents = true;
}
else {
$('#events-summary').slideDown(200);
scope.lessEvents = false;
DrawGraph({scope:scope});
}
};
scope.filterPlayStatus = function() {
scope.search_play_status = (scope.search_play_status === 'all') ? 'failed' : 'all';
if (!scope.liveEventProcessing || scope.pauseLiveEvents) {
@ -1409,16 +1434,10 @@ export default
$scope.$emit('LoadJob');
};
scope.editHost = function(id) {
HostsEdit({
host_scope: scope,
group_scope: null,
host_id: id,
inventory_id: scope.job.inventory,
mode: 'edit', // 'add' or 'edit'
selected_group_id: null
});
};
// Click binding for the expand/collapse button on the standard out log
$scope.toggleStdoutFullscreen = function() {
$scope.stdoutFullScreen = !$scope.stdoutFullScreen;
}
scope.editSchedule = function() {
// We need to get the schedule's ID out of the related links

View File

@ -1,419 +1,409 @@
<div class="tab-pane" id="jobs-detail">
<div ng-cloak id="htmlTemplate">
<div class="row" style="position: relative;">
<div id="job-detail-container">
<div class="JobDetail-resultsContainer Panel">
<div class="JobDetail-panelHeader">
<div ng-cloak id="htmlTemplate" class="JobDetail">
<!--beginning of job-detail-container (left side) -->
<div id="job-detail-container" class="JobDetail-leftSide" ng-class="{'JobDetail-stdoutActionButton--active': stdoutFullScreen}">
<!--beginning of results-->
<div id="job-results-panel" class="JobDetail-resultsContainer Panel" ng-show="!stdoutFullScreen">
<div class="JobDetail-panelHeader">
<div class="JobDetail-expandContainer">
<a class="JobDetail-panelHeaderText" ng-show="lessStatus" href="" ng-click="toggleLessStatus()">
RESULTS<i class="JobDetail-expandArrow fa fa-caret-left"></i>
</a>
<a class="JobDetail-panelHeaderText" ng-show="!lessStatus" href="" ng-click="toggleLessStatus()">
RESULTS<i class="JobDetail-expandArrow fa fa-caret-down"></i>
</a>
<button id="submit-action" class="List-actionButton JobDetail-launchButton" data-placement="top" mode="all" ng-click="relaunchJob()" aw-tool-tip="Start a job using this template" data-original-title="" title=""><i class="fa fa-rocket"></i> </button>
<button id="delete-action" class="List-actionButton List-actionButton--delete JobDetail-launchButton" data-placement="top" ng-click="deleteJobTemplate(job_template.id, job_template.name)" aw-tool-tip="Delete template" data-original-title="" title=""><i class="fa fa-trash-o"></i> </button>
</div>
<div class="form-horizontal JobDetail-resultsDetails" role="form" id="job-status-form">
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Status</label>
<div class="JobDetail-resultRowText"><i class="fa icon-job-{{ job_status.status }}"></i> {{ job_status.status_label }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.explanation">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 col-xs-12">Explanation</label>
<div class="col-lg-10 col-md-10 col-sm-10 col-xs-9 job_status_explanation"
ng-show="!previousTaskFailed" ng-bind-html="job_status.explanation"></div>
<div class="col-lg-10 col-md-10 col-sm-10 col-xs-9 job_status_explanation"
ng-show="previousTaskFailed">Previous Task Failed
<a
href=""
id="explanation_help"
aw-pop-over="{{ task_detail }}"
aw-pop-over-watch="task_detail"
data-placement="bottom"
data-container="body" class="help-link" over-title="Failure Detail"
title=""
tabindex="-1">
<i class="fa fa-question-circle">
</i>
</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.traceback">
<label class="col-lg-2 col-md-12 col-sm-12 col-xs-12">Results Traceback</label>
<div class="JobDetail-resultRowText col-lg-10 col-md-12 col-sm-12 col-xs-12 job_status_traceback" ng-bind-html="job_status.traceback"></div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Started</label>
<div class="JobDetail-resultRowText">{{ job_status.started | date:'MM/dd/yy HH:mm:ss' }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Finished</label>
<div class="JobDetail-resultRowText">{{ job_status.finished | date:'MM/dd/yy HH:mm:ss' }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Elapsed</label>
<div class="JobDetail-resultRowText">{{ job_status.elapsed }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_template_name">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Template</label>
<div class="JobDetail-resultRowText">
<a href="{{ job_template_url }}" aw-tool-tip="Edit the job template" data-placement="top">{{ job_template_name }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_type">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Job Type</label>
<div class="JobDetail-resultRowText">{{ job_type }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="created_by">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Launched By</label>
<div class="JobDetail-resultRowText">
<a href="{{ users_url }}" aw-tool-tip="Edit the User" data-placement="top">{{ created_by }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="scheduled_by">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Launched By</label>
<div class="JobDetail-resultRowText">
<a href aw-tool-tip="Edit the Schedule" data-placement="top" ng-click="editSchedule()">{{scheduled_by}}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="inventory_name">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Inventory</label>
<div class="JobDetail-resultRowText">
<a href="{{ inventory_url }}" aw-tool-tip="Edit the inventory" data-placement="top">{{ inventory_name }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="project_name">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Project</label>
<div class="JobDetail-resultRowText">
<a href="{{ project_url }}" aw-tool-tip="Edit the project" data-placement="top">{{ project_name }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job.playbook">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Playbook</label>
<div class="JobDetail-resultRowText">{{ job.playbook }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="credential_name">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Machine Credential</label>
<div class="JobDetail-resultRowText JobDetail-resultRowText">
<a href="{{ credential_url }}" aw-tool-tip="Edit the credential" data-placement="top">{{ credential_name }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="cloud_credential_name">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Cloud Credential</label>
<div class="JobDetail-resultRowText">
<a href="{{ cloud_credential_url }}" aw-tool-tip="Edit the credential" data-placement="top">{{ cloud_credential_name }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job.forks">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Forks</label>
<div class="JobDetail-resultRowText">{{ job.forks }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job.limit">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Limit</label>
<div class="JobDetail-resultRowText">{{ job.limit }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="verbosity">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Verbosity</label>
<div class="JobDetail-resultRowText">{{ verbosity }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job.job_tags">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Job Tags</label>
<div class="JobDetail-resultRowText">{{ job.job_tags }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="variables">
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Extra Variables</label>
<div class="JobDetail-resultRowText">
<div id="pre-formatted-variables">{{ variables }}</div>
<!-- <pre>{{ variables }}</pre> -->
</div>
</div>
<div class="JobDetail-actions">
<button id="relaunch-job-button" class="List-actionButton JobDetail-launchButton" data-placement="top" mode="all" ng-click="relaunchJob()" aw-tool-tip="Relaunch using the same parameters" data-original-title="" title=""><i class="fa fa-rocket"></i> </button>
<button id="cancel-job-button" class="List-actionButton List-actionButton--delete JobDetail-launchButton" data-placement="top" ng-click="deleteJob()" ng-show="job_status.status == 'running' || job_status.status=='pending' " aw-tool-tip="Cancel" data-original-title="" title=""><i class="fa fa-minus-circle"></i> </button>
<button id="delete-job-button" class="List-actionButton List-actionButton--delete JobDetail-launchButton" data-placement="top" ng-click="deleteJob()" ng-hide="job_status.status == 'running' || job_status.status == 'pending' " aw-tool-tip="Delete" data-original-title="" title=""><i class="fa fa-trash-o"></i> </button>
</div>
</div>
<!--- JobDetail-results---------------------------------------------->
<div id="job-detail-tables">
<div id="play-section" class="section">
<div class="form-horizontal JobDetail-resultsDetails" role="form" id="job-status-form">
<div class="form-group JobDetail-resultRow toggle-show">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Status</label>
<div class="JobDetail-resultRowText"><i class="JobDetail-statusIcon--results fa icon-job-{{ job_status.status }}"></i> {{ job_status.status_label }}</div>
</div>
<div class="row title-row">
<div class="col-lg-1 col-md-2 col-sm-2 col-xs-1 title">Plays</div>
<div class="col-lg-11 col-md-10 col-sm-10 col-xs-11" style="text-align:right;">
<div id="play-search-form" class="search-form form-inline">
<div class="form-group">
<div class="search-name" style="display:inline-block; position:relative;">
<input type="text" class="input-xs form-control" id="search_play_name" ng-model="search_play_name"
placeholder="Play Name" ng-keypress="searchPlaysKeyPress($event)" >
<div id="search-all-input-icons">
<a class="search-icon" ng-show="searchPlaysEnabled" ng-click="searchPlays()"><i class="fa fa-search"></i></a>
<a class="search-icon" ng-show="!searchPlaysEnabled" ng-click="search_play_name=''; searchPlays()"><i class="fa fa-times"></i></a>
</div>
</div>
</div>
<div class="form-group">
<div class="btn-group" aw-toggle-button data-after-toggle="filterPlayStatus">
<button class="btn btn-xs btn-primary active">All</button>
<button class="btn btn-xs btn-default">Failed</button>
</div>
</div>
</div>
</div>
</div>
<div id="plays-table-header" class="table-header">
<table class="table table-condensed">
<thead>
<tr>
<th class="col-lg-2 col-md-2 col-sm-2 col-xs-3">Started</th>
<th class="col-lg-2 col-md-2 col-sm-2 col-xs-3">Elapsed</th>
<th class="col-lg-1 col-md-2 col-sm-2 col-xs-2 status-column">Status</th>
<th class="col-lg-7 col-md-6 col-sm-6 col-xs-4">Name</th>
</tr>
</thead>
</table>
</div>
<div id="plays-table-detail" class="table-detail" lr-infinite-scroll="playsScrollDown"
scroll-threshold="10" time-threshold="500">
<table class="table table-condensed">
<tbody>
<tr class="cursor-pointer" ng-repeat="play in plays" ng-class="play.playActiveClass" ng-click="selectPlay(play.id, $event)">
<td class="col-lg-2 col-md-2 col-sm-2 col-xs-3">{{ play.created | date: 'HH:mm:ss' }}</td>
<td class="col-lg-2 col-md-2 col-sm-2 col-xs-3" aw-tool-tip="{{ play.finishedTip }}" data-tip-watch="play.finishedTip"
data-placement="top">{{ play.elapsed }}</td>
<td class="col-lg-1 col-md-2 col-sm-2 col-xs-2 status-column" aw-tool-tip="{{ play.status_tip }}" data-tip-watch="play.status_tip" data-placement="top"><i class="fa icon-job-{{ play.status }}"></i></td>
<td class="col-lg-7 col-md-6 col-sm-6 col-xs-4">{{ play.name }}</td>
</tr>
<tr ng-show="plays.length === 0 && waiting">
<td colspan="4" class="col-lg-12 loading-info">Waiting...</td>
</tr>
<tr ng-show="plays.length === 0 && playsLoading && !waiting">
<td colspan="4" class="col-lg-12 loading-info">Loading...</td>
</tr>
<tr ng-show="plays.length === 0 && !playsLoading && !waiting">
<td colspan="4" class="col-lg-12 loading-info">No matching plays</td>
</tr>
</tbody>
</table>
</div>
<div class="scroll-spinner" id="playsMoreRows"><i class="fa fa-cog fa-spin"></i></div>
</div><!-- section -->
<div id="task-section" class="section" tasks=>
<div class="row title-row">
<div class="col-lg-1 col-md-2 col-sm-2 title">Tasks</div>
<div class="col-lg-11 col-md-10 col-sm-10" style="text-align:right;">
<div id="task-search-form" class="search-form form-inline">
<div class="form-group">
<div class="search-name" style="display:inline-block; position:relative;">
<input type="text" class="input-xs form-control" id="search_task_name" ng-model="search_task_name"
placeholder="Task Name" ng-keypress="searchTasksKeyPress($event)" >
<div id="search-all-input-icons">
<a class="search-icon" ng-show="searchTasksEnabled" ng-click="searchTasks()"><i class="fa fa-search"></i></a>
<a class="search-icon" ng-show="!searchTasksEnabled" ng-click="search_task_name=''; searchTasks()"><i class="fa fa-times"></i></a>
</div>
</div>
</div>
<div class="form-group">
<div class="btn-group" aw-toggle-button data-after-toggle="filterTaskStatus">
<button class="btn btn-xs btn-primary active">All</button>
<button class="btn btn-xs btn-default">Failed</button>
</div>
</div>
</div>
</div>
</div>
<div class="table-header">
<table id="tasks-table-header" class="table table-condensed">
<thead>
<tr>
<th class="col-lg-2 col-md-2 col-sm-2 col-xs-3">Started</th>
<th class="col-lg-2 col-md-2 col-sm-2 col-xs-3">Elapsed</th>
<th class="col-lg-1 col-md-2 col-sm-2 col-xs-2 status-column">Status</th>
<th class="col-lg-3 col-md-3 col-sm-6 col-xs-4">Name</div>
<th class="col-lg-4 col-md-3 hidden-xs hidden-sm">Host Status</th>
</tr>
</thead>
</table>
</div>
<div id="tasks-table-detail" class="table-detail" lr-infinite-scroll="tasksScrollDown"
scroll-threshold="10" time-threshold="500">
<table class="table table-condensed">
<tbody>
<tr class="cursor-pointer" ng-repeat="task in taskList = (tasks) track by $index" ng-class="task.taskActiveClass" ng-click="selectTask(task.id)">
<td class="col-lg-2 col-md-2 col-sm-2 col-xs-3">{{ task.created | date: 'HH:mm:ss' }}</td>
<td class="col-lg-2 col-md-2 col-sm-2 col-xs-3" aw-tool-tip="{{ task.finishedTip }}" data-tip-watch="task.finishedTip"
data-placement="top">{{ task.elapsed }}</td>
<td class="col-lg-1 col-md-2 col-sm-2 col-xs-2 status-column" aw-tool-tip="{{ task.status_tip }}"
data-tip-watch="task.status_tip" data-placement="top"><i class="fa icon-job-{{ task.status }}"></i></td>
<td class="col-lg-3 col-md-3 col-sm-6 col-xs-4" id="">{{ task.name }}</td>
<td class="col-lg-4 col-md-3 hidden-sm hidden-xs">
<div class="status-bar">
<div class="successful-hosts inner-bar" id="{{ task.id }}-successful-bar" aw-tool-tip="{{ task.successfulCountTip }}" data-tip-watch="task.successfulCountTip" data-placement="top" ng-style="task.successfulStyle">
{{ task.successfulCount }}
</div>
<div class="changed-hosts inner-bar" id="{{ task.id }}-changed-bar" aw-tool-tip="{{ task.changedCountTip }}" data-tip-watch="task.changedCountTip" data-placement="top" ng-style="task.changedStyle">
{{ task.changedCount }}
</div>
<div class="skipped-hosts inner-bar" id="{{ task.id }}-skipped-bar" aw-tool-tip="{{ task.skippedCountTip }}" data-tip-watch="task.skippedCountTip" data-placement="top" ng-style="task.skippedStyle">
{{ task.skippedCount }}
</div>
<div class="failed-hosts inner-bar" id="{{ task.id }}-failed-bar" aw-tool-tip="{{ task.failedCountTip }}" data-tip-watch="task.failedCountTip" data-placement="top" ng-style="task.failedStyle">
{{ task.failedCount }}
</div>
<div class="unreachable-hosts inner-bar" id="{{ task.id }}-unreachable-hosts-bar" aw-tool-tip="{{ task.unreachableCountTip }}" data-tip-watch="task.unreachableCountTip" data-placement="top" ng-style="task.unreachableStyle">
{{ task.unreachableCount }}
</div>
<div class="missing-hosts inner-bar" id="{{ task.id }}-misssing-hosts-bar" aw-tool-tip="{{ task.missingCountTip }}" data-tip-watch="task.missingCountTip" data-placement="top" ng-style="task.missingStyle">
{{ task.missingCount }}
</div>
<div class="no-matching-hosts inner-bar" id="{{ task.id }}-{{ task.play_id }}-no-matching-hosts-bar" aw-tool-tip="No matching hosts were found." data-placement="top" style="width: 100%;" ng-show="task.status === 'no-matching-hosts'">
No matching hosts.
</div>
</div>
</td>
</tr>
<tr ng-show="taskList.length === 0 && waiting">
<td colspan="5" class="col-lg-12 loading-info">Waiting...</td>
</tr>
<tr ng-show="taskList.length === 0 && tasksLoading && !waiting">
<td colspan="5" class="col-lg-12 loading-info">Loading...</td>
</tr>
<tr ng-show="taskList.length === 0 && !tasksLoading && !waiting">
<td colspan="5" class="col-lg-12 loading-info">No matching tasks</td>
</tr>
</tbody>
</table>
</div>
<div class="scroll-spinner" id="tasksMoreRows"><i class="fa fa-cog fa-spin"></i></div>
</div><!-- section -->
<div id="task-hosts-section" class="section">
<div class="row title-row">
<div class="col-lg-2 col-md-2 col-sm-2 title">Host Events</div>
<div class="col-lg-10 col-md-10 col-sm-10" style="text-align:right;">
<div id="host-search-form" class="search-form form-inline">
<div class="form-group">
<div class="search-name" style="display:inline-block; position:relative;">
<input type="text" class="input-xs form-control" id="search_host_name" ng-model="search_host_name"
placeholder="Host Name" ng-keypress="searchHostsKeyPress($event)" >
<div id="search-all-input-icons">
<a class="search-icon" ng-show="searchHostsEnabled" ng-click="searchHosts()"><i class="fa fa-search"></i></a>
<a class="search-icon" ng-show="!searchHostsEnabled" ng-click="search_host_name=''; searchHosts()"><i class="fa fa-times"></i></a>
</div>
</div>
</div>
<div class="form-group">
<div class="btn-group" aw-toggle-button data-after-toggle="filterHostStatus">
<button class="btn btn-xs btn-primary active">All</button>
<button class="btn btn-xs btn-default">Failed</button>
</div>
</div>
</div>
</div>
</div>
<div class="table-header" id="hosts-table-header">
<table class="table table-condensed">
<thead>
<tr>
<th class="col-lg-1 col-md-1 col-sm-2 col-xs-2 status-column">Status</th>
<th class="col-lg-3 col-md-3 col-sm-3 col-xs-3">Host</th>
<th class="col-lg-4 col-md-4 col-sm-3 col-xs-3">Item</th>
<th class="col-lg-4 col-md-4 col-sm-3 col-xs-3">Message</th>
<th class="col-lg-1 col-md-1 col-sm-1 col-xs-1"></th>
</tr>
</thead>
</table>
</div>
<div id="hosts-table-detail" class="table-detail" lr-infinite-scroll="hostResultsScrollDown" scroll-threshold="10" time-threshold="500">
<table class="table table-condensed">
<tbody>
<tr ng-repeat="result in results = (hostResults) track by $index">
<td class="col-lg-1 col-md-1 col-sm-2 col-xs-2 status-column"><a href="" ng-click="viewHostResults(result.id)" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-placement="top"><i ng-show="result.status_text != 'Unreachable'" class="fa icon-job-{{ result.status }}"></i><i ng-show="result.status_text == 'Unreachable'" class="fa icon-job-unreachable"></i></a></td>
<td class="col-lg-3 col-md-3 col-sm-3 col-xs-3"><a href="" ng-click="viewHostResults(result.id)" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-placement="top">{{ result.name }}</a></td>
<td class="col-lg-4 col-md-4 col-sm-3 col-xs-3 item-column">{{ result.item }}</td>
<td class="col-lg-4 col-md-4 col-sm-3 col-xs-3">{{ result.msg }}</td>
<td class="col-lg-1 col-md-1 col-sm-1 col-xs-1"><a ng-show="result.host_id" href="" ng-click="editHost(result.host_id)" aw-tool-tip="Edit host" data-placement="top"><i class="fa fa-pencil"></i></a></td>
</tr>
<tr ng-show="results.length === 0 && waiting">
<td colspan="5" class="col-lg-12 loading-info">Waiting...</td>
</tr>
<tr ng-show="results.length === 0 && hostResultsLoading && !waiting">
<td colspan="5" class="col-lg-12 loading-info">Loading...</td>
</tr>
<tr ng-show="results.length === 0 && !hostResultsLoading && !waiting">
<td colspan="5" class="col-lg-12 loading-info">No matching host events</td>
</tr>
</tbody>
</table>
</div>
<div class="scroll-spinner" id="hostResultsMoreRows"><i class="fa fa-cog fa-spin"></i></div>
</div><!-- section -->
</div><!-- job-detail-tables -->
</div><!-- job-detail-container -->
<!-- <div id="job-summary-container"> -->
<div class="job_well">
<div id="summary-well-top-section">
<div id="hide-summary-button" style="display: hidden;">
<a href="" class="btn btn-xs btn-primary" ng-click="toggleSummary('hide')" aw-tool-tip="Hide summary" data-placement="top"><i class="fa fa-arrow-circle-right"></i></a>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.explanation">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 col-xs-12">Explanation</label>
<div class="JobDetail-resultRowText col-lg-10 col-md-10 col-sm-10 col-xs-9 job_status_explanation"
ng-show="!previousTaskFailed" ng-bind-html="job_status.explanation"></div>
<div class="JobDetail-resultRowText col-lg-10 col-md-10 col-sm-10 col-xs-9 job_status_explanation"
ng-show="previousTaskFailed">Previous Task Failed
<a
href=""
id="explanation_help"
aw-pop-over="{{ task_detail }}"
aw-pop-over-watch="task_detail"
data-placement="bottom"
data-container="body" class="help-link" over-title="Failure Detail"
title=""
tabindex="-1">
<i class="fa fa-question-circle">
</i>
</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.traceback">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-12 col-sm-12 col-xs-12">Results Traceback</label>
<div class="JobDetail-resultRowText col-lg-10 col-md-12 col-sm-12 col-xs-12 job_status_traceback" ng-bind-html="job_status.traceback"></div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_template_name">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Template</label>
<div class="JobDetail-resultRowText">
<a href="{{ job_template_url }}" aw-tool-tip="Edit the job template" data-placement="top">{{ job_template_name }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Started</label>
<div class="JobDetail-resultRowText">{{ job_status.started | date:'MM/dd/yy HH:mm:ss' }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_type">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Job Type</label>
<div class="JobDetail-resultRowText">{{ job_type }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Finished</label>
<div class="JobDetail-resultRowText">{{ job_status.finished | date:'MM/dd/yy HH:mm:ss' }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="created_by">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Launched By</label>
<div class="JobDetail-resultRowText">
<a href="{{ users_url }}" aw-tool-tip="Edit the User" data-placement="top">{{ created_by }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job_status.started">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Elapsed</label>
<div class="JobDetail-resultRowText">{{ job_status.elapsed }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="scheduled_by">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Launched By</label>
<div class="JobDetail-resultRowText">
<a href aw-tool-tip="Edit the Schedule" data-placement="top" ng-click="editSchedule()">{{scheduled_by}}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="inventory_name">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Inventory</label>
<div class="JobDetail-resultRowText">
<a href="{{ inventory_url }}" aw-tool-tip="Edit the inventory" data-placement="top">{{ inventory_name }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="project_name">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Project</label>
<div class="JobDetail-resultRowText">
<a href="{{ project_url }}" aw-tool-tip="Edit the project" data-placement="top">{{ project_name }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job.playbook">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Playbook</label>
<div class="JobDetail-resultRowText">{{ job.playbook }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="credential_name">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Machine Credential</label>
<div class="JobDetail-resultRowText JobDetail-resultRowText">
<a href="{{ credential_url }}" aw-tool-tip="Edit the credential" data-placement="top">{{ credential_name }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="cloud_credential_name">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Cloud Credential</label>
<div class="JobDetail-resultRowText">
<a href="{{ cloud_credential_url }}" aw-tool-tip="Edit the credential" data-placement="top">{{ cloud_credential_name }}</a>
</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job.forks">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Forks</label>
<div class="JobDetail-resultRowText">{{ job.forks }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job.limit">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Limit</label>
<div class="JobDetail-resultRowText">{{ job.limit }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="verbosity">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Verbosity</label>
<div class="JobDetail-resultRowText">{{ verbosity }}</div>
</div>
<div class="form-group JobDetail-resultRow toggle-show" ng-show="job.job_tags">
<label class="JobDetail-resultRowLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Job Tags</label>
<div class="JobDetail-resultRowText">{{ job.job_tags }}</div>
</div>
<div class="form-group JobDetail-resultRow JobDetail-resultRow--variables toggle-show" ng-show="variables">
<label class="JobDetail-resultRowLabel JobDetail-extraVarsLabel col-lg-2 col-md-2 col-sm-2 col-xs-3 control-label">Extra Variables</label>
<textarea rows="6" ng-model="variables" name="variables" class="JobDetail-extraVars" id="pre-formatted-variables"></textarea>
</div>
</div>
</div>
<!--- end of results-->
<!--beginning of details-->
<div id="job-detail-panel" class="JobDetail-resultsContainer Panel" ng-show="!stdoutFullScreen">
<div class="JobDetail-panelHeader">
<div class="JobDetail-expandContainer">
<a class="JobDetail-panelHeaderText" ng-show="lessDetail" href="" ng-click="toggleLessDetail()">
DETAILS<i class="JobDetail-expandArrow fa fa-caret-left"></i>
</a>
<a class="JobDetail-panelHeaderText" ng-show="!lessDetail" href="" ng-click="toggleLessDetail()">
DETAILS<i class="JobDetail-expandArrow fa fa-caret-down"></i>
</a>
</div>
</div>
<div id="job-detail-details">
<div id="play-section">
<div class="JobDetail-searchHeaderRow">
<div class="JobDetail-searchContainer form-group">
<div class="search-name">
<input type="text" class="JobDetail-searchInput form-control List-searchInput" id="search_play_name" ng-model="search_play_name" placeholder="Play Name" ng-keypress="searchPlaysKeyPress($event)" >
<a class="List-searchInputIcon search-icon" ng-show="searchPlaysEnabled" ng-click="searchPlays()"><i class="fa fa-search"></i></a>
<a class="List-searchInputIcon search-icon" ng-show="!searchPlaysEnabled" ng-click="search_play_name=''; searchPlays()"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="JobDetail-tableToggleContainer form-group">
<div class="btn-group" aw-toggle-button data-after-toggle="filterPlayStatus">
<button class="JobDetail-tableToggle btn btn-xs btn-primary active">All</button>
<button class="JobDetail-tableToggle btn btn-xs btn-default">Failed</button>
</div>
</div>
</div>
<div id="plays-table-header" class="table-header">
<table class="table table-condensed">
<thead>
<tr>
<th class="List-tableHeader col-lg-7 col-md-6 col-sm-6 col-xs-4">Plays</th>
<th class="List-tableHeader col-lg-2 col-md-2 col-sm-2 col-xs-3">Started</th>
<th class="List-tableHeader JobDetail-tableHeader col-lg-2 col-md-2 col-sm-2 col-xs-3">Elapsed</th>
</tr>
</thead>
</table>
</div>
<div id="plays-table-detail" class="table-detail" lr-infinite-scroll="playsScrollDown"
scroll-threshold="10" time-threshold="500">
<table class="table">
<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>
<td class="List-tableCell col-lg-2 col-md-2 col-sm-2 col-xs-3">{{ play.created | date: 'HH:mm:ss' }}</td>
<td class="List-tableCell col-lg-2 col-md-2 col-sm-2 col-xs-3" aw-tool-tip="{{ play.finishedTip }}" data-tip-watch="play.finishedTip"
data-placement="top">{{ play.elapsed }}</td>
</tr>
<tr ng-show="plays.length === 0 && waiting">
<td colspan="4" class="col-lg-12 loading-info">Waiting...</td>
</tr>
<tr ng-show="plays.length === 0 && playsLoading && !waiting">
<td colspan="4" class="col-lg-12 loading-info">Loading...</td>
</tr>
<tr ng-show="plays.length === 0 && !playsLoading && !waiting">
<td colspan="4" class="col-lg-12 loading-info">No matching plays</td>
</tr>
</tbody>
</table>
</div>
<div class="scroll-spinner" id="playsMoreRows">
<i class="fa fa-cog fa-spin"></i>
</div>
</div>
<!-- end of plays section of details-->
<div id="task-section" class="section" >
<div class="JobDetail-searchHeaderRow">
<div class="JobDetail-searchContainer form-group">
<div class="search-name">
<input type="text" class="JobDetail-searchInput form-control List-searchInput" id="search_task_name" ng-model="search_task_name" placeholder="Task Name" ng-keypress="searchTasksKeyPress($event)" >
<a class="List-searchInputIcon search-icon" ng-show="searchTasksEnabled" ng-click="searchTasks()"><i class="fa fa-search"></i></a>
<a class="List-searchInputIcon search-icon" ng-show="!searchTasksEnabled" ng-click="search_task_name=''; searchTasks()"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="JobDetail-tableToggleContainer form-group">
<div class="btn-group" aw-toggle-button data-after-toggle="filterTaskStatus">
<button class="JobDetail-tableToggle btn btn-xs btn-primary active">All</button>
<button class="JobDetail-tableToggle btn btn-xs btn-default">Failed</button>
</div>
</div>
</div>
<div class="table-header">
<table id="tasks-table-header" class="table table-condensed">
<thead>
<tr>
<th class="List-tableHeader col-lg-3 col-md-3 col-sm-6 col-xs-4">Tasks</th>
<th class="List-tableHeader col-lg-2 col-md-2 col-sm-2 col-xs-3">Started</th>
<th class="List-tableHeader col-lg-2 col-md-2 col-sm-2 col-xs-3">Elapsed</th>
<th class="List-tableHeader JobDetail-tableHeader col-lg-4 col-md-3 hidden-xs hidden-sm">Host Status</th>
</tr>
</thead>
</table>
</div>
<div id="tasks-table-detail" class="table-detail" lr-infinite-scroll="tasksScrollDown"
scroll-threshold="10" time-threshold="500">
<table class="table">
<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 }}"
data-tip-watch="task.status_tip" data-placement="top"><i class="JobDetail-statusIcon fa icon-job-{{ task.status }}"></i>{{ task.name }}</td>
<td class="List-tableCell col-lg-2 col-md-2 col-sm-2 col-xs-3">{{ task.created | date: 'HH:mm:ss' }}</td>
<td class="List-tableCell col-lg-2 col-md-2 col-sm-2 col-xs-3" aw-tool-tip="{{ task.finishedTip }}" data-tip-watch="task.finishedTip"
data-placement="top">{{ task.elapsed }}</td>
<td class="List-tableCell col-lg-4 col-md-3 hidden-sm hidden-xs">
<div>
<a href="" id="{{ task.id }}-successful-bar" aw-tool-tip="{{ task.successfulCountTip }}" data-tip-watch="task.successfulCountTip" data-placement="top" ng-style="task.successfulStyle">
<span class="badge successful-hosts">{{ task.successfulCount }}</span>
</a>
<a href="" id="{{ task.id }}-changed-bar" aw-tool-tip="{{ task.changedCountTip }}" data-tip-watch="task.changedCountTip" data-placement="top" ng-style="task.changedStyle">
<span class="badge changed-hosts">{{ task.changedCount }}</span>
</a>
<a href="" id="{{ task.id }}-skipped-bar" aw-tool-tip="{{ task.skippedCountTip }}" data-tip-watch="task.skippedCountTip" data-placement="top" ng-style="task.skippedStyle">
<span class="badge skipped-hosts">{{ task.skippedCount }}</span>
</a>
<a href="" id="{{ task.id }}-failed-bar" aw-tool-tip="{{ task.failedCountTip }}" data-tip-watch="task.failedCountTip" data-placement="top" ng-style="task.failedStyle">
<span class="badge failed-hosts">{{ task.failedCount }}</span>
</a>
<a href="" id="{{ task.id }}-unreachable-bar" aw-tool-tip="{{ task.unreachableCountTip }}" data-tip-watch="task.unreachableCountTip" data-placement="top" ng-style="task.unreachableStyle">
<span class="badge unreachable-hosts">{{ task.unreachableCount }}</span>
</a>
<a href="" id="{{ task.id }}-missing-bar" aw-tool-tip="{{ task.missingCountTip }}" data-tip-watch="task.missingCountTip" data-placement="top" ng-style="task.missingStyle">
<span class="badge missing-hosts">{{ task.missingCount }}</span>
</a>
<div class="no-matching-hosts inner-bar" id="{{ task.id }}-{{ task.play_id }}-no-matching-hosts-bar" aw-tool-tip="No matching hosts were found." data-placement="top" style="width: 100%;" ng-show="task.status === 'no-matching-hosts'">
No matching hosts.
</div>
</div>
</td>
</tr>
<tr ng-show="taskList.length === 0 && waiting">
<td colspan="5" class="col-lg-12 loading-info">Waiting...</td>
</tr>
<tr ng-show="taskList.length === 0 && tasksLoading && !waiting">
<td colspan="5" class="col-lg-12 loading-info">Loading...</td>
</tr>
<tr ng-show="taskList.length === 0 && !tasksLoading && !waiting">
<td colspan="5" class="col-lg-12 loading-info">No matching tasks</td>
</tr>
</tbody>
</table>
</div>
<div class="scroll-spinner" id="tasksMoreRows"><i class="fa fa-cog fa-spin"></i></div>
</div><!-- section -->
<!--end of tasks section of details-->
<div id="task-hosts-section" class="section">
<div class="JobDetail-searchHeaderRow">
<div class="JobDetail-searchContainer form-group">
<div class="search-name">
<input type="text" class="JobDetail-searchInput form-control List-searchInput" id="search_host_name" ng-model="search_host_name" placeholder="Host Name" ng-keypress="searchHostsKeyPress($event)" >
<a class="List-searchInputIcon search-icon" ng-show="searchHostsEnabled" ng-click="searchHosts()"><i class="fa fa-search"></i></a>
<a class="List-searchInputIcon search-icon" ng-show="!searchHostsEnabled" ng-click="search_host_name=''; searchHosts()"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="JobDetail-tableToggleContainer form-group">
<div class="btn-group" aw-toggle-button data-after-toggle="filterHostStatus">
<button class="JobDetail-tableToggle btn btn-xs btn-primary active">All</button>
<button class="JobDetail-tableToggle btn btn-xs btn-default">Failed</button>
</div>
</div>
</div>
<div class="table-header" id="hosts-table-header">
<table class="table table-condensed">
<thead>
<tr>
<th class="List-tableHeader col-lg-4 col-md-3 col-sm-3 col-xs-3">Hosts</th>
<th class="List-tableHeader col-lg-3 col-md-4 col-sm-3 col-xs-3">Item</th>
<th class="List-tableHeader JobDetail-tableHeader col-lg-3 col-md-4 col-sm-3 col-xs-3">Message</th>
</tr>
</thead>
</table>
</div>
<div id="hosts-table-detail" class="table-detail" lr-infinite-scroll="hostResultsScrollDown" scroll-threshold="10" time-threshold="500">
<table class="table">
<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"><a href="" ng-click="viewHostResults(result.id)" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-placement="top"><i ng-show="result.status_text != 'Unreachable'" class="JobDetail-statusIcon fa icon-job-{{ result.status }}"></i><span ng-show="result.status_text != 'Unreachable'">{{ result.name }}</span><i ng-show="result.status_text == 'Unreachable'" class="JobDetail-statusIcon fa icon-job-unreachable"></i><span ng-show="result.status_text == 'Unreachable'">{{ result.name }}</span></a></td>
<td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3 item-column">{{ result.item }}</td>
<td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3">{{ result.msg }}</td>
</tr>
<tr ng-show="results.length === 0 && waiting">
<td colspan="5" class="col-lg-12 loading-info">Waiting...</td>
</tr>
<tr ng-show="results.length === 0 && hostResultsLoading && !waiting">
<td colspan="5" class="col-lg-12 loading-info">Loading...</td>
</tr>
<tr ng-show="results.length === 0 && !hostResultsLoading && !waiting">
<td colspan="5" class="col-lg-12 loading-info">No matching host events</td>
</tr>
</tbody>
</table>
</div>
<div class="scroll-spinner" id="hostResultsMoreRows"><i class="fa fa-cog fa-spin"></i></div>
</div>
<!--end of hosts section of details-->
</div>
</div>
<!--end of details-->
</div>
<!--end of job-detail-container (left side)-->
<!--beginning of stdout-->
<div class="JobDetail-rightSide">
<!--beginning of events summary-->
<div id="events-summary-panel" class="JobDetail-resultsContainer Panel" ng-show="!stdoutFullScreen">
<div class="JobDetail-panelHeader">
<div class="JobDetail-expandContainer">
<a class="JobDetail-panelHeaderText" ng-show="lessEvents" href="" ng-click="toggleLessEvents()">
EVENT SUMMARY<i class="JobDetail-expandArrow fa fa-caret-left"></i>
</a>
<a class="JobDetail-panelHeaderText" ng-show="!lessEvents" href="" ng-click="toggleLessEvents()">
EVENT SUMMARY<i class="JobDetail-expandArrow fa fa-caret-down"></i>
</a>
</div>
</div>
<div id="events-summary" style="display:none">
<div id="hosts-summary-section" class="section">
<div class="row title-row">
<div class="col-lg-4 col-md-4 col-sm-4 title">Events Summary</div>
<div class="col-lg-8 col-md-8 col-sm-8" style="text-align:right;">
<div id="task-search-form" class="search-form form-inline">
<div class="form-group">
<div class="search-name" style="display:inline-block; position:relative;">
<input type="text" class="input-xs form-control" id="search_host_summary_name" ng-model="search_host_summary_name"
placeholder="Host Name" ng-keypress="searchHostSummaryKeyPress($event)" >
<div id="search-all-input-icons">
<a class="search-icon" ng-show="searchHostSummaryEnabled" ng-click="searchHostSummary()"><i class="fa fa-search"></i></a>
<a class="search-icon" ng-show="!searchHostSummaryEnabled" ng-click="search_host_summary_name=''; searchHostSummary()"><i class="fa fa-times"></i></a>
</div>
</div>
</div>
<div class="form-group">
<div class="btn-group" aw-toggle-button data-after-toggle="filterHostSummaryStatus">
<button class="btn btn-xs btn-primary active">All</button>
<button class="btn btn-xs btn-default">Failed</button>
</div>
</div>
<div class="JobDetail-searchHeaderRow">
<div class="JobDetail-searchContainer form-group">
<div class="search-name">
<input type="text" class="JobDetail-searchInput form-control List-searchInput" id="search_host_summary_name" ng-model="search_host_summary_name" placeholder="Host Name" ng-keypress="searchHostSummaryKeyPress($event)" >
<a class="List-searchInputIcon search-icon" ng-show="searchHostSummaryEnabled" ng-click="searchHostSummary()"><i class="fa fa-search"></i></a>
<a class="List-searchInputIcon search-icon" ng-show="!searchHostSummaryEnabled" ng-click="search_host_summary_name=''; searchHostSummary()"><i class="fa fa-times"></i></a>
</div>
</div>
</div>
<div class="row legend-row">
<div class="col-md-12">
<div class="legend"><i class="fa fa-circle successful-hosts-color"></i> OK <i class="fa fa-circle changed-hosts-color"></i> Changed
<i class="fa fa-circle unreachable-hosts-color"></i> Unreachable <i class="fa fa-circle failed-hosts-color"></i> Failed</div>
<div class="JobDetail-tableToggleContainer form-group">
<div class="btn-group" aw-toggle-button data-after-toggle="filterHostSummaryStatus">
<button class="JobDetail-tableToggle btn btn-xs btn-primary active">All</button>
<button class="JobDetail-tableToggle btn btn-xs btn-default">Failed</button>
</div>
</div>
</div>
@ -421,24 +411,26 @@
<table class="table table-condensed">
<thead>
<tr>
<th class="col-lg-6 col-md-6 col-sm-6 col-xs-6">Host</th>
<th class="col-lg-5 col-md-5 col-sm-5 col-xs-5">Completed Tasks</th>
<th class="col-lg-1 col-md-1 col-sm-1 col-xs-1"></th>
<th class="List-tableHeader col-lg-6 col-md-6 col-sm-6 col-xs-6">Hosts</th>
<th class="List-tableHeader JobDetail-tableHeader col-lg-6 col-md-5 col-sm-5 col-xs-5">Completed Tasks</th>
</tr>
</thead>
</table>
</div>
<div id="hosts-summary-table" class="table-detail" lr-infinite-scroll="hostSummariesScrollDown" scroll-threshold="10" time-threshold="500">
<table class="table table-condensed">
<table class="table">
<tbody>
<tr ng-repeat="host in summaryList = (hosts) track by $index" id="{{ host.id }}">
<td class="name col-lg-6 col-md-6 col-sm-6 col-xs-6"><a href="" ng-click="hostEventsViewer(host.id, host.name)" aw-tool-tip="View all events" data-placement="top">{{ host.name }}</a></td>
<td class="col-lg-5 col-md-5 col-sm-5 col-xs-5 badge-column">
<tr class="List-tableRow" ng-repeat="host in summaryList = (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">
<a href="" ng-click="hostEventsViewer(host.id, host.name)" aw-tool-tip="View events" data-placement="top">{{ host.name }}</a>
</td>
<td class="List-tableCell col-lg-6 col-md-5 col-sm-5 col-xs-5 badge-column">
<a href="" ng-click="hostEventsViewer(host.id, host.name, 'ok')" aw-tool-tip="{{ host.okTip }}" data-tip-watch="host.okTip" data-placement="top" ng-hide="host.ok == 0"><span class="badge successful-hosts">{{ host.ok }}</span></a>
<a href="" ng-click="hostEventsViewer(host.id, host.name, 'changed')" aw-tool-tip="{{ host.changedTip }}" data-tip-watch="host.changedTip" data-placement="top" ng-hide="host.changed == 0"><span class="badge changed-hosts">{{ host.changed }}</span></a>
<a href="" ng-click="hostEventsViewer(host.id, host.name, 'unreachable')" aw-tool-tip="{{ host.unreachableTip }}" data-tip-watch="host.unreachableTip" data-placement="top" ng-hide="host.unreachable == 0"><span class="badge unreachable-hosts">{{ host.unreachable }}</span></a>
<a href="" ng-click="hostEventsViewer(host.id, host.name, 'failed')" aw-tool-tip="{{ host.failedTip }}" data-tip-watch="host.failedTip" data-placement="top" ng-hide="host.failed == 0"><span class="badge failed-hosts">{{ host.failed }}</span></a></td>
<td class="col-lg-1 col-md-1 col-sm-1 col-xs-1"><a ng-show="host.id" href="" ng-click="editHost(host.id)" aw-tool-tip="Edit host" data-placement="top"><i class="fa fa-pencil"></i></a></td>
<a href="" ng-click="hostEventsViewer(host.id, host.name, 'failed')" aw-tool-tip="{{ host.failedTip }}" data-tip-watch="host.failedTip" data-placement="top" ng-hide="host.failed == 0"><span class="badge failed-hosts">{{ host.failed }}</span></a>
</td>
</tr>
<tr ng-show="summaryList.length === 0 && waiting">
<td colspan="5" class="col-lg-12 loading-info">Waiting...</td>
@ -452,30 +444,42 @@
</tbody>
</table>
</div>
<div class="scroll-spinner" id="hostSummariesMoreRows"><i class="fa fa-cog fa-spin"></i></div>
<div class="scroll-spinner" id="hostSummariesMoreRows">
<i class="fa fa-cog fa-spin"></i>
</div>
</div><!-- section -->
</div><!-- summary-well-top-section -->
<div class="row host_summary_row">
<div class="title">Host Summary</div>
<!-- <div ng-repeat="graph_data_object in graph_data">
<span>{{ (graph_data_object.value/total_count_for_graph) * 100 | number : 1 }}% of hosts <span style="color: {{ graph_data_object.color }}">{{ graph_data_object.label }}</span>.<br /></span>
</div> -->
<div id="graph-section" class="JobDetail-graphSection">
<svg width="100%" height="100%"></svg>
</div>
</div>
<!--end of events summary-->
</div>
<!-- end of events summary-->
<div class="JobDetail-stdoutPanel Panel">
<div class="StandardOut-panelHeader">
<div class="StandardOut-panelHeaderText">STANDARD OUT</div>
<div class="StandardOut-panelHeaderActions">
<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 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>
</a>
</div>
<div id="graph-section" >
<!-- <div class="header">
<div class="legend" style="display: none;"><i class="fa fa-circle successful-hosts-color"></i> OK <i class="fa fa-circle changed-hosts-color"></i> Changed
<i class="fa fa-circle unreachable-hosts-color"></i> Unreachable <i class="fa fa-circle failed-hosts-color"></i> Failed</div>
</div> -->
</div><!-- graph section -->
</div>
</div><!-- col-md-5 -->
<standard-out-log stdout-endpoint="job.related.stdout"></standard-out-log>
</div>
</div>
<div id="job-detail-footer" class="footer-row"></div>
<!--end of stdout-->
</div>
</div>
<div ng-include="'/static/partials/eventviewer.html'"></div>

View File

@ -0,0 +1,62 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
['$state', '$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', function($state, $rootScope, Rest, GetBasePath, ProcessErrors){
return {
get: function() {
var defaultUrl = GetBasePath('config');
Rest.setUrl(defaultUrl);
return Rest.get()
.success(function(res){
return res
})
.error(function(res, status){
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
post: function(license, eula){
var defaultUrl = GetBasePath('config');
Rest.setUrl(defaultUrl);
var data = license;
data.eula_accepted = eula;
return Rest.post(JSON.stringify(data))
.success(function(res){
return res
})
.error(function(res, status){
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
// Checks current license validity
// Intended to for runtime or pre-state checks
// Returns false if invalid
valid: function(license) {
if (!license.valid_key){
return false
}
else if (license.free_instances <= 0){
return false
}
// notify if less than 15 days remaining
else if (license.time_remaining / 1000 / 60 / 60 / 24 > 15){
return false
}
return true
},
notify: function(){
self = this;
this.get()
.then(function(res){
self.valid(res.data.license_info) ? null : $state.go('license');
});
}
}
}
];

View File

@ -0,0 +1,16 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
[function(){
return {
restrict: 'A',
link: function(scope, el, attrs){
var onChange = scope.$eval(attrs.fileOnChange);
el.bind('change', onChange);
}
}
}];

View File

@ -0,0 +1,66 @@
/*
* Style conventions
* .ModuleName-component-subComponent
* Naming describes components of the view
*/
@import "awx/ui/client/src/shared/branding/colors.default.less";
@import "awx/ui/client/src/shared/layouts/one-plus-two.less";
.License-container{
.OnePlusTwo-container;
}
.License-field--label{
.OnePlusTwo-left--detailsLabel;
}
.License-management .CodeMirror-scroll{
min-height: 140px;
}
.License-file textarea{
display: block;
width: 100%;
}
.License-eula textarea{
width: 100%;
height: 300px;
}
.License-field label{
width: 155px;
}
.License-field--content{
.OnePlusTwo-left--detailsContent;
}
.License-field{
.OnePlusTwo-left--detailsRow;
}
.License-greenText{
color: @submit-button-bg;
}
.License-redText{
color: #d9534f;
}
.License-fields{
.OnePlusTwo-left--details;
}
.License-details {
.OnePlusTwo-left--panel(600px);
}
.License-titleText {
.OnePlusTwo-panelHeader;
}
.License-management{
.OnePlusTwo-right--panel(600px);
}
.License-submit--container{
height: 33px;
}
.License-submit--success{
line-height:33px;
margin: 0 10px 0 0;
}
.License-file--container {
margin: 20px 0 20px 0;
input[type=file] {
display: none;
}
}

View File

@ -0,0 +1,66 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default
[ 'Wait', '$state', '$scope', '$location',
'GetBasePath', 'Rest', 'ProcessErrors', 'CheckLicense', 'moment',
function( Wait, $state, $scope, $location,
GetBasePath, Rest, ProcessErrors, CheckLicense, moment){
$scope.getKey = function(event){
// Mimic HTML5 spec, show filename
$scope.fileName = event.target.files[0].name;
// Grab the key from the raw license file
var raw = new FileReader();
// readAsFoo runs async
raw.onload = function(){
$scope.newLicense.file = JSON.parse(raw.result);
}
raw.readAsText(event.target.files[0]);
};
// HTML5 spec doesn't provide a way to customize file input css
// So we hide the default input, show our own, and simulate clicks to the hidden input
$scope.fakeClick = function(){
$('#License-file').click();
}
$scope.newLicense = {};
$scope.submit = function(event){
Wait('start');
CheckLicense.post($scope.newLicense.file, $scope.newLicense.eula)
.success(function(res){
reset();
init();
$scope.success = true;
});
};
var calcDaysRemaining = function(ms){
// calculate the number of days remaining on the license
var duration = moment.duration(ms);
return duration.days()
};
var calcExpiresOn = function(days){
// calculate the expiration date of the license
return moment().add(days, 'days').calendar()
};
var init = function(){
$scope.fileName = "Please choose a file..."
Wait('start');
CheckLicense.get()
.then(function(res){
$scope.license = res.data;
$scope.time = {};
$scope.time.remaining = calcDaysRemaining($scope.license.license_info.time_remaining);
$scope.time.expiresOn = calcExpiresOn($scope.time.remaining);
$scope.valid = CheckLicense.valid($scope.license.license_info);
Wait('stop');
});
};
var reset = function(){
document.getElementById('License-form').reset()
};
init();
}
];

View File

@ -0,0 +1,99 @@
<div class="License-container">
<div class="License-details">
<div class="Panel">
<div class="License-titleText">Details</div>
<div class="License-fields">
<div class="License-field">
<div class="License-field--label">License</div>
<div class="License-field--content">
<span ng-show='valid'><i class="fa fa-circle License-greenText"></i> Valid</span>
<span ng-show='invalid'><i class="fa fa-circle License-redText"></i> Invalid</span>
</div>
</div>
<div class="License-field">
<div class="License-field--label">Version</div>
<div class="License-field--content">
{{license.version}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">License Type</div>
<div class="License-field--content">
{{license.license_info.license_type}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">Subscription</div>
<div class="License-field--content">
{{license.license_info.subscription_name}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">License Key</div>
<div class="License-field--content">
{{license.license_info.license_key}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">Expires On</div>
<div class="License-field--content">
{{time.expiresOn}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">Time Remaining</div>
<div class="License-field--content">
{{time.remaining}} Day
</div>
</div>
<div class="License-field">
<div class="License-field--label">Hosts Available</div>
<div class="License-field--content">
{{license.license_info.available_instances}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">Hosts Used</div>
<div class="License-field--content">
{{license.license_info.current_instances}}
</div>
</div>
<div class="License-field License-greenText">
<div class="License-field--label">Hosts Remaining</div>
<div class="License-field--content">
{{license.license_info.free_instances}}
</div>
</div>
</div>
<p>If you are ready to upgrade, please contact us by clicking the button below</p>
<a href="https://www.ansible.com/renew" target="_blank"><button class="btn btn-default">Upgrade</button></a>
</div>
</div>
<div class="License-management">
<div class="Panel">
<div class="License-titleText">License Management</div>
<p>Choose your license file, agree to the End User License Agreement, and click submit.</p>
<form id="License-form" name="license">
<div class="input-group License-file--container">
<span class="btn btn-default input-group-addon" ng-click="fakeClick()">Browse...</span>
<input class="form-control" ng-disabled="true" placeholder="{{fileName}}" />
<input id="License-file" class="form-control" type="file" file-on-change="getKey"/>
</div>
<div class="License-titleText prepend-asterisk"> End User License Agreement</div>
<div class="form-group License-eula">
<textarea class="form-control">{{license.eula}}
</textarea>
</div>
<div class="form-group">
<div class="checkbox">
<div class="License-details--label"><input type="checkbox" ng-model="newLicense.eula" required> I agree to the End User License Agreement</div>
<div class="License-submit--container pull-right">
<span ng-hide="success == null || false" class="License-greenText License-submit--success pull-left">Save successful!</span>
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="newLicense.file.license_key == null || newLicense.eula == null">Submit</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,19 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../shared/template-url/template-url.factory';
export default {
name: 'license',
route: '/license',
templateUrl: templateUrl('license/license'),
controller: 'licenseController',
data: {},
ncyBreadcrumb: {
parent: 'setup',
label: 'LICENSE'
}
}

View File

@ -0,0 +1,19 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import route from './license.route';
import controller from './license.controller';
import CheckLicense from './checkLicense.factory';
import fileOnChange from './fileOnChange.directive';
export default
angular.module('license', [])
.controller('licenseController', controller)
.directive('fileOnChange', fileOnChange)
.factory('CheckLicense', CheckLicense)
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);

View File

@ -1,4 +0,0 @@
<div class="tab-pane" id="license-partial">
<div ng-cloak id="htmlTemplate" class="Panel"></div>
<div id="license-modal-dialog"></div>
</div>

View File

@ -3,9 +3,7 @@ import icon from '../shared/icon/main';
export default
angular.module('setupMenu',
[ 'AboutAnsibleHelpModal',
icon.name
])
[ icon.name])
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(route);
}]);

View File

@ -42,11 +42,12 @@
View and edit your license information.
</p>
</a>
<a ng-click="showAboutModal()" class="SetupItem">
<a ui-sref="setup.about" class="SetupItem">
<h4 class="SetupItem-title">About Tower</h4>
<p class="SetupItem-description">
View information about this version of Ansible Tower.
</p>
</a>
</div>
<div ui-view></div>
</section>

View File

@ -1,12 +0,0 @@
export default
[ '$scope',
'$rootScope',
'AboutAnsibleHelp',
function(
$scope,
$rootScope,
showAboutModal
) {
$scope.showAboutModal = showAboutModal;
}
];

View File

@ -1,10 +1,8 @@
import {templateUrl} from '../shared/template-url/template-url.factory';
import controller from './setup.controller';
export default {
name: 'setup',
route: '/setup',
controller: controller,
ncyBreadcrumb: {
label: "SETUP"
},

View File

@ -0,0 +1,79 @@
/*
* Large resolution: 1/3 + 2/3 width panels
* Small resolution: 100% width panels, stacked
* Options: static height, custom breakpoint
*
* Style conventions
* .ModuleName-component--subComponent
*/
@import "awx/ui/client/src/shared/branding/colors.default.less";
.OnePlusTwo-container(@height: 100%; @breakpoint: 900px){
height: @height;
display: flex;
flex-direction: row;
@media screen and (max-width: @breakpoint){
flex-direction: column;
}
}
.OnePlusTwo-left--panel(@height: 100%; @breakpoint: 900px) {
flex: 0 0;
height: @height;
width: 100%;
.Panel{
height: 100%;
}
@media screen and (min-width: @breakpoint){
max-width: 400px;
}
}
.OnePlusTwo-right--panel(@height: 100%; @breakpoint: 900px) {
height: @height;
flex: 1 0;
margin-left: 20px;
.Panel{
height: 100%;
}
@media screen and (max-width: @breakpoint){
flex-direction: column;
margin-left: 0px;
margin-top: 25px;
}
}
.OnePlusTwo-panelHeader {
color: @default-interface-txt;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
}
.OnePlusTwo-left--details {
margin-top: 25px;
}
.OnePlusTwo-left--detailsRow {
display: flex;
:not(:last-child){
margin-bottom: 20px;
}
}
.OnePlusTwo-left--detailsLabel {
width: 140px;
display: inline-block;
color: @default-interface-txt;
text-transform: uppercase;
font-weight: 400;
}
.OnePlusTwo-left--detailsContent {
display: inline-block;
max-width: 220px;
word-wrap: break-word;
}

View File

@ -1,4 +1,6 @@
<span ng-repeat="(name, options) in list.actions">
<span ng-repeat="(name, options) in list.actions" class="List-action" ng-hide="isHiddenByOptions(options) ||
hiddenOnCurrentPage(options.basePaths) ||
hiddenInCurrentMode(options.mode)">
<!-- TODO: Unfortunately, the data-tip-watch attribute is not loaded for
some reason -->
<button
@ -12,9 +14,6 @@
data-title="{{options.dataTitle}}"
ng-disabled="{{options.ngDisabled}}"
ng-click="$eval(options.ngClick)"
ng-hide="isHiddenByOptions(options) ||
hiddenOnCurrentPage(options.basePaths) ||
hiddenInCurrentMode(options.mode)"
toolbar="true"
aw-feature="{{options.awFeature}}">
<span ng-bind-html="options.buttonContent"></span>

View File

@ -316,6 +316,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
}
html += "</div>";
html += "<div class=\"List-actionHolder\">";
if(list.toolbarAuxAction) {
html += "<div class=\"List-auxAction\">";
html += list.toolbarAuxAction;
@ -333,6 +334,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
html += "</div>\n";
html += "</div>";
html += "</div>";
html += "</div>";
}
}

View File

@ -1,7 +1,7 @@
<div class="tab-pane" id="jobs-stdout">
<div ng-cloak id="htmlTemplate">
<div class="StandardOut">
<div class="StandardOut-leftPanel">
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
<div class="Panel">
<div class="StandardOut-panelHeader">
RESULTS
@ -98,17 +98,19 @@
<div class="StandardOut-rightPanel">
<div class="Panel">
<div class="StandardOut-panelHeader">
STANDARD OUT
</div>
<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 id="pre-container-content" class="StandardOut-preContent"></div>
</div>
<div class="scroll-spinner" id="stdoutMoreRowsBottom">
<i class="fa fa-cog fa-spin"></i>
<div class="StandardOut-panelHeaderText">STANDARD OUT</div>
<div class="StandardOut-panelHeaderActions">
<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 href="/api/v1/ad_hoc_commands/{{ 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>
</a>
</div>
</div>
<standard-out-log stdout-endpoint="job.related.stdout"></standard-out-log>
</div>
</div>
</div>

View File

@ -23,18 +23,16 @@ export default {
return FeaturesService.get();
}],
adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
// if (!$rootScope.adhoc_event_socket) {
// $rootScope.adhoc_event_socket = Socket({
// scope: $rootScope,
// endpoint: "ad_hoc_command_events"
// });
// $rootScope.adhoc_event_socket.init();
// return true;
// } else {
// return true;
// }
return true;
if (!$rootScope.adhoc_event_socket) {
$rootScope.adhoc_event_socket = Socket({
scope: $rootScope,
endpoint: "ad_hoc_command_events"
});
$rootScope.adhoc_event_socket.init();
return true;
} else {
return true;
}
}]
}
};

View File

@ -1,7 +1,7 @@
<div class="tab-pane" id="jobs-stdout">
<div ng-cloak id="htmlTemplate">
<div class="StandardOut">
<div class="StandardOut-leftPanel">
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
<div class="Panel">
<div class="StandardOut-panelHeader">
RESULTS
@ -112,17 +112,19 @@
<div class="StandardOut-rightPanel">
<div class="Panel">
<div class="StandardOut-panelHeader">
STANDARD OUT
</div>
<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 id="pre-container-content" class="StandardOut-preContent"></div>
</div>
<div class="scroll-spinner" id="stdoutMoreRowsBottom">
<i class="fa fa-cog fa-spin"></i>
<div class="StandardOut-panelHeaderText">STANDARD OUT</div>
<div class="StandardOut-panelHeaderActions">
<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 href="/api/v1/inventory_updates/{{ 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>
</a>
</div>
</div>
<standard-out-log stdout-endpoint="job.related.stdout"></standard-out-log>
</div>
</div>
</div>

View File

@ -24,18 +24,8 @@ export default {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}],
adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
// if (!$rootScope.adhoc_event_socket) {
// $rootScope.adhoc_event_socket = Socket({
// scope: $rootScope,
// endpoint: "ad_hoc_command_events"
// });
// $rootScope.adhoc_event_socket.init();
// return true;
// } else {
// return true;
// }
inventorySyncSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
// TODO: determine whether or not we have socket support for inventory sync standard out
return true;
}]
}

View File

@ -0,0 +1,10 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import standardOutLog from './standard-out-log.directive';
export default
angular.module('standardOutLogDirective', [])
.directive('standardOutLog', standardOutLog);

View File

@ -0,0 +1,215 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'ProcessErrors', 'Rest', 'Wait',
function ($log, $rootScope, $scope, $state, $stateParams, ProcessErrors, Rest, Wait) {
var api_complete = false,
stdout_url,
current_range,
loaded_sections = [],
event_queue = 0,
auto_scroll_down=true, // programmatic scroll to bottom
live_event_processing = true,
page_size = 500,
job_id = $stateParams.id;
$scope.should_apply_live_events = true;
// Open up a socket for events depending on the type of job
function openSockets() {
if ($state.current.name == 'jobDetail') {
$log.debug("socket watching on job_events-" + job_id);
$rootScope.event_socket.on("job_events-" + job_id, function() {
$log.debug("socket fired on job_events-" + job_id);
if (api_complete) {
event_queue++;
}
});
}
if ($state.current.name == 'adHocJobStdout') {
$log.debug("socket watching on ad_hoc_command_events-" + job_id);
$rootScope.adhoc_event_socket.on("ad_hoc_command_events-" + job_id, function() {
$log.debug("socket fired on ad_hoc_command_events-" + job_id);
if (api_complete) {
event_queue++;
}
});
}
// TODO: do we need to add socket listeners for scmUpdateStdout, inventorySyncStdout, managementJobStdout?
}
openSockets();
// This is a trigger for loading up the standard out
if ($scope.removeLoadStdout) {
$scope.removeLoadStdout();
}
$scope.removeLoadStdout = $scope.$on('LoadStdout', function() {
if (loaded_sections.length === 0) {
loadStdout()
}
else if (live_event_processing) {
getNextSection();
}
});
// This interval checks to see whether or not we've gotten a new
// event via sockets. If so, go out and update the standard out
// log.
$rootScope.jobStdOutInterval = setInterval( function() {
if (event_queue > 0) {
// events happened since the last check
$log.debug('checking for stdout...');
if (loaded_sections.length === 0) { ////this if statement for refresh
$log.debug('calling LoadStdout');
loadStdout();
}
else if (live_event_processing) {
$log.debug('calling getNextSection');
getNextSection();
}
event_queue = 0;
}
}, 2000);
// stdoutEndpoint gets passed through in the directive declaration.
// This watcher fires off loadStdout() when the endpoint becomes
// available.
$scope.$watch('stdoutEndpoint', function(newVal, oldVal) {
if(newVal && newVal != oldVal) {
// Fire off the server call
loadStdout();
}
});
function loadStdout() {
Rest.setUrl($scope.stdoutEndpoint + '?format=json&start_line=-' + page_size);
Rest.get()
.success(function(data) {
Wait('stop');
if (data.content) {
api_complete = true;
$('#pre-container-content').html(data.content);
current_range = data.range;
if (data.content !== "Waiting for results...") {
loaded_sections.push({
start: (data.range.start < 0) ? 0 : data.range.start,
end: data.range.end
});
}
$('#pre-container').scrollTop($('#pre-container').prop("scrollHeight"));
}
else {
api_complete = true;
}
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status });
});
};
function getNextSection() {
// get the next range of data from the API
var start = loaded_sections[loaded_sections.length - 1].end, url;
url = $scope.stdoutEndpoint + '?format=json&start_line=' + start + '&end_line=' + (start + page_size);
$('#stdoutMoreRowsBottom').fadeIn();
Rest.setUrl(url);
Rest.get()
.success( function(data) {
if ($('#pre-container-content').html() === "Waiting for results...") {
$('#pre-container-content').html(data.content);
} else {
$('#pre-container-content').append(data.content);
}
loaded_sections.push({
start: (data.range.start < 0) ? 0 : data.range.start,
end: data.range.end
});
//console.log('loaded start: ' + data.range.start + ' end: ' + data.range.end);
//console.log(data.content);
if ($scope.should_apply_live_events) {
// if user has not disabled live event view by scrolling upward, then scroll down to the new content
current_range = data.range;
auto_scroll_down = true; // prevent auto load from happening
$('#pre-container').scrollTop($('#pre-container').prop("scrollHeight"));
}
$('#stdoutMoreRowsBottom').fadeOut(400);
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status });
});
}
$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;
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
});
current_range = data.range;
$('#stdoutMoreRowsTop').fadeOut(400);
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status });
});
}
};
// We watch for job status changes here. If the job completes we want to clear out the
// stdout interval and kill the live_event_processing flag.
if ($scope.removeJobStatusChange) {
$scope.removeJobStatusChange();
}
$scope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobStdout', function(e, data) {
if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10)) {
if (data.status === 'failed' || data.status === 'canceled' ||
data.status === 'error' || data.status === 'successful') {
if ($rootScope.jobStdOutInterval) {
window.clearInterval($rootScope.jobStdOutInterval);
}
if (live_event_processing) {
if (loaded_sections.length === 0) {
loadStdout();
}
else {
getNextSection();
}
}
live_event_processing = false;
}
}
});
}];

View File

@ -0,0 +1,46 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import standardOutLogController from './standard-out-log.controller';
export default [ 'templateUrl',
function(templateUrl) {
return {
scope: {
stdoutEndpoint: '=',
jobId: '='
},
templateUrl: templateUrl('standard-out/log/standard-out-log'),
restrict: 'E',
controller: standardOutLogController,
link: function(scope) {
// All of our DOM related stuff will go in here
var lastScrollTop,
direction;
function detectDirection() {
var st = $('#pre-container').scrollTop();
if (st > lastScrollTop) {
direction = "down";
} else {
direction = "up";
}
lastScrollTop = st;
return direction;
}
$('#pre-container').bind('scroll', function() {
if (detectDirection() === "up") {
scope.should_apply_live_events = false;
}
if ($(this).scrollTop() + $(this).height() === $(this).prop("scrollHeight")) {
scope.should_apply_live_events = true;
}
});
}
};
}];

View File

@ -0,0 +1,9 @@
<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 id="pre-container-content" class="StandardOut-preContent"></div>
</div>
<div class="scroll-spinner" id="stdoutMoreRowsBottom">
<i class="fa fa-cog fa-spin"></i>
</div>
</div>

View File

@ -10,8 +10,9 @@ import stdoutInventorySyncRoute from './inventory-sync/standard-out-inventory-sy
import stdoutScmUpdateRoute from './scm-update/standard-out-scm-update.route';
import {JobStdoutController} from './standard-out.controller';
import StandardOutHelper from './standard-out-factories/main';
import standardOutLogDirective from './log/main';
export default angular.module('standardOut', [StandardOutHelper.name])
export default angular.module('standardOut', [StandardOutHelper.name, standardOutLogDirective.name])
.controller('JobStdoutController', JobStdoutController)
.run(['$stateExtender', function($stateExtender) {
$stateExtender.addState(stdoutAdhocRoute);

View File

@ -1,7 +1,7 @@
<div class="tab-pane" id="jobs-stdout">
<div ng-cloak id="htmlTemplate">
<div class="StandardOut">
<div class="StandardOut-leftPanel">
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
<div class="Panel">
<div class="StandardOut-panelHeader">
RESULTS
@ -64,17 +64,14 @@
<div class="StandardOut-rightPanel">
<div class="Panel">
<div class="StandardOut-panelHeader">
STANDARD OUT
</div>
<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 id="pre-container-content" class="StandardOut-preContent"></div>
</div>
<div class="scroll-spinner" id="stdoutMoreRowsBottom">
<i class="fa fa-cog fa-spin"></i>
<div class="StandardOut-panelHeaderText">STANDARD OUT</div>
<div class="StandardOut-panelHeaderActions">
<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>
</div>
</div>
<standard-out-log stdout-endpoint="job.related.stdout"></standard-out-log>
</div>
</div>
</div>

View File

@ -22,18 +22,8 @@ export default {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}],
adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
// if (!$rootScope.adhoc_event_socket) {
// $rootScope.adhoc_event_socket = Socket({
// scope: $rootScope,
// endpoint: "ad_hoc_command_events"
// });
// $rootScope.adhoc_event_socket.init();
// return true;
// } else {
// return true;
// }
managementJobSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
// TODO: determine whether or not we have socket support for management job standard out
return true;
}]
}

View File

@ -1,7 +1,7 @@
<div class="tab-pane" id="jobs-stdout">
<div ng-cloak id="htmlTemplate">
<div class="StandardOut">
<div class="StandardOut-leftPanel">
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
<div class="Panel">
<div class="StandardOut-panelHeader">
RESULTS
@ -77,17 +77,19 @@
<div class="StandardOut-rightPanel">
<div class="Panel">
<div class="StandardOut-panelHeader">
STANDARD OUT
</div>
<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 id="pre-container-content" class="StandardOut-preContent"></div>
</div>
<div class="scroll-spinner" id="stdoutMoreRowsBottom">
<i class="fa fa-cog fa-spin"></i>
<div class="StandardOut-panelHeaderText">STANDARD OUT</div>
<div class="StandardOut-panelHeaderActions">
<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 href="/api/v1/project_updates/{{ 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>
</a>
</div>
</div>
<standard-out-log stdout-endpoint="job.related.stdout"></standard-out-log>
</div>
</div>
</div>

View File

@ -24,18 +24,8 @@ export default {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}],
adhocEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
// if (!$rootScope.adhoc_event_socket) {
// $rootScope.adhoc_event_socket = Socket({
// scope: $rootScope,
// endpoint: "ad_hoc_command_events"
// });
// $rootScope.adhoc_event_socket.init();
// return true;
// } else {
// return true;
// }
scmUpdateSocket: ['Socket', '$rootScope', function(Socket, $rootScope) {
// TODO: determine whether or not we have socket support for scm update standard out
return true;
}]
}

View File

@ -10,19 +10,19 @@
.StandardOut-leftPanel {
flex: 0 0 400px;
margin-right: 20px;
}
.StandardOut-rightPanel {
flex: 1 0;
margin-left: 20px;
}
.StandardOut-panelHeader {
color: @default-interface-txt;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
display: flex;
}
.StandardOut-consoleOutput {
@ -30,6 +30,8 @@
min-height: 200px;
background-color: @default-secondary-bg;
border-radius: 5px;
height: 300px;
overflow: scroll;
}
.StandardOut-details {
@ -63,13 +65,54 @@
text-transform: capitalize;
}
.StandardOut-preContainer {
height: 300px;
}
.StandardOut-panelHeaderText {
align-items: center;
flex: 1 0 auto;
display: flex;
}
.StandardOut-panelHeaderActions {
justify-content: flex-end;
display: flex;
margin-left: 10px;
font-size: 20px;
}
.StandardOut-actionButton {
font-size: 16px;
height: 30px;
min-width: 30px;
color: #b7b7b7;
background-color: inherit;
border: none;
border-radius: 50%;
}
.StandardOut-actionButton:hover {
background-color: @list-actn-bg-hov !important;
color: @list-actn-icn-hov;
}
.StandardOut-actionButton--active {
background-color: @list-actn-bg-hov !important;
color: @list-actn-icn-hov;
}
.StandardOut-actionButton + a {
margin-left: 15px;
}
@standardout-breakpoint: 900px;
@media screen and (max-width: @standardout-breakpoint) {
.StandardOut {
flex-direction: column;
}
.StandardOut-rightPanel {
margin-left: 0px;
.StandardOut-leftPanel {
margin-right: 0px;
}
}

View File

@ -11,153 +11,34 @@
*/
export function JobStdoutController ($location, $log, $rootScope, $scope, $compile, $state, $stateParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors, ModelToBasePathKey, Empty, GetChoices, LookUpName) {
export function JobStdoutController ($rootScope, $scope, $state, $stateParams, ClearScope, GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName) {
ClearScope();
var job_id = $stateParams.id,
jobType = $state.current.data.jobType,
api_complete = false,
stdout_url,
current_range,
loaded_sections = [],
event_queue = 0,
auto_scroll_down=true, // programmatic scroll to bottom
live_event_processing = true,
should_apply_live_events = true,
page_size = 500,
lastScrollTop = 0,
st,
direction;
jobType = $state.current.data.jobType;
$scope.isClosed = true;
// This scope variable controls whether or not the left panel is shown and the right panel
// is expanded to take up the full screen
$scope.stdoutFullScreen = false;
// function openSockets() {
// if (/\/jobs\/(\d)+\/stdout/.test($location.$$url)) {
// $log.debug("socket watching on job_events-" + job_id);
// $rootScope.event_socket.on("job_events-" + job_id, function() {
// $log.debug("socket fired on job_events-" + job_id);
// if (api_complete) {
// event_queue++;
// }
// });
// } else if (/\/ad_hoc_commands\/(\d)+/.test($location.$$url)) {
// $log.debug("socket watching on ad_hoc_command_events-" + job_id);
// $rootScope.adhoc_event_socket.on("ad_hoc_command_events-" + job_id, function() {
// $log.debug("socket fired on ad_hoc_command_events-" + job_id);
// if (api_complete) {
// event_queue++;
// }
// });
// }
// }
//
// openSockets();
if ($rootScope.removeJobStatusChange) {
$rootScope.removeJobStatusChange();
// Listen for job status updates that may come across via sockets. We need to check the payload
// to see whethere the updated job is the one that we're currently looking at.
if ($scope.removeJobStatusChange) {
$scope.removeJobStatusChange();
}
$rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobStdout', function(e, data) {
$scope.removeJobStatusChange = $rootScope.$on('JobStatusChange-jobStdout', function(e, data) {
if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10) && $scope.job) {
$scope.job.status = data.status;
if (data.status === 'failed' || data.status === 'canceled' ||
data.status === 'error' || data.status === 'successful') {
if ($rootScope.jobStdOutInterval) {
window.clearInterval($rootScope.jobStdOutInterval);
}
if (live_event_processing) {
if (loaded_sections.length === 0) {
$scope.$emit('LoadStdout');
}
else {
getNextSection();
}
}
live_event_processing = false;
}
}
// TODO: when the job completes we should refresh the job data so that we pull in the finish
// timestamp as well as the run time.
});
$rootScope.jobStdOutInterval = setInterval( function() {
if (event_queue > 0) {
// events happened since the last check
$log.debug('checking for stdout...');
if (loaded_sections.length === 0) { ////this if statement for refresh
$log.debug('calling LoadStdout');
$scope.$emit('LoadStdout');
}
else if (live_event_processing) {
$log.debug('calling getNextSection');
getNextSection();
}
event_queue = 0;
}
}, 2000);
if ($scope.removeLoadStdout) {
$scope.removeLoadStdout();
}
$scope.removeLoadStdout = $scope.$on('LoadStdout', function() {
Rest.setUrl(stdout_url + '?format=json&start_line=-' + page_size);
Rest.get()
.success(function(data) {
Wait('stop');
if (data.content) {
api_complete = true;
$('#pre-container-content').html(data.content);
current_range = data.range;
if (data.content !== "Waiting for results...") {
loaded_sections.push({
start: (data.range.start < 0) ? 0 : data.range.start,
end: data.range.end
});
}
$('#pre-container').scrollTop($('#pre-container').prop("scrollHeight"));
}
else {
api_complete = true;
}
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status });
});
});
function detectDirection() {
st = $('#pre-container').scrollTop();
if (st > lastScrollTop) {
direction = "down";
} else {
direction = "up";
}
lastScrollTop = st;
return direction;
}
$('#pre-container').bind('scroll', function() {
if (detectDirection() === "up") {
should_apply_live_events = false;
}
if ($(this).scrollTop() + $(this).height() === $(this).prop("scrollHeight")) {
should_apply_live_events = true;
}
});
$scope.toggleClosedStatus = function() {
if (!$scope.isClosed) {
$('.StandardOutDetails-detailRow--closable').slideUp(200);
$scope.isClosed = true;
}
else {
$('.StandardOutDetails-detailRow--closable').slideDown(200);
$scope.isClosed = false;
}
};
// Go out and get the job details based on the job type. jobType gets defined
// in the data block of the route declaration for each of the different types
// of stdout jobs.
Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/');
Rest.get()
.success(function(data) {
@ -179,7 +60,6 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi
$scope.limit = data.limit;
$scope.verbosity = data.verbosity;
$scope.job_tags = data.job_tags;
stdout_url = data.related.stdout;
// If we have a source then we have to go get the source choices from the server
if (!Empty(data.source)) {
@ -252,14 +132,12 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi
});
}
// if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') {
// live_event_processing = false;
// if ($rootScope.jobStdOutInterval) {
// window.clearInterval($rootScope.jobStdOutInterval);
// }
// }
if(stdout_url) {
$scope.$emit('LoadStdout');
// If the job isn't running we want to clear out the interval that goes out and checks for stdout updates.
// This interval is defined in the standard out log directive controller.
if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') {
if ($rootScope.jobStdOutInterval) {
window.clearInterval($rootScope.jobStdOutInterval);
}
}
})
.error(function(data, status) {
@ -267,88 +145,17 @@ export function JobStdoutController ($location, $log, $rootScope, $scope, $compi
msg: 'Failed to retrieve job: ' + job_id + '. GET returned: ' + status });
});
// TODO: this is currently not used but is necessary for cases where sockets
// are not available and a manual refresh trigger is needed.
$scope.refresh = function(){
if (loaded_sections.length === 0) { ////this if statement for refresh
$scope.$emit('LoadStdout');
}
else if (live_event_processing) {
getNextSection();
}
$scope.$emit('LoadStdout');
};
$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;
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
});
current_range = data.range;
$('#stdoutMoreRowsTop').fadeOut(400);
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status });
});
}
};
function getNextSection() {
// get the next range of data from the API
var start = loaded_sections[loaded_sections.length - 1].end, url;
url = stdout_url + '?format=json&start_line=' + start + '&end_line=' + (start + page_size);
$('#stdoutMoreRowsBottom').fadeIn();
Rest.setUrl(url);
Rest.get()
.success( function(data) {
if ($('#pre-container-content').html() === "Waiting for results...") {
$('#pre-container-content').html(data.content);
} else {
$('#pre-container-content').append(data.content);
}
loaded_sections.push({
start: (data.range.start < 0) ? 0 : data.range.start,
end: data.range.end
});
//console.log('loaded start: ' + data.range.start + ' end: ' + data.range.end);
//console.log(data.content);
if (should_apply_live_events) {
// if user has not disabled live event view by scrolling upward, then scroll down to the new content
current_range = data.range;
auto_scroll_down = true; // prevent auto load from happening
$('#pre-container').scrollTop($('#pre-container').prop("scrollHeight"));
}
$('#stdoutMoreRowsBottom').fadeOut(400);
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status });
});
// Click binding for the expand/collapse button on the standard out log
$scope.toggleStdoutFullscreen = function() {
$scope.stdoutFullScreen = !$scope.stdoutFullScreen;
}
}
JobStdoutController.$inject = [ '$location', '$log', '$rootScope', '$scope', '$compile', '$state', '$stateParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors', 'ModelToBasePathKey', 'Empty', 'GetChoices', 'LookUpName'];
JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', '$stateParams', 'ClearScope', 'GetBasePath', 'Rest', 'ProcessErrors', 'Empty', 'GetChoices', 'LookUpName'];

View File

@ -303,7 +303,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti
}
// Generate the list
view.inject(list, { mode: 'edit', id: 'stream-content', searchSize: 'col-lg-3', secondWidget: true, activityStream: true, scope: scope });
view.inject(list, { mode: 'edit', id: 'stream-content', searchSize: 'col-lg-4 col-md-4 col-sm-12 col-xs-12', secondWidget: true, activityStream: true, scope: scope });
// descriptive title describing what AS is showing
scope.streamTitle = (params && params.title) ? params.title : null;

View File

@ -41,7 +41,7 @@
<main-menu></main-menu>
<bread-crumb></bread-crumb>
<div class="container-fluid" id="#content-container">
<div class="container-fluid" id="content-container">
<div class="row">
<div class="col-lg-12">
<div ui-view id="main-view"></div>
@ -221,8 +221,7 @@
<div class="overlay"></div>
<div class="spinny"><i class="fa fa-cog fa-spin fa-2x"></i> <p>working...</p></div>
<!-- <div class="site-footer"></div> -->
</div>
<tower-footer></tower-footer>
<script>
// HACK: Need this to support global-dependent

View File

@ -6,3 +6,4 @@ python_files = *.py
addopts = --reuse-db
markers =
ac: access control test
license_feature: ensure license features are accessible or not depending on license

View File

@ -3,6 +3,7 @@ django-debug-toolbar==1.4
unittest2
pep8
flake8
pyflakes==1.0.0 # Pinned until PR merges https://gitlab.com/pycqa/flake8/merge_requests/56
pytest
pytest-cov
pytest-django

View File

@ -3,7 +3,7 @@ ansible==1.9.4
# Based on django-jenkins==0.16.3, with a fix for properly importing coverage
git+https://github.com/jlaska/django-jenkins.git@release_0.16.4#egg=django-jenkins
coverage
pyflakes
pyflakes==1.0.0 # Pinned until PR merges https://gitlab.com/pycqa/flake8/merge_requests/56
pep8
pylint
flake8

View File

@ -23,6 +23,9 @@ RUN pip2 install honcho
RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.0.0/dumb-init_1.0.0_amd64.deb
RUN dpkg -i dumb-init_*.deb
ADD start_development.sh /start_development.sh
ADD ansible-tower.egg-link /usr/local/lib/python2.7/dist-packages/ansible-tower.egg-link
ADD tower-manage /usr/local/bin/tower-manage
ADD ansible_tower.egg-info /tmp/ansible_tower.egg-info
EXPOSE 8013 8080 22
ENTRYPOINT ["/usr/bin/dumb-init"]

View File

@ -0,0 +1 @@
/tower_devel

View File

@ -0,0 +1,23 @@
Metadata-Version: 1.1
Name: ansible-tower
Version: 3.0.0-0.devel
Summary: ansible-tower: API, UI and Task Engine for Ansible
Home-page: http://github.com/ansible/ansible-commander
Author: Ansible, Inc.
Author-email: support@ansible.com
License: Proprietary
Description: AWX provides a web-based user interface, REST API and task engine built on top of Ansible
Keywords: ansible
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System AdministratorsLicense :: Other/Proprietary License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python
Classifier: Topic :: System :: Installation/Setup
Classifier: Topic :: System :: Systems Administration

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
[console_scripts]
tower-manage = awx:manage
awx-manage = awx:manage

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
awx

View File

@ -20,12 +20,8 @@ else
echo "Failed to find tower source tree, map your development tree volume"
fi
if [ -f "/.develop_run" ]; then
echo "Skipping 'make develop' step since it has already run - remove /.develop_run to force it"
else
make develop
touch /.develop_run
fi
rm -rf /tower_devel/ansible_tower.egg-info
mv /tmp/ansible_tower.egg-info /tower_devel/
# Check if we need to build dependencies
if [ -f "awx/lib/.deps_built" ]; then