mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 10:30:03 -03:30
Merge branch 'devel' of github.com:ansible/ansible-tower into rbac
This commit is contained in:
commit
e9c3d98a44
2
.gitignore
vendored
2
.gitignore
vendored
@ -34,7 +34,7 @@ __pycache__
|
||||
/tar-build
|
||||
/setup-bundle-build
|
||||
/dist
|
||||
*.egg-info
|
||||
/*.egg-info
|
||||
*.py[c,o]
|
||||
|
||||
# JavaScript
|
||||
|
||||
4
Makefile
4
Makefile
@ -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.
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
200
awx/main/tests/functional/commands/test_cleanup_facts.py
Normal file
200
awx/main/tests/functional/commands/test_cleanup_facts.py
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ body {
|
||||
}
|
||||
|
||||
#content-container {
|
||||
margin-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.group-breadcrumbs {
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
#pre-container {
|
||||
overflow-x: scroll;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
31
awx/ui/client/src/about/about.controller.js
Normal file
31
awx/ui/client/src/about/about.controller.js
Normal 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();
|
||||
}
|
||||
];
|
||||
32
awx/ui/client/src/about/about.partial.html
Normal file
32
awx/ui/client/src/about/about.partial.html
Normal 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>
|
||||
12
awx/ui/client/src/about/about.route.js
Normal file
12
awx/ui/client/src/about/about.route.js
Normal 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')
|
||||
};
|
||||
15
awx/ui/client/src/about/main.js
Normal file
15
awx/ui/client/src/about/main.js
Normal 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);
|
||||
}]);
|
||||
@ -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});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
|
||||
@ -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"];
|
||||
@ -6,7 +6,7 @@
|
||||
color: #848992;
|
||||
width: 100%;
|
||||
z-index: 1040;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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%');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
]);
|
||||
@ -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;
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}])
|
||||
|
||||
|
||||
@ -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 — 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 — 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>Don’t worry — 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 you’ve successfully installed or upgraded Ansible Tower, the next step is to add a license file. " +
|
||||
"If you don’t 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
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
62
awx/ui/client/src/license/checkLicense.factory.js
Normal file
62
awx/ui/client/src/license/checkLicense.factory.js
Normal 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');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
];
|
||||
16
awx/ui/client/src/license/fileOnChange.directive.js
Normal file
16
awx/ui/client/src/license/fileOnChange.directive.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}];
|
||||
66
awx/ui/client/src/license/license.block.less
Normal file
66
awx/ui/client/src/license/license.block.less
Normal 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;
|
||||
}
|
||||
}
|
||||
66
awx/ui/client/src/license/license.controller.js
Normal file
66
awx/ui/client/src/license/license.controller.js
Normal 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();
|
||||
}
|
||||
];
|
||||
99
awx/ui/client/src/license/license.partial.html
Normal file
99
awx/ui/client/src/license/license.partial.html
Normal 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>
|
||||
19
awx/ui/client/src/license/license.route.js
Normal file
19
awx/ui/client/src/license/license.route.js
Normal 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'
|
||||
}
|
||||
}
|
||||
19
awx/ui/client/src/license/main.js
Normal file
19
awx/ui/client/src/license/main.js
Normal 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);
|
||||
}]);
|
||||
@ -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>
|
||||
@ -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);
|
||||
}]);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
export default
|
||||
[ '$scope',
|
||||
'$rootScope',
|
||||
'AboutAnsibleHelp',
|
||||
function(
|
||||
$scope,
|
||||
$rootScope,
|
||||
showAboutModal
|
||||
) {
|
||||
$scope.showAboutModal = showAboutModal;
|
||||
}
|
||||
];
|
||||
@ -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"
|
||||
},
|
||||
|
||||
79
awx/ui/client/src/shared/layouts/one-plus-two.less
Normal file
79
awx/ui/client/src/shared/layouts/one-plus-two.less
Normal 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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}]
|
||||
}
|
||||
|
||||
10
awx/ui/client/src/standard-out/log/main.js
Normal file
10
awx/ui/client/src/standard-out/log/main.js
Normal 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);
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}];
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}];
|
||||
@ -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>
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}]
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}]
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"]
|
||||
|
||||
1
tools/docker-compose/ansible-tower.egg-link
Normal file
1
tools/docker-compose/ansible-tower.egg-link
Normal file
@ -0,0 +1 @@
|
||||
/tower_devel
|
||||
23
tools/docker-compose/ansible_tower.egg-info/PKG-INFO
Normal file
23
tools/docker-compose/ansible_tower.egg-info/PKG-INFO
Normal 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
|
||||
19030
tools/docker-compose/ansible_tower.egg-info/SOURCES.txt
Normal file
19030
tools/docker-compose/ansible_tower.egg-info/SOURCES.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
[console_scripts]
|
||||
tower-manage = awx:manage
|
||||
awx-manage = awx:manage
|
||||
|
||||
1
tools/docker-compose/ansible_tower.egg-info/not-zip-safe
Normal file
1
tools/docker-compose/ansible_tower.egg-info/not-zip-safe
Normal file
@ -0,0 +1 @@
|
||||
|
||||
@ -0,0 +1 @@
|
||||
awx
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user