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
commit 9d4e6dfc16
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
from rest_framework.exceptions import PermissionDenied, ParseError
from rest_framework.parsers import FormParser
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.settings import api_settings
@ -2064,6 +2065,7 @@ class JobTemplateCallback(GenericAPIView):
model = JobTemplate
permission_classes = (JobTemplateCallbackPermission,)
serializer_class = EmptySerializer
parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [FormParser]
@csrf_exempt
@transaction.non_atomic_requests

View File

@ -67,7 +67,7 @@ class FactCacheReceiver(object):
self.timestamp = datetime.fromtimestamp(date_key, None)
# 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:
fact_obj.facts = facts
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'],
instance_name,
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', []))],
job_id=task_actual['id'])

View File

@ -11,6 +11,7 @@ import shutil
import sys
import tempfile
import time
import urllib
from multiprocessing import Process
from subprocess import Popen
import re
@ -463,6 +464,8 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
response = method(url, json.dumps(data), 'application/json')
elif data_type == '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:
self.fail('Unsupported data_type %s' % data_type)
else:

View File

@ -803,6 +803,21 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
self.assertEqual(job.hosts.count(), 1)
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
data.update(dict(extra_vars=dict(key="value")))
result = self.post(url, data, expect=202, remote_addr=host_ip)
@ -853,9 +868,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
if host_ip:
break
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.post(url, data, expect=202, remote_addr=host_ip)
self.assertEqual(jobs_qs.count(), 4)
job = jobs_qs[0]
self.assertEqual(job.launch_type, 'callback')
self.assertEqual(job.limit, host.name)
@ -878,9 +893,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
if host_ip:
break
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.post(url, data, expect=202, remote_addr=host_ip)
self.assertEqual(jobs_qs.count(), 5)
job = jobs_qs[0]
self.assertEqual(job.launch_type, 'callback')
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 = host_qs[0]
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.post(url, data, expect=202, remote_addr=host_ip)
self.assertEqual(jobs_qs.count(), 6)
job = jobs_qs[0]
self.assertEqual(job.launch_type, 'callback')
self.assertEqual(job.limit, host.name)
@ -926,9 +941,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
host_ip = list(ips)[0]
break
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.post(url, data, expect=202, remote_addr=host_ip)
self.assertEqual(jobs_qs.count(), 7)
job = jobs_qs[0]
self.assertEqual(job.launch_type, 'callback')
self.assertEqual(job.limit, ':&'.join([job_template.limit, host.name]))

View File

