1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-28 02:25:27 +03:00

Merge remote-tracking branch 'origin/release_3.1.2' into devel

This commit is contained in:
Ryan Petrello 2017-03-10 10:57:03 -05:00
commit bad9670a0b
100 changed files with 576 additions and 341 deletions

View File

@ -830,7 +830,7 @@ amazon-ebs:
cd packaging/packer && $(PACKER) build -only $@ $(PACKER_BUILD_OPTS) -var "aws_instance_count=$(AWS_INSTANCE_COUNT)" -var "product_version=$(VERSION)" packer-$(NAME).json
# Vagrant box using virtualbox provider
vagrant-virtualbox: packaging/packer/ansible-tower-$(VERSION)-virtualbox.box
vagrant-virtualbox: packaging/packer/ansible-tower-$(VERSION)-virtualbox.box tar-build/$(SETUP_TAR_FILE)
packaging/packer/ansible-tower-$(VERSION)-virtualbox.box: packaging/packer/output-virtualbox-iso/centos-7.ovf
cd packaging/packer && $(PACKER) build -only virtualbox-ovf $(PACKER_BUILD_OPTS) -var "aws_instance_count=$(AWS_INSTANCE_COUNT)" -var "product_version=$(VERSION)" packer-$(NAME).json
@ -841,7 +841,7 @@ packaging/packer/output-virtualbox-iso/centos-7.ovf:
virtualbox-iso: packaging/packer/output-virtualbox-iso/centos-7.ovf
# Vagrant box using VMware provider
vagrant-vmware: packaging/packer/ansible-tower-$(VERSION)-vmware.box
vagrant-vmware: packaging/packer/ansible-tower-$(VERSION)-vmware.box tar-build/$(SETUP_TAR_FILE)
packaging/packer/output-vmware-iso/centos-7.vmx:
cd packaging/packer && $(PACKER) build -only vmware-iso packer-centos-7.json

View File

@ -3,6 +3,7 @@
# Python
import re
import json
# Django
from django.core.exceptions import FieldError, ValidationError
@ -296,7 +297,7 @@ class FieldLookupBackend(BaseFilterBackend):
except (FieldError, FieldDoesNotExist, ValueError, TypeError) as e:
raise ParseError(e.args[0])
except ValidationError as e:
raise ParseError(e.messages)
raise ParseError(json.dumps(e.messages, ensure_ascii=False))
class OrderByBackend(BaseFilterBackend):

View File

@ -3503,6 +3503,7 @@ class BaseJobEventsList(SubListAPIView):
parent_model = None # Subclasses must define this attribute.
relationship = 'job_events'
view_name = _('Job Events List')
search_fields = ('stdout',)
def finalize_response(self, request, response, *args, **kwargs):
response['X-UI-Max-Events'] = settings.RECOMMENDED_MAX_EVENTS_DISPLAY_HEADER

View File

