mirror of
https://github.com/ansible/awx.git
synced 2026-03-08 21:19:26 -02:30
Merge branch 'devel' of github.com:ansible/ansible-tower into rbac
This commit is contained in:
31
ISSUE_TEMPLATE.md
Normal file
31
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
### Summary
|
||||||
|
|
||||||
|
<!-- Briefly describe the problem. -->
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
<!--
|
||||||
|
* Tower version: X.Y.Z
|
||||||
|
* Ansible version: X.Y.Z
|
||||||
|
* Operating System:
|
||||||
|
* Web Browser:
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Steps To Reproduce:
|
||||||
|
|
||||||
|
<!-- For bugs, please show exactly how to reproduce the problem. For new
|
||||||
|
features, show how the feature would be used. -->
|
||||||
|
|
||||||
|
### Expected Results:
|
||||||
|
|
||||||
|
<!-- For bug reports, what did you expect to happen when running the steps
|
||||||
|
above? -->
|
||||||
|
|
||||||
|
### Actual Results:
|
||||||
|
|
||||||
|
<!-- For bug reports, what actually happened? -->
|
||||||
|
|
||||||
|
### Additional Information:
|
||||||
|
|
||||||
|
<!-- Include any links to sosreport, database dumps, screenshots or other
|
||||||
|
information. -->
|
||||||
@@ -32,6 +32,7 @@ from django.http import HttpResponse
|
|||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
from rest_framework.exceptions import PermissionDenied, ParseError
|
from rest_framework.exceptions import PermissionDenied, ParseError
|
||||||
|
from rest_framework.parsers import FormParser
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
@@ -2064,6 +2065,7 @@ class JobTemplateCallback(GenericAPIView):
|
|||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
permission_classes = (JobTemplateCallbackPermission,)
|
permission_classes = (JobTemplateCallbackPermission,)
|
||||||
serializer_class = EmptySerializer
|
serializer_class = EmptySerializer
|
||||||
|
parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [FormParser]
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@transaction.non_atomic_requests
|
@transaction.non_atomic_requests
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class FactCacheReceiver(object):
|
|||||||
self.timestamp = datetime.fromtimestamp(date_key, None)
|
self.timestamp = datetime.fromtimestamp(date_key, None)
|
||||||
|
|
||||||
# Update existing Fact entry
|
# Update existing Fact entry
|
||||||
fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp)
|
fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp)
|
||||||
if fact_obj:
|
if fact_obj:
|
||||||
fact_obj.facts = facts
|
fact_obj.facts = facts
|
||||||
fact_obj.save()
|
fact_obj.save()
|
||||||
|
|||||||
115
awx/main/migrations/0005_v300_changes.py
Normal file
115
awx/main/migrations/0005_v300_changes.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from awx.api.license import feature_enabled
|
||||||
|
|
||||||
|
|
||||||
|
def create_system_job_templates(apps, schema_editor):
|
||||||
|
'''
|
||||||
|
Create default system job templates if not present. Create default schedules
|
||||||
|
only if new system job templates were created (i.e. new database).
|
||||||
|
'''
|
||||||
|
|
||||||
|
SystemJobTemplate = apps.get_model('main', 'SystemJobTemplate')
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
sjt_ct = ContentType.objects.get_for_model(SystemJobTemplate)
|
||||||
|
now_dt = now()
|
||||||
|
now_str = now_dt.strftime('%Y%m%dT%H%M%SZ')
|
||||||
|
|
||||||
|
sjt, created = SystemJobTemplate.objects.get_or_create(
|
||||||
|
job_type='cleanup_jobs',
|
||||||
|
defaults=dict(
|
||||||
|
name='Cleanup Job Details',
|
||||||
|
description='Remove job history older than X days',
|
||||||
|
created=now_dt,
|
||||||
|
modified=now_dt,
|
||||||
|
polymorphic_ctype=sjt_ct,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
sjt.schedules.create(
|
||||||
|
name='Cleanup Job Schedule',
|
||||||
|
rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU' % now_str,
|
||||||
|
description='Automatically Generated Schedule',
|
||||||
|
enabled=True,
|
||||||
|
extra_data={'days': '120'},
|
||||||
|
created=now_dt,
|
||||||
|
modified=now_dt,
|
||||||
|
)
|
||||||
|
|
||||||
|
sjt, created = SystemJobTemplate.objects.get_or_create(
|
||||||
|
job_type='cleanup_deleted',
|
||||||
|
defaults=dict(
|
||||||
|
name='Cleanup Deleted Data',
|
||||||
|
description='Remove deleted object history older than X days',
|
||||||
|
created=now_dt,
|
||||||
|
modified=now_dt,
|
||||||
|
polymorphic_ctype=sjt_ct,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
sjt.schedules.create(
|
||||||
|
name='Cleanup Deleted Data Schedule',
|
||||||
|
rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO' % now_str,
|
||||||
|
description='Automatically Generated Schedule',
|
||||||
|
enabled=True,
|
||||||
|
extra_data={'days': '30'},
|
||||||
|
created=now_dt,
|
||||||
|
modified=now_dt,
|
||||||
|
)
|
||||||
|
|
||||||
|
sjt, created = SystemJobTemplate.objects.get_or_create(
|
||||||
|
job_type='cleanup_activitystream',
|
||||||
|
defaults=dict(
|
||||||
|
name='Cleanup Activity Stream',
|
||||||
|
description='Remove activity stream history older than X days',
|
||||||
|
created=now_dt,
|
||||||
|
modified=now_dt,
|
||||||
|
polymorphic_ctype=sjt_ct,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
sjt.schedules.create(
|
||||||
|
name='Cleanup Activity Schedule',
|
||||||
|
rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=TU' % now_str,
|
||||||
|
description='Automatically Generated Schedule',
|
||||||
|
enabled=True,
|
||||||
|
extra_data={'days': '355'},
|
||||||
|
created=now_dt,
|
||||||
|
modified=now_dt,
|
||||||
|
)
|
||||||
|
|
||||||
|
sjt, created = SystemJobTemplate.objects.get_or_create(
|
||||||
|
job_type='cleanup_facts',
|
||||||
|
defaults=dict(
|
||||||
|
name='Cleanup Fact Details',
|
||||||
|
description='Remove system tracking history',
|
||||||
|
created=now_dt,
|
||||||
|
modified=now_dt,
|
||||||
|
polymorphic_ctype=sjt_ct,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if created and feature_enabled('system_tracking', bypass_database=True):
|
||||||
|
sjt.schedules.create(
|
||||||
|
name='Cleanup Fact Schedule',
|
||||||
|
rrule='DTSTART:%s RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1' % now_str,
|
||||||
|
description='Automatically Generated Schedule',
|
||||||
|
enabled=True,
|
||||||
|
extra_data={'older_than': '120d', 'granularity': '1w'},
|
||||||
|
created=now_dt,
|
||||||
|
modified=now_dt,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0004_v300_changes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_system_job_templates, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -233,7 +233,8 @@ def handle_work_success(self, result, task_actual):
|
|||||||
task_actual['id'],
|
task_actual['id'],
|
||||||
instance_name,
|
instance_name,
|
||||||
notification_body['url'])
|
notification_body['url'])
|
||||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body)
|
notification_body['friendly_name'] = friendly_name
|
||||||
|
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||||
for n in set(notifiers.get('success', []) + notifiers.get('any', []))],
|
for n in set(notifiers.get('success', []) + notifiers.get('any', []))],
|
||||||
job_id=task_actual['id'])
|
job_id=task_actual['id'])
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
import urllib
|
||||||
from multiprocessing import Process
|
from multiprocessing import Process
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
import re
|
import re
|
||||||
@@ -463,6 +464,8 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
|
|||||||
response = method(url, json.dumps(data), 'application/json')
|
response = method(url, json.dumps(data), 'application/json')
|
||||||
elif data_type == 'yaml':
|
elif data_type == 'yaml':
|
||||||
response = method(url, yaml.safe_dump(data), 'application/yaml')
|
response = method(url, yaml.safe_dump(data), 'application/yaml')
|
||||||
|
elif data_type == 'form':
|
||||||
|
response = method(url, urllib.urlencode(data), 'application/x-www-form-urlencoded')
|
||||||
else:
|
else:
|
||||||
self.fail('Unsupported data_type %s' % data_type)
|
self.fail('Unsupported data_type %s' % data_type)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -803,6 +803,21 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
|||||||
self.assertEqual(job.hosts.count(), 1)
|
self.assertEqual(job.hosts.count(), 1)
|
||||||
self.assertEqual(job.hosts.all()[0], host)
|
self.assertEqual(job.hosts.all()[0], host)
|
||||||
|
|
||||||
|
# Create the job itself using URL-encoded form data instead of JSON.
|
||||||
|
result = self.post(url, data, expect=202, remote_addr=host_ip, data_type='form')
|
||||||
|
|
||||||
|
# Establish that we got back what we expect, and made the changes
|
||||||
|
# that we expect.
|
||||||
|
self.assertTrue('Location' in result.response, result.response)
|
||||||
|
self.assertEqual(jobs_qs.count(), 2)
|
||||||
|
job = jobs_qs[0]
|
||||||
|
self.assertEqual(urlparse.urlsplit(result.response['Location']).path,
|
||||||
|
job.get_absolute_url())
|
||||||
|
self.assertEqual(job.launch_type, 'callback')
|
||||||
|
self.assertEqual(job.limit, host.name)
|
||||||
|
self.assertEqual(job.hosts.count(), 1)
|
||||||
|
self.assertEqual(job.hosts.all()[0], host)
|
||||||
|
|
||||||
# Run the callback job again with extra vars and verify their presence
|
# Run the callback job again with extra vars and verify their presence
|
||||||
data.update(dict(extra_vars=dict(key="value")))
|
data.update(dict(extra_vars=dict(key="value")))
|
||||||
result = self.post(url, data, expect=202, remote_addr=host_ip)
|
result = self.post(url, data, expect=202, remote_addr=host_ip)
|
||||||
@@ -853,9 +868,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
|||||||
if host_ip:
|
if host_ip:
|
||||||
break
|
break
|
||||||
self.assertTrue(host)
|
self.assertTrue(host)
|
||||||
self.assertEqual(jobs_qs.count(), 2)
|
|
||||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
|
||||||
self.assertEqual(jobs_qs.count(), 3)
|
self.assertEqual(jobs_qs.count(), 3)
|
||||||
|
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||||
|
self.assertEqual(jobs_qs.count(), 4)
|
||||||
job = jobs_qs[0]
|
job = jobs_qs[0]
|
||||||
self.assertEqual(job.launch_type, 'callback')
|
self.assertEqual(job.launch_type, 'callback')
|
||||||
self.assertEqual(job.limit, host.name)
|
self.assertEqual(job.limit, host.name)
|
||||||
@@ -878,9 +893,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
|||||||
if host_ip:
|
if host_ip:
|
||||||
break
|
break
|
||||||
self.assertTrue(host)
|
self.assertTrue(host)
|
||||||
self.assertEqual(jobs_qs.count(), 3)
|
|
||||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
|
||||||
self.assertEqual(jobs_qs.count(), 4)
|
self.assertEqual(jobs_qs.count(), 4)
|
||||||
|
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||||
|
self.assertEqual(jobs_qs.count(), 5)
|
||||||
job = jobs_qs[0]
|
job = jobs_qs[0]
|
||||||
self.assertEqual(job.launch_type, 'callback')
|
self.assertEqual(job.launch_type, 'callback')
|
||||||
self.assertEqual(job.limit, host.name)
|
self.assertEqual(job.limit, host.name)
|
||||||
@@ -892,9 +907,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
|||||||
host_qs = host_qs.filter(variables__icontains='ansible_ssh_host')
|
host_qs = host_qs.filter(variables__icontains='ansible_ssh_host')
|
||||||
host = host_qs[0]
|
host = host_qs[0]
|
||||||
host_ip = host.variables_dict['ansible_ssh_host']
|
host_ip = host.variables_dict['ansible_ssh_host']
|
||||||
self.assertEqual(jobs_qs.count(), 4)
|
|
||||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
|
||||||
self.assertEqual(jobs_qs.count(), 5)
|
self.assertEqual(jobs_qs.count(), 5)
|
||||||
|
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||||
|
self.assertEqual(jobs_qs.count(), 6)
|
||||||
job = jobs_qs[0]
|
job = jobs_qs[0]
|
||||||
self.assertEqual(job.launch_type, 'callback')
|
self.assertEqual(job.launch_type, 'callback')
|
||||||
self.assertEqual(job.limit, host.name)
|
self.assertEqual(job.limit, host.name)
|
||||||
@@ -926,9 +941,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
|||||||
host_ip = list(ips)[0]
|
host_ip = list(ips)[0]
|
||||||
break
|
break
|
||||||
self.assertTrue(host)
|
self.assertTrue(host)
|
||||||
self.assertEqual(jobs_qs.count(), 5)
|
|
||||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
|
||||||
self.assertEqual(jobs_qs.count(), 6)
|
self.assertEqual(jobs_qs.count(), 6)
|
||||||
|
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||||
|
self.assertEqual(jobs_qs.count(), 7)
|
||||||
job = jobs_qs[0]
|
job = jobs_qs[0]
|
||||||
self.assertEqual(job.launch_type, 'callback')
|
self.assertEqual(job.launch_type, 'callback')
|
||||||
self.assertEqual(job.limit, ':&'.join([job_template.limit, host.name]))
|
self.assertEqual(job.limit, ':&'.join([job_template.limit, host.name]))
|
||||||
|
|||||||
@@ -44,7 +44,8 @@
|
|||||||
@import "text-label.less";
|
@import "text-label.less";
|
||||||
@import "./bootstrap-datepicker.less";
|
@import "./bootstrap-datepicker.less";
|
||||||
@import "awx/ui/client/src/shared/branding/colors.default.less";
|
@import "awx/ui/client/src/shared/branding/colors.default.less";
|
||||||
|
// Bootstrap default overrides
|
||||||
|
@import "awx/ui/client/src/shared/bootstrap-settings.less";
|
||||||
/* Bootstrap fix that's causing a right margin to appear
|
/* Bootstrap fix that's causing a right margin to appear
|
||||||
whenver a modal is opened */
|
whenver a modal is opened */
|
||||||
body.modal-open {
|
body.modal-open {
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
export default
|
export default
|
||||||
['$scope', '$state', 'CheckLicense', function($scope, $state, CheckLicense){
|
['$scope', '$state', 'CheckLicense', function($scope, $state, CheckLicense){
|
||||||
var processVersion = function(version){
|
var processVersion = function(version){
|
||||||
// prettify version & calculate padding
|
// prettify version & calculate padding
|
||||||
// e,g 3.0.0-0.git201602191743/ -> 3.0.0
|
// e,g 3.0.0-0.git201602191743/ -> 3.0.0
|
||||||
var split = version.split('-')[0]
|
var split = version.split('-')[0]
|
||||||
var spaces = Math.floor((16-split.length)/2),
|
var spaces = Math.floor((16-split.length)/2),
|
||||||
paddedStr = "";
|
paddedStr = "";
|
||||||
for(var i=0; i<=spaces; i++){
|
for(var i=0; i<=spaces; i++){
|
||||||
paddedStr = paddedStr +" ";
|
paddedStr = paddedStr +" ";
|
||||||
}
|
}
|
||||||
paddedStr = paddedStr + split;
|
paddedStr = paddedStr + split;
|
||||||
for(var j = paddedStr.length; j<16; j++){
|
for(var j = paddedStr.length; j<16; j++){
|
||||||
paddedStr = paddedStr + " ";
|
paddedStr = paddedStr + " ";
|
||||||
}
|
}
|
||||||
return paddedStr
|
return paddedStr
|
||||||
}
|
};
|
||||||
var init = function(){
|
var init = function(){
|
||||||
CheckLicense.get()
|
CheckLicense.get()
|
||||||
.then(function(res){
|
.then(function(res){
|
||||||
@@ -23,9 +23,9 @@ export default
|
|||||||
$('#about-modal').modal('show');
|
$('#about-modal').modal('show');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
var back = function(){
|
$('#about-modal').on('hidden.bs.modal', function () {
|
||||||
$state.go('setup');
|
$state.go('setup');
|
||||||
}
|
});
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<img class="About-brand--ansible img-responsive" src="/static/assets/ansible_tower_logo_minimalc.png" />
|
<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()">
|
<button data-dismiss="modal" type="button" class="close About-close">
|
||||||
<span class="fa fa-times-circle"></span>
|
<span class="fa fa-times-circle"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import inventoryScripts from './inventory-scripts/main';
|
|||||||
import permissions from './permissions/main';
|
import permissions from './permissions/main';
|
||||||
import managementJobs from './management-jobs/main';
|
import managementJobs from './management-jobs/main';
|
||||||
import jobDetail from './job-detail/main';
|
import jobDetail from './job-detail/main';
|
||||||
|
import notifications from './notifications/main';
|
||||||
|
|
||||||
// modules
|
// modules
|
||||||
import about from './about/main';
|
import about from './about/main';
|
||||||
@@ -76,7 +77,7 @@ __deferLoadIfEnabled();
|
|||||||
/*#endif#*/
|
/*#endif#*/
|
||||||
|
|
||||||
var tower = angular.module('Tower', [
|
var tower = angular.module('Tower', [
|
||||||
// 'ngAnimate',
|
//'ngAnimate',
|
||||||
'ngSanitize',
|
'ngSanitize',
|
||||||
'ngCookies',
|
'ngCookies',
|
||||||
about.name,
|
about.name,
|
||||||
@@ -98,6 +99,7 @@ var tower = angular.module('Tower', [
|
|||||||
activityStream.name,
|
activityStream.name,
|
||||||
footer.name,
|
footer.name,
|
||||||
jobDetail.name,
|
jobDetail.name,
|
||||||
|
notifications.name,
|
||||||
standardOut.name,
|
standardOut.name,
|
||||||
'templates',
|
'templates',
|
||||||
'Utilities',
|
'Utilities',
|
||||||
@@ -882,13 +884,13 @@ var tower = angular.module('Tower', [
|
|||||||
}]);
|
}]);
|
||||||
}])
|
}])
|
||||||
|
|
||||||
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense',
|
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense',
|
||||||
'$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
|
'$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
|
||||||
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService',
|
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService',
|
||||||
function (
|
function (
|
||||||
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
|
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
|
||||||
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
|
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
|
||||||
LoadConfig, Store, ShowSocketHelp, pendoService)
|
LoadConfig, Store, ShowSocketHelp, pendoService)
|
||||||
{
|
{
|
||||||
var sock;
|
var sock;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
/** @define DashboardCounts */
|
/** @define DashboardCounts */
|
||||||
|
|
||||||
.Footer {
|
.Footer {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
color: #848992;
|
color: #848992;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1040;
|
z-index: 1040;
|
||||||
position: fixed;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
<img id="footer-logo" alt="Red Hat, Inc. | Ansible, Inc." class="Footer-logoImage" src="/static/assets/footer-logo.png">
|
<img id="footer-logo" alt="Red Hat, Inc. | Ansible, Inc." class="Footer-logoImage" src="/static/assets/footer-logo.png">
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="Footer-copyright" ng-class="{'is-loggedOut' : !$root.current_user.username}">Copyright © 2015 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc. All Rights Reserved.</div>
|
<div class="Footer-copyright" ng-class="{'is-loggedOut' : !$root.current_user.username}">Copyright © 2016 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc.</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -20,6 +20,16 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.License-submit--success.ng-hide-add, .License-submit--success.ng-hide-remove {
|
||||||
|
transition: all ease-in-out 0.5s;
|
||||||
|
}
|
||||||
|
.License-submit--success{
|
||||||
|
opacity: 1;
|
||||||
|
transition: all ease-in-out 0.5s;
|
||||||
|
}
|
||||||
|
.License-submit--success.ng-hide{
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
.License-eula textarea{
|
.License-eula textarea{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
@@ -33,6 +43,9 @@
|
|||||||
.License-field{
|
.License-field{
|
||||||
.OnePlusTwo-left--detailsRow;
|
.OnePlusTwo-left--detailsRow;
|
||||||
}
|
}
|
||||||
|
.License-field + .License-field {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
.License-greenText{
|
.License-greenText{
|
||||||
color: @submit-button-bg;
|
color: @submit-button-bg;
|
||||||
}
|
}
|
||||||
@@ -40,16 +53,16 @@
|
|||||||
color: #d9534f;
|
color: #d9534f;
|
||||||
}
|
}
|
||||||
.License-fields{
|
.License-fields{
|
||||||
.OnePlusTwo-left--details;
|
.OnePlusTwo-left--details;
|
||||||
}
|
}
|
||||||
.License-details {
|
.License-details {
|
||||||
.OnePlusTwo-left--panel(600px);
|
.OnePlusTwo-left--panel(650px);
|
||||||
}
|
}
|
||||||
.License-titleText {
|
.License-titleText {
|
||||||
.OnePlusTwo-panelHeader;
|
.OnePlusTwo-panelHeader;
|
||||||
}
|
}
|
||||||
.License-management{
|
.License-management{
|
||||||
.OnePlusTwo-right--panel(600px);
|
.OnePlusTwo-right--panel(650px);
|
||||||
}
|
}
|
||||||
.License-submit--container{
|
.License-submit--container{
|
||||||
height: 33px;
|
height: 33px;
|
||||||
@@ -59,8 +72,25 @@
|
|||||||
margin: 0 10px 0 0;
|
margin: 0 10px 0 0;
|
||||||
}
|
}
|
||||||
.License-file--container {
|
.License-file--container {
|
||||||
margin: 20px 0 20px 0;
|
|
||||||
input[type=file] {
|
input[type=file] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.License-upgradeText {
|
||||||
|
margin: 20px 0px;
|
||||||
|
}
|
||||||
|
.License-body {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
.License-subTitleText {
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 20px 0px 5px 0px;
|
||||||
|
color: @default-interface-txt;
|
||||||
|
}
|
||||||
|
.License-helperText {
|
||||||
|
color: @default-interface-txt;
|
||||||
|
}
|
||||||
|
.License-input--fake{
|
||||||
|
border-top-right-radius: 4px !important;
|
||||||
|
border-bottom-right-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
export default
|
export default
|
||||||
[ 'Wait', '$state', '$scope', '$location',
|
[ 'Wait', '$state', '$scope', '$rootScope', '$location',
|
||||||
'GetBasePath', 'Rest', 'ProcessErrors', 'CheckLicense', 'moment',
|
'GetBasePath', 'Rest', 'ProcessErrors', 'CheckLicense', 'moment',
|
||||||
function( Wait, $state, $scope, $location,
|
function( Wait, $state, $scope, $rootScope, $location,
|
||||||
GetBasePath, Rest, ProcessErrors, CheckLicense, moment){
|
GetBasePath, Rest, ProcessErrors, CheckLicense, moment){
|
||||||
$scope.getKey = function(event){
|
$scope.getKey = function(event){
|
||||||
// Mimic HTML5 spec, show filename
|
// Mimic HTML5 spec, show filename
|
||||||
@@ -16,9 +16,19 @@ export default
|
|||||||
var raw = new FileReader();
|
var raw = new FileReader();
|
||||||
// readAsFoo runs async
|
// readAsFoo runs async
|
||||||
raw.onload = function(){
|
raw.onload = function(){
|
||||||
$scope.newLicense.file = JSON.parse(raw.result);
|
try {
|
||||||
|
$scope.newLicense.file = JSON.parse(raw.result);
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
ProcessErrors($rootScope, null, null, null, {msg: 'Invalid file format. Please upload valid JSON.'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
raw.readAsText(event.target.files[0]);
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
ProcessErrors($rootScope, null, null, null, {msg: 'Invalid file format. Please upload valid JSON.'});
|
||||||
}
|
}
|
||||||
raw.readAsText(event.target.files[0]);
|
|
||||||
};
|
};
|
||||||
// HTML5 spec doesn't provide a way to customize file input css
|
// 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
|
// So we hide the default input, show our own, and simulate clicks to the hidden input
|
||||||
@@ -33,6 +43,11 @@ export default
|
|||||||
reset();
|
reset();
|
||||||
init();
|
init();
|
||||||
$scope.success = true;
|
$scope.success = true;
|
||||||
|
// for animation purposes
|
||||||
|
var successTimeout = setTimeout(function(){
|
||||||
|
$scope.success = false;
|
||||||
|
clearTimeout(successTimeout);
|
||||||
|
}, 4000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
var calcDaysRemaining = function(ms){
|
var calcDaysRemaining = function(ms){
|
||||||
@@ -51,6 +66,7 @@ export default
|
|||||||
CheckLicense.get()
|
CheckLicense.get()
|
||||||
.then(function(res){
|
.then(function(res){
|
||||||
$scope.license = res.data;
|
$scope.license = res.data;
|
||||||
|
$scope.license.version = res.data.version.split('-')[0];
|
||||||
$scope.time = {};
|
$scope.time = {};
|
||||||
$scope.time.remaining = calcDaysRemaining($scope.license.license_info.time_remaining);
|
$scope.time.remaining = calcDaysRemaining($scope.license.license_info.time_remaining);
|
||||||
$scope.time.expiresOn = calcExpiresOn($scope.time.remaining);
|
$scope.time.expiresOn = calcExpiresOn($scope.time.remaining);
|
||||||
|
|||||||
@@ -5,95 +5,98 @@
|
|||||||
<div class="License-fields">
|
<div class="License-fields">
|
||||||
<div class="License-field">
|
<div class="License-field">
|
||||||
<div class="License-field--label">License</div>
|
<div class="License-field--label">License</div>
|
||||||
<div class="License-field--content">
|
<div class="License-field--content">
|
||||||
<span ng-show='valid'><i class="fa fa-circle License-greenText"></i> Valid</span>
|
<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>
|
<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>
|
</div>
|
||||||
<div class="License-field">
|
<div class="License-field">
|
||||||
<div class="License-field--label">License Type</div>
|
<div class="License-field--label">Version</div>
|
||||||
<div class="License-field--content">
|
<div class="License-field--content">
|
||||||
{{license.license_info.license_type}}
|
{{license.version || "No result found"}}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="License-field">
|
||||||
|
<div class="License-field--label">License Type</div>
|
||||||
|
<div class="License-field--content">
|
||||||
|
{{license.license_info.license_type || "No result found"}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="License-field">
|
<div class="License-field">
|
||||||
<div class="License-field--label">Subscription</div>
|
<div class="License-field--label">Subscription</div>
|
||||||
<div class="License-field--content">
|
<div class="License-field--content">
|
||||||
{{license.license_info.subscription_name}}
|
{{license.license_info.subscription_name || "No result found"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="License-field">
|
<div class="License-field">
|
||||||
<div class="License-field--label">License Key</div>
|
<div class="License-field--label">License Key</div>
|
||||||
<div class="License-field--content">
|
<div class="License-field--content">
|
||||||
{{license.license_info.license_key}}
|
{{license.license_info.license_key || "No result found"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="License-field">
|
<div class="License-field">
|
||||||
<div class="License-field--label">Expires On</div>
|
<div class="License-field--label">Expires On</div>
|
||||||
<div class="License-field--content">
|
<div class="License-field--content">
|
||||||
{{time.expiresOn}}
|
{{time.expiresOn}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="License-field">
|
<div class="License-field">
|
||||||
<div class="License-field--label">Time Remaining</div>
|
<div class="License-field--label">Time Remaining</div>
|
||||||
<div class="License-field--content">
|
<div class="License-field--content">
|
||||||
{{time.remaining}} Day
|
{{time.remaining}} Days
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="License-field">
|
<div class="License-field">
|
||||||
<div class="License-field--label">Hosts Available</div>
|
<div class="License-field--label">Hosts Available</div>
|
||||||
<div class="License-field--content">
|
<div class="License-field--content">
|
||||||
{{license.license_info.available_instances}}
|
{{license.license_info.available_instances || "No result found"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="License-field">
|
<div class="License-field">
|
||||||
<div class="License-field--label">Hosts Used</div>
|
<div class="License-field--label">Hosts Used</div>
|
||||||
<div class="License-field--content">
|
<div class="License-field--content">
|
||||||
{{license.license_info.current_instances}}
|
{{license.license_info.current_instances || "No result found"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="License-field License-greenText">
|
<div class="License-field License-greenText">
|
||||||
<div class="License-field--label">Hosts Remaining</div>
|
<div class="License-field--label">Hosts Remaining</div>
|
||||||
<div class="License-field--content">
|
<div class="License-field--content">
|
||||||
{{license.license_info.free_instances}}
|
{{license.license_info.free_instances || "No result found"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>If you are ready to upgrade, please contact us by clicking the button below</p>
|
<div class="License-upgradeText">If you are ready to upgrade, please contact us by clicking the button below</div>
|
||||||
<a href="https://www.ansible.com/renew" target="_blank"><button class="btn btn-default">Upgrade</button></a>
|
<a href="https://www.ansible.com/renew" target="_blank"><button class="btn btn-default">Upgrade</button></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="License-management">
|
<div class="License-management">
|
||||||
<div class="Panel">
|
<div class="Panel">
|
||||||
<div class="License-titleText">License Management</div>
|
<div class="License-titleText">License Management</div>
|
||||||
<p>Choose your license file, agree to the End User License Agreement, and click submit.</p>
|
<div class="License-body">
|
||||||
<form id="License-form" name="license">
|
<p class="License-helperText">Choose your license file, agree to the End User License Agreement, and click submit.</p>
|
||||||
<div class="input-group License-file--container">
|
<form id="License-form" name="license">
|
||||||
<span class="btn btn-default input-group-addon" ng-click="fakeClick()">Browse...</span>
|
<div class="License-subTitleText prepend-asterisk"> License File</div>
|
||||||
<input class="form-control" ng-disabled="true" placeholder="{{fileName}}" />
|
<div class="input-group License-file--container">
|
||||||
<input id="License-file" class="form-control" type="file" file-on-change="getKey"/>
|
<span class="btn btn-default input-group-addon" ng-click="fakeClick()">Browse...</span>
|
||||||
</div>
|
<input class="form-control License-input--fake" ng-disabled="true" placeholder="{{fileName}}" />
|
||||||
<div class="License-titleText prepend-asterisk"> End User License Agreement</div>
|
<input id="License-file" class="form-control" type="file" file-on-change="getKey"/>
|
||||||
<div class="form-group License-eula">
|
</div>
|
||||||
<textarea class="form-control">{{license.eula}}
|
<div class="License-subTitleText prepend-asterisk"> End User License Agreement</div>
|
||||||
</textarea>
|
<div class="form-group License-eula">
|
||||||
</div>
|
<textarea class="form-control">{{license.eula}}
|
||||||
<div class="form-group">
|
</textarea>
|
||||||
<div class="checkbox">
|
</div>
|
||||||
<div class="License-details--label"><input type="checkbox" ng-model="newLicense.eula" required> I agree to the End User License Agreement</div>
|
<div class="form-group">
|
||||||
<div class="License-submit--container pull-right">
|
<div class="checkbox">
|
||||||
<span ng-hide="success == null || false" class="License-greenText License-submit--success pull-left">Save successful!</span>
|
<div class="License-details--label"><input type="checkbox" ng-model="newLicense.eula" required> I agree to the End User License Agreement</div>
|
||||||
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="newLicense.file.license_key == null || newLicense.eula == null">Submit</button>
|
<div class="License-submit--container pull-right">
|
||||||
|
<span ng-show="success == true" 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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
67
awx/ui/client/src/notifications/add/add.controller.js
Normal file
67
awx/ui/client/src/notifications/add/add.controller.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default
|
||||||
|
[ '$rootScope', 'pagination', '$compile','SchedulerInit', 'Rest', 'Wait',
|
||||||
|
'notificationsFormObject', 'ProcessErrors', 'GetBasePath', 'Empty',
|
||||||
|
'GenerateForm', 'SearchInit' , 'PaginateInit',
|
||||||
|
'LookUpInit', 'OrganizationList', '$scope', '$state',
|
||||||
|
function(
|
||||||
|
$rootScope, pagination, $compile, SchedulerInit, Rest, Wait,
|
||||||
|
notificationsFormObject, ProcessErrors, GetBasePath, Empty,
|
||||||
|
GenerateForm, SearchInit, PaginateInit,
|
||||||
|
LookUpInit, OrganizationList, $scope, $state
|
||||||
|
) {
|
||||||
|
var scope = $scope,
|
||||||
|
generator = GenerateForm,
|
||||||
|
form = notificationsFormObject,
|
||||||
|
url = GetBasePath('notifications');
|
||||||
|
|
||||||
|
generator.inject(form, {
|
||||||
|
mode: 'add' ,
|
||||||
|
scope:scope,
|
||||||
|
related: false
|
||||||
|
});
|
||||||
|
generator.reset();
|
||||||
|
|
||||||
|
LookUpInit({
|
||||||
|
url: GetBasePath('organization'),
|
||||||
|
scope: scope,
|
||||||
|
form: form,
|
||||||
|
list: OrganizationList,
|
||||||
|
field: 'organization',
|
||||||
|
input_type: 'radio'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save
|
||||||
|
scope.formSave = function () {
|
||||||
|
|
||||||
|
generator.clearApiErrors();
|
||||||
|
Wait('start');
|
||||||
|
Rest.setUrl(url);
|
||||||
|
Rest.post({
|
||||||
|
name: scope.name,
|
||||||
|
description: scope.description,
|
||||||
|
organization: scope.organization,
|
||||||
|
script: scope.script
|
||||||
|
})
|
||||||
|
.success(function (data) {
|
||||||
|
$rootScope.addedItem = data.id;
|
||||||
|
$state.go('inventoryScripts', {}, {reload: true});
|
||||||
|
Wait('stop');
|
||||||
|
})
|
||||||
|
.error(function (data, status) {
|
||||||
|
ProcessErrors(scope, data, status, form, { hdr: 'Error!',
|
||||||
|
msg: 'Failed to add new inventory script. POST returned status: ' + status });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.formCancel = function () {
|
||||||
|
$state.transitionTo('inventoryScripts');
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
];
|
||||||
3
awx/ui/client/src/notifications/add/add.partial.html
Normal file
3
awx/ui/client/src/notifications/add/add.partial.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="tab-pane" id="notifications_add">
|
||||||
|
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||||
|
</div>
|
||||||
23
awx/ui/client/src/notifications/add/add.route.js
Normal file
23
awx/ui/client/src/notifications/add/add.route.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import {templateUrl} from '../../shared/template-url/template-url.factory';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'notifications.add',
|
||||||
|
route: '/add',
|
||||||
|
templateUrl: templateUrl('notifications/add/add'),
|
||||||
|
controller: 'notificationsAddController',
|
||||||
|
resolve: {
|
||||||
|
features: ['FeaturesService', function(FeaturesService) {
|
||||||
|
return FeaturesService.get();
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
parent: 'notifications',
|
||||||
|
label: 'Create Notification'
|
||||||
|
}
|
||||||
|
};
|
||||||
15
awx/ui/client/src/notifications/add/main.js
Normal file
15
awx/ui/client/src/notifications/add/main.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import route from './add.route';
|
||||||
|
import controller from './add.controller';
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('notificationsAdd', [])
|
||||||
|
.controller('notificationsAddController', controller)
|
||||||
|
.run(['$stateExtender', function($stateExtender) {
|
||||||
|
$stateExtender.addState(route);
|
||||||
|
}]);
|
||||||
97
awx/ui/client/src/notifications/edit/edit.controller.js
Normal file
97
awx/ui/client/src/notifications/edit/edit.controller.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default
|
||||||
|
[ 'Rest', 'Wait',
|
||||||
|
'notificationsFormObject', 'ProcessErrors', 'GetBasePath',
|
||||||
|
'GenerateForm', 'SearchInit' , 'PaginateInit',
|
||||||
|
'LookUpInit', 'OrganizationList', 'inventory_script',
|
||||||
|
'$scope', '$state',
|
||||||
|
function(
|
||||||
|
Rest, Wait,
|
||||||
|
notificationsFormObject, ProcessErrors, GetBasePath,
|
||||||
|
GenerateForm, SearchInit, PaginateInit,
|
||||||
|
LookUpInit, OrganizationList, inventory_script,
|
||||||
|
$scope, $state
|
||||||
|
) {
|
||||||
|
var generator = GenerateForm,
|
||||||
|
id = inventory_script.id,
|
||||||
|
form = notificationsFormObject,
|
||||||
|
master = {},
|
||||||
|
url = GetBasePath('notifications');
|
||||||
|
|
||||||
|
$scope.inventory_script = inventory_script;
|
||||||
|
generator.inject(form, {
|
||||||
|
mode: 'edit' ,
|
||||||
|
scope:$scope,
|
||||||
|
related: false,
|
||||||
|
activityStream: false
|
||||||
|
});
|
||||||
|
generator.reset();
|
||||||
|
LookUpInit({
|
||||||
|
url: GetBasePath('organization'),
|
||||||
|
scope: $scope,
|
||||||
|
form: form,
|
||||||
|
// hdr: "Select Custom Inventory",
|
||||||
|
list: OrganizationList,
|
||||||
|
field: 'organization',
|
||||||
|
input_type: 'radio'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retrieve detail record and prepopulate the form
|
||||||
|
Wait('start');
|
||||||
|
Rest.setUrl(url + id+'/');
|
||||||
|
Rest.get()
|
||||||
|
.success(function (data) {
|
||||||
|
var fld;
|
||||||
|
for (fld in form.fields) {
|
||||||
|
if (data[fld]) {
|
||||||
|
$scope[fld] = data[fld];
|
||||||
|
master[fld] = data[fld];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.fields[fld].sourceModel && data.summary_fields &&
|
||||||
|
data.summary_fields[form.fields[fld].sourceModel]) {
|
||||||
|
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||||
|
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||||
|
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||||
|
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Wait('stop');
|
||||||
|
})
|
||||||
|
.error(function (data, status) {
|
||||||
|
ProcessErrors($scope, data, status, form, { hdr: 'Error!',
|
||||||
|
msg: 'Failed to retrieve inventory script: ' + id + '. GET status: ' + status });
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.formSave = function () {
|
||||||
|
generator.clearApiErrors();
|
||||||
|
Wait('start');
|
||||||
|
Rest.setUrl(url+ id+'/');
|
||||||
|
Rest.put({
|
||||||
|
name: $scope.name,
|
||||||
|
description: $scope.description,
|
||||||
|
organization: $scope.organization,
|
||||||
|
script: $scope.script
|
||||||
|
})
|
||||||
|
.success(function () {
|
||||||
|
$state.transitionTo('inventoryScriptsList');
|
||||||
|
Wait('stop');
|
||||||
|
|
||||||
|
})
|
||||||
|
.error(function (data, status) {
|
||||||
|
ProcessErrors($scope, data, status, form, { hdr: 'Error!',
|
||||||
|
msg: 'Failed to add new inventory script. PUT returned status: ' + status });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.formCancel = function () {
|
||||||
|
$state.transitionTo('inventoryScripts');
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
];
|
||||||
3
awx/ui/client/src/notifications/edit/edit.partial.html
Normal file
3
awx/ui/client/src/notifications/edit/edit.partial.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="tab-pane" id="notficiations_edit">
|
||||||
|
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||||
|
</div>
|
||||||
23
awx/ui/client/src/notifications/edit/edit.route.js
Normal file
23
awx/ui/client/src/notifications/edit/edit.route.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import {templateUrl} from '../../shared/template-url/template-url.factory';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'notifications.edit',
|
||||||
|
route: '/edit',
|
||||||
|
templateUrl: templateUrl('notifications/edit/edit'),
|
||||||
|
controller: 'notificationsEditController',
|
||||||
|
resolve: {
|
||||||
|
features: ['FeaturesService', function(FeaturesService) {
|
||||||
|
return FeaturesService.get();
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
parent: 'notifications',
|
||||||
|
label: 'Edit Notification'
|
||||||
|
}
|
||||||
|
};
|
||||||
15
awx/ui/client/src/notifications/edit/main.js
Normal file
15
awx/ui/client/src/notifications/edit/main.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import route from './edit.route';
|
||||||
|
import controller from './edit.controller';
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('notificationsEdit', [])
|
||||||
|
.controller('notificationsEditController', controller)
|
||||||
|
.run(['$stateExtender', function($stateExtender) {
|
||||||
|
$stateExtender.addState(route);
|
||||||
|
}]);
|
||||||
83
awx/ui/client/src/notifications/list/list.controller.js
Normal file
83
awx/ui/client/src/notifications/list/list.controller.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default
|
||||||
|
[ '$rootScope','Wait', 'generateList', 'notificationsListObject',
|
||||||
|
'GetBasePath' , 'SearchInit' , 'PaginateInit',
|
||||||
|
'Rest' , 'ProcessErrors', 'Prompt', '$state',
|
||||||
|
function(
|
||||||
|
$rootScope,Wait, GenerateList, notificationsListObject,
|
||||||
|
GetBasePath, SearchInit, PaginateInit,
|
||||||
|
Rest, ProcessErrors, Prompt, $state
|
||||||
|
) {
|
||||||
|
var scope = $rootScope.$new(),
|
||||||
|
defaultUrl = GetBasePath('notifications'),
|
||||||
|
list = notificationsListObject,
|
||||||
|
view = GenerateList;
|
||||||
|
|
||||||
|
view.inject( list, {
|
||||||
|
mode: 'edit',
|
||||||
|
scope: scope
|
||||||
|
});
|
||||||
|
|
||||||
|
// SearchInit({
|
||||||
|
// scope: scope,
|
||||||
|
// set: 'notifications',
|
||||||
|
// list: list,
|
||||||
|
// url: defaultUrl
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// if ($rootScope.addedItem) {
|
||||||
|
// scope.addedItem = $rootScope.addedItem;
|
||||||
|
// delete $rootScope.addedItem;
|
||||||
|
// }
|
||||||
|
// PaginateInit({
|
||||||
|
// scope: scope,
|
||||||
|
// list: list,
|
||||||
|
// url: defaultUrl
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// scope.search(list.iterator);
|
||||||
|
|
||||||
|
scope.editNotification = function(){
|
||||||
|
$state.transitionTo('notifications.edit',{
|
||||||
|
inventory_script_id: this.inventory_script.id,
|
||||||
|
inventory_script: this.inventory_script
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.deleteNotification = function(id, name){
|
||||||
|
|
||||||
|
var action = function () {
|
||||||
|
$('#prompt-modal').modal('hide');
|
||||||
|
Wait('start');
|
||||||
|
var url = defaultUrl + id + '/';
|
||||||
|
Rest.setUrl(url);
|
||||||
|
Rest.destroy()
|
||||||
|
.success(function () {
|
||||||
|
scope.search(list.iterator);
|
||||||
|
})
|
||||||
|
.error(function (data, status) {
|
||||||
|
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||||
|
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var bodyHtml = '<div class="Prompt-bodyQuery">Are you sure you want to delete the inventory script below?</div><div class="Prompt-bodyTarget">' + name + '</div>';
|
||||||
|
Prompt({
|
||||||
|
hdr: 'Delete',
|
||||||
|
body: bodyHtml,
|
||||||
|
action: action,
|
||||||
|
actionText: 'DELETE'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.addNotification = function(){
|
||||||
|
$state.transitionTo('notifications.add');
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
];
|
||||||
4
awx/ui/client/src/notifications/list/list.partial.html
Normal file
4
awx/ui/client/src/notifications/list/list.partial.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="tab-pane" id="notifications">
|
||||||
|
<div ui-view></div>
|
||||||
|
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||||
|
</div>
|
||||||
19
awx/ui/client/src/notifications/list/list.route.js
Normal file
19
awx/ui/client/src/notifications/list/list.route.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import {templateUrl} from '../../shared/template-url/template-url.factory';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'notifications',
|
||||||
|
route: '/notifications',
|
||||||
|
templateUrl: templateUrl('notifications/list/list'),
|
||||||
|
controller: 'notificationsListController',
|
||||||
|
resolve: {
|
||||||
|
features: ['FeaturesService', function(FeaturesService) {
|
||||||
|
return FeaturesService.get();
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
};
|
||||||
15
awx/ui/client/src/notifications/list/main.js
Normal file
15
awx/ui/client/src/notifications/list/main.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import route from './list.route';
|
||||||
|
import controller from './list.controller';
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('notificationsList', [])
|
||||||
|
.controller('notificationsListController', controller)
|
||||||
|
.run(['$stateExtender', function($stateExtender) {
|
||||||
|
$stateExtender.addState(route);
|
||||||
|
}]);
|
||||||
22
awx/ui/client/src/notifications/main.js
Normal file
22
awx/ui/client/src/notifications/main.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2016 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
import notificationsList from './list/main';
|
||||||
|
import notificationsAdd from './add/main';
|
||||||
|
import notificationsEdit from './edit/main';
|
||||||
|
|
||||||
|
import list from './notifications.list';
|
||||||
|
import form from './notifications.form';
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('notifications', [
|
||||||
|
notificationsList.name,
|
||||||
|
notificationsAdd.name,
|
||||||
|
notificationsEdit.name
|
||||||
|
])
|
||||||
|
.factory('notificationsListObject', list)
|
||||||
|
.factory('notificationsFormObject', form);
|
||||||
47
awx/ui/client/src/notifications/notifications.form.js
Normal file
47
awx/ui/client/src/notifications/notifications.form.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name forms.function:CustomInventory
|
||||||
|
* @description This form is for adding/editing an organization
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
addTitle: 'New Notification',
|
||||||
|
editTitle: '{{ name }}',
|
||||||
|
name: 'notification',
|
||||||
|
showActions: true,
|
||||||
|
|
||||||
|
fields: {
|
||||||
|
name: {
|
||||||
|
label: 'Name',
|
||||||
|
type: 'text',
|
||||||
|
addRequired: true,
|
||||||
|
editRequired: true,
|
||||||
|
capitalize: false
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
label: 'Description',
|
||||||
|
type: 'text',
|
||||||
|
addRequired: false,
|
||||||
|
editRequired: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
buttons: { //for now always generates <button> tags
|
||||||
|
save: {
|
||||||
|
ngClick: 'formSave()', //$scope.function to call on click, optional
|
||||||
|
ngDisabled: true //Disable when $pristine or $invalid, optional
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
ngClick: 'formCancel()',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
63
awx/ui/client/src/notifications/notifications.list.js
Normal file
63
awx/ui/client/src/notifications/notifications.list.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2015 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function(){
|
||||||
|
return {
|
||||||
|
name: 'notifications' ,
|
||||||
|
listTitle: 'Notifications',
|
||||||
|
iterator: 'notification',
|
||||||
|
index: false,
|
||||||
|
hover: false,
|
||||||
|
|
||||||
|
fields: {
|
||||||
|
name: {
|
||||||
|
key: true,
|
||||||
|
label: 'Name',
|
||||||
|
columnClass: 'col-md-3 col-sm-9 col-xs-9',
|
||||||
|
modalColumnClass: 'col-md-8'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
label: 'Description',
|
||||||
|
excludeModal: true,
|
||||||
|
columnClass: 'col-md-4 hidden-sm hidden-xs'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
add: {
|
||||||
|
mode: 'all', // One of: edit, select, all
|
||||||
|
ngClick: 'addNotification()',
|
||||||
|
awToolTip: 'Create a new custom inventory',
|
||||||
|
actionClass: 'btn List-buttonSubmit',
|
||||||
|
buttonContent: '+ ADD'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fieldActions: {
|
||||||
|
|
||||||
|
columnClass: 'col-md-2 col-sm-3 col-xs-3',
|
||||||
|
|
||||||
|
edit: {
|
||||||
|
ngClick: "editNotification(inventory_script.id)",
|
||||||
|
icon: 'fa-edit',
|
||||||
|
label: 'Edit',
|
||||||
|
"class": 'btn-sm',
|
||||||
|
awToolTip: 'Edit credential',
|
||||||
|
dataPlacement: 'top'
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
ngClick: "deleteNotification(notification.id, notification.name)",
|
||||||
|
icon: 'fa-trash',
|
||||||
|
label: 'Delete',
|
||||||
|
"class": 'btn-sm',
|
||||||
|
awToolTip: 'Delete credential',
|
||||||
|
dataPlacement: 'top'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -36,6 +36,12 @@
|
|||||||
Create and edit scripts to dynamically load hosts from any source.
|
Create and edit scripts to dynamically load hosts from any source.
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
<a ui-sref="notifications" class="SetupItem">
|
||||||
|
<h4 class="SetupItem-title">Notifications</h4>
|
||||||
|
<p class="SetupItem-description">
|
||||||
|
Create templates for sending notifications with Email, HipChat, Slack, and SMS.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
<a ui-sref="license" class="SetupItem">
|
<a ui-sref="license" class="SetupItem">
|
||||||
<h4 class="SetupItem-title">View Your License</h4>
|
<h4 class="SetupItem-title">View Your License</h4>
|
||||||
<p class="SetupItem-description">
|
<p class="SetupItem-description">
|
||||||
|
|||||||
22
awx/ui/client/src/shared/bootstrap-settings.less
vendored
Normal file
22
awx/ui/client/src/shared/bootstrap-settings.less
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@import "awx/ui/client/src/shared/branding/colors.default.less";
|
||||||
|
.btn-success{
|
||||||
|
background: @default-succ;
|
||||||
|
border-color: transparent;
|
||||||
|
:hover{
|
||||||
|
background: @default-succ-hov;
|
||||||
|
}
|
||||||
|
:disabled{
|
||||||
|
background: @default-succ-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-default{
|
||||||
|
background: @btn-bg;
|
||||||
|
border-color: @btn-bord;
|
||||||
|
color: @btn-txt;
|
||||||
|
:hover{
|
||||||
|
background: @btn-bg-hov;
|
||||||
|
}
|
||||||
|
:focus{
|
||||||
|
background: @btn-bg-sel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,25 +22,24 @@
|
|||||||
flex: 0 0;
|
flex: 0 0;
|
||||||
height: @height;
|
height: @height;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-right: 20px;
|
||||||
.Panel{
|
.Panel{
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@media screen and (min-width: @breakpoint){
|
@media screen and (max-width: @breakpoint){
|
||||||
max-width: 400px;
|
margin-right: 0px;
|
||||||
}
|
height: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.OnePlusTwo-right--panel(@height: 100%; @breakpoint: 900px) {
|
.OnePlusTwo-right--panel(@height: 100%; @breakpoint: 900px) {
|
||||||
height: @height;
|
height: @height;
|
||||||
flex: 1 0;
|
flex: 1 0;
|
||||||
margin-left: 20px;
|
|
||||||
.Panel{
|
.Panel{
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: @breakpoint){
|
@media screen and (max-width: @breakpoint){
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-left: 0px;
|
|
||||||
margin-top: 25px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +49,7 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.OnePlusTwo-left--details {
|
.OnePlusTwo-left--details {
|
||||||
@@ -58,9 +58,6 @@
|
|||||||
|
|
||||||
.OnePlusTwo-left--detailsRow {
|
.OnePlusTwo-left--detailsRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
:not(:last-child){
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.OnePlusTwo-left--detailsLabel {
|
.OnePlusTwo-left--detailsLabel {
|
||||||
@@ -73,7 +70,6 @@
|
|||||||
|
|
||||||
.OnePlusTwo-left--detailsContent {
|
.OnePlusTwo-left--detailsContent {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 220px;
|
width: 220px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="tab-pane" id="jobs-stdout">
|
<div class="tab-pane" id="jobs-stdout">
|
||||||
<div ng-cloak id="htmlTemplate">
|
<div ng-cloak id="htmlTemplate">
|
||||||
<div class="StandardOut">
|
<div class="StandardOut-container">
|
||||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||||
<div class="Panel">
|
<div class="Panel">
|
||||||
<div class="StandardOut-panelHeader">
|
<div class="StandardOut-panelHeader">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="tab-pane" id="jobs-stdout">
|
<div class="tab-pane" id="jobs-stdout">
|
||||||
<div ng-cloak id="htmlTemplate">
|
<div ng-cloak id="htmlTemplate">
|
||||||
<div class="StandardOut">
|
<div class="StandardOut-container">
|
||||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||||
<div class="Panel">
|
<div class="Panel">
|
||||||
<div class="StandardOut-panelHeader">
|
<div class="StandardOut-panelHeader">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="tab-pane" id="jobs-stdout">
|
<div class="tab-pane" id="jobs-stdout">
|
||||||
<div ng-cloak id="htmlTemplate">
|
<div ng-cloak id="htmlTemplate">
|
||||||
<div class="StandardOut">
|
<div class="StandardOut-container">
|
||||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||||
<div class="Panel">
|
<div class="Panel">
|
||||||
<div class="StandardOut-panelHeader">
|
<div class="StandardOut-panelHeader">
|
||||||
@@ -49,13 +49,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO: figure out how to show the extra vars on different rows like the mockup -->
|
<div class="StandardOut-detailsRow--extraVars" ng-show="job.extra_vars">
|
||||||
|
|
||||||
<div class="StandardOut-detailsRow" ng-show="job.extra_vars">
|
|
||||||
<div class="StandardOut-detailsLabel">EXTRA VARS</div>
|
<div class="StandardOut-detailsLabel">EXTRA VARS</div>
|
||||||
<div class="StandardOut-detailsContent">
|
</div>
|
||||||
{{ job.extra_vars }}
|
|
||||||
</div>
|
<div ng-show="job.extra_vars">
|
||||||
|
<textarea rows="6" ng-model="variables" name="variables" class="StandardOut-extraVars" id="pre-formatted-variables"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="tab-pane" id="jobs-stdout">
|
<div class="tab-pane" id="jobs-stdout">
|
||||||
<div ng-cloak id="htmlTemplate">
|
<div ng-cloak id="htmlTemplate">
|
||||||
<div class="StandardOut">
|
<div class="StandardOut-container">
|
||||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||||
<div class="Panel">
|
<div class="Panel">
|
||||||
<div class="StandardOut-panelHeader">
|
<div class="StandardOut-panelHeader">
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
@import "../shared/branding/colors.default.less";
|
@import "../shared/branding/colors.default.less";
|
||||||
|
@import "awx/ui/client/src/shared/layouts/one-plus-two.less";
|
||||||
|
|
||||||
/** @define StandardOut */
|
/** @define StandardOut */
|
||||||
|
|
||||||
.StandardOut {
|
.StandardOut-container {
|
||||||
height: 100%;
|
.OnePlusTwo-container;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-leftPanel {
|
.StandardOut-leftPanel {
|
||||||
flex: 0 0 400px;
|
.OnePlusTwo-left--panel(590px);
|
||||||
margin-right: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-rightPanel {
|
.StandardOut-rightPanel {
|
||||||
flex: 1 0;
|
.OnePlusTwo-right--panel(590px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-panelHeader {
|
.StandardOut-panelHeader {
|
||||||
color: @default-interface-txt;
|
.OnePlusTwo-panelHeader
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-consoleOutput {
|
.StandardOut-consoleOutput {
|
||||||
@@ -30,31 +24,32 @@
|
|||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
background-color: @default-secondary-bg;
|
background-color: @default-secondary-bg;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
height: 300px;
|
height: ~"calc(100% - 74px)";
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-details {
|
.StandardOut-details {
|
||||||
margin-top: 25px;
|
.OnePlusTwo-left--details;
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-detailsRow {
|
.StandardOut-detailsRow {
|
||||||
display: flex;
|
.OnePlusTwo-left--detailsRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-detailsRow:not(:last-child) {
|
.StandardOut-detailsRow + .StandardOut-detailsRow {
|
||||||
margin-bottom: 20px;
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.StandardOut-detailsRow--extraVars {
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-detailsLabel {
|
.StandardOut-detailsLabel {
|
||||||
width: 130px;
|
.OnePlusTwo-left--detailsLabel;
|
||||||
flex: 0 0 130px;
|
|
||||||
color: @default-interface-txt;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-detailsContent {
|
.StandardOut-detailsContent {
|
||||||
flex: 1 0;
|
.OnePlusTwo-left--detailsContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-statusText {
|
.StandardOut-statusText {
|
||||||
@@ -66,7 +61,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-preContainer {
|
.StandardOut-preContainer {
|
||||||
height: 300px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.StandardOut-panelHeaderText {
|
.StandardOut-panelHeaderText {
|
||||||
@@ -105,14 +100,3 @@
|
|||||||
.StandardOut-actionButton + a {
|
.StandardOut-actionButton + a {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@standardout-breakpoint: 900px;
|
|
||||||
|
|
||||||
@media screen and (max-width: @standardout-breakpoint) {
|
|
||||||
.StandardOut {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.StandardOut-leftPanel {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
export function JobStdoutController ($rootScope, $scope, $state, $stateParams, ClearScope, GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName) {
|
export function JobStdoutController ($rootScope, $scope, $state, $stateParams, ClearScope, GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName, ParseTypeChange, ParseVariableString) {
|
||||||
|
|
||||||
ClearScope();
|
ClearScope();
|
||||||
|
|
||||||
@@ -32,118 +32,128 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, C
|
|||||||
$scope.job.status = data.status;
|
$scope.job.status = data.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: when the job completes we should refresh the job data so that we pull in the finish
|
if (data.status === 'failed' || data.status === 'canceled' || data.status === 'error' || data.status === 'successful') {
|
||||||
// timestamp as well as the run time.
|
// Go out and refresh the job details
|
||||||
|
getJobDetails();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Go out and get the job details based on the job type. jobType gets defined
|
// Set the parse type so that CodeMirror knows how to display extra params YAML/JSON
|
||||||
// in the data block of the route declaration for each of the different types
|
$scope.parseType = 'yaml';
|
||||||
// of stdout jobs.
|
|
||||||
Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/');
|
|
||||||
Rest.get()
|
|
||||||
.success(function(data) {
|
|
||||||
$scope.job = data;
|
|
||||||
$scope.job_template_name = data.name;
|
|
||||||
$scope.created_by = data.summary_fields.created_by;
|
|
||||||
$scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : '';
|
|
||||||
$scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : '';
|
|
||||||
$scope.job_template_url = '/#/job_templates/' + data.unified_job_template;
|
|
||||||
$scope.inventory_url = ($scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : '';
|
|
||||||
$scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : '';
|
|
||||||
$scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : '';
|
|
||||||
$scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : '';
|
|
||||||
$scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : '';
|
|
||||||
$scope.playbook = data.playbook;
|
|
||||||
$scope.credential = data.credential;
|
|
||||||
$scope.cloud_credential = data.cloud_credential;
|
|
||||||
$scope.forks = data.forks;
|
|
||||||
$scope.limit = data.limit;
|
|
||||||
$scope.verbosity = data.verbosity;
|
|
||||||
$scope.job_tags = data.job_tags;
|
|
||||||
|
|
||||||
// If we have a source then we have to go get the source choices from the server
|
|
||||||
if (!Empty(data.source)) {
|
function getJobDetails() {
|
||||||
if ($scope.removeChoicesReady) {
|
|
||||||
$scope.removeChoicesReady();
|
// 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
|
||||||
$scope.removeChoicesReady = $scope.$on('ChoicesReady', function() {
|
// of stdout jobs.
|
||||||
$scope.source_choices.every(function(e) {
|
Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/');
|
||||||
if (e.value === data.source) {
|
Rest.get()
|
||||||
$scope.source = e.label;
|
.success(function(data) {
|
||||||
return false;
|
$scope.job = data;
|
||||||
}
|
$scope.job_template_name = data.name;
|
||||||
return true;
|
$scope.created_by = data.summary_fields.created_by;
|
||||||
|
$scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : '';
|
||||||
|
$scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : '';
|
||||||
|
$scope.job_template_url = '/#/job_templates/' + data.unified_job_template;
|
||||||
|
$scope.inventory_url = ($scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : '';
|
||||||
|
$scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : '';
|
||||||
|
$scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : '';
|
||||||
|
$scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : '';
|
||||||
|
$scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : '';
|
||||||
|
$scope.playbook = data.playbook;
|
||||||
|
$scope.credential = data.credential;
|
||||||
|
$scope.cloud_credential = data.cloud_credential;
|
||||||
|
$scope.forks = data.forks;
|
||||||
|
$scope.limit = data.limit;
|
||||||
|
$scope.verbosity = data.verbosity;
|
||||||
|
$scope.job_tags = data.job_tags;
|
||||||
|
|
||||||
|
// If we have a source then we have to go get the source choices from the server
|
||||||
|
if (!Empty(data.source)) {
|
||||||
|
if ($scope.removeChoicesReady) {
|
||||||
|
$scope.removeChoicesReady();
|
||||||
|
}
|
||||||
|
$scope.removeChoicesReady = $scope.$on('ChoicesReady', function() {
|
||||||
|
$scope.source_choices.every(function(e) {
|
||||||
|
if (e.value === data.source) {
|
||||||
|
$scope.source = e.label;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// GetChoices can be found in the helper: Utilities.js
|
||||||
|
// It attaches the source choices to $scope.source_choices.
|
||||||
|
// Then, when the callback is fired, $scope.source is bound
|
||||||
|
// to the corresponding label.
|
||||||
|
GetChoices({
|
||||||
|
scope: $scope,
|
||||||
|
url: GetBasePath('inventory_sources'),
|
||||||
|
field: 'source',
|
||||||
|
variable: 'source_choices',
|
||||||
|
choice_name: 'choices',
|
||||||
|
callback: 'ChoicesReady'
|
||||||
});
|
});
|
||||||
});
|
|
||||||
// GetChoices can be found in the helper: StandardOut.js
|
|
||||||
// It attaches the source choices to $scope.source_choices.
|
|
||||||
// Then, when the callback is fired, $scope.source is bound
|
|
||||||
// to the corresponding label.
|
|
||||||
GetChoices({
|
|
||||||
scope: $scope,
|
|
||||||
url: GetBasePath('inventory_sources'),
|
|
||||||
field: 'source',
|
|
||||||
variable: 'source_choices',
|
|
||||||
choice_name: 'choices',
|
|
||||||
callback: 'ChoicesReady'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookUpName can be found in the helper: StandardOut.js
|
|
||||||
// It attaches the name that it gets (based on the url)
|
|
||||||
// to the $scope variable defined by the attribute scope_var.
|
|
||||||
if (!Empty(data.credential)) {
|
|
||||||
LookUpName({
|
|
||||||
scope: $scope,
|
|
||||||
scope_var: 'credential',
|
|
||||||
url: GetBasePath('credentials') + data.credential + '/'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Empty(data.inventory)) {
|
|
||||||
LookUpName({
|
|
||||||
scope: $scope,
|
|
||||||
scope_var: 'inventory',
|
|
||||||
url: GetBasePath('inventory') + data.inventory + '/'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Empty(data.project)) {
|
|
||||||
LookUpName({
|
|
||||||
scope: $scope,
|
|
||||||
scope_var: 'project',
|
|
||||||
url: GetBasePath('projects') + data.project + '/'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Empty(data.cloud_credential)) {
|
|
||||||
LookUpName({
|
|
||||||
scope: $scope,
|
|
||||||
scope_var: 'cloud_credential',
|
|
||||||
url: GetBasePath('credentials') + data.cloud_credential + '/'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Empty(data.inventory_source)) {
|
|
||||||
LookUpName({
|
|
||||||
scope: $scope,
|
|
||||||
scope_var: 'inventory_source',
|
|
||||||
url: GetBasePath('inventory_sources') + data.inventory_source + '/'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
// LookUpName can be found in the lookup-name.factory
|
||||||
.error(function(data, status) {
|
// It attaches the name that it gets (based on the url)
|
||||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
// to the $scope variable defined by the attribute scope_var.
|
||||||
msg: 'Failed to retrieve job: ' + job_id + '. GET returned: ' + status });
|
if (!Empty(data.credential)) {
|
||||||
});
|
LookUpName({
|
||||||
|
scope: $scope,
|
||||||
|
scope_var: 'credential',
|
||||||
|
url: GetBasePath('credentials') + data.credential + '/'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Empty(data.inventory)) {
|
||||||
|
LookUpName({
|
||||||
|
scope: $scope,
|
||||||
|
scope_var: 'inventory',
|
||||||
|
url: GetBasePath('inventory') + data.inventory + '/'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Empty(data.project)) {
|
||||||
|
LookUpName({
|
||||||
|
scope: $scope,
|
||||||
|
scope_var: 'project',
|
||||||
|
url: GetBasePath('projects') + data.project + '/'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Empty(data.cloud_credential)) {
|
||||||
|
LookUpName({
|
||||||
|
scope: $scope,
|
||||||
|
scope_var: 'cloud_credential',
|
||||||
|
url: GetBasePath('credentials') + data.cloud_credential + '/'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Empty(data.inventory_source)) {
|
||||||
|
LookUpName({
|
||||||
|
scope: $scope,
|
||||||
|
scope_var: 'inventory_source',
|
||||||
|
url: GetBasePath('inventory_sources') + data.inventory_source + '/'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||||
|
msg: 'Failed to retrieve job: ' + job_id + '. GET returned: ' + status });
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: this is currently not used but is necessary for cases where sockets
|
// TODO: this is currently not used but is necessary for cases where sockets
|
||||||
// are not available and a manual refresh trigger is needed.
|
// are not available and a manual refresh trigger is needed.
|
||||||
@@ -156,6 +166,8 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, C
|
|||||||
$scope.stdoutFullScreen = !$scope.stdoutFullScreen;
|
$scope.stdoutFullScreen = !$scope.stdoutFullScreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getJobDetails();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', '$stateParams', 'ClearScope', 'GetBasePath', 'Rest', 'ProcessErrors', 'Empty', 'GetChoices', 'LookUpName'];
|
JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', '$stateParams', 'ClearScope', 'GetBasePath', 'Rest', 'ProcessErrors', 'Empty', 'GetChoices', 'LookUpName', 'ParseTypeChange', 'ParseVariableString'];
|
||||||
|
|||||||
@@ -154,8 +154,6 @@
|
|||||||
<div id="login-modal-dialog" style="display: none;"></div>
|
<div id="login-modal-dialog" style="display: none;"></div>
|
||||||
<div id="help-modal-dialog" style="display: none;"></div>
|
<div id="help-modal-dialog" style="display: none;"></div>
|
||||||
|
|
||||||
<div class="About" id="about-modal-dialog" style="display: none;" ng-include=" '{{ STATIC_URL }}assets/cowsay-about.html ' "></div>
|
|
||||||
|
|
||||||
<div id="prompt-for-days" style="display:none">
|
<div id="prompt-for-days" style="display:none">
|
||||||
<form name="prompt_for_days_form" id="prompt_for_days_form">
|
<form name="prompt_for_days_form" id="prompt_for_days_form">
|
||||||
Set how many days of data should be retained. <br>
|
Set how many days of data should be retained. <br>
|
||||||
|
|||||||
Reference in New Issue
Block a user