@ -44,7 +44,8 @@
@import "text-label.less";
@import "./bootstrap-datepicker.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
whenver a modal is opened */
body.modal-open {

View File

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

View File

@ -3,7 +3,7 @@
<div class="modal-content">
<div class="modal-header">
<img class="About-brand--ansible img-responsive" src="/static/assets/ansible_tower_logo_minimalc.png" />
<button type="button" class="close About-close" ng-click="back()">
<button data-dismiss="modal" type="button" class="close About-close">
<span class="fa fa-times-circle"></span>
</button>
</div>

View File

@ -30,6 +30,7 @@ import inventoryScripts from './inventory-scripts/main';
import permissions from './permissions/main';
import managementJobs from './management-jobs/main';
import jobDetail from './job-detail/main';
import notifications from './notifications/main';
// modules
import about from './about/main';
@ -76,7 +77,7 @@ __deferLoadIfEnabled();
/*#endif#*/
var tower = angular.module('Tower', [
// 'ngAnimate',
//'ngAnimate',
'ngSanitize',
'ngCookies',
about.name,
@ -98,6 +99,7 @@ var tower = angular.module('Tower', [
activityStream.name,
footer.name,
jobDetail.name,
notifications.name,
standardOut.name,
'templates',
'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',
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService',
function (
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
LoadConfig, Store, ShowSocketHelp, pendoService)
LoadConfig, Store, ShowSocketHelp, pendoService)
{
var sock;

View File

@ -1,12 +1,11 @@
/** @define DashboardCounts */
.Footer {
height: 40px;
background-color: #f6f6f6;
color: #848992;
width: 100%;
z-index: 1040;
position: fixed;
position: absolute;
right: 0;
left: 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">
</div>
</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>

View File

@ -20,6 +20,16 @@
display: block;
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{
width: 100%;
height: 300px;
@ -33,6 +43,9 @@
.License-field{
.OnePlusTwo-left--detailsRow;
}
.License-field + .License-field {
margin-top: 20px;
}
.License-greenText{
color: @submit-button-bg;
}
@ -40,16 +53,16 @@
color: #d9534f;
}
.License-fields{
.OnePlusTwo-left--details;
.OnePlusTwo-left--details;
}
.License-details {
.OnePlusTwo-left--panel(600px);
.OnePlusTwo-left--panel(650px);
}
.License-titleText {
.OnePlusTwo-panelHeader;
}
.License-management{
.OnePlusTwo-right--panel(600px);
.OnePlusTwo-right--panel(650px);
}
.License-submit--container{
height: 33px;
@ -59,8 +72,25 @@
margin: 0 10px 0 0;
}
.License-file--container {
margin: 20px 0 20px 0;
input[type=file] {
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
[ 'Wait', '$state', '$scope', '$location',
[ 'Wait', '$state', '$scope', '$rootScope', '$location',
'GetBasePath', 'Rest', 'ProcessErrors', 'CheckLicense', 'moment',
function( Wait, $state, $scope, $location,
function( Wait, $state, $scope, $rootScope, $location,
GetBasePath, Rest, ProcessErrors, CheckLicense, moment){
$scope.getKey = function(event){
// Mimic HTML5 spec, show filename
@ -16,9 +16,19 @@ export default
var raw = new FileReader();
// readAsFoo runs async
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
// So we hide the default input, show our own, and simulate clicks to the hidden input
@ -33,6 +43,11 @@ export default
reset();
init();
$scope.success = true;
// for animation purposes
var successTimeout = setTimeout(function(){
$scope.success = false;
clearTimeout(successTimeout);
}, 4000);
});
};
var calcDaysRemaining = function(ms){
@ -51,6 +66,7 @@ export default
CheckLicense.get()
.then(function(res){
$scope.license = res.data;
$scope.license.version = res.data.version.split('-')[0];
$scope.time = {};
$scope.time.remaining = calcDaysRemaining($scope.license.license_info.time_remaining);
$scope.time.expiresOn = calcExpiresOn($scope.time.remaining);

View File

@ -5,95 +5,98 @@
<div class="License-fields">
<div class="License-field">
<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='invalid'><i class="fa fa-circle License-redText"></i> Invalid</span>
</div>
</div>
<div class="License-field">
<div class="License-field--label">Version</div>
<div class="License-field--content">
{{license.version}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">License Type</div>
<div class="License-field--label">Version</div>
<div class="License-field--content">
{{license.license_info.license_type}}
</div>
{{license.version || "No result found"}}
</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 class="License-field">
<div class="License-field--label">Subscription</div>
<div class="License-field--content">
{{license.license_info.subscription_name}}
</div>
<div class="License-field--content">
{{license.license_info.subscription_name || "No result found"}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">License Key</div>
<div class="License-field--content">
{{license.license_info.license_key}}
</div>
<div class="License-field--label">License Key</div>
<div class="License-field--content">
{{license.license_info.license_key || "No result found"}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">Expires On</div>
<div class="License-field--content">
<div class="License-field--content">
{{time.expiresOn}}
</div>
</div>
</div>
<div class="License-field">
<div class="License-field--label">Time Remaining</div>
<div class="License-field--content">
{{time.remaining}} Day
</div>
<div class="License-field--content">
{{time.remaining}} Days
</div>
</div>
<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">
{{license.license_info.available_instances}}
</div>
{{license.license_info.available_instances || "No result found"}}
</div>
</div>
<div class="License-field">
<div class="License-field--label">Hosts Used</div>
<div class="License-field--content">
{{license.license_info.current_instances}}
</div>
<div class="License-field--content">
{{license.license_info.current_instances || "No result found"}}
</div>
</div>
<div class="License-field License-greenText">
<div class="License-field--label">Hosts Remaining</div>
<div class="License-field--content">
{{license.license_info.free_instances}}
</div>
<div class="License-field--content">
{{license.license_info.free_instances || "No result found"}}
</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>
</div>
</div>
<div class="License-management">
<div class="Panel">
<div class="License-titleText">License Management</div>
<p>Choose your license file, agree to the End User License Agreement, and click submit.</p>
<form id="License-form" name="license">
<div class="input-group License-file--container">
<span class="btn btn-default input-group-addon" ng-click="fakeClick()">Browse...</span>
<input class="form-control" ng-disabled="true" placeholder="{{fileName}}" />
<input id="License-file" class="form-control" type="file" file-on-change="getKey"/>
</div>
<div class="License-titleText prepend-asterisk"> End User License Agreement</div>
<div class="form-group License-eula">
<textarea class="form-control">{{license.eula}}
</textarea>
</div>
<div class="form-group">
<div class="checkbox">
<div class="License-details--label"><input type="checkbox" ng-model="newLicense.eula" required> I agree to the End User License Agreement</div>
<div class="License-submit--container pull-right">
<span ng-hide="success == null || false" class="License-greenText License-submit--success pull-left">Save successful!</span>
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="newLicense.file.license_key == null || newLicense.eula == null">Submit</button>
<div class="License-body">
<p class="License-helperText">Choose your license file, agree to the End User License Agreement, and click submit.</p>
<form id="License-form" name="license">
<div class="License-subTitleText prepend-asterisk"> License File</div>
<div class="input-group License-file--container">
<span class="btn btn-default input-group-addon" ng-click="fakeClick()">Browse...</span>
<input class="form-control License-input--fake" ng-disabled="true" placeholder="{{fileName}}" />
<input id="License-file" class="form-control" type="file" file-on-change="getKey"/>
</div>
<div class="License-subTitleText prepend-asterisk"> End User License Agreement</div>
<div class="form-group License-eula">
<textarea class="form-control">{{license.eula}}
</textarea>
</div>
<div class="form-group">
<div class="checkbox">
<div class="License-details--label"><input type="checkbox" ng-model="newLicense.eula" required> I agree to the End User License Agreement</div>
<div class="License-submit--container pull-right">
<span ng-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>
</form>
</form>
</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.
</p>
</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">
<h4 class="SetupItem-title">View Your License</h4>
<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;
height: @height;
width: 100%;
margin-right: 20px;
.Panel{
height: 100%;
}
@media screen and (min-width: @breakpoint){
max-width: 400px;
}
@media screen and (max-width: @breakpoint){
margin-right: 0px;
height: inherit;
}
}
.OnePlusTwo-right--panel(@height: 100%; @breakpoint: 900px) {
height: @height;
flex: 1 0;
margin-left: 20px;
.Panel{
height: 100%;
}
@media screen and (max-width: @breakpoint){
flex-direction: column;
margin-left: 0px;
margin-top: 25px;
}
}
@ -50,6 +49,7 @@
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
display: flex;
}
.OnePlusTwo-left--details {
@ -58,9 +58,6 @@
.OnePlusTwo-left--detailsRow {
display: flex;
:not(:last-child){
margin-bottom: 20px;
}
}
.OnePlusTwo-left--detailsLabel {
@ -73,7 +70,6 @@
.OnePlusTwo-left--detailsContent {
display: inline-block;
max-width: 220px;
width: 220px;
word-wrap: break-word;
}

View File

@ -1,6 +1,6 @@
<div class="tab-pane" id="jobs-stdout">
<div ng-cloak id="htmlTemplate">
<div class="StandardOut">
<div class="StandardOut-container">
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
<div class="Panel">
<div class="StandardOut-panelHeader">

View File

@ -1,6 +1,6 @@
<div class="tab-pane" id="jobs-stdout">
<div ng-cloak id="htmlTemplate">
<div class="StandardOut">
<div class="StandardOut-container">
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
<div class="Panel">
<div class="StandardOut-panelHeader">

View File

@ -1,6 +1,6 @@
<div class="tab-pane" id="jobs-stdout">
<div ng-cloak id="htmlTemplate">
<div class="StandardOut">
<div class="StandardOut-container">
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
<div class="Panel">
<div class="StandardOut-panelHeader">
@ -49,13 +49,12 @@
</div>
</div>
<!-- TODO: figure out how to show the extra vars on different rows like the mockup -->
<div class="StandardOut-detailsRow" ng-show="job.extra_vars">
<div class="StandardOut-detailsRow--extraVars" ng-show="job.extra_vars">
<div class="StandardOut-detailsLabel">EXTRA VARS</div>
<div class="StandardOut-detailsContent">
{{ job.extra_vars }}
</div>
</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>

View File

@ -1,6 +1,6 @@
<div class="tab-pane" id="jobs-stdout">
<div ng-cloak id="htmlTemplate">
<div class="StandardOut">
<div class="StandardOut-container">
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
<div class="Panel">
<div class="StandardOut-panelHeader">

View File

@ -1,28 +1,22 @@
@import "../shared/branding/colors.default.less";
@import "awx/ui/client/src/shared/layouts/one-plus-two.less";
/** @define StandardOut */
.StandardOut {
height: 100%;
display: flex;
flex-direction: row;
.StandardOut-container {
.OnePlusTwo-container;
}
.StandardOut-leftPanel {
flex: 0 0 400px;
margin-right: 20px;
.OnePlusTwo-left--panel(590px);
}
.StandardOut-rightPanel {
flex: 1 0;
.OnePlusTwo-right--panel(590px);
}
.StandardOut-panelHeader {
color: @default-interface-txt;
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
display: flex;
.OnePlusTwo-panelHeader
}
.StandardOut-consoleOutput {
@ -30,31 +24,32 @@
min-height: 200px;
background-color: @default-secondary-bg;
border-radius: 5px;
height: 300px;
height: ~"calc(100% - 74px)";
overflow: scroll;
}
.StandardOut-details {
margin-top: 25px;
.OnePlusTwo-left--details;
}
.StandardOut-detailsRow {
display: flex;
.OnePlusTwo-left--detailsRow;
}
.StandardOut-detailsRow:not(:last-child) {
margin-bottom: 20px;
.StandardOut-detailsRow + .StandardOut-detailsRow {
margin-top: 20px;
}
.StandardOut-detailsRow--extraVars {
margin-bottom: 10px;
}
.StandardOut-detailsLabel {
width: 130px;
flex: 0 0 130px;
color: @default-interface-txt;
text-transform: uppercase;
.OnePlusTwo-left--detailsLabel;
}
.StandardOut-detailsContent {
flex: 1 0;
.OnePlusTwo-left--detailsContent;
}
.StandardOut-statusText {
@ -66,7 +61,7 @@
}
.StandardOut-preContainer {
height: 300px;
height: 100%;
}
.StandardOut-panelHeaderText {
@ -105,14 +100,3 @@
.StandardOut-actionButton + a {
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();
@ -32,118 +32,128 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, C
$scope.job.status = data.status;
}
// TODO: when the job completes we should refresh the job data so that we pull in the finish
// timestamp as well as the run time.
if (data.status === 'failed' || data.status === 'canceled' || data.status === 'error' || data.status === 'successful') {
// Go out and refresh the job details
getJobDetails();
}
});
// Go out and get the job details based on the job type. jobType gets defined
// in the data block of the route declaration for each of the different types
// of stdout jobs.
Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/');
Rest.get()
.success(function(data) {
$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;
// Set the parse type so that CodeMirror knows how to display extra params YAML/JSON
$scope.parseType = 'yaml';
// 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;
function getJobDetails() {
// Go out and get the job details based on the job type. jobType gets defined
// in the data block of the route declaration for each of the different types
// of stdout jobs.
Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/');
Rest.get()
.success(function(data) {
$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)) {
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);
}
}
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve job: ' + job_id + '. GET returned: ' + status });
});
// LookUpName can be found in the lookup-name.factory
// 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);
}
}
})
.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
// 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;
}
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="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">
<form name="prompt_for_days_form" id="prompt_for_days_form">
Set how many days of data should be retained. <br>