Merge branch 'release_3.1.0' into fixJobResults

This commit is contained in:
jlmitch5
2016-12-14 15:51:51 -05:00
committed by GitHub
29 changed files with 637 additions and 252 deletions

View File

@@ -242,7 +242,7 @@ class FieldLookupBackend(BaseFilterBackend):
queryset = queryset.filter(q) queryset = queryset.filter(q)
queryset = queryset.filter(*args).distinct() queryset = queryset.filter(*args).distinct()
return queryset return queryset
except (FieldError, FieldDoesNotExist, ValueError) as e: except (FieldError, FieldDoesNotExist, ValueError, TypeError) as e:
raise ParseError(e.args[0]) raise ParseError(e.args[0])
except ValidationError as e: except ValidationError as e:
raise ParseError(e.messages) raise ParseError(e.messages)

View File

@@ -26,7 +26,7 @@ import uuid
from ansible.utils.display import Display from ansible.utils.display import Display
# Tower Display Callback # Tower Display Callback
from tower_display_callback.events import event_context from .events import event_context
__all__ = [] __all__ = []

View File

@@ -22,14 +22,75 @@ import base64
import contextlib import contextlib
import datetime import datetime
import json import json
import logging
import multiprocessing import multiprocessing
import os import os
import threading import threading
import uuid import uuid
# Kombu
from kombu import Connection, Exchange, Producer
__all__ = ['event_context'] __all__ = ['event_context']
class CallbackQueueEventDispatcher(object):
def __init__(self):
self.callback_connection = os.getenv('CALLBACK_CONNECTION', None)
self.connection_queue = os.getenv('CALLBACK_QUEUE', '')
self.connection = None
self.exchange = None
self._init_logging()
def _init_logging(self):
try:
self.job_callback_debug = int(os.getenv('JOB_CALLBACK_DEBUG', '0'))
except ValueError:
self.job_callback_debug = 0
self.logger = logging.getLogger('awx.plugins.callback.job_event_callback')
if self.job_callback_debug >= 2:
self.logger.setLevel(logging.DEBUG)
elif self.job_callback_debug >= 1:
self.logger.setLevel(logging.INFO)
else:
self.logger.setLevel(logging.WARNING)
handler = logging.StreamHandler()
formatter = logging.Formatter('%(levelname)-8s %(process)-8d %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.propagate = False
def dispatch(self, obj):
if not self.callback_connection or not self.connection_queue:
return
active_pid = os.getpid()
for retry_count in xrange(4):
try:
if not hasattr(self, 'connection_pid'):
self.connection_pid = active_pid
if self.connection_pid != active_pid:
self.connection = None
if self.connection is None:
self.connection = Connection(self.callback_connection)
self.exchange = Exchange(self.connection_queue, type='direct')
producer = Producer(self.connection)
producer.publish(obj,
serializer='json',
compression='bzip2',
exchange=self.exchange,
declare=[self.exchange],
routing_key=self.connection_queue)
return
except Exception, e:
self.logger.info('Publish Job Event Exception: %r, retry=%d', e,
retry_count, exc_info=True)
retry_count += 1
if retry_count >= 3:
break
class EventContext(object): class EventContext(object):
''' '''
Store global and local (per thread/process) data associated with callback Store global and local (per thread/process) data associated with callback
@@ -38,6 +99,7 @@ class EventContext(object):
def __init__(self): def __init__(self):
self.display_lock = multiprocessing.RLock() self.display_lock = multiprocessing.RLock()
self.dispatcher = CallbackQueueEventDispatcher()
def add_local(self, **kwargs): def add_local(self, **kwargs):
if not hasattr(self, '_local'): if not hasattr(self, '_local'):
@@ -111,7 +173,9 @@ class EventContext(object):
if event_data.get(key, False): if event_data.get(key, False):
event = key event = key
break break
max_res = int(os.getenv("MAX_EVENT_RES", 700000))
if event not in ('playbook_on_stats',) and "res" in event_data and len(str(event_data['res'])) > max_res:
event_data['res'] = {}
event_dict = dict(event=event, event_data=event_data) event_dict = dict(event=event, event_data=event_data)
for key in event_data.keys(): for key in event_data.keys():
if key in ('job_id', 'ad_hoc_command_id', 'uuid', 'parent_uuid', 'created', 'artifact_data'): if key in ('job_id', 'ad_hoc_command_id', 'uuid', 'parent_uuid', 'created', 'artifact_data'):
@@ -136,7 +200,9 @@ class EventContext(object):
fileobj.flush() fileobj.flush()
def dump_begin(self, fileobj): def dump_begin(self, fileobj):
self.dump(fileobj, self.get_begin_dict()) begin_dict = self.get_begin_dict()
self.dispatcher.dispatch(begin_dict)
self.dump(fileobj, {'uuid': begin_dict['uuid']})
def dump_end(self, fileobj): def dump_end(self, fileobj):
self.dump(fileobj, self.get_end_dict(), flush=True) self.dump(fileobj, self.get_end_dict(), flush=True)

View File

@@ -29,8 +29,8 @@ from ansible.plugins.callback import CallbackBase
from ansible.plugins.callback.default import CallbackModule as DefaultCallbackModule from ansible.plugins.callback.default import CallbackModule as DefaultCallbackModule
# Tower Display Callback # Tower Display Callback
from tower_display_callback.events import event_context from .events import event_context
from tower_display_callback.minimal import CallbackModule as MinimalCallbackModule from .minimal import CallbackModule as MinimalCallbackModule
class BaseCallbackModule(CallbackBase): class BaseCallbackModule(CallbackBase):

View File

@@ -6,6 +6,7 @@ from channels import Group
from channels.sessions import channel_session from channels.sessions import channel_session
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.serializers.json import DjangoJSONEncoder
from awx.main.models.organization import AuthToken from awx.main.models.organization import AuthToken
@@ -86,4 +87,4 @@ def ws_receive(message):
def emit_channel_notification(group, payload): def emit_channel_notification(group, payload):
Group(group).send({"text": json.dumps(payload)}) Group(group).send({"text": json.dumps(payload, cls=DjangoJSONEncoder)})

View File

@@ -21,6 +21,7 @@ logger = logging.getLogger('awx.main.commands.run_callback_receiver')
class CallbackBrokerWorker(ConsumerMixin): class CallbackBrokerWorker(ConsumerMixin):
def __init__(self, connection): def __init__(self, connection):
self.connection = connection self.connection = connection
self.partial_events = {}
def get_consumers(self, Consumer, channel): def get_consumers(self, Consumer, channel):
return [Consumer(queues=[Queue(settings.CALLBACK_QUEUE, return [Consumer(queues=[Queue(settings.CALLBACK_QUEUE,
@@ -31,18 +32,28 @@ class CallbackBrokerWorker(ConsumerMixin):
def process_task(self, body, message): def process_task(self, body, message):
try: try:
if 'event' not in body:
raise Exception('Payload does not have an event')
if 'job_id' not in body and 'ad_hoc_command_id' not in body: if 'job_id' not in body and 'ad_hoc_command_id' not in body:
raise Exception('Payload does not have a job_id or ad_hoc_command_id') raise Exception('Payload does not have a job_id or ad_hoc_command_id')
if settings.DEBUG: if settings.DEBUG:
logger.info('Body: {}'.format(body)) logger.info('Body: {}'.format(body))
logger.info('Message: {}'.format(message)) logger.info('Message: {}'.format(message))
try: try:
if 'job_id' in body: # If event came directly from callback without counter/stdout,
JobEvent.create_from_data(**body) # save it until the rest of the event arrives.
elif 'ad_hoc_command_id' in body: if 'counter' not in body:
AdHocCommandEvent.create_from_data(**body) if 'uuid' in body:
self.partial_events[body['uuid']] = body
# If event has counter, try to combine it with any event data
# already received for the same uuid, then create the actual
# job event record.
else:
if 'uuid' in body:
partial_event = self.partial_events.pop(body['uuid'], {})
body.update(partial_event)
if 'job_id' in body:
JobEvent.create_from_data(**body)
elif 'ad_hoc_command_id' in body:
AdHocCommandEvent.create_from_data(**body)
except DatabaseError as e: except DatabaseError as e:
logger.error('Database Error Saving Job Event: {}'.format(e)) logger.error('Database Error Saving Job Event: {}'.format(e))
except Exception as exc: except Exception as exc:

View File

@@ -1073,8 +1073,8 @@ class JobEvent(CreatedModifiedModel):
from awx.main.models.inventory import Host from awx.main.models.inventory import Host
hostnames = set() hostnames = set()
try: try:
for v in self.event_data.values(): for stat in ('changed', 'dark', 'failures', 'ok', 'processed', 'skipped'):
hostnames.update(v.keys()) hostnames.update(self.event_data.get(stat, {}).keys())
except AttributeError: # In case event_data or v isn't a dict. except AttributeError: # In case event_data or v isn't a dict.
pass pass
with ignore_inventory_computed_fields(): with ignore_inventory_computed_fields():

View File

@@ -809,6 +809,7 @@ class RunJob(BaseTask):
env['REST_API_URL'] = settings.INTERNAL_API_URL env['REST_API_URL'] = settings.INTERNAL_API_URL
env['REST_API_TOKEN'] = job.task_auth_token or '' env['REST_API_TOKEN'] = job.task_auth_token or ''
env['TOWER_HOST'] = settings.TOWER_URL_BASE env['TOWER_HOST'] = settings.TOWER_URL_BASE
env['MAX_EVENT_RES'] = settings.MAX_EVENT_RES_DATA
env['CALLBACK_QUEUE'] = settings.CALLBACK_QUEUE env['CALLBACK_QUEUE'] = settings.CALLBACK_QUEUE
env['CALLBACK_CONNECTION'] = settings.BROKER_URL env['CALLBACK_CONNECTION'] = settings.BROKER_URL
if getattr(settings, 'JOB_CALLBACK_DEBUG', False): if getattr(settings, 'JOB_CALLBACK_DEBUG', False):

View File

@@ -152,6 +152,10 @@ REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST']
# Note: This setting may be overridden by database settings. # Note: This setting may be overridden by database settings.
STDOUT_MAX_BYTES_DISPLAY = 1048576 STDOUT_MAX_BYTES_DISPLAY = 1048576
# The maximum size of the ansible callback event's res data structure
# beyond this limit and the value will be removed
MAX_EVENT_RES_DATA = 700000
# Note: This setting may be overridden by database settings. # Note: This setting may be overridden by database settings.
EVENT_STDOUT_MAX_BYTES_DISPLAY = 1024 EVENT_STDOUT_MAX_BYTES_DISPLAY = 1024
@@ -522,17 +526,6 @@ ANSIBLE_FORCE_COLOR = True
# the celery task. # the celery task.
AWX_TASK_ENV = {} AWX_TASK_ENV = {}
# Maximum number of job events processed by the callback receiver worker process
# before it recycles
JOB_EVENT_RECYCLE_THRESHOLD = 3000
# Number of workers used to proecess job events in parallel
JOB_EVENT_WORKERS = 4
# Maximum number of job events that can be waiting on a single worker queue before
# it can be skipped as too busy
JOB_EVENT_MAX_QUEUE_SIZE = 100
# Flag to enable/disable updating hosts M2M when saving job events. # Flag to enable/disable updating hosts M2M when saving job events.
CAPTURE_JOB_EVENT_HOSTS = False CAPTURE_JOB_EVENT_HOSTS = False

View File

@@ -331,6 +331,7 @@ register(
category=_('LDAP'), category=_('LDAP'),
category_slug='ldap', category_slug='ldap',
feature_required='ldap', feature_required='ldap',
default='MemberDNGroupType',
) )
register( register(

View File

@@ -151,6 +151,9 @@ body .prettyprint .lit {
body .prettyprint .str { body .prettyprint .str {
color: #D9534F; color: #D9534F;
} }
body div.ansi_back {
display: inline-block;
}
body .well.tab-content { body .well.tab-content {
padding: 20px; padding: 20px;

View File

@@ -99,9 +99,9 @@ export default [
var dropdownOptions = [ var dropdownOptions = [
{label: i18n._('Azure AD'), value: 'azure'}, {label: i18n._('Azure AD'), value: 'azure'},
{label: i18n._('Github'), value: 'github'}, {label: i18n._('GitHub'), value: 'github'},
{label: i18n._('Github Org'), value: 'github_org'}, {label: i18n._('GitHub Org'), value: 'github_org'},
{label: i18n._('Github Team'), value: 'github_team'}, {label: i18n._('GithHub Team'), value: 'github_team'},
{label: i18n._('Google OAuth2'), value: 'google_oauth'}, {label: i18n._('Google OAuth2'), value: 'google_oauth'},
{label: i18n._('LDAP'), value: 'ldap'}, {label: i18n._('LDAP'), value: 'ldap'},
{label: i18n._('RADIUS'), value: 'radius'}, {label: i18n._('RADIUS'), value: 'radius'},

View File

@@ -1,7 +1,7 @@
<div class="tab-pane Configuration-container" id="configuration_edit"> <div class="tab-pane Configuration-container" id="configuration_edit">
<!-- <div ui-view="form"></div> <div class="Form-nav--dropdownContainer">
<div ng-cloak id="htmlTemplate"> --> <div class="Form-nav--dropdownLabel">Sub Category</div>
<div class="Form-nav--dropdown"> <div class="Form-nav--dropdown">
<select <select
id="configure-dropdown-nav" id="configure-dropdown-nav"
class="form-control" class="form-control"
@@ -9,6 +9,7 @@
ng-options="opt.value as opt.label for opt in authVm.dropdownOptions" ng-options="opt.value as opt.label for opt in authVm.dropdownOptions"
ng-change="authVm.activeForm()"> ng-change="authVm.activeForm()">
</select> </select>
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">

View File

@@ -21,11 +21,26 @@
margin-left: 0; margin-left: 0;
} }
.Form-nav--dropdown { .Form-nav--dropdownContainer {
width: 175px; width: 285px;
margin-top: -52px; margin-top: -52px;
margin-bottom: 22px; margin-bottom: 22px;
margin-left: auto; margin-left: auto;
display: flex;
justify-content: space-between;
}
.Form-nav--dropdown {
width: 60%;
}
.Form-nav--dropdownLabel {
text-transform: uppercase;
color: @default-interface-txt;
font-size: 14px;
font-weight: bold;
padding-right: 5px;
padding-top: 5px;
} }
.Form-tabRow { .Form-tabRow {

View File

@@ -17,8 +17,10 @@ export default [
'configurationLdapForm', 'configurationLdapForm',
'configurationRadiusForm', 'configurationRadiusForm',
'configurationSamlForm', 'configurationSamlForm',
'systemActivityStreamForm',
'systemLoggingForm',
'systemMiscForm',
'ConfigurationJobsForm', 'ConfigurationJobsForm',
'ConfigurationSystemForm',
'ConfigurationUiForm', 'ConfigurationUiForm',
function( function(
$scope, $rootScope, $state, $stateParams, $timeout, $q, Alert, ClearScope, $scope, $rootScope, $state, $stateParams, $timeout, $q, Alert, ClearScope,
@@ -33,8 +35,10 @@ export default [
configurationLdapForm, configurationLdapForm,
configurationRadiusForm, configurationRadiusForm,
configurationSamlForm, configurationSamlForm,
systemActivityStreamForm,
systemLoggingForm,
systemMiscForm,
ConfigurationJobsForm, ConfigurationJobsForm,
ConfigurationSystemForm,
ConfigurationUiForm ConfigurationUiForm
) { ) {
var vm = this; var vm = this;
@@ -48,8 +52,10 @@ export default [
'ldap': configurationLdapForm, 'ldap': configurationLdapForm,
'radius': configurationRadiusForm, 'radius': configurationRadiusForm,
'saml': configurationSamlForm, 'saml': configurationSamlForm,
'activity_stream': systemActivityStreamForm,
'logging': systemLoggingForm,
'misc': systemMiscForm,
'jobs': ConfigurationJobsForm, 'jobs': ConfigurationJobsForm,
'system': ConfigurationSystemForm,
'ui': ConfigurationUiForm 'ui': ConfigurationUiForm
}; };
@@ -84,19 +90,24 @@ export default [
lastForm: '', lastForm: '',
currentForm: '', currentForm: '',
currentAuth: '', currentAuth: '',
currentSystem: '',
setCurrent: function(form) { setCurrent: function(form) {
this.lastForm = this.currentForm; this.lastForm = this.currentForm;
this.currentForm = form; this.currentForm = form;
}, },
setCurrentAuth: function(form) {
this.currentAuth = form;
this.setCurrent(this.currentAuth);
},
getCurrent: function() { getCurrent: function() {
return this.currentForm; return this.currentForm;
}, },
currentFormName: function() { currentFormName: function() {
return 'configuration_' + this.currentForm + '_template_form'; return 'configuration_' + this.currentForm + '_template_form';
},
setCurrentAuth: function(form) {
this.currentAuth = form;
this.setCurrent(this.currentAuth);
},
setCurrentSystem: function(form) {
this.currentSystem = form;
this.setCurrent(this.currentSystem);
} }
}; };
@@ -182,6 +193,7 @@ export default [
} }
function active(setForm) { function active(setForm) {
// Authentication and System's sub-module dropdowns handled first:
if (setForm === 'auth') { if (setForm === 'auth') {
// Default to 'azure' on first load // Default to 'azure' on first load
if (formTracker.currentAuth === '') { if (formTracker.currentAuth === '') {
@@ -190,7 +202,15 @@ export default [
// If returning to auth tab reset current form to previously viewed // If returning to auth tab reset current form to previously viewed
formTracker.setCurrentAuth(formTracker.currentAuth); formTracker.setCurrentAuth(formTracker.currentAuth);
} }
} else { } else if (setForm === 'system') {
if (formTracker.currentSystem === '') {
formTracker.setCurrentSystem('misc');
} else {
// If returning to system tab reset current form to previously viewed
formTracker.setCurrentSystem(formTracker.currentSystem);
}
}
else {
formTracker.setCurrent(setForm); formTracker.setCurrent(setForm);
} }
vm.activeTab = setForm; vm.activeTab = setForm;

View File

@@ -46,7 +46,19 @@
}, },
AWX_PROOT_ENABLED: { AWX_PROOT_ENABLED: {
type: 'toggleSwitch', type: 'toggleSwitch',
} },
DEFAULT_JOB_TIMEOUT: {
type: 'text',
reset: 'DEFAULT_JOB_TIMEOUT',
},
DEFAULT_INVENTORY_UPDATE_TIMEOUT: {
type: 'text',
reset: 'DEFAULT_INVENTORY_UPDATE_TIMEOUT',
},
DEFAULT_PROJECT_UPDATE_TIMEOUT: {
type: 'text',
reset: 'DEFAULT_PROJECT_UPDATE_TIMEOUT',
},
}, },
buttons: { buttons: {

View File

@@ -20,8 +20,12 @@ import configurationLdapForm from './auth-form/sub-forms/auth-ldap.form.js';
import configurationRadiusForm from './auth-form/sub-forms/auth-radius.form.js'; import configurationRadiusForm from './auth-form/sub-forms/auth-radius.form.js';
import configurationSamlForm from './auth-form/sub-forms/auth-saml.form'; import configurationSamlForm from './auth-form/sub-forms/auth-saml.form';
//system sub-forms
import systemActivityStreamForm from './system-form/sub-forms/system-activity-stream.form.js';
import systemLoggingForm from './system-form/sub-forms/system-logging.form.js';
import systemMiscForm from './system-form/sub-forms/system-misc.form.js';
import configurationJobsForm from './jobs-form/configuration-jobs.form'; import configurationJobsForm from './jobs-form/configuration-jobs.form';
import configurationSystemForm from './system-form/configuration-system.form';
import configurationUiForm from './ui-form/configuration-ui.form'; import configurationUiForm from './ui-form/configuration-ui.form';
export default export default
@@ -36,10 +40,15 @@ angular.module('configuration', [])
.factory('configurationLdapForm', configurationLdapForm) .factory('configurationLdapForm', configurationLdapForm)
.factory('configurationRadiusForm', configurationRadiusForm) .factory('configurationRadiusForm', configurationRadiusForm)
.factory('configurationSamlForm', configurationSamlForm) .factory('configurationSamlForm', configurationSamlForm)
//system forms
.factory('systemActivityStreamForm', systemActivityStreamForm)
.factory('systemLoggingForm', systemLoggingForm)
.factory('systemMiscForm', systemMiscForm)
//other forms //other forms
.factory('ConfigurationJobsForm', configurationJobsForm) .factory('ConfigurationJobsForm', configurationJobsForm)
.factory('ConfigurationSystemForm', configurationSystemForm)
.factory('ConfigurationUiForm', configurationUiForm) .factory('ConfigurationUiForm', configurationUiForm)
//helpers and services //helpers and services
.factory('ConfigurationUtils', ConfigurationUtils) .factory('ConfigurationUtils', ConfigurationUtils)
.service('ConfigurationService', configurationService) .service('ConfigurationService', configurationService)

View File

@@ -5,22 +5,120 @@
*************************************************/ *************************************************/
export default [ export default [
'$rootScope', '$scope', '$state', 'AngularCodeMirror', 'Authorization', 'ConfigurationSystemForm', 'ConfigurationService', '$rootScope', '$scope', '$state', '$stateParams',
'ConfigurationUtils', 'GenerateForm', 'AngularCodeMirror',
'systemActivityStreamForm',
'systemLoggingForm',
'systemMiscForm',
'ConfigurationService',
'ConfigurationUtils',
'CreateSelect2',
'GenerateForm',
'i18n',
function( function(
$rootScope, $scope, $state, AngularCodeMirror, Authorization, ConfigurationSystemForm, ConfigurationService, ConfigurationUtils, GenerateForm $rootScope, $scope, $state, $stateParams,
AngularCodeMirror,
systemActivityStreamForm,
systemLoggingForm,
systemMiscForm,
ConfigurationService,
ConfigurationUtils,
CreateSelect2,
GenerateForm,
i18n
) { ) {
var systemVm = this; var systemVm = this;
var generator = GenerateForm;
var form = ConfigurationSystemForm;
var keys = _.keys(form.fields);
_.each(keys, function(key) { var generator = GenerateForm;
addFieldInfo(form, key); var formTracker = $scope.$parent.vm.formTracker;
var dropdownValue = 'misc';
var activeSystemForm = 'misc';
if ($stateParams.currentTab === 'system') {
formTracker.setCurrentSystem(activeSystemForm);
}
var activeForm = function() {
if(!$scope.$parent[formTracker.currentFormName()].$dirty) {
systemVm.activeSystemForm = systemVm.dropdownValue;
formTracker.setCurrentSystem(systemVm.activeSystemForm);
} else {
var msg = i18n._('You have unsaved changes. Would you like to proceed <strong>without</strong> saving?');
var title = i18n._('Warning: Unsaved Changes');
var buttons = [{
label: i18n._('Discard changes'),
"class": "btn Form-cancelButton",
"id": "formmodal-cancel-button",
onClick: function() {
$scope.$parent.vm.populateFromApi();
$scope.$parent[formTracker.currentFormName()].$setPristine();
systemVm.activeSystemForm = systemVm.dropdownValue;
formTracker.setCurrentSystem(systemVm.activeSystemForm);
$('#FormModal-dialog').dialog('close');
}
}, {
label: i18n._('Save changes'),
onClick: function() {
$scope.$parent.vm.formSave()
.then(function() {
$scope.$parent[formTracker.currentFormName()].$setPristine();
$scope.$parent.vm.populateFromApi();
systemVm.activeSystemForm = systemVm.dropdownValue;
formTracker.setCurrentSystem(systemVm.activeSystemForm);
$('#FormModal-dialog').dialog('close');
});
},
"class": "btn btn-primary",
"id": "formmodal-save-button"
}];
$scope.$parent.vm.triggerModal(msg, title, buttons);
}
formTracker.setCurrentSystem(systemVm.activeSystemForm);
};
var dropdownOptions = [
{label: i18n._('Misc. System'), value: 'misc'},
{label: i18n._('Activity Stream'), value: 'activity_stream'},
{label: i18n._('Logging'), value: 'logging'},
];
CreateSelect2({
element: '#system-configure-dropdown-nav',
multiple: false,
}); });
// Disable the save button for system auditors var systemForms = [{
form.buttons.save.disabled = $rootScope.user_is_system_auditor; formDef: systemLoggingForm,
id: 'system-logging-form'
}, {
formDef: systemActivityStreamForm,
id: 'system-activity-stream-form'
}, {
formDef: systemMiscForm,
id: 'system-misc-form'
}];
var forms = _.pluck(systemForms, 'formDef');
_.each(forms, function(form) {
var keys = _.keys(form.fields);
_.each(keys, function(key) {
if($scope.$parent.configDataResolve[key].type === 'choice') {
// Create options for dropdowns
var optionsGroup = key + '_options';
$scope.$parent[optionsGroup] = [];
_.each($scope.$parent.configDataResolve[key].choices, function(choice){
$scope.$parent[optionsGroup].push({
name: choice[0],
label: choice[1],
value: choice[0]
});
});
}
addFieldInfo(form, key);
});
// Disable the save button for system auditors
form.buttons.save.disabled = $rootScope.user_is_system_auditor;
});
function addFieldInfo(form, key) { function addFieldInfo(form, key) {
_.extend(form.fields[key], { _.extend(form.fields[key], {
@@ -29,21 +127,56 @@ export default [
name: key, name: key,
toggleSource: key, toggleSource: key,
dataPlacement: 'top', dataPlacement: 'top',
placeholder: ConfigurationUtils.formatPlaceholder($scope.$parent.configDataResolve[key].placeholder, key) || null,
dataTitle: $scope.$parent.configDataResolve[key].label, dataTitle: $scope.$parent.configDataResolve[key].label,
required: $scope.$parent.configDataResolve[key].required, required: $scope.$parent.configDataResolve[key].required,
ngDisabled: $rootScope.user_is_system_auditor ngDisabled: $rootScope.user_is_system_auditor
}); });
} }
generator.inject(form, { $scope.$parent.parseType = 'json';
id: 'configure-system-form',
mode: 'edit', _.each(systemForms, function(form) {
scope: $scope.$parent, generator.inject(form.formDef, {
related: true id: form.id,
mode: 'edit',
scope: $scope.$parent,
related: true
});
});
var dropdownRendered = false;
$scope.$on('populated', function() {
var opts = [];
if($scope.$parent.LOG_AGGREGATOR_TYPE !== null) {
_.each(ConfigurationUtils.listToArray($scope.$parent.LOG_AGGREGATOR_TYPE), function(type) {
opts.push({
id: type,
text: type
});
});
}
if(!dropdownRendered) {
dropdownRendered = true;
CreateSelect2({
element: '#configuration_logging_template_LOG_AGGREGATOR_TYPE',
multiple: true,
placeholder: i18n._('Select types'),
opts: opts
});
}
}); });
angular.extend(systemVm, { angular.extend(systemVm, {
activeForm: activeForm,
activeSystemForm: activeSystemForm,
dropdownOptions: dropdownOptions,
dropdownValue: dropdownValue,
systemForms: systemForms
}); });
} }
]; ];

View File

@@ -1,9 +1,34 @@
<div class="tab-pane Configuration-container"> <div class="tab-pane Configuration-container">
<!-- <div ui-view="form"></div> <div class="Form-nav--dropdownContainer">
<div ng-cloak id="htmlTemplate"> --> <div class="Form-nav--dropdownLabel">Sub Category</div>
<div class="Form-nav--dropdown">
<select
id="system-configure-dropdown-nav"
class="form-control"
ng-model="systemVm.dropdownValue"
ng-options="opt.value as opt.label for opt in systemVm.dropdownOptions"
ng-change="systemVm.activeForm()">
</select>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<div id="configure-system-form"></div> <!-- <div id="configure-system-form"></div> -->
<div ng-show="systemVm.activeSystemForm === 'misc'">
<div id="system-misc-form">
</div>
</div>
<div ng-show="systemVm.activeSystemForm === 'activity_stream'">
<div id="system-activity-stream-form">
</div>
</div>
<div ng-show="systemVm.activeSystemForm === 'logging'">
<div id="system-logging-form">
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,38 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['i18n', function(i18n) {
return {
name: 'configuration_activity_stream_template',
showActions: true,
showHeader: false,
fields: {
ACTIVITY_STREAM_ENABLED: {
type: 'toggleSwitch',
},
ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: {
type: 'toggleSwitch'
}
},
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
},
cancel: {
ngClick: 'vm.formCancel()',
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: true
}
}
};
}
];

View File

@@ -0,0 +1,63 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['i18n', function(i18n) {
return {
name: 'configuration_logging_template',
showActions: true,
showHeader: false,
fields: {
LOG_AGGREGATOR_HOST: {
type: 'text',
reset: 'LOG_AGGREGATOR_HOST'
},
LOG_AGGREGATOR_PORT: {
type: 'text',
reset: 'LOG_AGGREGATOR_PORT'
},
LOG_AGGREGATOR_TYPE: {
type: 'select',
reset: 'LOG_AGGREGATOR_TYPE',
ngOptions: 'type.label for type in LOG_AGGREGATOR_TYPE_options track by type.value',
},
LOG_AGGREGATOR_USERNAME: {
type: 'text',
reset: 'LOG_AGGREGATOR_USERNAME'
},
LOG_AGGREGATOR_PASSWORD: {
type: 'text',
reset: 'LOG_AGGREGATOR_PASSWORD'
},
LOG_AGGREGATOR_LOGGERS: {
type: 'textarea',
reset: 'LOG_AGGREGATOR_PASSWORD'
},
LOG_AGGREGATOR_INDIVIDUAL_FACTS: {
type: 'toggleSwitch',
},
LOG_AGGREGATOR_ENABLED: {
type: 'toggleSwitch',
}
},
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
},
cancel: {
ngClick: 'vm.formCancel()',
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: true
}
}
};
}
];

View File

@@ -0,0 +1,42 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['i18n', function(i18n) {
return {
showHeader: false,
name: 'configuration_misc_template',
showActions: true,
fields: {
TOWER_URL_BASE: {
type: 'text',
reset: 'TOWER_URL_BASE',
},
TOWER_ADMIN_ALERTS: {
type: 'toggleSwitch',
},
ORG_ADMINS_CAN_SEE_ALL_USERS: {
type: 'toggleSwitch',
}
},
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
},
cancel: {
ngClick: 'vm.formCancel()',
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: true
}
}
};
}
];

View File

@@ -289,14 +289,12 @@ angular.module('CredentialsHelper', ['Utilities'])
Wait('stop'); Wait('stop');
var base = $location.path().replace(/^\//, '').split('/')[0]; var base = $location.path().replace(/^\//, '').split('/')[0];
if (base === 'credentials') { if (base === 'credentials') {
ReturnToCaller(); $state.go('credentials.edit', {credential_id: data.id}, {reload: true});
} }
else { else {
ReturnToCaller(1); ReturnToCaller(1);
} }
$state.go('credentials.edit', {credential_id: data.id}, {reload: true});
}) })
.error(function (data, status) { .error(function (data, status) {
Wait('stop'); Wait('stop');

View File

@@ -36,6 +36,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count'
$scope.machine_credential_link = getTowerLink('credential'); $scope.machine_credential_link = getTowerLink('credential');
$scope.cloud_credential_link = getTowerLink('cloud_credential'); $scope.cloud_credential_link = getTowerLink('cloud_credential');
$scope.network_credential_link = getTowerLink('network_credential'); $scope.network_credential_link = getTowerLink('network_credential');
$scope.schedule_link = getTowerLink('schedule');
}; };
// uses options to set scope variables to their readable string // uses options to set scope variables to their readable string
@@ -69,7 +70,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count'
// turn related api browser routes into tower routes // turn related api browser routes into tower routes
getTowerLinks(); getTowerLinks();
// the links below can't be set in getTowerLinks because the // the links below can't be set in getTowerLinks because the
// links on the UI don't directly match the corresponding URL // links on the UI don't directly match the corresponding URL
// on the API browser // on the API browser
@@ -89,7 +90,9 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count'
jobData.summary_fields.source_workflow_job.id){ jobData.summary_fields.source_workflow_job.id){
$scope.workflow_result_link = `/#/workflows/${jobData.summary_fields.source_workflow_job.id}`; $scope.workflow_result_link = `/#/workflows/${jobData.summary_fields.source_workflow_job.id}`;
} }
if(jobData.result_traceback) {
$scope.job.result_traceback = jobData.result_traceback.trim().split('\n').join('<br />');
}
// use options labels to manipulate display of details // use options labels to manipulate display of details
getTowerLabels(); getTowerLabels();

View File

@@ -62,6 +62,19 @@
<!-- LEFT PANE DETAILS GROUP --> <!-- LEFT PANE DETAILS GROUP -->
<div> <div>
<!-- STATUS DETAIL -->
<div class="JobResults-resultRow">
<label class="JobResults-resultRowLabel">
Status
</label>
<div class="JobResults-resultRowText">
<i class="JobResults-statusResultIcon
fa
icon-job-{{ job.status }}">
</i> {{ status_label }}
</div>
</div>
<!-- START TIME DETAIL --> <!-- START TIME DETAIL -->
<div class="JobResults-resultRow" <div class="JobResults-resultRow"
ng-show="job.started"> ng-show="job.started">
@@ -85,6 +98,29 @@
</div> </div>
</div> </div>
<!-- EXPLANATION DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.job_explanation">
<label class="JobResults-resultRowLabel">
Explanation
</label>
<div class="JobResults-resultRowText">
{{job.job_explanation}}
</div>
</div>
<!-- RESULTS TRACEBACK DETAIL -->
<div class="JobResults-resultRow"
ng-show="job.result_traceback">
<label class="JobResults-resultRowLabel">
Results Traceback
</label>
<div class="JobResults-resultRowText"
ng-bind-html="job.result_traceback">
</div>
</div>
<!-- TEMPLATE DETAIL --> <!-- TEMPLATE DETAIL -->
<div class="JobResults-resultRow" <div class="JobResults-resultRow"
ng-show="job.summary_fields.job_template.name"> ng-show="job.summary_fields.job_template.name">
@@ -135,6 +171,22 @@
</div> </div>
</div> </div>
<!-- SCHEDULED BY DETAIL -->
<div class="JobResults-resultRow toggle-show"
ng-show="job.summary_fields.schedule.name">
<label
class="JobResults-resultRowLabel">
Launched By
</label>
<div class="JobResults-resultRowText">
<a href="{{ scheduled_by_link }}"
aw-tool-tip="Edit the Schedule"
data-placement="top">
{{ job.summary_fields.schedule.name }}
</a>
</div>
</div>
<!-- INVENTORY DETAIL --> <!-- INVENTORY DETAIL -->
<div class="JobResults-resultRow" <div class="JobResults-resultRow"
ng-show="job.summary_fields.inventory.name"> ng-show="job.summary_fields.inventory.name">
@@ -348,135 +400,6 @@
</div> </div>
</div> </div>
<!-- STATUS DETAIL -->
<!-- <div
class="form-group
JobResults-resultRow
toggle-show">
<label
class="JobResults-resultRowLabel
col-lg-2 col-md-2
col-sm-2 col-xs-3
control-label">
Status
</label>
<div class="JobResults-resultRowText
col-lg-10 col-md-10 col-sm-10 col-xs-9">
<i
class="JobResults-statusIcon--results
fa
icon-job-{{ job.status }}">
</i> {{ status_label }}
</div>
</div> -->
<!-- SCHEDULED BY DETAIL -->
<!-- <div
class="form-group
JobResults-resultRow toggle-show"
ng-show="job.summary_fields.schedule_by.username">
<label
class="JobResults-resultRowLabel
col-lg-2 col-md-2
col-sm-2 col-xs-3
control-label">
Launched By
</label>
<div class="JobResults-resultRowText">
<a href="{{ scheduled_by_link }}"
aw-tool-tip="Edit the Schedule"
data-placement="top">
{{ job.summary_fields.scheduled_by.username }}
</a>
</div>
</div> -->
<!-- ELAPSED TIME DETAIL -->
<!-- <div
class="form-group
JobResults-resultRow toggle-show"
ng-show="job_status.started">
<label
class="JobResults-resultRowLabel
col-lg-2 col-md-2
col-sm-2 col-xs-3
control-label">
Elapsed
</label>
<div class="JobResults-resultRowText">
{{ job_status.elapsed }}
</div>
</div> -->
<!-- EXPLANATION DETAIL -->
<!-- <div
class="form-group
JobResults-resultRow
toggle-show"
ng-show="job_status.explanation">
<label
class="JobResults-resultRowLabel
col-lg-2 col-md-2
col-sm-2 col-xs-3
control-label">
Explanation
</label> -->
<!-- PREVIOUS TASK SUCCEEDED -->
<!-- <div class="JobResults-resultRowText
col-lg-10 col-md-10 col-sm-10 col-xs-9
job_status_explanation"
ng-show="!previousTaskFailed"
ng-bind-html="job_status.explanation">
<i
class="JobResults-statusIcon--results
fa
icon-job-{{ job_status.status }}">
</i> {{ job_status.status_label }}
</div> -->
<!-- PREVIOUS TASK FAILED -->
<!-- <div class="JobResults-resultRowText
col-lg-10 col-md-10 col-sm-10 col-xs-9
job_status_explanation"
ng-show="previousTaskFailed">
Previous Task Failed
<a
href=""
id="explanation_help"
aw-pop-over="{{ task_detail }}"
aw-pop-over-watch="task_detail"
data-placement="bottom"
data-container="body"
class="help-link"
over-title="Failure Detail"
title=""
tabindex="-1">
<i class="fa fa-question-circle">
</i>
</a>
</div> -->
<!-- </div> -->
<!-- RESULTS TRACEBACK DETAIL -->
<!-- <div
class="form-group
JobResults-resultRow
toggle-show" ng-show="job.result_traceback">
<label
class="JobResults-resultRowLabel
col-lg-2 col-md-12
col-sm-12 col-xs-12">
Results Traceback
</label>
<div class="JobResults-resultRowText
col-lg-10 col-md-12 col-sm-12 col-xs-12
job_status_traceback"
ng-bind-html="job.result_traceback">
</div>
</div> -->
</div> </div>
</div> </div>
@@ -490,7 +413,10 @@
<div class="StandardOut-panelHeader"> <div class="StandardOut-panelHeader">
<div class="StandardOut-panelHeaderText"> <div class="StandardOut-panelHeaderText">
<i class="JobResults-statusResultIcon <i class="JobResults-statusResultIcon
fa icon-job-{{ job_status }}"> fa icon-job-{{ job_status }}"
aw-tool-tip="Job {{status_label}}"
aw-tip-placement="top"
data-original-title>
</i> </i>
{{ job.name }} {{ job.name }}
</div> </div>

View File

@@ -105,3 +105,11 @@
.WorkflowChart-activeNode { .WorkflowChart-activeNode {
fill: @default-link; fill: @default-link;
} }
.WorkflowChart-elapsedHolder {
background-color: @b7grey;
color: @default-bg;
height: 13px;
width: 39px;
padding: 1px 3px;
border-radius: 4px;
}

View File

@@ -190,6 +190,7 @@ export default [ '$state',
.attr("dy", ".35em") .attr("dy", ".35em")
.attr("class", "WorkflowChart-startText") .attr("class", "WorkflowChart-startText")
.text(function () { return "START"; }) .text(function () { return "START"; })
.attr("display", function() { return scope.mode === 'details' ? 'none' : null;})
.call(add_node); .call(add_node);
} }
else { else {
@@ -225,10 +226,10 @@ export default [ '$state',
.style("display", function(d) { return d.isActiveEdit ? null : "none"; }); .style("display", function(d) { return d.isActiveEdit ? null : "none"; });
thisNode.append("text") thisNode.append("text")
.attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 20 : nodeW / 2; }) .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; })
.attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 10 : nodeH / 2; }) .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 2; })
.attr("dy", ".35em") .attr("dy", ".35em")
.attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? "inherit" : "middle"; }) .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; })
.attr("class", "WorkflowChart-defaultText WorkflowChart-nameText") .attr("class", "WorkflowChart-defaultText WorkflowChart-nameText")
.text(function (d) { .text(function (d) {
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : ""; return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
@@ -293,7 +294,7 @@ export default [ '$state',
.attr("y", nodeH - 10) .attr("y", nodeH - 10)
.attr("dy", ".35em") .attr("dy", ".35em")
.attr("class", "WorkflowChart-detailsLink") .attr("class", "WorkflowChart-detailsLink")
.style("display", function(d){ return d.job && d.job.jobStatus && d.job.unified_job_id ? null : "none"; }) .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; })
.text(function () { .text(function () {
return "DETAILS"; return "DETAILS";
}) })
@@ -388,7 +389,7 @@ export default [ '$state',
let statusClass = "WorkflowChart-nodeStatus "; let statusClass = "WorkflowChart-nodeStatus ";
if(d.job){ if(d.job){
switch(d.job.jobStatus) { switch(d.job.status) {
case "pending": case "pending":
statusClass = "workflowChart-nodeStatus--running"; statusClass = "workflowChart-nodeStatus--running";
break; break;
@@ -404,15 +405,37 @@ export default [ '$state',
case "failed": case "failed":
statusClass = "workflowChart-nodeStatus--failed"; statusClass = "workflowChart-nodeStatus--failed";
break; break;
case "error":
statusClass = "workflowChart-nodeStatus--failed";
break;
} }
} }
return statusClass; return statusClass;
}) })
.style("display", function(d) { return d.job && d.job.jobStatus ? null : "none"; }) .style("display", function(d) { return d.job && d.job.status ? null : "none"; })
.attr("cy", 10) .attr("cy", 10)
.attr("cx", 10) .attr("cx", 10)
.attr("r", 6); .attr("r", 6);
thisNode.append("foreignObject")
.attr("x", 5)
.attr("y", 43)
.style("font-size","0.7em")
.attr("class", "WorkflowChart-elapsed")
.html(function (d) {
if(d.job && d.job.elapsed) {
let elapsedMs = d.job.elapsed * 1000;
let elapsedMoment = moment.duration(elapsedMs);
let paddedElapsedMoment = Math.floor(elapsedMoment.asHours()) < 10 ? "0" + Math.floor(elapsedMoment.asHours()) : Math.floor(elapsedMoment.asHours());
let elapsedString = paddedElapsedMoment + moment.utc(elapsedMs).format(":mm:ss");
return "<div class=\"WorkflowChart-elapsedHolder\"><span>" + elapsedString + "</span></div>";
}
else {
return "";
}
})
.style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; });
} }
}); });
@@ -608,7 +631,7 @@ export default [ '$state',
let statusClass = "WorkflowChart-nodeStatus "; let statusClass = "WorkflowChart-nodeStatus ";
if(d.job){ if(d.job){
switch(d.job.jobStatus) { switch(d.job.status) {
case "pending": case "pending":
statusClass += "workflowChart-nodeStatus--running"; statusClass += "workflowChart-nodeStatus--running";
break; break;
@@ -624,17 +647,20 @@ export default [ '$state',
case "failed": case "failed":
statusClass += "workflowChart-nodeStatus--failed"; statusClass += "workflowChart-nodeStatus--failed";
break; break;
case "error":
statusClass = "workflowChart-nodeStatus--failed";
break;
} }
} }
return statusClass; return statusClass;
}) })
.style("display", function(d) { return d.job && d.job.jobStatus ? null : "none"; }) .style("display", function(d) { return d.job && d.job.status ? null : "none"; })
.transition() .transition()
.duration(0) .duration(0)
.attr("r", 6) .attr("r", 6)
.each(function(d) { .each(function(d) {
if(d.job && d.job.jobStatus && (d.job.jobStatus === "pending" || d.job.jobStatus === "waiting" || d.job.jobStatus === "running")) { if(d.job && d.job.status && (d.job.status === "pending" || d.job.status === "waiting" || d.job.status === "running")) {
// Pulse the circle // Pulse the circle
var circle = d3.select(this); var circle = d3.select(this);
(function repeat() { (function repeat() {
@@ -651,15 +677,15 @@ export default [ '$state',
}); });
t.selectAll(".WorkflowChart-nameText") t.selectAll(".WorkflowChart-nameText")
.attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 20 : nodeW / 2; }) .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : nodeW / 2; })
.attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 10 : nodeH / 2; }) .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : nodeH / 2; })
.attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? "inherit" : "middle"; }) .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; })
.text(function (d) { .text(function (d) {
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? wrap(d.unifiedJobTemplate.name) : ""; return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? wrap(d.unifiedJobTemplate.name) : "";
}); });
t.selectAll(".WorkflowChart-detailsLink") t.selectAll(".WorkflowChart-detailsLink")
.style("display", function(d){ return d.job && d.job.jobStatus && d.job.unified_job_id ? null : "none"; }); .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; });
t.selectAll(".WorkflowChart-incompleteText") t.selectAll(".WorkflowChart-incompleteText")
.style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); .style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; });
@@ -670,6 +696,9 @@ export default [ '$state',
t.selectAll(".WorkflowChart-activeNode") t.selectAll(".WorkflowChart-activeNode")
.style("display", function(d) { return d.isActiveEdit ? null : "none"; }); .style("display", function(d) { return d.isActiveEdit ? null : "none"; });
t.selectAll(".WorkflowChart-elapsed")
.style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; });
} }
function add_node() { function add_node() {
@@ -722,15 +751,15 @@ export default [ '$state',
d3.select(this).style("text-decoration", null); d3.select(this).style("text-decoration", null);
}); });
this.on("click", function(d) { this.on("click", function(d) {
if(d.job.unified_job_id && d.unifiedJobTemplate) { if(d.job.id && d.unifiedJobTemplate) {
if(d.unifiedJobTemplate.unified_job_type === 'job') { if(d.unifiedJobTemplate.unified_job_type === 'job') {
$state.go('jobDetail', {id: d.job.unified_job_id}); $state.go('jobDetail', {id: d.job.id});
} }
else if(d.unifiedJobTemplate.unified_job_type === 'inventory_update') { else if(d.unifiedJobTemplate.unified_job_type === 'inventory_update') {
$state.go('inventorySyncStdout', {id: d.job.unified_job_id}); $state.go('inventorySyncStdout', {id: d.job.id});
} }
else if(d.unifiedJobTemplate.unified_job_type === 'project_update') { else if(d.unifiedJobTemplate.unified_job_type === 'project_update') {
$state.go('scmUpdateStdout', {id: d.job.unified_job_id}); $state.go('scmUpdateStdout', {id: d.job.id});
} }
} }
}); });