@ -304,6 +304,12 @@ class BaseCallbackModule(CallbackBase):
def v2_runner_on_ok(self, result):
# FIXME: Display detailed results or not based on verbosity.
# strip environment vars from the job event; it already exists on the
# job and sensitive values are filtered there
if result._task.get_name() == 'setup':
result._result.get('ansible_facts', {}).pop('ansible_env', None)
event_data = dict(
host=result._host.get_name(),
remote_addr=result._host.address,

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0035_v310_remove_tower_settings'),
]
operations = [
migrations.AlterField(
model_name='project',
name='scm_type',
field=models.CharField(default=b'', choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion'), (b'insights', 'Red Hat Insights')], max_length=8, blank=True, help_text='Specifies the source control system used to store the project.', verbose_name='SCM Type'),
),
migrations.AlterField(
model_name='projectupdate',
name='scm_type',
field=models.CharField(default=b'', choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion'), (b'insights', 'Red Hat Insights')], max_length=8, blank=True, help_text='Specifies the source control system used to store the project.', verbose_name='SCM Type'),
),
]

View File

@ -43,6 +43,7 @@ class ProjectOptions(models.Model):
('git', _('Git')),
('hg', _('Mercurial')),
('svn', _('Subversion')),
('insights', _('Red Hat Insights')),
]
class Meta:
@ -120,6 +121,8 @@ class ProjectOptions(models.Model):
return self.scm_type or ''
def clean_scm_url(self):
if self.scm_type == 'insights':
self.scm_url = settings.INSIGHTS_URL_BASE
scm_url = unicode(self.scm_url or '')
if not self.scm_type:
return ''
@ -141,6 +144,8 @@ class ProjectOptions(models.Model):
if cred.kind != 'scm':
raise ValidationError(_("Credential kind must be 'scm'."))
try:
if self.scm_type == 'insights':
self.scm_url = settings.INSIGHTS_URL_BASE
scm_url = update_scm_url(self.scm_type, self.scm_url,
check_special_cases=False)
scm_url_parts = urlparse.urlsplit(scm_url)

View File

@ -471,24 +471,24 @@ class BaseTask(Task):
env['PROOT_TMP_DIR'] = settings.AWX_PROOT_BASE_PATH
return env
def build_safe_env(self, instance, **kwargs):
def build_safe_env(self, env, **kwargs):
'''
Build environment dictionary, hiding potentially sensitive information
such as passwords or keys.
'''
hidden_re = re.compile(r'API|TOKEN|KEY|SECRET|PASS', re.I)
urlpass_re = re.compile(r'^.*?://.?:(.*?)@.*?$')
env = self.build_env(instance, **kwargs)
for k,v in env.items():
urlpass_re = re.compile(r'^.*?://[^:]+:(.*?)@.*?$')
safe_env = dict(env)
for k,v in safe_env.items():
if k in ('REST_API_URL', 'AWS_ACCESS_KEY', 'AWS_ACCESS_KEY_ID'):
continue
elif k.startswith('ANSIBLE_') and not k.startswith('ANSIBLE_NET'):
continue
elif hidden_re.search(k):
env[k] = HIDDEN_PASSWORD
safe_env[k] = HIDDEN_PASSWORD
elif type(v) == str and urlpass_re.match(v):
env[k] = urlpass_re.sub(HIDDEN_PASSWORD, v)
return env
safe_env[k] = urlpass_re.sub(HIDDEN_PASSWORD, v)
return safe_env
def args2cmdline(self, *args):
return ' '.join([pipes.quote(a) for a in args])
@ -699,7 +699,7 @@ class BaseTask(Task):
output_replacements = self.build_output_replacements(instance, **kwargs)
cwd = self.build_cwd(instance, **kwargs)
env = self.build_env(instance, **kwargs)
safe_env = self.build_safe_env(instance, **kwargs)
safe_env = self.build_safe_env(env, **kwargs)
stdout_handle = self.get_stdout_handle(instance)
if self.should_use_proot(instance, **kwargs):
if not check_proot_installed():
@ -1189,6 +1189,9 @@ class RunProjectUpdate(BaseTask):
scm_username = False
elif scm_url_parts.scheme.endswith('ssh'):
scm_password = False
elif scm_type == 'insights':
extra_vars['scm_username'] = scm_username
extra_vars['scm_password'] = scm_password
scm_url = update_scm_url(scm_type, scm_url, scm_username,
scm_password, scp_format=True)
else:
@ -1218,6 +1221,7 @@ class RunProjectUpdate(BaseTask):
scm_branch = project_update.scm_branch or {'hg': 'tip'}.get(project_update.scm_type, 'HEAD')
extra_vars.update({
'project_path': project_update.get_project_path(check_if_exists=False),
'insights_url': settings.INSIGHTS_URL_BASE,
'scm_type': project_update.scm_type,
'scm_url': scm_url,
'scm_branch': scm_branch,
@ -1314,10 +1318,10 @@ class RunProjectUpdate(BaseTask):
lines = fd.readlines()
if lines:
p.scm_revision = lines[0].strip()
p.playbook_files = p.playbooks
p.save()
else:
logger.error("Could not find scm revision in check")
logger.info("Could not find scm revision in check")
p.playbook_files = p.playbooks
p.save()
try:
os.remove(self.revision_path)
except Exception, e:

View File

@ -1,11 +1,13 @@
import os
import re
import pytest
from pip.operations import freeze
from django.conf import settings
@pytest.mark.skip(reason="This test needs some love")
def test_env_matches_requirements_txt():
def check_is_in(src, dests):
if src not in dests:

View File

@ -71,6 +71,25 @@ def test_run_admin_checks_usage(mocker, current_instances, call_count):
assert 'expire' in mock_sm.call_args_list[0][0][0]
@pytest.mark.parametrize("key,value", [
('REST_API_TOKEN', 'SECRET'),
('SECRET_KEY', 'SECRET'),
('RABBITMQ_PASS', 'SECRET'),
('VMWARE_PASSWORD', 'SECRET'),
('API_SECRET', 'SECRET'),
('CALLBACK_CONNECTION', 'amqp://tower:password@localhost:5672/tower'),
])
def test_safe_env_filtering(key, value):
task = tasks.RunJob()
assert task.build_safe_env({key: value})[key] == tasks.HIDDEN_PASSWORD
def test_safe_env_returns_new_copy():
task = tasks.RunJob()
env = {'foo': 'bar'}
assert task.build_safe_env(env) is not env
def test_openstack_client_config_generation(mocker):
update = tasks.RunInventoryUpdate()
inventory_update = mocker.Mock(**{

View File

@ -261,7 +261,7 @@ def update_scm_url(scm_type, url, username=True, password=True,
# git: https://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
# hg: http://www.selenic.com/mercurial/hg.1.html#url-paths
# svn: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.advanced.reposurls
if scm_type not in ('git', 'hg', 'svn'):
if scm_type not in ('git', 'hg', 'svn', 'insights'):
raise ValueError(_('Unsupported SCM type "%s"') % str(scm_type))
if not url.strip():
return ''
@ -307,6 +307,7 @@ def update_scm_url(scm_type, url, username=True, password=True,
'git': ('ssh', 'git', 'git+ssh', 'http', 'https', 'ftp', 'ftps', 'file'),
'hg': ('http', 'https', 'ssh', 'file'),
'svn': ('http', 'https', 'svn', 'svn+ssh', 'file'),
'insights': ('http', 'https')
}
if parts.scheme not in scm_type_schemes.get(scm_type, ()):
raise ValueError(_('Unsupported %s URL') % scm_type)
@ -342,7 +343,7 @@ def update_scm_url(scm_type, url, username=True, password=True,
#raise ValueError('Password not supported for SSH with Mercurial.')
netloc_password = ''
if netloc_username and parts.scheme != 'file':
if netloc_username and parts.scheme != 'file' and scm_type != "insights":
netloc = u':'.join([urllib.quote(x) for x in (netloc_username, netloc_password) if x])
else:
netloc = u''

View File

@ -13,7 +13,9 @@ class LogstashFormatter(LogstashFormatterVersion1):
ret = super(LogstashFormatter, self).__init__(**kwargs)
if settings_module:
self.host_id = settings_module.CLUSTER_HOST_ID
self.tower_uuid = settings_module.LOG_AGGREGATOR_TOWER_UUID
if hasattr(settings_module, 'LOG_AGGREGATOR_TOWER_UUID'):
self.tower_uuid = settings_module.LOG_AGGREGATOR_TOWER_UUID
self.message_type = settings_module.LOG_AGGREGATOR_TYPE
return ret
def reformat_data_for_log(self, raw_data, kind=None):

View File

@ -105,6 +105,45 @@
scm_version: "{{ scm_result['after'] }}"
when: "'after' in scm_result"
- name: update project using insights
uri:
url: "{{insights_url}}/r/insights/v1/maintenance?ansible=true"
user: "{{scm_username|quote}}"
password: "{{scm_password|quote}}"
force_basic_auth: yes
when: scm_type == 'insights'
register: insights_output
- name: Ensure the project directory is present
file:
dest: "{{project_path|quote}}"
state: directory
when: scm_type == 'insights'
- name: Fetch Insights Playbook With Name
get_url:
url: "{{insights_url}}/r/insights/v3/maintenance/{{item.maintenance_id}}/playbook"
dest: "{{project_path|quote}}/{{item.name}}-{{item.maintenance_id}}.yml"
url_username: "{{scm_username|quote}}"
url_password: "{{scm_password|quote}}"
force_basic_auth: yes
force: yes
when: scm_type == 'insights' and item.name != None
with_items: "{{insights_output.json}}"
failed_when: false
- name: Fetch Insights Playbook
get_url:
url: "{{insights_url}}/r/insights/v3/maintenance/{{item.maintenance_id}}/playbook"
dest: "{{project_path|quote}}/insights-plan-{{item.maintenance_id}}.yml"
url_username: "{{scm_username|quote}}"
url_password: "{{scm_password|quote}}"
force_basic_auth: yes
force: yes
when: scm_type == 'insights' and item.name == None
with_items: "{{insights_output.json}}"
failed_when: false
- name: detect requirements.yml
stat: path={{project_path|quote}}/roles/requirements.yml
register: doesRequirementsExist

View File

@ -862,6 +862,8 @@ TOWER_ADMIN_ALERTS = True
# Note: This setting may be overridden by database settings.
TOWER_URL_BASE = "https://towerhost"
INSIGHTS_URL_BASE = "https://access.redhat.com"
TOWER_SETTINGS_MANIFEST = {}
LOG_AGGREGATOR_ENABLED = False

View File

@ -377,9 +377,9 @@ register(
help_text=_('User profile flags updated from group membership (key is user '
'attribute name, value is group DN). These are boolean fields '
'that are matched based on whether the user is a member of the '
'given group. So far only is_superuser is settable via this '
'method. This flag is set both true and false at login time '
'based on current LDAP settings.'),
'given group. So far only is_superuser and is_system_auditor '
'are settable via this method. This flag is set both true and '
'false at login time based on current LDAP settings.'),
category=_('LDAP'),
category_slug='ldap',
placeholder=collections.OrderedDict([

View File

@ -322,7 +322,7 @@ class LDAPUserFlagsField(fields.DictField):
default_error_messages = {
'invalid_flag': _('Invalid user flag: "{invalid_flag}".'),
}
valid_user_flags = {'is_superuser'}
valid_user_flags = {'is_superuser', 'is_system_auditor'}
child = LDAPDNField()
def to_internal_value(self, data):

View File

@ -40,7 +40,6 @@
.Form-title{
flex: 0 1 auto;
text-transform: uppercase;
color: @list-header-txt;
font-size: 14px;
font-weight: bold;
@ -50,6 +49,10 @@
margin-bottom: 20px;
}
.Form-title--uppercase {
text-transform: uppercase;
}
.Form-secondaryTitle{
color: @default-icon;
padding-bottom: 20px;
@ -98,8 +101,8 @@
.Form-tabHolder{
display: flex;
margin-bottom: 20px;
min-height: 30px;
flex-wrap:wrap;
}
.Form-tabs {
@ -115,6 +118,7 @@
height: 30px;
border-radius: 5px;
margin-right: 20px;
margin-bottom: 20px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 5px;
@ -560,6 +564,8 @@ input[type='radio']:checked:before {
padding-left:15px;
padding-right: 15px;
margin-right: 20px;
min-height: 30px;
margin-bottom: 20px;
}
.Form-primaryButton:hover {

View File

@ -147,7 +147,6 @@ table, tbody {
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
}
.List-actionHolder {

View File

@ -1,47 +1,60 @@
/** @define About */
@import "./client/src/shared/branding/colors.default.less";
.About-cowsay--container{
width: 340px;
margin: 0 auto;
.About-ansibleVersion,
.About-cowsayCode {
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
}
.About-cowsay--code{
background-color: @default-bg;
padding-left: 30px;
border-style: none;
max-width: 340px;
padding-left: 30px;
.About-cowsayContainer {
width: 340px;
margin: 0 auto;
}
.About .modal-header{
border: none;
padding-bottom: 0px;
.About-cowsayCode {
background-color: @default-bg;
padding-left: 30px;
border-style: none;
max-width: 340px;
padding-left: 30px;
}
.About .modal-dialog{
max-width: 500px;
.About-modalHeader {
border: none;
padding-bottom: 0px;
}
.About .modal-body{
padding-top: 0px;
.About-modalDialog {
max-width: 500px;
}
.About-brand--redhat{
.About-modalBody {
padding-top: 0px;
padding-bottom: 0px;
}
.About-brandImg {
float: left;
width: 112px;
padding-top: 13px;
}
.About-brand--ansible{
max-width: 120px;
margin: 0 auto;
.About-close {
position: absolute;
top: 15px;
right: 15px;
z-index: 10;
}
.About-close{
position: absolute;
top: 15px;
right: 15px;
z-index: 10;
.About-modalFooter {
clear: both;
}
.About p{
color: @default-interface-txt;
.About-footerText {
text-align: right;
color: @default-interface-txt;
margin: 0;
font-size: 12px;
padding-top: 10px;
}
.About-modal--footer {
clear: both;
.About-ansibleVersion {
color: @default-data-txt;
}

View File

@ -1,27 +1,12 @@
export default
['$scope', '$state', 'ConfigService', 'i18n',
function($scope, $state, ConfigService, i18n){
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;
};
['$scope', '$state', 'ConfigService',
function($scope, $state, ConfigService){
var init = function(){
ConfigService.getConfig()
.then(function(config){
$scope.version = config.version.split('-')[0];
$scope.ansible_version = config.ansible_version;
$scope.subscription = config.license_info.subscription_name;
$scope.version = processVersion(config.version);
$scope.version_str = i18n._("Version");
$('#about-modal').modal('show');
});
};

View File

@ -1,19 +1,18 @@
<div class="About modal fade" id="about-modal">
<div class="modal-dialog">
<div class="modal-dialog About-modalDialog">
<div class="modal-content">
<div class="modal-header">
<div class="modal-header About-modalHeader">
<button data-dismiss="modal" type="button" class="close About-close">
<span class="fa fa-times-circle"></span>
</button>
</div>
<div class="modal-body">
<div class="About-cowsay--container">
<div class="modal-body About-modalBody">
<div class="About-cowsayContainer">
<!-- Don't indent this properly, you'll break the cow -->
<pre class="About-cowsay--code">
________________
/ Tower {{version_str}} \
\<span>{{version}}</span>/
----------------
<pre class="About-cowsayCode">
_______________
< Tower {{version}} >
---------------
\ ^__^
\ (oo)\_______
(__) A )\/\
@ -21,10 +20,15 @@
|| ||
</pre>
</div>
<div class="About-modal--footer">
<img class="About-brand--redhat img-responsive" src="/static/assets/tower-logo-login.svg" />
<p class="text-right">Copyright &copy; 2017 Red Hat, Inc. <br>
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
<div class="About-modalFooter">
<img class="About-brandImg img-responsive" src="/static/assets/tower-logo-login.svg" />
<p class="About-footerText">
<span class="About-ansibleVersion">
Ansible {{ ansible_version }}
</span> <br>
Copyright &copy; 2017 Red Hat, Inc. <br>
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
</p>
</div>
</div>
</div>

View File

@ -65,7 +65,6 @@
.BreadCrumb-item {
display: inline-block;
color: @default-interface-txt;
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@ -36,19 +36,11 @@
.Form-nav--dropdownContainer {
width: 285px;
margin-top: -52px;
margin-bottom: 22px;
margin-left: auto;
display: flex;
justify-content: space-between;
}
@media (max-width: 900px) {
.Form-nav--dropdownContainer {
margin: 0;
}
}
.Form-nav--dropdown {
width: 60%;
}

View File

@ -7,7 +7,7 @@
<div class="tab-pane" id="configuration-panel">
<div ng-cloak id="htmlTemplate" class="Panel">
<div class="Form-header">
<div class="Form-title" translate>Configure Tower</div>
<div class="Form-title" translate>CONFIGURE TOWER</div>
</div>
<div class="row Form-tabRow">
<div class="col-lg-12">

View File

@ -27,7 +27,7 @@
},
ncyBreadcrumb: {
parent: 'setup',
label: N_("Edit Configuration")
label: N_("EDIT CONFIGURATION")
},
controller: ConfigurationController,
resolve: {

View File

@ -22,7 +22,7 @@ export default
return {
name: 'activity',
editTitle: i18n._('Activity Detail'),
editTitle: i18n._('ACTIVITY DETAIL'),
well: false,
'class': 'horizontal-narrow',
formFieldSize: 'col-lg-10',

View File

@ -15,7 +15,7 @@ export default
.factory('CredentialForm', ['i18n', function(i18n) {
return {
addTitle: i18n._('Create Credential'), //Legend in add mode
addTitle: i18n._('CREATE CREDENTIAL'), //Legend in add mode
editTitle: '{{ name }}', //Legend in edit mode
name: 'credential',
// the top-most node of generated state tree

View File

@ -14,7 +14,7 @@ export default
angular.module('GroupFormDefinition', [])
.value('GroupFormObject', {
addTitle: 'Create Group',
addTitle: 'CREATE GROUP',
editTitle: '{{ name }}',
showTitle: true,
name: 'group',

View File

@ -14,7 +14,7 @@ export default
angular.module('HostGroupsFormDefinition', [])
.value('HostGroupsForm', {
editTitle: 'Host Groups',
editTitle: 'HOST GROUPS',
name: 'host',
well: false,
formLabelSize: 'col-lg-3',

View File

@ -15,7 +15,7 @@ export default
.factory('HostForm', ['i18n', function(i18n) {
return {
addTitle: i18n._('Create Host'),
addTitle: i18n._('CREATE HOST'),
editTitle: '{{ host.name }}',
name: 'host',
basePath: 'hosts',

View File

@ -15,7 +15,7 @@ angular.module('InventoryFormDefinition', [])
.factory('InventoryForm', ['i18n', function(i18n) {
return {
addTitle: i18n._('New Inventory'),
addTitle: i18n._('NEW INVENTORY'),
editTitle: '{{ inventory_name }}',
name: 'inventory',
basePath: 'inventory',

View File

@ -3,7 +3,7 @@
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name forms.function:InventoryStatus
@ -14,7 +14,7 @@ export default
.value('InventoryStatusForm', {
name: 'inventory_update',
editTitle: 'Inventory Status',
editTitle: 'INVENTORY STATUS',
well: false,
'class': 'horizontal-narrow',

View File

@ -17,7 +17,7 @@ export default
.factory('JobTemplateFormObject', ['i18n', function(i18n) {
return {
addTitle: i18n._('New Job Template'),
addTitle: i18n._('NEW JOB TEMPLATE'),
editTitle: '{{ name }}',
name: 'job_template',
breadcrumbName: i18n._('JOB TEMPLATE'),

View File

@ -15,7 +15,7 @@ export default
.factory('OrganizationFormObject', ['i18n', function(i18n) {
return {
addTitle: i18n._('New Organization'), //Title in add mode
addTitle: i18n._('NEW ORGANIZATION'), //Title in add mode
editTitle: '{{ name }}', //Title in edit mode
name: 'organization', //entity or model name in singular form
stateTree: 'organizations',

View File

@ -3,7 +3,7 @@
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name forms.function:ProjectStatus
@ -15,7 +15,7 @@ export default
.value('ProjectStatusForm', {
name: 'project_update',
editTitle: 'SCM Status',
editTitle: 'SCM STATUS',
well: false,
'class': 'horizontal-narrow',

View File

@ -15,7 +15,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
.factory('ProjectsFormObject', ['i18n', function(i18n) {
return {
addTitle: i18n._('New Project'),
addTitle: i18n._('NEW PROJECT'),
editTitle: '{{ name }}',
name: 'project',
basePath: 'projects',
@ -105,7 +105,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
scm_url: {
label: 'SCM URL',
type: 'text',
ngShow: "scm_type && scm_type.value !== 'manual'",
ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights' ",
awRequiredWhen: {
reqExpression: "scmRequired",
init: false
@ -122,12 +122,12 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
scm_branch: {
labelBind: "scmBranchLabel",
type: 'text',
ngShow: "scm_type && scm_type.value !== 'manual'",
ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights'",
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)',
subForm: 'sourceSubForm',
},
credential: {
label: i18n._('SCM Credential'),
labelBind: 'credentialLabel',
type: 'lookup',
basePath: 'credentials',
list: 'CredentialList',

View File

@ -15,7 +15,7 @@ export default
.factory('TeamForm', ['i18n', function(i18n) {
return {
addTitle: i18n._('New Team'), //Legend in add mode
addTitle: i18n._('NEW TEAM'), //Legend in add mode
editTitle: '{{ name }}', //Legend in edit mode
name: 'team',
// the top-most node of generated state tree

View File

@ -15,7 +15,7 @@ export default
.factory('UserForm', ['i18n', function(i18n) {
return {
addTitle: i18n._('New User'),
addTitle: i18n._('NEW USER'),
editTitle: '{{ username }}',
name: 'user',
// the top-most node of generated state tree
@ -38,6 +38,17 @@ export default
required: true,
capitalize: true
},
organization: {
label: i18n._('Organization'),
type: 'lookup',
list: 'OrganizationList',
basePath: 'organizations',
sourceModel: 'organization',
sourceField: 'name',
required: true,
excludeMode: 'edit',
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
email: {
label: i18n._('Email'),
type: 'email',
@ -55,17 +66,6 @@ export default
autocomplete: false,
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
organization: {
label: i18n._('Organization'),
type: 'lookup',
list: 'OrganizationList',
basePath: 'organizations',
sourceModel: 'organization',
sourceField: 'name',
required: true,
excludeMode: 'edit',
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
password: {
label: i18n._('Password'),
type: 'sensitive',

View File

@ -16,7 +16,7 @@ export default
.factory('WorkflowFormObject', ['i18n', function(i18n) {
return {
addTitle: i18n._('New Workflow Job Template'),
addTitle: i18n._('NEW WORKFLOW JOB TEMPLATE'),
editTitle: '{{ name }}',
name: 'workflow_job_template',
breadcrumbName: i18n._('WORKFLOW'),

View File

@ -10,8 +10,8 @@ export default [ 'i18n', function(i18n){
name: 'hosts',
iterator: 'host',
selectTitle: i18n._('Add Existing Hosts'),
editTitle: i18n._('Hosts'),
listTitle: i18n._('Hosts'),
editTitle: i18n._('HOSTS'),
listTitle: i18n._('HOSTS'),
index: false,
hover: true,
well: true,

View File

@ -97,7 +97,7 @@ angular.module('inventory', [
'@': {
templateProvider: function(ScheduleList, generateList, ParentObject) {
// include name of parent resource in listTitle
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('Schedules');
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('SCHEDULES');
let html = generateList.build({
list: ScheduleList,
mode: 'edit'

View File

@ -12,7 +12,7 @@
export default function() {
return {
addTitle: 'Execute Command',
addTitle: 'EXECUTE COMMAND',
name: 'adhoc',
well: true,
forceListeners: true,

View File

@ -13,7 +13,7 @@
export default ['i18n', function(i18n) {
return {
addTitle: i18n._('New Custom Inventory'),
addTitle: i18n._('NEW CUSTOM INVENTORY'),
editTitle: '{{ name }}',
name: 'inventory_script',
basePath: 'inventory_scripts',

View File

@ -9,7 +9,7 @@
export default ['i18n', function(i18n){
return {
name: 'inventory_scripts' ,
listTitle: i18n._('Inventory Scripts'),
listTitle: i18n._('INVENTORY SCRIPTS'),
iterator: 'inventory_script',
index: false,
hover: false,

View File

@ -161,6 +161,24 @@
flex-direction: column;
}
.JobResults-panelRightTitle{
flex-wrap: wrap;
}
.JobResults-panelRightTitleText{
word-wrap: break-word;
word-break: break-all;
max-width: 100%;
}
.JobResults-badgeAndActionRow{
display:flex;
flex: 1 0 auto;
justify-content: flex-end;
flex-wrap: wrap;
max-width: 100%;
}
.StandardOut-panelHeader {
flex: initial;
}

View File

@ -38,12 +38,7 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
// used for tag search
$scope.list = {
basePath: jobData.related.job_events,
defaultSearchParams: function(term){
return {
or__stdout__icontains: term,
};
},
basePath: jobData.related.job_events
};
// used for tag search

View File

@ -431,8 +431,8 @@
<div class="Panel JobResults-panelRight">
<!-- RIGHT PANE HEADER -->
<div class="StandardOut-panelHeader">
<div class="StandardOut-panelHeaderText">
<div class="StandardOut-panelHeader JobResults-panelRightTitle">
<div class="StandardOut-panelHeaderText JobResults-panelRightTitleText">
<i class="JobResults-statusResultIcon
fa icon-job-{{ job_status }}"
ng-show="stdoutFullScreen"
@ -443,76 +443,77 @@
</i>
{{ job.name }}
</div>
<div class="JobResults-badgeAndActionRow">
<!-- HEADER COUNTS -->
<div class="JobResults-badgeRow">
<!-- PLAYS COUNT -->
<div class="JobResults-badgeTitle">
Plays
</div>
<span class="badge List-titleBadge">
{{ playCount || 0}}
</span>
<!-- HEADER COUNTS -->
<div class="JobResults-badgeRow">
<!-- PLAYS COUNT -->
<div class="JobResults-badgeTitle">
Plays
<!-- TASKS COUNT -->
<div class="JobResults-badgeTitle">
Tasks
</div>
<span class="badge List-titleBadge">
{{ taskCount || 0}}
</span>
<!-- HOSTS COUNT -->
<div class="JobResults-badgeTitle">
Hosts
</div>
<span class="badge List-titleBadge"
ng-if="jobFinished">
{{ hostCount || 0}}
</span>
<span class="badge List-titleBadge"
aw-tool-tip="The host count will update when the job is complete."
data-placement="top"
ng-if="!jobFinished">
<i class="fa fa-ellipsis-h"></i>
</span>
<!-- ELAPSED TIME -->
<div class="JobResults-badgeTitle">
Elapsed
</div>
<span class="badge List-titleBadge">
{{ job.elapsed * 1000 | duration: "hh:mm:ss" }}
</span>
</div>
<span class="badge List-titleBadge">
{{ playCount || 0}}
</span>
<!-- TASKS COUNT -->
<div class="JobResults-badgeTitle">
Tasks
</div>
<span class="badge List-titleBadge">
{{ taskCount || 0}}
</span>
<!-- HEADER ACTIONS -->
<div class="StandardOut-panelHeaderActions">
<!-- HOSTS COUNT -->
<div class="JobResults-badgeTitle">
Hosts
</div>
<span class="badge List-titleBadge"
ng-if="jobFinished">
{{ hostCount || 0}}
</span>
<span class="badge List-titleBadge"
aw-tool-tip="The host count will update when the job is complete."
data-placement="top"
ng-if="!jobFinished">
<i class="fa fa-ellipsis-h"></i>
</span>
<!-- ELAPSED TIME -->
<div class="JobResults-badgeTitle">
Elapsed
</div>
<span class="badge List-titleBadge">
{{ job.elapsed * 1000 | duration: "hh:mm:ss" }}
</span>
</div>
<!-- HEADER ACTIONS -->
<div class="StandardOut-panelHeaderActions">
<!-- FULL-SCREEN TOGGLE ACTION -->
<button class="StandardOut-actionButton"
aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}"
data-tip-watch="toggleStdoutFullscreenTooltip"
data-placement="top"
ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"
ng-click="toggleStdoutFullscreen()">
<i class="fa fa-arrows-alt"></i>
</button>
<!-- DOWNLOAD ACTION -->
<a ng-show="job.status === 'failed' ||
job.status === 'successful' ||
job.status === 'canceled'"
href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download">
<!-- FULL-SCREEN TOGGLE ACTION -->
<button class="StandardOut-actionButton"
aw-tool-tip="{{ standardOutTooltip }}"
data-tip-watch="standardOutTooltip"
data-placement="top">
<i class="fa fa-download"></i>
aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}"
data-tip-watch="toggleStdoutFullscreenTooltip"
data-placement="top"
ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"
ng-click="toggleStdoutFullscreen()">
<i class="fa fa-arrows-alt"></i>
</button>
</a>
<!-- DOWNLOAD ACTION -->
<a ng-show="job.status === 'failed' ||
job.status === 'successful' ||
job.status === 'canceled'"
href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download">
<button class="StandardOut-actionButton"
aw-tool-tip="{{ standardOutTooltip }}"
data-tip-watch="standardOutTooltip"
data-placement="top">
<i class="fa fa-download"></i>
</button>
</a>
</div>
</div>
</div>
<host-status-bar></host-status-bar>

View File

@ -23,19 +23,20 @@
.JobSubmission-header {
display: flex;
flex: 0 0 auto;
align-items: center;
}
.JobSubmission-title {
align-items: center;
flex: 1 0 auto;
display: flex;
word-wrap: break-word;
word-break: break-all;
max-width: 98%;
}
.JobSubmission-titleText {
color: @list-title-txt;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
}
.JobSubmission-titleLockup {
margin-left: 4px;

View File

@ -13,7 +13,7 @@ export default
name: 'jobs',
basePath: 'unified_jobs',
iterator: 'job',
editTitle: i18n._('All Jobs'),
editTitle: i18n._('ALL JOBS'),
index: false,
hover: true,
well: false,

View File

@ -14,7 +14,7 @@ export default
name: 'cloudcredentials',
iterator: 'cloudcredential',
selectTitle: 'Add Cloud Credentials',
editTitle: 'Cloud Credentials',
editTitle: 'CLOUD CREDENTIALS',
selectInstructions: '<p>Select existing credentials by clicking each credential or checking the related checkbox. When finished, click the blue ' +
'<em>Select</em> button, located bottom right.</p> <p>Create a brand new credential by clicking the <i class=\"fa fa-plus"></i> button.</p>',
index: false,

View File

@ -15,7 +15,7 @@ export default
name: 'completed_jobs',
basePath: 'api/v1/job_templates/{{$stateParams.job_template_id}}/jobs/?or__status=successful&or__status=failed&or__status=error&or__status=canceled',
iterator: 'completed_job',
editTitle: i18n._('Completed Jobs'),
editTitle: i18n._('COMPLETED JOBS'),
index: false,
hover: true,
well: false,

View File

@ -15,8 +15,8 @@ export default
name: 'credentials',
iterator: 'credential',
selectTitle: i18n._('Add Credentials'),
editTitle: i18n._('Credentials'),
listTitle: i18n._('Credentials'),
editTitle: i18n._('CREDENTIALS'),
listTitle: i18n._('CREDENTIALS'),
selectInstructions: "<p>Select existing credentials by clicking each credential or checking the related checkbox. When " +
"finished, click the blue <em>Select</em> button, located bottom right.</p> <p>Create a brand new credential by clicking ",
index: false,

View File

@ -13,8 +13,8 @@ export default
name: 'inventories',
iterator: 'inventory',
selectTitle: i18n._('Add Inventories'),
editTitle: i18n._('Inventories'),
listTitle: i18n._('Inventories'),
editTitle: i18n._('INVENTORIES'),
listTitle: i18n._('INVENTORIES'),
selectInstructions: i18n.sprintf(i18n._("Click on a row to select it, and click Finished when done. Click the %s button to create a new inventory."), "<i class=\"icon-plus\"></i> "),
index: false,
hover: true,

View File

@ -11,7 +11,7 @@ export default
name: 'groups',
iterator: 'group',
editTitle: '{{ inventory.name }}',
listTitle: 'Groups',
listTitle: 'GROUPS',
searchSize: 'col-lg-12 col-md-12 col-sm-12 col-xs-12',
showTitle: false,
well: true,

View File

@ -11,7 +11,7 @@ export default
name: 'hosts',
iterator: 'host',
editTitle: '{{ selected_group }}',
listTitle: 'Hosts',
listTitle: 'HOSTS',
searchSize: 'col-lg-12 col-md-12 col-sm-12 col-xs-12',
showTitle: false,
well: true,

View File

@ -12,7 +12,7 @@ export default
name: 'workflow_inventory_sources',
iterator: 'inventory_source',
basePath: 'inventory_sources',
listTitle: 'Inventory Sources',
listTitle: 'INVENTORY SOURCES',
index: false,
hover: true,

View File

@ -12,7 +12,7 @@ export default
name: 'jobevents',
iterator: 'jobevent',
editTitle: i18n._('Job Events'),
editTitle: i18n._('JOB EVENTS'),
index: false,
hover: true,
"class": "condensed",

View File

@ -11,7 +11,7 @@ export default
name: 'jobhosts',
iterator: 'jobhost',
editTitle: 'All summaries',
editTitle: 'ALL SUMMARIES',
"class": "table-condensed",
index: false,
hover: true,

View File

@ -11,7 +11,7 @@ export default
name: 'jobs',
iterator: 'job',
editTitle: 'Jobs',
editTitle: 'JOBS',
'class': 'table-condensed',
index: false,
hover: true,

View File

@ -15,7 +15,7 @@ export default
selectInstructions: '<p>Select existing organizations by clicking each organization or checking the related checkbox. When finished, ' +
'click the blue <em>Select</em> button, located bottom right.</p><p>Create a new organization by clicking the ' +
'<i class=\"fa fa-plus\"></i> button.</p>',
editTitle: 'Organizations',
editTitle: 'ORGANIZATIONS',
hover: true,
index: false,

View File

@ -12,8 +12,8 @@ export default
name: 'job_templates',
iterator: 'job_template',
editTitle: i18n._('Job Templates'),
listTitle: i18n._('Job Templates'),
editTitle: i18n._('JOB TEMPLATES'),
listTitle: i18n._('JOB TEMPLATES'),
index: false,
hover: true,
well: true,

View File

@ -12,11 +12,11 @@ export default
name: 'jobs',
iterator: 'job',
editTitle: i18n._('Jobs'),
editTitle: i18n._('JOBS'),
index: false,
hover: true,
well: true,
listTitle: i18n._('Jobs'),
listTitle: i18n._('JOBS'),
emptyListText: i18n._('There are no jobs to display at this time'),
fields: {

View File

@ -13,8 +13,8 @@ export default
iterator: 'project',
basePath: 'projects',
selectTitle: i18n._('Add Project'),
editTitle: i18n._('Projects'),
listTitle: i18n._('Projects'),
editTitle: i18n._('PROJECTS'),
listTitle: i18n._('PROJECTS'),
selectInstructions: '<p>Select existing projects by clicking each project or checking the related checkbox. When finished, click the blue ' +
'<em>Select</em> button, located bottom right.</p><p>Create a new project by clicking the <i class=\"fa fa-plus\"></i> button.</p>',
index: false,

View File

@ -12,7 +12,7 @@ export default
name: 'schedules',
iterator: 'schedule',
editTitle: i18n._('Scheduled Jobs'),
editTitle: i18n._('SCHEDULED JOBS'),
hover: true,
well: false,
emptyListText: i18n._('No schedules exist'),

View File

@ -13,8 +13,8 @@ export default
name: 'schedules',
iterator: 'schedule',
selectTitle: '',
editTitle: 'Schedules',
listTitle: '{{parentObject}} || Schedules',
editTitle: 'SCHEDULES',
listTitle: '{{parentObject}} || SCHEDULES',
index: false,
hover: true,

View File

@ -13,8 +13,8 @@ export default
name: 'activities',
iterator: 'activity',
basePath: 'activity_stream',
editTitle: i18n._('Activity Stream'),
listTitle: i18n._('Activity Stream') + '<span ng-show="streamSubTitle"><div class="List-titleLockup"></div>{{streamSubTitle}}<span>',
editTitle: i18n._('ACTIVITY STREAM'),
listTitle: i18n._('ACTIVITY STREAM') + '<span ng-show="streamSubTitle"><div class="List-titleLockup"></div>{{streamSubTitle}}<span>',
listTitleBadge: false,
emptyListText: i18n._('There are no events to display at this time'),
selectInstructions: '',

View File

@ -13,8 +13,8 @@ export default
name: 'teams',
iterator: 'team',
selectTitle: i18n._('Add Team'),
editTitle: i18n._('Teams'),
listTitle: i18n._('Teams'),
editTitle: i18n._('TEAMS'),
listTitle: i18n._('TEAMS'),
selectInstructions: i18n.sprintf(i18n._("Click on a row to select it, and click Finished when done. Click the %s button to create a new team."), "<i class=\"icon-plus\"></i> "),
index: false,
hover: true,

View File

@ -14,8 +14,8 @@ export default
iterator: 'template',
basePath: 'unified_job_templates',
selectTitle: i18n._('Template'),
editTitle: i18n._('Templates'),
listTitle: i18n._('Templates'),
editTitle: i18n._('TEMPLATES'),
listTitle: i18n._('TEMPLATES'),
selectInstructions: i18n.sprintf(i18n._("Click on a row to select it, and click Finished when done. Use the %s button to create a new job template."), "<i class=\"icon-plus\"></i> "),
index: false,
hover: true,

View File

@ -17,8 +17,8 @@ export default
},
iterator: 'user',
selectTitle: i18n._('Add Users'),
editTitle: i18n._('Users'),
listTitle: i18n._('Users'),
editTitle: i18n._('USERS'),
listTitle: i18n._('USERS'),
selectInstructions: '<p>Select existing users by clicking each user or checking the related checkbox. When finished, click the blue ' +
'<em>Select</em> button, located bottom right.</p> <p>When available, a brand new user can be created by clicking the ' +
'<i class=\"fa fa-plus\"></i> button.</p>',

View File

@ -2,7 +2,7 @@
<div class="tab-pane Panel" id="management_jobs">
<div class="List-title">
<div class="List-titleText" translate>
Management Jobs
MANAGEMENT JOBS
</div>
<span class="badge List-titleBadge ng-binding">
{{ mgmtCards.length }}

View File

@ -39,7 +39,6 @@
.MgmtCards-label {
margin-top: 0px;
text-transform: uppercase;
font-size: 14px;
font-weight: bold;
color: @default-interface-txt;

View File

@ -10,7 +10,7 @@ export default function(){
iterator: 'configure_job',
index: false,
hover: true,
listTitle: 'Management Jobs',
listTitle: 'MANAGEMENT JOBS',
fields: {
name: {

View File

@ -29,7 +29,7 @@ angular.module('managementJobScheduler', [])
'@': {
templateProvider: function(ScheduleList, generateList, ParentObject) {
// include name of parent resource in listTitle
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('Schedules');
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('SCHEDULES');
let html = generateList.build({
list: ScheduleList,
mode: 'edit'

View File

@ -1,7 +1,7 @@
<div id="htmlTemplate" class=" SchedulerFormPanel Panel" ng-hide="hideForm">
<div class="Form-header">
<div class="Form-title" ng-show="!isEdit">{{ schedulerName || "Add Schedule"}}</div>
<div class="Form-title" ng-show="isEdit">{{ schedulerName || "Edit Schedule"}}</div>
<div class="Form-title" ng-show="!isEdit">{{ schedulerName || "ADD SCHEDULE"}}</div>
<div class="Form-title" ng-show="isEdit">{{ schedulerName || "EDIT SCHEDULE"}}</div>
<div class="Form-header--fields"></div>
<div class="Form-exitHolder">
<button class="Form-exit" ng-click="formCancel()">

View File

@ -13,7 +13,7 @@
export default ['i18n', function(i18n) {
return {
addTitle: i18n._('New Notification Template'),
addTitle: i18n._('NEW NOTIFICATION TEMPLATE'),
editTitle: '{{ name }}',
name: 'notification_template',
// I18N for "CREATE NOTIFICATION_TEMPLATE"
@ -70,20 +70,20 @@ export default ['i18n', function(i18n) {
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
host: {
label: i18n._('Host'),
type: 'text',
password: {
labelBind: 'passwordLabel',
type: 'sensitive',
hasShowInputButton: true,
awRequiredWhen: {
reqExpression: "email_required",
reqExpression: "password_required" ,
init: "false"
},
ngShow: "notification_type.value == 'email' ",
ngShow: "notification_type.value == 'email' || notification_type.value == 'irc' ",
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
sender: {
label: i18n._('Sender Email'),
host: {
label: i18n._('Host'),
type: 'text',
awRequiredWhen: {
reqExpression: "email_required",
@ -110,15 +110,14 @@ export default ['i18n', function(i18n) {
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
password: {
labelBind: 'passwordLabel',
type: 'sensitive',
hasShowInputButton: true,
sender: {
label: i18n._('Sender Email'),
type: 'text',
awRequiredWhen: {
reqExpression: "password_required" ,
reqExpression: "email_required",
init: "false"
},
ngShow: "notification_type.value == 'email' || notification_type.value == 'irc' ",
ngShow: "notification_type.value == 'email' ",
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
@ -269,9 +268,10 @@ export default ['i18n', function(i18n) {
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
message_from: {
label: i18n._('Label to be shown with notification'),
api_url: {
label: 'API URL',
type: 'text',
placeholder: 'https://mycompany.hipchat.com',
awRequiredWhen: {
reqExpression: "hipchat_required",
init: "false"
@ -280,10 +280,9 @@ export default ['i18n', function(i18n) {
subForm: 'typeSubForm',
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
},
api_url: {
label: 'API URL',
message_from: {
label: i18n._('Notification Label'),
type: 'text',
placeholder: 'https://mycompany.hipchat.com',
awRequiredWhen: {
reqExpression: "hipchat_required",
init: "false"

View File

@ -11,7 +11,7 @@
export default ['i18n', function(i18n){
return {
name: 'notification_templates' ,
listTitle: i18n._('Notification Templates'),
listTitle: i18n._('NOTIFICATION TEMPLATES'),
iterator: 'notification_template',
index: false,
hover: false,

View File

@ -5,7 +5,7 @@
<div class="AddUsers-header">
<div class="List-header">
<div class="List-title">
<div class="List-titleText ng-binding">{{ $parent.organization_name }}<div class="List-titleLockup"></div><span translate>Add</span> {{ addType | capitalize}}
<div class="List-titleText ng-binding">{{ $parent.organization_name }}<div class="List-titleLockup"></div><span translate>ADD</span> {{ addType | capitalize}}
</div>
</div>
<div class="Form-exitHolder">

View File

@ -58,8 +58,8 @@
.OrgCards-label {
margin-top: 0px;
text-transform: uppercase;
font-size: 14px;
height: 17px;
font-weight: bold;
color: @default-interface-txt;
margin-bottom: 25px;

View File

@ -4,7 +4,7 @@
<div class="List-header">
<div class="List-title">
<div class="List-titleText">
organizations
ORGANIZATIONS
</div>
<span class="badge List-titleBadge">
{{ orgCount }}
@ -59,4 +59,4 @@
</div>
<div id="pagination-container" ng-hide="organization_num_pages < 2">
</div>
</div>
</div>

View File

@ -94,7 +94,7 @@ export default ['$scope', '$rootScope', '$location', '$log', '$stateParams',
$scope.reloadList = function(){
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
qs.search(path, $stateParams[`${list.iterator}_search`])
qs.search(path, $state.params[`${list.iterator}_search`])
.then(function(searchResponse) {
$scope[`${list.iterator}_dataset`] = searchResponse.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;

View File

@ -83,7 +83,7 @@ export default
'@': {
templateProvider: function(ScheduleList, generateList, ParentObject){
// include name of parent resource in listTitle
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('Schedules');
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('SCHEDULES');
let html = generateList.build({
list: ScheduleList,
mode: 'edit'
@ -176,7 +176,7 @@ export default
'@': {
templateProvider: function(ScheduleList, generateList, ParentObject){
// include name of parent resource in listTitle
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('Schedules');
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('SCHEDULES');
let html = generateList.build({
list: ScheduleList,
mode: 'edit'
@ -266,7 +266,7 @@ export default
'@': {
templateProvider: function(ScheduleList, generateList, ParentObject){
// include name of parent resource in listTitle
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('Schedules');
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('SCHEDULES');
let html = generateList.build({
list: ScheduleList,
mode: 'edit'

View File

@ -1,7 +1,7 @@
<div id="htmlTemplate" class=" SchedulerFormPanel Panel" ng-hide="hideForm">
<div class="Form-header">
<div class="Form-title" ng-show="!isEdit">{{ schedulerName || "Add Schedule"}}</div>
<div class="Form-title" ng-show="isEdit">{{ schedulerName || "Edit Schedule"}}</div>
<div class="Form-title" ng-show="!isEdit">{{ schedulerName || "ADD SCHEDULE"}}</div>
<div class="Form-title" ng-show="isEdit">{{ schedulerName || "EDIT SCHEDULE"}}</div>
<div class="Form-header--fields"></div>
<div class="Form-exitHolder">
<button class="Form-exit" ng-click="formCancel()">

View File

@ -110,7 +110,12 @@ export default [
};
$scope.addSchedule = function() {
$state.go('.add');
if($state.current.name.endsWith('.edit')) {
$state.go('^.add');
}
else if(!$state.current.name.endsWith('.add')) {
$state.go('.add');
}
};
$scope.editSchedule = function(schedule) {
@ -118,7 +123,15 @@ export default [
routeToScheduleForm(schedule, 'edit');
}
else {
$state.go('.edit', { schedule_id: schedule.id });
if($state.current.name.endsWith('.add')) {
$state.go('^.edit', { schedule_id: schedule.id });
}
else if($state.current.name.endsWith('.edit')) {
$state.go('.', { schedule_id: schedule.id });
}
else {
$state.go('.edit', { schedule_id: schedule.id });
}
}
function buildStateMap(schedule){

View File

@ -662,6 +662,20 @@ angular.module('Utilities', ['RestServices', 'Utilities'])
$(element).select2(config);
// Don't toggle the dropdown when a multiselect option is
// being removed
if (multiple) {
$(element).on('select2:opening', (e) => {
var unselecting = $(e.target).data('select2-unselecting');
if (unselecting === true) {
$(e.target).removeData('select2-unselecting');
e.preventDefault();
}
}).on('select2:unselecting', (e) => {
$(e.target).data('select2-unselecting', true);
});
}
if (options) {
for (var d = 0; d < $(element + " option").length; d++) {
var item = $(element + " option")[d];

View File

@ -169,6 +169,15 @@ function(ConfigurationUtils, i18n, $rootScope) {
scope.imageData = $rootScope.custom_logo;
});
scope.$watch('imagePresent', (val) => {
if(val){
filePickerButton.html(removeText);
}
else{
filePickerButton.html(browseText);
}
});
scope.$on(fieldKey+'_reverted', function(e) {
scope.update(e, true);
});

View File

@ -52,6 +52,5 @@
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
display: flex;
}

View File

@ -48,7 +48,6 @@
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
display: flex;
}

View File

@ -2,7 +2,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header Form-header">
<div class="Form-title">Select {{list.iterator}}</div>
<div class="Form-title Form-title--uppercase">Select {{list.iterator}}</div>
<!-- optional: transclude header fields -->
<div class="Form-header--fields"></div>
<div class="Form-exitHolder">
@ -17,7 +17,7 @@
</div>
<div class="modal-footer">
<button ng-click="cancelForm()" class="Lookup-cancel btn btn-default">Cancel</button>
<button ng-click="saveForm()" class="Lookup-save btn btn-primary" ng-disabled="!currentSelection || !currentSelection.id" ng-bind="list.lookupConfirmText ? list.lookupConfirmText : 'Save'"></button>
<button ng-click="saveForm()" class="Lookup-save btn btn-primary" ng-disabled="!currentSelection || !currentSelection.id" ng-bind="list.lookupConfirmText ? list.lookupConfirmText : 'Select'"></button>
</div>
</div>
</div>

View File

@ -260,9 +260,15 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear
}.bind(this));
},
error(data, status) {
ProcessErrors($rootScope, null, status, null, {
if(data && data.detail){
let error = JSON.parse(data.detail);
if(_.isArray(error)){
data.detail = error[0];
}
}
ProcessErrors($rootScope, data, status, null, {
hdr: 'Error!',
msg: "Invalid search term entered."
msg: `Invalid search term entered. GET returned: ${status}`
});
}
};

View File

@ -1,5 +1,5 @@
<div>
<select class="form-control SurveyMaker-previewSelect" ng-model="selectedValue" multi-select ng-required="isRequired" ng-disabled="isDisabled">
<option ng-repeat="choice in choices" value="{{choice}}" ng-selected="selectedValue.indexOf(choice) !== -1">{{choice}}</option>
<option ng-repeat="choice in choices" value="{{choice}}">{{choice}}</option>
</select>
</div>

View File

@ -13,8 +13,8 @@
export default
{
addTitle: 'Add Survey Prompt',
editTitle: 'Edit Survey Prompt',
addTitle: 'ADD SURVEY PROMPT',
editTitle: 'EDIT SURVEY PROMPT',
titleClass: 'Form-secondaryTitle',
base: 'survey_question',
name: 'survey_question',

View File

@ -10,19 +10,21 @@
}
.SurveyMaker-header {
display: flex;
align-items: center;
}
.SurveyMaker-title {
align-items: center;
flex: 1 0 auto;
display: flex;
word-wrap: break-word;
word-break: break-all;
max-width: 98%;
}
.SurveyMaker-titleText {
color: @list-title-txt;
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
max-width: 75%;
}
.SurveyMaker-titleLockup {
margin-left: 4px;

View File

@ -114,3 +114,23 @@
padding: 1px 3px;
border-radius: 4px;
}
.WorkflowChart-nameText {
font-size: 10px;
}
.WorkflowChart-tooltipContents {
padding: 10px;
background-color: #707070;
color: #FFFFFF;
border-radius: 4px;
word-wrap: break-word;
max-width: 325px;
}
.WorkflowChart-tooltipArrow {
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #707070;
margin: auto;
}

View File

@ -23,12 +23,13 @@ export default [ '$state','moment', '$timeout', '$window',
let margin = {top: 20, right: 20, bottom: 20, left: 20},
i = 0,
nodeW = 120,
nodeW = 180,
nodeH = 60,
rootW = 60,
rootH = 40,
startNodeOffsetY = scope.mode === 'details' ? 17 : 10,
verticalSpaceBetweenNodes = 20,
maxNodeTextLength = 27,
windowHeight,
windowWidth,
tree,
@ -125,11 +126,8 @@ export default [ '$state','moment', '$timeout', '$window',
// TODO: this function is hacky and we need to come up with a better solution
// see: http://stackoverflow.com/questions/15975440/add-ellipses-to-overflowing-text-in-svg#answer-27723752
function wrap(text) {
let maxLength = scope.mode === 'details' ? 14 : 15;
if(text && text.length > maxLength) {
return text.substring(0,maxLength) + '...';
if(text && text.length > maxNodeTextLength) {
return text.substring(0,maxNodeTextLength) + '...';
}
else {
return text;
@ -216,7 +214,7 @@ export default [ '$state','moment', '$timeout', '$window',
links = tree.links(nodes);
let node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
d.y = d.depth * 180;
d.y = d.depth * 240;
return d.id || (d.id = ++i);
});
@ -347,11 +345,32 @@ export default [ '$state','moment', '$timeout', '$window',
.call(edit_node)
.on("mouseover", function(d) {
if(!d.isStartNode) {
let resourceName = (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
if(resourceName && resourceName.length > maxNodeTextLength) {
// Render the tooltip quickly in the dom and then remove. This lets us know how big the tooltip is so that we can place
// it properly on the workflow
let tooltipDimensionChecker = $("<div style='visibility:hidden;font-size:12px;position:absolute;' class='WorkflowChart-tooltipContents'><span>" + resourceName + "</span></div>");
$('body').append(tooltipDimensionChecker);
let tipWidth = $(tooltipDimensionChecker).outerWidth();
let tipHeight = $(tooltipDimensionChecker).outerHeight();
$(tooltipDimensionChecker).remove();
thisNode.append("foreignObject")
.attr("x", (nodeW / 2) - (tipWidth / 2))
.attr("y", (tipHeight + 15) * -1)
.attr("width", tipWidth)
.attr("height", tipHeight+20)
.attr("class", "WorkflowChart-tooltip")
.html(function(){
return "<div class='WorkflowChart-tooltipContents'><span>" + resourceName + "</span></div><div class='WorkflowChart-tooltipArrow'></div>";
});
}
d3.select("#node-" + d.id)
.classed("hovering", true);
}
})
.on("mouseout", function(d){
$('.WorkflowChart-tooltip').remove();
if(!d.isStartNode) {
d3.select("#node-" + d.id)
.classed("hovering", false);

View File

@ -15,7 +15,6 @@
font-size: 14px;
font-weight: bold;
margin-right: 10px;
text-transform: uppercase;
}
.WorkflowMaker-exitHolder {
justify-content: flex-end;

View File

@ -35,6 +35,12 @@
}
}
@media (max-width: @breakpoint-md) {
.WorkflowResults-rightSide {
height: inherit;
}
}
.WorkflowResults-stdoutActionButton--active {
display: none;
visibility: hidden;
@ -102,6 +108,24 @@
padding-right: 10px;
}
.WorkflowResults-panelRightTitle{
flex-wrap: wrap;
}
.WorkflowResults-panelRightTitleText{
word-wrap: break-word;
word-break: break-all;
max-width: 100%;
}
.WorkflowResults-badgeAndActionRow{
display:flex;
flex: 1 0 auto;
justify-content: flex-end;
flex-wrap: wrap;
max-width: 100%;
}
.WorkflowResults-badgeRow {
display: flex;
align-items: center;

View File

@ -213,8 +213,8 @@
<div class="Panel">
<!-- RIGHT PANE HEADER -->
<div class="StandardOut-panelHeader">
<div class="StandardOut-panelHeaderText">
<div class="StandardOut-panelHeader WorkflowResults-panelRightTitle">
<div class="StandardOut-panelHeaderText WorkflowResults-panelRightTitleText">
<i class="WorkflowResults-statusResultIcon
fa icon-job-{{ workflow.status }}"
ng-show="stdoutFullScreen"
@ -224,39 +224,40 @@
</i>
{{ workflow.name }}
</div>
<div class="WorkflowResults-badgeAndActionRow">
<!-- HEADER COUNTS -->
<div class="WorkflowResults-badgeRow">
<!-- PLAYS COUNT -->
<div class="WorkflowResults-badgeTitle">
Total Jobs
</div>
<span class="badge List-titleBadge">
{{ workflow_nodes.length || 0}}
</span>
<!-- HEADER COUNTS -->
<div class="WorkflowResults-badgeRow">
<!-- PLAYS COUNT -->
<div class="WorkflowResults-badgeTitle">
Total Jobs
<!-- ELAPSED TIME -->
<div class="WorkflowResults-badgeTitle">
Elapsed
</div>
<span class="badge List-titleBadge">
{{ workflow.elapsed * 1000 | duration: "hh:mm:ss"}}
</span>
</div>
<span class="badge List-titleBadge">
{{ workflow_nodes.length || 0}}
</span>
<!-- ELAPSED TIME -->
<div class="WorkflowResults-badgeTitle">
Elapsed
<!-- HEADER ACTIONS -->
<div class="StandardOut-panelHeaderActions">
<!-- FULL-SCREEN TOGGLE ACTION -->
<button class="StandardOut-actionButton"
aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}"
data-tip-watch="toggleStdoutFullscreenTooltip"
data-placement="top"
ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"
ng-click="toggleStdoutFullscreen()">
<i class="fa fa-arrows-alt"></i>
</button>
</div>
<span class="badge List-titleBadge">
{{ workflow.elapsed * 1000 | duration: "hh:mm:ss"}}
</span>
</div>
<!-- HEADER ACTIONS -->
<div class="StandardOut-panelHeaderActions">
<!-- FULL-SCREEN TOGGLE ACTION -->
<button class="StandardOut-actionButton"
aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}"
data-tip-watch="toggleStdoutFullscreenTooltip"
data-placement="top"
ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"
ng-click="toggleStdoutFullscreen()">
<i class="fa fa-arrows-alt"></i>
</button>
</div>
</div>
<workflow-status-bar></workflow-status-bar>

View File

@ -1,4 +1,4 @@
version: '2'
version: '3'
services:
# Primary Tower Development Container
tower:
@ -20,13 +20,17 @@ services:
- postgres
- memcached
- rabbitmq
- logstash
# - sync
# volumes_from:
# - sync
volumes:
- "../:/tower_devel"
privileged: true
logstash:
build:
context: ./docker-compose
dockerfile: Dockerfile-logstash
# Postgres Database Container
postgres:
image: postgres:9.4.1
@ -38,16 +42,3 @@ services:
image: rabbitmq:3-management
ports:
- "15672:15672"
# Source Code Synchronization Container
# sync:
# build:
# context: ./docker-compose
# dockerfile: Dockerfile-sync
# command: "lsyncd -delay 1 -nodaemon -rsync /src /tower_devel"
# volumes:
# - /tower_devel
# - "../:/src"
# working_dir: /src
# stdin_open: true
# tty: true

View File

@ -0,0 +1,3 @@
FROM logstash:5-alpine
COPY logstash.conf /
CMD ["-f", "/logstash.conf"]

View File

@ -0,0 +1,19 @@
input {
http {
port => 8085
user => awx_logger
password => "workflows"
}
}
## Add your filters / logstash plugins configuration here
filter {
json {
source => "message"
}
}
output {
stdout { codec => rubydebug }
}

View File

@ -1,4 +1,4 @@
FROM logstash:5
# Add your logstash plugins setup here
# Example: RUN logstash-plugin install logstash-filter-json
# Example: RUN logstash-plugin install logstash-filter-json