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

This commit is contained in:
Akita Noek
2016-03-09 12:04:05 -05:00
41 changed files with 998 additions and 264 deletions

31
ISSUE_TEMPLATE.md Normal file
View 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. -->

View File

@@ -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

View File

@@ -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()

View 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),
]

View File

@@ -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'])

View File

@@ -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:

View File

@@ -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]))

View File

@@ -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 {

View File

@@ -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();
} }
]; ];

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 &copy 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 &copy 2016 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc.</div>
</footer> </footer>

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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>

View 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');
};
}
];

View File

@@ -0,0 +1,3 @@
<div class="tab-pane" id="notifications_add">
<div ng-cloak id="htmlTemplate" class="Panel"></div>
</div>

View 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'
}
};

View 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);
}]);

View 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');
};
}
];

View File

@@ -0,0 +1,3 @@
<div class="tab-pane" id="notficiations_edit">
<div ng-cloak id="htmlTemplate" class="Panel"></div>
</div>

View 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'
}
};

View 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);
}]);

View 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');
};
}
];

View File

@@ -0,0 +1,4 @@
<div class="tab-pane" id="notifications">
<div ui-view></div>
<div ng-cloak id="htmlTemplate" class="Panel"></div>
</div>

View 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();
}]
}
};

View 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);
}]);

View 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);

View 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()',
}
}
};
}

View 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: '&#43; 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'
}
}
};
}

View File

@@ -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">

View 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;
}
}

View File

@@ -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;
} }

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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;
}
}

View File

@@ -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'];

View File

@@ -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>