View File

@@ -224,10 +224,7 @@ export default [function(){
} }
if(params.nodesObj[params.nodeId].summary_fields.job) { if(params.nodesObj[params.nodeId].summary_fields.job) {
treeNode.job = { treeNode.job = _.clone(params.nodesObj[params.nodeId].summary_fields.job);
jobStatus: params.nodesObj[params.nodeId].summary_fields.job.status,
unified_job_id: params.nodesObj[params.nodeId].summary_fields.job.id
};
} }
if(params.nodesObj[params.nodeId].summary_fields.unified_job_template) { if(params.nodesObj[params.nodeId].summary_fields.unified_job_template) {
@@ -282,8 +279,8 @@ export default [function(){
if(matchingNode) { if(matchingNode) {
matchingNode.job = { matchingNode.job = {
jobStatus: params.status, status: params.status,
unified_job_id: params.unified_job_id id: params.unified_job_id
}; };
} }

View File

@@ -62,6 +62,19 @@
<!-- LEFT PANE DETAILS GROUP --> <!-- LEFT PANE DETAILS GROUP -->
<div> <div>
<!-- STATUS DETAIL -->
<div class="WorkflowResults-resultRow">
<label class="WorkflowResults-resultRowLabel">
Status
</label>
<div class="WorkflowResults-resultRowText">
<i class="WorkflowResults-statusResultIcon
fa
icon-job-{{ workflow.status }}">
</i> {{ status_label }}
</div>
</div>
<!-- START TIME DETAIL --> <!-- START TIME DETAIL -->
<div class="WorkflowResults-resultRow" <div class="WorkflowResults-resultRow"
ng-show="workflow.started"> ng-show="workflow.started">
@@ -85,32 +98,6 @@
</div> </div>
</div> </div>
<!-- TEMPLATE DETAIL -->
<div class="WorkflowResults-resultRow"
ng-show="workflow.name">
<label class="WorkflowResults-resultRowLabel">
Template
</label>
<div class="WorkflowResults-resultRowText">
<a href="{{ workflow_template_link }}"
aw-tool-tip="Edit the job template"
data-placement="top">
{{ workflow.name }}
</a>
</div>
</div>
<!-- JOB TYPE DETAIL -->
<div class="WorkflowResults-resultRow"
ng-show="workflow.type">
<label class="WorkflowResults-resultRowLabel">
Job Type
</label>
<div class="WorkflowResults-resultRowText">
Workflow Job
</div>
</div>
<!-- CREATED BY DETAIL --> <!-- CREATED BY DETAIL -->
<div class="WorkflowResults-resultRow" <div class="WorkflowResults-resultRow"
ng-show="workflow.summary_fields.created_by.username"> ng-show="workflow.summary_fields.created_by.username">
@@ -180,7 +167,7 @@
</div> </div>
</div> </div>
</div> </div>
<!-- end of labels--> <!-- end of labels-->
</div> </div>
@@ -195,7 +182,10 @@
<div class="StandardOut-panelHeader"> <div class="StandardOut-panelHeader">
<div class="StandardOut-panelHeaderText"> <div class="StandardOut-panelHeaderText">
<i class="WorkflowResults-statusResultIcon <i class="WorkflowResults-statusResultIcon
fa icon-job-{{ workflow.status }}"> fa icon-job-{{ workflow.status }}"
aw-tool-tip="Job {{status_label}}"
aw-tip-placement="top"
data-original-title>
</i> </i>
{{ workflow.name }} {{ workflow.name }}
</div> </div>
@@ -265,7 +255,7 @@
</div> </div>
</div> </div>
</div> </div>
<workflow-chart tree-data="treeData.data" can-add-workflow-job-template="canAddWorkflowJobTemplate" mode="details" class="WorkflowMaker-chart"></workflow-chart> <workflow-chart tree-data="treeData.data" workflow-zoomed="workflowZoomed(zoom)" can-add-workflow-job-template="canAddWorkflowJobTemplate" mode="details" class="WorkflowMaker-chart"></workflow-chart>
</div> </div>
</div> </div>