Merge pull request #42 from ansible/devel

Rebase
This commit is contained in:
Sean Sullivan
2021-02-20 05:58:30 -06:00
committed by GitHub
75 changed files with 947 additions and 4141 deletions

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
---
version: 2
updates:
- package-ecosystem: "pip"
directory: "/requirements"
schedule:
interval: "monthly"

View File

@@ -2,6 +2,19 @@
This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/<version>`. This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/<version>`.
# (TBD)
- Added user interface for the activity stream: https://github.com/ansible/awx/pull/9083
- Converted many of the top-level list views (Jobs, Teams, Hosts, Inventories, Projects, and more) to a new, permanent table component for substantially increased responsiveness, usability, maintainability, and other 'ility's: https://github.com/ansible/awx/pull/8970, https://github.com/ansible/awx/pull/9182 and many others!
- Added click-to-expand details for job tables
- Add search filtering to job output https://github.com/ansible/awx/pull/9208
- Added the new migration, update, and "installation in progress" page https://github.com/ansible/awx/pull/9123
- Added the user interface for job settings https://github.com/ansible/awx/pull/8661
- Runtime errors from jobs are now displayed, along with an explanation for what went wrong, on the output page https://github.com/ansible/awx/pull/9264
- You can now cancel a running job from its output and details panel https://github.com/ansible/awx/pull/9199
- Fixed a bug where launch prompt inputs were unexpectedly deposited in the url: https://github.com/ansible/awx/pull/9231
- Playbook, credential type, and inventory file inputs now support type-ahead and manual type-in! https://github.com/ansible/awx/pull/9120
- Added ability to relaunch against failed hosts: https://github.com/ansible/awx/pull/9225
# 17.0.1 (January 26, 2021) # 17.0.1 (January 26, 2021)
- Fixed pgdocker directory permissions issue with Local Docker installer: https://github.com/ansible/awx/pull/9152 - Fixed pgdocker directory permissions issue with Local Docker installer: https://github.com/ansible/awx/pull/9152
- Fixed a bug in the UI which caused toggle settings to not be changed when clicked: https://github.com/ansible/awx/pull/9093 - Fixed a bug in the UI which caused toggle settings to not be changed when clicked: https://github.com/ansible/awx/pull/9093

View File

@@ -515,7 +515,7 @@ ui-zuul-lint-and-test:
$(NPM_BIN) --prefix awx/ui_next install $(NPM_BIN) --prefix awx/ui_next install
$(NPM_BIN) run --prefix awx/ui_next lint $(NPM_BIN) run --prefix awx/ui_next lint
$(NPM_BIN) run --prefix awx/ui_next prettier-check $(NPM_BIN) run --prefix awx/ui_next prettier-check
$(NPM_BIN) run --prefix awx/ui_next test $(NPM_BIN) run --prefix awx/ui_next test -- --coverage --watchAll=false
# Build a pip-installable package into dist/ with a timestamped version number. # Build a pip-installable package into dist/ with a timestamped version number.

View File

@@ -6,6 +6,7 @@ from multiprocessing import Process
from django.conf import settings from django.conf import settings
from django.db import connections from django.db import connections
from schedule import Scheduler from schedule import Scheduler
from django_guid.middleware import GuidMiddleware
from awx.main.dispatch.worker import TaskWorker from awx.main.dispatch.worker import TaskWorker
@@ -35,6 +36,7 @@ class Scheduler(Scheduler):
# If the database connection has a hiccup, re-establish a new # If the database connection has a hiccup, re-establish a new
# connection # connection
conn.close_if_unusable_or_obsolete() conn.close_if_unusable_or_obsolete()
GuidMiddleware.set_guid(GuidMiddleware._generate_guid())
self.run_pending() self.run_pending()
except Exception: except Exception:
logger.exception( logger.exception(

View File

@@ -16,6 +16,7 @@ from queue import Full as QueueFull, Empty as QueueEmpty
from django.conf import settings from django.conf import settings
from django.db import connection as django_connection, connections from django.db import connection as django_connection, connections
from django.core.cache import cache as django_cache from django.core.cache import cache as django_cache
from django_guid.middleware import GuidMiddleware
from jinja2 import Template from jinja2 import Template
import psutil import psutil
@@ -445,6 +446,8 @@ class AutoscalePool(WorkerPool):
return super(AutoscalePool, self).up() return super(AutoscalePool, self).up()
def write(self, preferred_queue, body): def write(self, preferred_queue, body):
if 'guid' in body:
GuidMiddleware.set_guid(body['guid'])
try: try:
# when the cluster heartbeat occurs, clean up internally # when the cluster heartbeat occurs, clean up internally
if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']: if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']:

View File

@@ -5,6 +5,7 @@ import json
from uuid import uuid4 from uuid import uuid4
from django.conf import settings from django.conf import settings
from django_guid.middleware import GuidMiddleware
from . import pg_bus_conn from . import pg_bus_conn
@@ -83,6 +84,9 @@ class task:
'kwargs': kwargs, 'kwargs': kwargs,
'task': cls.name 'task': cls.name
} }
guid = GuidMiddleware.get_guid()
if guid:
obj['guid'] = guid
obj.update(**kw) obj.update(**kw)
if callable(queue): if callable(queue):
queue = queue() queue = queue()

View File

@@ -9,6 +9,7 @@ from django.conf import settings
from django.utils.timezone import now as tz_now from django.utils.timezone import now as tz_now
from django.db import DatabaseError, OperationalError, connection as django_connection from django.db import DatabaseError, OperationalError, connection as django_connection
from django.db.utils import InterfaceError, InternalError from django.db.utils import InterfaceError, InternalError
from django_guid.middleware import GuidMiddleware
import psutil import psutil
@@ -152,6 +153,8 @@ class CallbackBrokerWorker(BaseWorker):
if body.get('event') == 'EOF': if body.get('event') == 'EOF':
try: try:
if 'guid' in body:
GuidMiddleware.set_guid(body['guid'])
final_counter = body.get('final_counter', 0) final_counter = body.get('final_counter', 0)
logger.info('Event processing is finished for Job {}, sending notifications'.format(job_identifier)) logger.info('Event processing is finished for Job {}, sending notifications'.format(job_identifier))
# EOF events are sent when stdout for the running task is # EOF events are sent when stdout for the running task is
@@ -176,6 +179,8 @@ class CallbackBrokerWorker(BaseWorker):
handle_success_and_failure_notifications.apply_async([uj.id]) handle_success_and_failure_notifications.apply_async([uj.id])
except Exception: except Exception:
logger.exception('Worker failed to emit notifications: Job {}'.format(job_identifier)) logger.exception('Worker failed to emit notifications: Job {}'.format(job_identifier))
finally:
GuidMiddleware.set_guid('')
return return
event = cls.create_from_data(**body) event = cls.create_from_data(**body)

View File

@@ -6,6 +6,8 @@ import traceback
from kubernetes.config import kube_config from kubernetes.config import kube_config
from django_guid.middleware import GuidMiddleware
from awx.main.tasks import dispatch_startup, inform_cluster_of_shutdown from awx.main.tasks import dispatch_startup, inform_cluster_of_shutdown
from .base import BaseWorker from .base import BaseWorker
@@ -52,6 +54,8 @@ class TaskWorker(BaseWorker):
uuid = body.get('uuid', '<unknown>') uuid = body.get('uuid', '<unknown>')
args = body.get('args', []) args = body.get('args', [])
kwargs = body.get('kwargs', {}) kwargs = body.get('kwargs', {})
if 'guid' in body:
GuidMiddleware.set_guid(body.pop('guid'))
_call = TaskWorker.resolve_callable(task) _call = TaskWorker.resolve_callable(task)
if inspect.isclass(_call): if inspect.isclass(_call):
# the callable is a class, e.g., RunJob; instantiate and # the callable is a class, e.g., RunJob; instantiate and

View File

@@ -92,8 +92,8 @@ def _openstack_data(cred):
}, },
} }
if cred.has_input('project_region_name'): if cred.has_input('region'):
openstack_data['clouds']['devstack']['region_name'] = cred.get_input('project_region_name', default='') openstack_data['clouds']['devstack']['region_name'] = cred.get_input('region', default='')
return openstack_data return openstack_data

View File

@@ -388,12 +388,17 @@ class JobNotificationMixin(object):
and a url to the job run.""" and a url to the job run."""
job_context = {'host_status_counts': {}} job_context = {'host_status_counts': {}}
summary = None summary = None
if hasattr(self, 'job_host_summaries'): try:
summary = self.job_host_summaries.first() has_event_property = any([f for f in self.event_class._meta.fields if f.name == 'event'])
if summary: except NotImplementedError:
from awx.api.serializers import JobHostSummarySerializer has_event_property = False
summary_data = JobHostSummarySerializer(summary).to_representation(summary) if has_event_property:
job_context['host_status_counts'] = summary_data qs = self.get_event_queryset()
if qs:
event = qs.only('event_data').filter(event='playbook_on_stats').first()
if event:
summary = event.get_host_status_counts()
job_context['host_status_counts'] = summary
context = { context = {
'job': job_context, 'job': job_context,
'job_friendly_name': self.get_notification_friendly_name(), 'job_friendly_name': self.get_notification_friendly_name(),

View File

@@ -34,6 +34,7 @@ from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _, gettext_noop from django.utils.translation import ugettext_lazy as _, gettext_noop
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django_guid.middleware import GuidMiddleware
# Kubernetes # Kubernetes
from kubernetes.client.rest import ApiException from kubernetes.client.rest import ApiException
@@ -839,6 +840,7 @@ class BaseTask(object):
self.cleanup_paths = [] self.cleanup_paths = []
self.parent_workflow_job_id = None self.parent_workflow_job_id = None
self.host_map = {} self.host_map = {}
self.guid = GuidMiddleware.get_guid()
def update_model(self, pk, _attempt=0, **updates): def update_model(self, pk, _attempt=0, **updates):
"""Reload the model instance from the database and update the """Reload the model instance from the database and update the
@@ -1274,6 +1276,9 @@ class BaseTask(object):
except json.JSONDecodeError: except json.JSONDecodeError:
pass pass
if 'event_data' in event_data:
event_data['event_data']['guid'] = self.guid
event_data.setdefault(self.event_data_key, self.instance.id) event_data.setdefault(self.event_data_key, self.instance.id)
self.dispatcher.dispatch(event_data) self.dispatcher.dispatch(event_data)
self.event_ct += 1 self.event_ct += 1
@@ -1310,6 +1315,7 @@ class BaseTask(object):
event_data = { event_data = {
'event': 'EOF', 'event': 'EOF',
'final_counter': self.event_ct, 'final_counter': self.event_ct,
'guid': self.guid,
} }
event_data.setdefault(self.event_data_key, self.instance.id) event_data.setdefault(self.event_data_key, self.instance.id)
self.dispatcher.dispatch(event_data) self.dispatcher.dispatch(event_data)

View File

@@ -8,4 +8,5 @@ clouds:
project_name: fooo project_name: fooo
username: fooo username: fooo
private: true private: true
region_name: fooo
verify: false verify: false

View File

@@ -1,283 +0,0 @@
{
"ansible_all_ipv4_addresses": [
"172.17.0.7"
],
"ansible_all_ipv6_addresses": [
"fe80::42:acff:fe11:7"
],
"ansible_architecture": "x86_64",
"ansible_bios_date": "12/01/2006",
"ansible_bios_version": "VirtualBox",
"ansible_cmdline": {
"BOOT_IMAGE": "/boot/vmlinuz64",
"base": true,
"console": "tty0",
"initrd": "/boot/initrd.img",
"loglevel": "3",
"noembed": true,
"nomodeset": true,
"norestore": true,
"user": "docker",
"waitusb": "10:LABEL=boot2docker-data"
},
"ansible_date_time": {
"date": "2016-02-02",
"day": "02",
"epoch": "1454424257",
"hour": "14",
"iso8601": "2016-02-02T14:44:17Z",
"iso8601_basic": "20160202T144417348424",
"iso8601_basic_short": "20160202T144417",
"iso8601_micro": "2016-02-02T14:44:17.348496Z",
"minute": "44",
"month": "02",
"second": "17",
"time": "14:44:17",
"tz": "UTC",
"tz_offset": "+0000",
"weekday": "Tuesday",
"weekday_number": "2",
"weeknumber": "05",
"year": "2016"
},
"ansible_default_ipv4": {
"address": "172.17.0.7",
"alias": "eth0",
"broadcast": "global",
"gateway": "172.17.0.1",
"interface": "eth0",
"macaddress": "02:42:ac:11:00:07",
"mtu": 1500,
"netmask": "255.255.0.0",
"network": "172.17.0.0",
"type": "ether"
},
"ansible_default_ipv6": {},
"ansible_devices": {
"sda": {
"holders": [],
"host": "",
"model": "VBOX HARDDISK",
"partitions": {
"sda1": {
"sectors": "510015555",
"sectorsize": 512,
"size": "243.19 GB",
"start": "1975995"
},
"sda2": {
"sectors": "1975932",
"sectorsize": 512,
"size": "964.81 MB",
"start": "63"
}
},
"removable": "0",
"rotational": "0",
"scheduler_mode": "deadline",
"sectors": "512000000",
"sectorsize": "512",
"size": "244.14 GB",
"support_discard": "0",
"vendor": "ATA"
},
"sr0": {
"holders": [],
"host": "",
"model": "CD-ROM",
"partitions": {},
"removable": "1",
"rotational": "1",
"scheduler_mode": "deadline",
"sectors": "61440",
"sectorsize": "2048",
"size": "120.00 MB",
"support_discard": "0",
"vendor": "VBOX"
}
},
"ansible_distribution": "Ubuntu",
"ansible_distribution_major_version": "14",
"ansible_distribution_release": "trusty",
"ansible_distribution_version": "14.04",
"ansible_dns": {
"nameservers": [
"8.8.8.8"
]
},
"ansible_domain": "",
"ansible_env": {
"HOME": "/root",
"HOSTNAME": "ede894599989",
"LANG": "en_US.UTF-8",
"LC_ALL": "en_US.UTF-8",
"LC_MESSAGES": "en_US.UTF-8",
"LESSCLOSE": "/usr/bin/lesspipe %s %s",
"LESSOPEN": "| /usr/bin/lesspipe %s",
"LS_COLORS": "",
"OLDPWD": "/ansible",
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"PWD": "/ansible/examples",
"SHLVL": "1",
"_": "/usr/local/bin/ansible",
"container": "docker"
},
"ansible_eth0": {
"active": true,
"device": "eth0",
"ipv4": {
"address": "172.17.0.7",
"broadcast": "global",
"netmask": "255.255.0.0",
"network": "172.17.0.0"
},
"ipv6": [
{
"address": "fe80::42:acff:fe11:7",
"prefix": "64",
"scope": "link"
}
],
"macaddress": "02:42:ac:11:00:07",
"mtu": 1500,
"promisc": false,
"type": "ether"
},
"ansible_fips": false,
"ansible_form_factor": "Other",
"ansible_fqdn": "ede894599989",
"ansible_hostname": "ede894599989",
"ansible_interfaces": [
"lo",
"eth0"
],
"ansible_kernel": "4.1.12-boot2docker",
"ansible_lo": {
"active": true,
"device": "lo",
"ipv4": {
"address": "127.0.0.1",
"broadcast": "host",
"netmask": "255.0.0.0",
"network": "127.0.0.0"
},
"ipv6": [
{
"address": "::1",
"prefix": "128",
"scope": "host"
}
],
"mtu": 65536,
"promisc": false,
"type": "loopback"
},
"ansible_lsb": {
"codename": "trusty",
"description": "Ubuntu 14.04.3 LTS",
"id": "Ubuntu",
"major_release": "14",
"release": "14.04"
},
"ansible_machine": "x86_64",
"ansible_memfree_mb": 3746,
"ansible_memory_mb": {
"nocache": {
"free": 8896,
"used": 3638
},
"real": {
"free": 3746,
"total": 12534,
"used": 8788
},
"swap": {
"cached": 0,
"free": 4048,
"total": 4048,
"used": 0
}
},
"ansible_memtotal_mb": 12534,
"ansible_mounts": [
{
"device": "/dev/sda1",
"fstype": "ext4",
"mount": "/etc/resolv.conf",
"options": "rw,relatime,data=ordered",
"size_available": 201281392640,
"size_total": 256895700992,
"uuid": "NA"
},
{
"device": "/dev/sda1",
"fstype": "ext4",
"mount": "/etc/hostname",
"options": "rw,relatime,data=ordered",
"size_available": 201281392640,
"size_total": 256895700992,
"uuid": "NA"
},
{
"device": "/dev/sda1",
"fstype": "ext4",
"mount": "/etc/hosts",
"options": "rw,relatime,data=ordered",
"size_available": 201281392640,
"size_total": 256895700992,
"uuid": "NA"
}
],
"ansible_nodename": "ede894599989",
"ansible_os_family": "Debian",
"ansible_pkg_mgr": "apt",
"ansible_processor": [
"GenuineIntel",
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
"GenuineIntel",
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
"GenuineIntel",
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
"GenuineIntel",
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
"GenuineIntel",
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
"GenuineIntel",
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
"GenuineIntel",
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz",
"GenuineIntel",
"Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz"
],
"ansible_processor_cores": 8,
"ansible_processor_count": 1,
"ansible_processor_threads_per_core": 1,
"ansible_processor_vcpus": 8,
"ansible_product_name": "VirtualBox",
"ansible_product_serial": "0",
"ansible_product_uuid": "25C5EA5A-1DF1-48D9-A2C6-81227DA153C0",
"ansible_product_version": "1.2",
"ansible_python_version": "2.7.6",
"ansible_selinux": false,
"ansible_service_mgr": "upstart",
"ansible_ssh_host_key_dsa_public": "AAAAB3NzaC1kc3MAAACBALF0xsM8UMXgSKiWNw4t19wxbxLnxQX742t/dIM0O8YLx+/lIP+Q69Dv5uoVt0zKV39eFziRlCh96qj2KYkGEJ6XfVZFnhpculL2Pv2CPpSwKuQ1vTbDO/xxUrvY+bHpfNJf9Rh69bFEE2pTsjomFPCgp8M0qGaFtwg6czSaeBONAAAAFQCGEfVtj97JiexTVRqgQITYlFp/eQAAAIEAg+S9qWn+AIb3amwVoLL/usQYOPCmZY9RVPzpkjJ6OG+HI4B7cXeauPtNTJwT0f9vGEqzf4mPpmS+aCShj6iwdmJ+cOwR5+SJlNalab3CMBoXKVLbT1J2XWFlK0szKKnoReP96IDbkAkGQ3fkm4jz0z6Wy0u6wOQVNcd4G5cwLZ4AAACAFvBm+H1LwNrwWBjWio+ayhglZ4Y25mLMEn2+dqBz0gLK5szEbft1HMPOWIVHvl6vi3v34pAJHKpxXpkLlNliTn8iw9BzCOrgP4V8sp2/85mxEuCdI1w/QERj9cHu5iS2pZ0cUwDE3pfuuGBB3IEliaJyaapowdrM8lN12jQl11E=",
"ansible_ssh_host_key_ecdsa_public": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHiYp4e9RfXpxDcEWpK4EuXPHW9++xcFI9hiB0TYAZgxEF9RIgwfucpPawFk7HIFoNc7EXQMlryilLSbg155KWM=",
"ansible_ssh_host_key_ed25519_public": "AAAAC3NzaC1lZDI1NTE5AAAAILclD2JaC654azEsAfcHRIOA2Ig9/Qk6MX80i/VCEdSH",
"ansible_ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDeSUGxZaZsgBsezld0mj3HcbAwx6aykGnejceBjcs6lVwSGMHevofzSXIQDPYBhZoyWNl0PYAHv6AsQ8+3khd2SitUMJAuHSz1ZjgHCCGQP9ijXTKHn+lWCKA8rhLG/dwYwiouoOPZfn1G+erbKO6XiVbELrrf2RadnMGuMinESIOKVj3IunXsaGRMsDOQferOnUf7MvH7xpQnoySyQ1+p4rGruaohWG+Y2cDo7+B2FylPVbrpRDDJkfbt4J96WHx0KOdD0qzOicQP8JqDflqQPJJCWcgrvjQOSe4gXdPB6GZDtBl2qgQRwt1IgizPMm+b7Bwbd2VDe1TeWV2gT/7H",
"ansible_swapfree_mb": 4048,
"ansible_swaptotal_mb": 4048,
"ansible_system": "Linux",
"ansible_system_vendor": "innotek GmbH",
"ansible_uptime_seconds": 178398,
"ansible_user_dir": "/root",
"ansible_user_gecos": "root",
"ansible_user_gid": 0,
"ansible_user_id": "root",
"ansible_user_shell": "/bin/bash",
"ansible_user_uid": 0,
"ansible_userspace_architecture": "x86_64",
"ansible_userspace_bits": "64",
"ansible_virtualization_role": "guest",
"ansible_virtualization_type": "docker",
"module_setup": true
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,697 +0,0 @@
[
{
"source": "sysv",
"state": "running",
"name": "iprdump"
},
{
"source": "sysv",
"state": "running",
"name": "iprinit"
},
{
"source": "sysv",
"state": "running",
"name": "iprupdate"
},
{
"source": "sysv",
"state": "stopped",
"name": "netconsole"
},
{
"source": "sysv",
"state": "running",
"name": "network"
},
{
"source": "systemd",
"state": "stopped",
"name": "arp-ethers.service"
},
{
"source": "systemd",
"state": "running",
"name": "auditd.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "autovt@.service"
},
{
"source": "systemd",
"state": "running",
"name": "avahi-daemon.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "blk-availability.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "brandbot.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "console-getty.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "console-shell.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "cpupower.service"
},
{
"source": "systemd",
"state": "running",
"name": "crond.service"
},
{
"source": "systemd",
"state": "running",
"name": "dbus-org.fedoraproject.FirewallD1.service"
},
{
"source": "systemd",
"state": "running",
"name": "dbus-org.freedesktop.Avahi.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dbus-org.freedesktop.hostname1.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dbus-org.freedesktop.locale1.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dbus-org.freedesktop.login1.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dbus-org.freedesktop.machine1.service"
},
{
"source": "systemd",
"state": "running",
"name": "dbus-org.freedesktop.NetworkManager.service"
},
{
"source": "systemd",
"state": "running",
"name": "dbus-org.freedesktop.nm-dispatcher.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dbus-org.freedesktop.timedate1.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dbus.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "debug-shell.service"
},
{
"source": "systemd",
"state": "running",
"name": "dhcpd.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dhcpd6.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dhcrelay.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dm-event.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dnsmasq.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dracut-cmdline.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dracut-initqueue.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dracut-mount.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dracut-pre-mount.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dracut-pre-pivot.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dracut-pre-trigger.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dracut-pre-udev.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "dracut-shutdown.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "ebtables.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "emergency.service"
},
{
"source": "systemd",
"state": "running",
"name": "firewalld.service"
},
{
"source": "systemd",
"state": "running",
"name": "getty@.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "halt-local.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "initrd-cleanup.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "initrd-parse-etc.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "initrd-switch-root.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "initrd-udevadm-cleanup-db.service"
},
{
"source": "systemd",
"state": "running",
"name": "irqbalance.service"
},
{
"source": "systemd",
"state": "running",
"name": "kdump.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "kmod-static-nodes.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "lvm2-lvmetad.service"
},
{
"source": "systemd",
"state": "running",
"name": "lvm2-monitor.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "lvm2-pvscan@.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "messagebus.service"
},
{
"source": "systemd",
"state": "running",
"name": "microcode.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "named-setup-rndc.service"
},
{
"source": "systemd",
"state": "running",
"name": "named.service"
},
{
"source": "systemd",
"state": "running",
"name": "NetworkManager-dispatcher.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "NetworkManager-wait-online.service"
},
{
"source": "systemd",
"state": "running",
"name": "NetworkManager.service"
},
{
"source": "systemd",
"state": "running",
"name": "ntpd.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "ntpdate.service"
},
{
"source": "systemd",
"state": "running",
"name": "openvpn@.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "plymouth-halt.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "plymouth-kexec.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "plymouth-poweroff.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "plymouth-quit-wait.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "plymouth-quit.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "plymouth-read-write.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "plymouth-reboot.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "plymouth-start.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "plymouth-switch-root.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "polkit.service"
},
{
"source": "systemd",
"state": "running",
"name": "postfix.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "quotaon.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rc-local.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rdisc.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rescue.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rhel-autorelabel-mark.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rhel-autorelabel.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rhel-configure.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rhel-dmesg.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rhel-domainname.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rhel-import-state.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rhel-loadmodules.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "rhel-readonly.service"
},
{
"source": "systemd",
"state": "running",
"name": "rsyslog.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "serial-getty@.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "sshd-keygen.service"
},
{
"source": "systemd",
"state": "running",
"name": "sshd.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "sshd@.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-ask-password-console.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-ask-password-plymouth.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-ask-password-wall.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-backlight@.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-binfmt.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-fsck-root.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-fsck@.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-halt.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-hibernate.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-hostnamed.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-hybrid-sleep.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-initctl.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-journal-flush.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-journald.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-kexec.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-localed.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-logind.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-machined.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-modules-load.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-nspawn@.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-poweroff.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-quotacheck.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-random-seed.service"
},
{
"source": "systemd",
"state": "running",
"name": "systemd-readahead-collect.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-readahead-done.service"
},
{
"source": "systemd",
"state": "running",
"name": "systemd-readahead-drop.service"
},
{
"source": "systemd",
"state": "running",
"name": "systemd-readahead-replay.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-reboot.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-remount-fs.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-shutdownd.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-suspend.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-sysctl.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-timedated.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-tmpfiles-clean.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-tmpfiles-setup-dev.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-tmpfiles-setup.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-udev-settle.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-udev-trigger.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-udevd.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-update-utmp-runlevel.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-update-utmp.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-user-sessions.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "systemd-vconsole-setup.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "teamd@.service"
},
{
"source": "systemd",
"state": "running",
"name": "tuned.service"
},
{
"source": "systemd",
"state": "running",
"name": "vmtoolsd.service"
},
{
"source": "systemd",
"state": "stopped",
"name": "wpa_supplicant.service"
}
]

View File

@@ -249,7 +249,7 @@ def test_openstack_client_config_generation_with_project_domain_name(mocker, sou
@pytest.mark.parametrize("source,expected", [ @pytest.mark.parametrize("source,expected", [
(None, True), (False, False), (True, True) (None, True), (False, False), (True, True)
]) ])
def test_openstack_client_config_generation_with_project_region_name(mocker, source, expected, private_data_dir): def test_openstack_client_config_generation_with_region(mocker, source, expected, private_data_dir):
update = tasks.RunInventoryUpdate() update = tasks.RunInventoryUpdate()
credential_type = CredentialType.defaults['openstack']() credential_type = CredentialType.defaults['openstack']()
inputs = { inputs = {
@@ -259,7 +259,7 @@ def test_openstack_client_config_generation_with_project_region_name(mocker, sou
'project': 'demo-project', 'project': 'demo-project',
'domain': 'my-demo-domain', 'domain': 'my-demo-domain',
'project_domain_name': 'project-domain', 'project_domain_name': 'project-domain',
'project_region_name': 'region-name', 'region': 'region-name',
} }
if source is not None: if source is not None:
inputs['verify_ssl'] = source inputs['verify_ssl'] = source
@@ -527,7 +527,7 @@ class TestGenericRun():
task.instance = Job(pk=1, id=1) task.instance = Job(pk=1, id=1)
task.event_ct = 17 task.event_ct = 17
task.finished_callback(None) task.finished_callback(None)
task.dispatcher.dispatch.assert_called_with({'event': 'EOF', 'final_counter': 17, 'job_id': 1}) task.dispatcher.dispatch.assert_called_with({'event': 'EOF', 'final_counter': 17, 'job_id': 1, 'guid': None})
def test_save_job_metadata(self, job, update_model_wrapper): def test_save_job_metadata(self, job, update_model_wrapper):
class MockMe(): class MockMe():

View File

@@ -15,6 +15,10 @@ from django.apps import apps
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django_guid.log_filters import CorrelationId
from django_guid.middleware import GuidMiddleware
from awx import MODE
from awx.main.constants import LOGGER_BLOCKLIST from awx.main.constants import LOGGER_BLOCKLIST
from awx.main.utils.common import get_search_fields from awx.main.utils.common import get_search_fields
@@ -364,3 +368,14 @@ class SmartFilter(object):
return res[0].result return res[0].result
raise RuntimeError("Parsing the filter_string %s went terribly wrong" % filter_string) raise RuntimeError("Parsing the filter_string %s went terribly wrong" % filter_string)
class DefaultCorrelationId(CorrelationId):
def filter(self, record):
guid = GuidMiddleware.get_guid() or '-'
if MODE == 'development':
guid = guid[:8]
record.guid = guid
return True

View File

@@ -154,6 +154,9 @@ class LogstashFormatter(LogstashFormatterBase):
if kind == 'job_events' and raw_data.get('python_objects', {}).get('job_event'): if kind == 'job_events' and raw_data.get('python_objects', {}).get('job_event'):
job_event = raw_data['python_objects']['job_event'] job_event = raw_data['python_objects']['job_event']
guid = job_event.event_data.pop('guid', None)
if guid:
data_for_log['guid'] = guid
for field_object in job_event._meta.fields: for field_object in job_event._meta.fields:
if not field_object.__class__ or not field_object.__class__.__name__: if not field_object.__class__ or not field_object.__class__.__name__:

View File

@@ -285,6 +285,7 @@ INSTALLED_APPS = [
'polymorphic', 'polymorphic',
'taggit', 'taggit',
'social_django', 'social_django',
'django_guid',
'corsheaders', 'corsheaders',
'awx.conf', 'awx.conf',
'awx.main', 'awx.main',
@@ -828,11 +829,14 @@ LOGGING = {
}, },
'dynamic_level_filter': { 'dynamic_level_filter': {
'()': 'awx.main.utils.filters.DynamicLevelFilter' '()': 'awx.main.utils.filters.DynamicLevelFilter'
} },
'guid': {
'()': 'awx.main.utils.filters.DefaultCorrelationId'
},
}, },
'formatters': { 'formatters': {
'simple': { 'simple': {
'format': '%(asctime)s %(levelname)-8s %(name)s %(message)s', 'format': '%(asctime)s %(levelname)-8s [%(guid)s] %(name)s %(message)s',
}, },
'json': { 'json': {
'()': 'awx.main.utils.formatters.LogstashFormatter' '()': 'awx.main.utils.formatters.LogstashFormatter'
@@ -842,7 +846,7 @@ LOGGING = {
'format': '%(relativeSeconds)9.3f %(levelname)-8s %(message)s' 'format': '%(relativeSeconds)9.3f %(levelname)-8s %(message)s'
}, },
'dispatcher': { 'dispatcher': {
'format': '%(asctime)s %(levelname)-8s %(name)s PID:%(process)d %(message)s', 'format': '%(asctime)s %(levelname)-8s [%(guid)s] %(name)s PID:%(process)d %(message)s',
}, },
'job_lifecycle': { 'job_lifecycle': {
'()': 'awx.main.utils.formatters.JobLifeCycleFormatter', '()': 'awx.main.utils.formatters.JobLifeCycleFormatter',
@@ -852,7 +856,7 @@ LOGGING = {
'console': { 'console': {
'()': 'logging.StreamHandler', '()': 'logging.StreamHandler',
'level': 'DEBUG', 'level': 'DEBUG',
'filters': ['require_debug_true_or_test'], 'filters': ['require_debug_true_or_test', 'guid'],
'formatter': 'simple', 'formatter': 'simple',
}, },
'null': { 'null': {
@@ -872,33 +876,33 @@ LOGGING = {
'class': 'awx.main.utils.handlers.RSysLogHandler', 'class': 'awx.main.utils.handlers.RSysLogHandler',
'formatter': 'json', 'formatter': 'json',
'address': '/var/run/awx-rsyslog/rsyslog.sock', 'address': '/var/run/awx-rsyslog/rsyslog.sock',
'filters': ['external_log_enabled', 'dynamic_level_filter'], 'filters': ['external_log_enabled', 'dynamic_level_filter', 'guid'],
}, },
'tower_warnings': { 'tower_warnings': {
# don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL # don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL
'class': 'logging.handlers.WatchedFileHandler', 'class': 'logging.handlers.WatchedFileHandler',
'filters': ['require_debug_false', 'dynamic_level_filter'], 'filters': ['require_debug_false', 'dynamic_level_filter', 'guid'],
'filename': os.path.join(LOG_ROOT, 'tower.log'), 'filename': os.path.join(LOG_ROOT, 'tower.log'),
'formatter':'simple', 'formatter':'simple',
}, },
'callback_receiver': { 'callback_receiver': {
# don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL # don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL
'class': 'logging.handlers.WatchedFileHandler', 'class': 'logging.handlers.WatchedFileHandler',
'filters': ['require_debug_false', 'dynamic_level_filter'], 'filters': ['require_debug_false', 'dynamic_level_filter', 'guid'],
'filename': os.path.join(LOG_ROOT, 'callback_receiver.log'), 'filename': os.path.join(LOG_ROOT, 'callback_receiver.log'),
'formatter':'simple', 'formatter':'simple',
}, },
'dispatcher': { 'dispatcher': {
# don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL # don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL
'class': 'logging.handlers.WatchedFileHandler', 'class': 'logging.handlers.WatchedFileHandler',
'filters': ['require_debug_false', 'dynamic_level_filter'], 'filters': ['require_debug_false', 'dynamic_level_filter', 'guid'],
'filename': os.path.join(LOG_ROOT, 'dispatcher.log'), 'filename': os.path.join(LOG_ROOT, 'dispatcher.log'),
'formatter':'dispatcher', 'formatter':'dispatcher',
}, },
'wsbroadcast': { 'wsbroadcast': {
# don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL # don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL
'class': 'logging.handlers.WatchedFileHandler', 'class': 'logging.handlers.WatchedFileHandler',
'filters': ['require_debug_false', 'dynamic_level_filter'], 'filters': ['require_debug_false', 'dynamic_level_filter', 'guid'],
'filename': os.path.join(LOG_ROOT, 'wsbroadcast.log'), 'filename': os.path.join(LOG_ROOT, 'wsbroadcast.log'),
'formatter':'simple', 'formatter':'simple',
}, },
@@ -914,7 +918,7 @@ LOGGING = {
'task_system': { 'task_system': {
# don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL # don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL
'class': 'logging.handlers.WatchedFileHandler', 'class': 'logging.handlers.WatchedFileHandler',
'filters': ['require_debug_false', 'dynamic_level_filter'], 'filters': ['require_debug_false', 'dynamic_level_filter', 'guid'],
'filename': os.path.join(LOG_ROOT, 'task_system.log'), 'filename': os.path.join(LOG_ROOT, 'task_system.log'),
'formatter':'simple', 'formatter':'simple',
}, },
@@ -1094,6 +1098,7 @@ AWX_CALLBACK_PROFILE = False
AWX_CLEANUP_PATHS = True AWX_CLEANUP_PATHS = True
MIDDLEWARE = [ MIDDLEWARE = [
'django_guid.middleware.GuidMiddleware',
'awx.main.middleware.TimingMiddleware', 'awx.main.middleware.TimingMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'awx.main.middleware.MigrationRanCheckMiddleware', 'awx.main.middleware.MigrationRanCheckMiddleware',
@@ -1134,3 +1139,7 @@ BROADCAST_WEBSOCKET_NEW_INSTANCE_POLL_RATE_SECONDS = 10
# How often websocket process will generate stats # How often websocket process will generate stats
BROADCAST_WEBSOCKET_STATS_POLL_RATE_SECONDS = 5 BROADCAST_WEBSOCKET_STATS_POLL_RATE_SECONDS = 5
DJANGO_GUID = {
'GUID_HEADER_NAME': 'X-API-Request-Id',
}

View File

@@ -58,12 +58,12 @@ class MetadataView(View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
from social_django.utils import load_backend, load_strategy from social_django.utils import load_backend, load_strategy
complete_url = reverse('social:complete', args=('saml', )) complete_url = reverse('social:complete', args=('saml', ))
saml_backend = load_backend(
load_strategy(request),
'saml',
redirect_uri=complete_url,
)
try: try:
saml_backend = load_backend(
load_strategy(request),
'saml',
redirect_uri=complete_url,
)
metadata, errors = saml_backend.generate_metadata_xml() metadata, errors = saml_backend.generate_metadata_xml()
except Exception as e: except Exception as e:
logger.exception('unable to generate SAML metadata') logger.exception('unable to generate SAML metadata')

View File

@@ -18,7 +18,7 @@ div.response-info span.meta {
<div class="container"> <div class="container">
<div class="navbar-header"> <div class="navbar-header">
<a class="navbar-brand" href="/"> <a class="navbar-brand" href="/">
<img class="logo" src="{% static 'assets/logo-header.svg' %}"> <img class="logo" src="{% static 'media/logo-header.svg' %}">
</a> </a>
<a class="navbar-title" href="{{ request.get_full_path }}"> <a class="navbar-title" href="{{ request.get_full_path }}">
<span>&nbsp;&mdash; {{name}}</span> <span>&nbsp;&mdash; {{name}}</span>

View File

@@ -9,7 +9,7 @@
{% endblock %} {% endblock %}
{% block style %} {% block style %}
<link href="{% static 'assets/favicon.ico' %}?v={{tower_version}}" rel="shortcut icon" /> <link href="{% static 'favicon.ico' %}?v={{tower_version}}" rel="shortcut icon" />
{{ block.super }} {{ block.super }}
{% endblock %} {% endblock %}
@@ -24,7 +24,7 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="{% url 'api:api_root_view' %}"> <a class="navbar-brand" href="{% url 'api:api_root_view' %}">
<img class="logo" src="{% static 'assets/logo-header.svg' %}"> <img class="logo" src="{% static 'media/logo-header.svg' %}">
<span>{% trans 'REST API' %}</span> <span>{% trans 'REST API' %}</span>
</a> </a>
<a class="navbar-title" href="{{ request.get_full_path }}"> <a class="navbar-title" href="{{ request.get_full_path }}">

View File

@@ -53,6 +53,9 @@ npm --prefix awx/ui_next test -- src/screens/Login/Login.test.jsx
# start the test watcher and run tests on files that you've changed # start the test watcher and run tests on files that you've changed
npm --prefix awx/ui_next run test-watch npm --prefix awx/ui_next run test-watch
# start the tests and get the coverage report after the tests have completed
npm --prefix awx/ui_next run test -- --coverage
``` ```
#### Note: #### Note:
- Once the test watcher is up and running you can hit `a` to run all the tests. - Once the test watcher is up and running you can hit `a` to run all the tests.

View File

@@ -2121,22 +2121,39 @@
"dev": true "dev": true
}, },
"@patternfly/patternfly": { "@patternfly/patternfly": {
"version": "4.70.2", "version": "4.80.3",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.70.2.tgz", "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.80.3.tgz",
"integrity": "sha512-XKCHnOjx1JThY3s98AJhsApSsGHPvEdlY7r+b18OecqUnmThVGw3nslzYYrwfCGlJ/xQtV5so29SduH2/uhHzA==" "integrity": "sha512-YLUk4L6iCBXql92YP6zHg0FdlnEkd5/3V+uz/A3UoBuuDdEoyDpx4M/Tf56R7IXmYiRaHE1mToJHPDYypIlnmw=="
}, },
"@patternfly/react-core": { "@patternfly/react-core": {
"version": "4.84.3", "version": "4.90.2",
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.84.3.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.90.2.tgz",
"integrity": "sha512-VeCv/r09ay6yIER7Eb8Dp5ZhbDu6SCW9smwgUTNp80kt83wIfKvGvQOKg+/7cev/GC6VzfgPHhiS04Jm/N5loA==", "integrity": "sha512-Ld4HCXYrVZR+ycWKvygmOEDyyMpb/pfZFklJiT/9ktSSSU9SfKwH2XmXotgjwBy1lYUoRYJ58xTpV19duouEoA==",
"requires": { "requires": {
"@patternfly/react-icons": "^4.7.22", "@patternfly/react-icons": "^4.8.4",
"@patternfly/react-styles": "^4.7.22", "@patternfly/react-styles": "^4.7.29",
"@patternfly/react-tokens": "^4.9.22", "@patternfly/react-tokens": "^4.9.26",
"focus-trap": "6.2.2", "focus-trap": "6.2.2",
"react-dropzone": "9.0.0", "react-dropzone": "9.0.0",
"tippy.js": "5.1.2", "tippy.js": "5.1.2",
"tslib": "1.13.0" "tslib": "1.13.0"
},
"dependencies": {
"@patternfly/react-icons": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.8.4.tgz",
"integrity": "sha512-nGrZ6F57drim7t5FlFz7YusHVs8xngiayB20oSsJeewppOZ3vJDVhHaiJjsIRMB9PcVOY99QT2wnyiY1eYPVyA=="
},
"@patternfly/react-styles": {
"version": "4.7.29",
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.7.29.tgz",
"integrity": "sha512-eAO9xh2+IQHIBCihmwNuDVCeAWhGXIhbUNJEwZzvevYuH4Pnl0X8YaWoYmM2ZfL8ZdagRTLvjGW+hoZGkUyCBQ=="
},
"@patternfly/react-tokens": {
"version": "4.9.26",
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.9.26.tgz",
"integrity": "sha512-SSbY6BaUb7ycXhvdBNk2yzOlteRUHnchHSYPBFvhUk+SqpsjQ7j13/ZTok6pcHXrcj3FaL9f82/ep4eTXPzmWg=="
}
} }
}, },
"@patternfly/react-icons": { "@patternfly/react-icons": {
@@ -2163,6 +2180,11 @@
"tslib": "1.13.0" "tslib": "1.13.0"
}, },
"dependencies": { "dependencies": {
"@patternfly/patternfly": {
"version": "4.70.2",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.70.2.tgz",
"integrity": "sha512-XKCHnOjx1JThY3s98AJhsApSsGHPvEdlY7r+b18OecqUnmThVGw3nslzYYrwfCGlJ/xQtV5so29SduH2/uhHzA=="
},
"@patternfly/react-core": { "@patternfly/react-core": {
"version": "4.84.4", "version": "4.84.4",
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.84.4.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.84.4.tgz",

View File

@@ -7,8 +7,8 @@
}, },
"dependencies": { "dependencies": {
"@lingui/react": "^2.9.1", "@lingui/react": "^2.9.1",
"@patternfly/patternfly": "4.70.2", "@patternfly/patternfly": "^4.80.3",
"@patternfly/react-core": "4.84.3", "@patternfly/react-core": "^4.90.2",
"@patternfly/react-icons": "4.7.22", "@patternfly/react-icons": "4.7.22",
"@patternfly/react-table": "^4.19.15", "@patternfly/react-table": "^4.19.15",
"ansi-to-html": "^0.6.11", "ansi-to-html": "^0.6.11",
@@ -58,7 +58,7 @@
"start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start", "start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start",
"start-instrumented": "DEBUG=instrument-cra PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts -r @cypress/instrument-cra start", "start-instrumented": "DEBUG=instrument-cra PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts -r @cypress/instrument-cra start",
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build", "build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
"test": "TZ='UTC' react-scripts test --coverage --watchAll=false", "test": "TZ='UTC' react-scripts test --watchAll=false",
"test-watch": "TZ='UTC' react-scripts test", "test-watch": "TZ='UTC' react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"lint": "eslint --ext .js --ext .jsx .", "lint": "eslint --ext .js --ext .jsx .",

View File

@@ -3,7 +3,7 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, Tooltip } from '@patternfly/react-core'; import { Button } from '@patternfly/react-core';
import { CopyIcon } from '@patternfly/react-icons'; import { CopyIcon } from '@patternfly/react-icons';
import useRequest, { useDismissableError } from '../../util/useRequest'; import useRequest, { useDismissableError } from '../../util/useRequest';
import AlertModal from '../AlertModal'; import AlertModal from '../AlertModal';
@@ -15,7 +15,7 @@ function CopyButton({
isDisabled, isDisabled,
onCopyStart, onCopyStart,
onCopyFinish, onCopyFinish,
helperText, errorMessage,
i18n, i18n,
}) { }) {
const { isLoading, error: copyError, request: copyItemToAPI } = useRequest( const { isLoading, error: copyError, request: copyItemToAPI } = useRequest(
@@ -33,17 +33,15 @@ function CopyButton({
return ( return (
<> <>
<Tooltip content={helperText.tooltip} position="top"> <Button
<Button id={id}
id={id} isDisabled={isLoading || isDisabled}
isDisabled={isLoading || isDisabled} aria-label={i18n._(t`Copy`)}
aria-label={i18n._(t`Copy`)} variant="plain"
variant="plain" onClick={copyItemToAPI}
onClick={copyItemToAPI} >
> <CopyIcon />
<CopyIcon /> </Button>
</Button>
</Tooltip>
<AlertModal <AlertModal
aria-label={i18n._(t`Copy Error`)} aria-label={i18n._(t`Copy Error`)}
isOpen={error} isOpen={error}
@@ -51,7 +49,7 @@ function CopyButton({
title={i18n._(t`Error!`)} title={i18n._(t`Error!`)}
onClose={dismissError} onClose={dismissError}
> >
{helperText.errorMessage} {errorMessage}
<ErrorDetail error={error} /> <ErrorDetail error={error} />
</AlertModal> </AlertModal>
</> </>
@@ -62,10 +60,7 @@ CopyButton.propTypes = {
copyItem: PropTypes.func.isRequired, copyItem: PropTypes.func.isRequired,
onCopyStart: PropTypes.func.isRequired, onCopyStart: PropTypes.func.isRequired,
onCopyFinish: PropTypes.func.isRequired, onCopyFinish: PropTypes.func.isRequired,
helperText: PropTypes.shape({ errorMessage: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
errorMessage: PropTypes.string.isRequired,
}).isRequired,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
}; };

View File

@@ -1,36 +1,44 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import CopyButton from './CopyButton'; import CopyButton from './CopyButton';
jest.mock('../../api'); jest.mock('../../api');
let wrapper;
describe('<CopyButton/>', () => { describe('<CopyButton/>', () => {
test('shold mount properly', () => { afterEach(() => {
const wrapper = mountWithContexts( wrapper.unmount();
<CopyButton });
onCopyStart={() => {}} test('should mount properly', async () => {
onCopyFinish={() => {}} await act(async () => {
copyItem={() => {}} wrapper = mountWithContexts(
helperText={{ <CopyButton
tooltip: `Copy Template`, onCopyStart={() => {}}
errorMessage: `Failed to copy template.`, onCopyFinish={() => {}}
}} copyItem={() => {}}
/> errorMessage="Failed to copy template."
); />
);
});
expect(wrapper.find('CopyButton').length).toBe(1); expect(wrapper.find('CopyButton').length).toBe(1);
}); });
test('should render proper tooltip', () => { test('should call the correct function on button click', async () => {
const wrapper = mountWithContexts( const copyItem = jest.fn();
<CopyButton await act(async () => {
onCopyStart={() => {}} wrapper = mountWithContexts(
onCopyFinish={() => {}} <CopyButton
copyItem={() => {}} onCopyStart={() => {}}
helperText={{ onCopyFinish={() => {}}
tooltip: `Copy Template`, copyItem={copyItem}
errorMessage: `Failed to copy template.`, errorMessage="Failed to copy template."
}} />
/> );
); });
expect(wrapper.find('Tooltip').prop('content')).toBe('Copy Template'); await act(async () => {
wrapper.find('button').simulate('click');
});
expect(copyItem).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@@ -7,7 +7,7 @@ import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
import { RocketIcon } from '@patternfly/react-icons'; import { RocketIcon } from '@patternfly/react-icons';
import styled from 'styled-components'; import styled from 'styled-components';
import { ActionsTd, ActionItem } from '../PaginatedTable'; import { ActionsTd, ActionItem } from '../PaginatedTable';
import LaunchButton from '../LaunchButton'; import { LaunchButton, ReLaunchDropDown } from '../LaunchButton';
import StatusLabel from '../StatusLabel'; import StatusLabel from '../StatusLabel';
import { DetailList, Detail, LaunchedByDetail } from '../DetailList'; import { DetailList, Detail, LaunchedByDetail } from '../DetailList';
import ChipGroup from '../ChipGroup'; import ChipGroup from '../ChipGroup';
@@ -83,19 +83,31 @@ function JobListItem({
job.type !== 'system_job' && job.type !== 'system_job' &&
job.summary_fields?.user_capabilities?.start job.summary_fields?.user_capabilities?.start
} }
tooltip={i18n._(t`Relaunch Job`)} tooltip={
job.status === 'failed' && job.type === 'job'
? i18n._(t`Relaunch using host parameters`)
: i18n._(t`Relaunch Job`)
}
> >
<LaunchButton resource={job}> {job.status === 'failed' && job.type === 'job' ? (
{({ handleRelaunch }) => ( <LaunchButton resource={job}>
<Button {({ handleRelaunch }) => (
variant="plain" <ReLaunchDropDown handleRelaunch={handleRelaunch} />
onClick={handleRelaunch} )}
aria-label={i18n._(t`Relaunch`)} </LaunchButton>
> ) : (
<RocketIcon /> <LaunchButton resource={job}>
</Button> {({ handleRelaunch }) => (
)} <Button
</LaunchButton> variant="plain"
onClick={handleRelaunch}
aria-label={i18n._(t`Relaunch`)}
>
<RocketIcon />
</Button>
)}
</LaunchButton>
)}
</ActionItem> </ActionItem>
</ActionsTd> </ActionsTd>
</Tr> </Tr>

View File

@@ -53,6 +53,10 @@ describe('<JobListItem />', () => {
expect(wrapper.find('LaunchButton').length).toBe(1); expect(wrapper.find('LaunchButton').length).toBe(1);
}); });
test('launch button shown to users with launch capabilities', () => {
expect(wrapper.find('LaunchButton').length).toBe(1);
});
test('launch button hidden from users without launch capabilities', () => { test('launch button hidden from users without launch capabilities', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<table> <table>
@@ -92,3 +96,83 @@ describe('<JobListItem />', () => {
expect(wrapper.find('Td[dataLabel="Type"]').length).toBe(1); expect(wrapper.find('Td[dataLabel="Type"]').length).toBe(1);
}); });
}); });
describe('<JobListItem with failed job />', () => {
let wrapper;
beforeEach(() => {
const history = createMemoryHistory({
initialEntries: ['/jobs'],
});
wrapper = mountWithContexts(
<table>
<tbody>
<JobListItem
job={{ ...mockJob, status: 'failed' }}
isSelected
onSelect={() => {}}
/>
</tbody>
</table>,
{ context: { router: { history } } }
);
});
afterEach(() => {
wrapper.unmount();
});
test('launch button shown to users with launch capabilities', () => {
expect(wrapper.find('LaunchButton').length).toBe(1);
});
test('dropdown should be displayed in case of failed job', () => {
expect(wrapper.find('LaunchButton').length).toBe(1);
const dropdown = wrapper.find('Dropdown');
expect(dropdown).toHaveLength(1);
expect(dropdown.find('DropdownItem')).toHaveLength(0);
dropdown.find('button').simulate('click');
wrapper.update();
expect(wrapper.find('DropdownItem')).toHaveLength(3);
});
test('dropdown should not be rendered for job type different of playbook run', () => {
wrapper = mountWithContexts(
<table>
<tbody>
<JobListItem
job={{
...mockJob,
status: 'failed',
type: 'project_update',
}}
onSelect={() => {}}
isSelected
/>
</tbody>
</table>
);
expect(wrapper.find('LaunchButton').length).toBe(1);
expect(wrapper.find('Dropdown')).toHaveLength(0);
});
test('launch button hidden from users without launch capabilities', () => {
wrapper = mountWithContexts(
<table>
<tbody>
<JobListItem
job={{
...mockJob,
status: 'failed',
summary_fields: { user_capabilities: { start: false } },
}}
detailUrl={`/jobs/playbook/${mockJob.id}`}
onSelect={() => {}}
isSelected={false}
/>
</tbody>
</table>
);
expect(wrapper.find('LaunchButton').length).toBe(0);
});
});

View File

@@ -87,7 +87,7 @@ function LaunchButton({ resource, i18n, children, history }) {
} }
}; };
const handleRelaunch = async () => { const handleRelaunch = async params => {
let readRelaunch; let readRelaunch;
let relaunch; let relaunch;
@@ -125,7 +125,7 @@ function LaunchButton({ resource, i18n, children, history }) {
} else if (resource.type === 'ad_hoc_command') { } else if (resource.type === 'ad_hoc_command') {
relaunch = AdHocCommandsAPI.relaunch(resource.id); relaunch = AdHocCommandsAPI.relaunch(resource.id);
} else if (resource.type === 'job') { } else if (resource.type === 'job') {
relaunch = JobsAPI.relaunch(resource.id); relaunch = JobsAPI.relaunch(resource.id, params || {});
} }
const { data: job } = await relaunch; const { data: job } = await relaunch;
history.push(`/jobs/${job.id}/output`); history.push(`/jobs/${job.id}/output`);

View File

@@ -147,7 +147,7 @@ describe('LaunchButton', () => {
await act(() => button.prop('onClick')()); await act(() => button.prop('onClick')());
expect(JobsAPI.readRelaunch).toHaveBeenCalledWith(1); expect(JobsAPI.readRelaunch).toHaveBeenCalledWith(1);
await sleep(0); await sleep(0);
expect(JobsAPI.relaunch).toHaveBeenCalledWith(1); expect(JobsAPI.relaunch).toHaveBeenCalledWith(1, {});
expect(history.location.pathname).toEqual('/jobs/9000/output'); expect(history.location.pathname).toEqual('/jobs/9000/output');
}); });

View File

@@ -0,0 +1,96 @@
import React, { useState } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Dropdown,
DropdownToggle,
DropdownItem,
DropdownPosition,
DropdownSeparator,
DropdownDirection,
} from '@patternfly/react-core';
import { RocketIcon } from '@patternfly/react-icons';
function ReLaunchDropDown({ isPrimary = false, handleRelaunch, i18n }) {
const [isOpen, setIsOPen] = useState(false);
const onToggle = () => {
setIsOPen(prev => !prev);
};
const dropdownItems = [
<DropdownItem
aria-label={i18n._(t`Relaunch on`)}
key="relaunch_on"
component="div"
isPlainText
>
{i18n._(t`Relaunch on`)}
</DropdownItem>,
<DropdownSeparator key="separator" />,
<DropdownItem
key="relaunch_all"
aria-label={i18n._(t`Relaunch all hosts`)}
component="button"
onClick={() => {
handleRelaunch({ hosts: 'all' });
}}
>
{i18n._(t`All`)}
</DropdownItem>,
<DropdownItem
key="relaunch_failed"
aria-label={i18n._(t`Relaunch failed hosts`)}
component="button"
onClick={() => {
handleRelaunch({ hosts: 'failed' });
}}
>
{i18n._(t`Failed hosts`)}
</DropdownItem>,
];
if (isPrimary) {
return (
<Dropdown
position={DropdownPosition.left}
direction={DropdownDirection.up}
isOpen={isOpen}
dropdownItems={dropdownItems}
toggle={
<DropdownToggle
toggleIndicator={null}
onToggle={onToggle}
aria-label={i18n._(`relaunch jobs`)}
id="relaunch_jobs"
isPrimary
>
{i18n._(t`Relaunch`)}
</DropdownToggle>
}
/>
);
}
return (
<Dropdown
isPlain
position={DropdownPosition.right}
isOpen={isOpen}
dropdownItems={dropdownItems}
toggle={
<DropdownToggle
toggleIndicator={null}
onToggle={onToggle}
aria-label={i18n._(`relaunch jobs`)}
id="relaunch_jobs"
>
<RocketIcon />
</DropdownToggle>
}
/>
);
}
export default withI18n()(ReLaunchDropDown);

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import ReLaunchDropDown from './ReLaunchDropDown';
describe('ReLaunchDropDown', () => {
const handleRelaunch = jest.fn();
test('expected content is rendered on initialization', () => {
const wrapper = mountWithContexts(
<ReLaunchDropDown handleRelaunch={handleRelaunch} />
);
expect(wrapper.find('Dropdown')).toHaveLength(1);
});
test('dropdown have expected items and callbacks', () => {
const wrapper = mountWithContexts(
<ReLaunchDropDown handleRelaunch={handleRelaunch} />
);
expect(wrapper.find('DropdownItem')).toHaveLength(0);
wrapper.find('button').simulate('click');
wrapper.update();
expect(wrapper.find('DropdownItem')).toHaveLength(3);
wrapper
.find('DropdownItem[aria-label="Relaunch failed hosts"]')
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'failed' });
wrapper
.find('DropdownItem[aria-label="Relaunch all hosts"]')
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'all' });
});
test('dropdown isPrimary have expected items and callbacks', () => {
const wrapper = mountWithContexts(
<ReLaunchDropDown isPrimary handleRelaunch={handleRelaunch} />
);
expect(wrapper.find('DropdownItem')).toHaveLength(0);
wrapper.find('button').simulate('click');
wrapper.update();
expect(wrapper.find('DropdownItem')).toHaveLength(3);
wrapper
.find('DropdownItem[aria-label="Relaunch failed hosts"]')
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'failed' });
wrapper
.find('DropdownItem[aria-label="Relaunch all hosts"]')
.simulate('click');
expect(handleRelaunch).toHaveBeenCalledWith({ hosts: 'all' });
});
});

View File

@@ -1 +1,2 @@
export { default } from './LaunchButton'; export { default as LaunchButton } from './LaunchButton';
export { default as ReLaunchDropDown } from './ReLaunchDropDown';

View File

@@ -311,7 +311,7 @@ describe('<MultiCredentialsLookup />', () => {
}); });
wrapper.update(); wrapper.update();
const typeSelect = wrapper.find('AnsibleSelect'); const typeSelect = wrapper.find('AnsibleSelect');
act(() => { await act(async () => {
typeSelect.invoke('onChange')({}, 500); typeSelect.invoke('onChange')({}, 500);
}); });
wrapper.update(); wrapper.update();

View File

@@ -29,6 +29,7 @@ function OrganizationLookup({
value, value,
history, history,
autoPopulate, autoPopulate,
isDisabled,
}) { }) {
const autoPopulateLookup = useAutoPopulateLookup(onChange); const autoPopulateLookup = useAutoPopulateLookup(onChange);
@@ -80,6 +81,7 @@ function OrganizationLookup({
label={i18n._(t`Organization`)} label={i18n._(t`Organization`)}
> >
<Lookup <Lookup
isDisabled={isDisabled}
id="organization" id="organization"
header={i18n._(t`Organization`)} header={i18n._(t`Organization`)}
value={value} value={value}
@@ -139,6 +141,7 @@ OrganizationLookup.propTypes = {
required: bool, required: bool,
value: Organization, value: Organization,
autoPopulate: bool, autoPopulate: bool,
isDisabled: bool,
}; };
OrganizationLookup.defaultProps = { OrganizationLookup.defaultProps = {
@@ -148,6 +151,7 @@ OrganizationLookup.defaultProps = {
required: false, required: false,
value: null, value: null,
autoPopulate: false, autoPopulate: false,
isDisabled: false,
}; };
export { OrganizationLookup as _OrganizationLookup }; export { OrganizationLookup as _OrganizationLookup };

View File

@@ -9,7 +9,7 @@ function LookupErrorMessage({ error, i18n }) {
return ( return (
<div className="pf-c-form__helper-text pf-m-error" aria-live="polite"> <div className="pf-c-form__helper-text pf-m-error" aria-live="polite">
{error.message || i18n._(t`An error occured`)} {error.message || i18n._(t`An error occurred`)}
</div> </div>
); );
} }

View File

@@ -5,8 +5,10 @@ import { Button } from '@patternfly/react-core';
import ButtonGroup from './ButtonGroup'; import ButtonGroup from './ButtonGroup';
const SmallButton = styled(Button)` const SmallButton = styled(Button)`
padding: 3px 8px; && {
font-size: var(--pf-global--FontSize--xs); padding: 3px 8px;
font-size: var(--pf-global--FontSize--xs);
}
`; `;
function MultiButtonToggle({ buttons, value, onChange }) { function MultiButtonToggle({ buttons, value, onChange }) {

View File

@@ -14,7 +14,7 @@ export default function ActionItem({ column, tooltip, visible, children }) {
`} `}
> >
<Tooltip content={tooltip} position="top"> <Tooltip content={tooltip} position="top">
{children} <div>{children}</div>
</Tooltip> </Tooltip>
</div> </div>
); );

View File

@@ -12,7 +12,7 @@ describe('<ActionItem />', () => {
const tooltip = wrapper.find('Tooltip'); const tooltip = wrapper.find('Tooltip');
expect(tooltip.prop('content')).toEqual('a tooltip'); expect(tooltip.prop('content')).toEqual('a tooltip');
expect(tooltip.prop('children')).toEqual('foo'); expect(tooltip.prop('children')).toEqual(<div>foo</div>);
}); });
test('should render null if not visible', async () => { test('should render null if not visible', async () => {

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { TeamsAPI, UsersAPI } from '../../api'; import { RolesAPI, TeamsAPI, UsersAPI } from '../../api';
import AddResourceRole from '../AddRole/AddResourceRole'; import AddResourceRole from '../AddRole/AddResourceRole';
import AlertModal from '../AlertModal'; import AlertModal from '../AlertModal';
import DataListToolbar from '../DataListToolbar'; import DataListToolbar from '../DataListToolbar';
@@ -26,7 +26,13 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
const location = useLocation(); const location = useLocation();
const { const {
result: { accessRecords, itemCount, relatedSearchableKeys, searchableKeys }, result: {
accessRecords,
itemCount,
relatedSearchableKeys,
searchableKeys,
organizationRoles,
},
error: contentError, error: contentError,
isLoading, isLoading,
request: fetchAccessRecords, request: fetchAccessRecords,
@@ -37,6 +43,41 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
apiModel.readAccessList(resource.id, params), apiModel.readAccessList(resource.id, params),
apiModel.readAccessOptions(resource.id), apiModel.readAccessOptions(resource.id),
]); ]);
// Eventually this could be expanded to other access lists.
// We will need to combine the role ids of all the different level
// of resource level roles.
let orgRoles;
if (location.pathname.includes('/organizations')) {
const {
data: { results: roles },
} = await RolesAPI.read({ content_type__isnull: true });
const sysAdmin = roles.filter(
role => role.name === 'System Administrator'
);
const sysAud = roles.filter(role => {
let auditor;
if (role.name === 'System Auditor') {
auditor = role.id;
}
return auditor;
});
orgRoles = Object.values(resource.summary_fields.object_roles).map(
opt => {
let item;
if (opt.name === 'Admin') {
item = [`${opt.id}, ${sysAdmin[0].id}`, opt.name];
} else if (sysAud[0].id && opt.name === 'Auditor') {
item = [`${sysAud[0].id}, ${opt.id}`, opt.name];
} else {
item = [`${opt.id}`, opt.name];
}
return item;
}
);
}
return { return {
accessRecords: response.data.results, accessRecords: response.data.results,
itemCount: response.data.count, itemCount: response.data.count,
@@ -46,8 +87,9 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
searchableKeys: Object.keys( searchableKeys: Object.keys(
actionsResponse.data.actions?.GET || {} actionsResponse.data.actions?.GET || {}
).filter(key => actionsResponse.data.actions?.GET[key].filterable), ).filter(key => actionsResponse.data.actions?.GET[key].filterable),
organizationRoles: orgRoles,
}; };
}, [apiModel, location, resource.id]), }, [apiModel, location, resource]),
{ {
accessRecords: [], accessRecords: [],
itemCount: 0, itemCount: 0,
@@ -78,6 +120,29 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
fetchItems: fetchAccessRecords, fetchItems: fetchAccessRecords,
} }
); );
const toolbarSearchColumns = [
{
name: i18n._(t`Username`),
key: 'username__icontains',
isDefault: true,
},
{
name: i18n._(t`First Name`),
key: 'first_name__icontains',
},
{
name: i18n._(t`Last Name`),
key: 'last_name__icontains',
},
];
if (organizationRoles?.length > 0) {
toolbarSearchColumns.push({
name: i18n._(t`Roles`),
key: `or__roles__in`,
options: organizationRoles,
});
}
return ( return (
<> <>
@@ -88,21 +153,7 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
itemCount={itemCount} itemCount={itemCount}
pluralizedItemName={i18n._(t`Roles`)} pluralizedItemName={i18n._(t`Roles`)}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
toolbarSearchColumns={[ toolbarSearchColumns={toolbarSearchColumns}
{
name: i18n._(t`Username`),
key: 'username__icontains',
isDefault: true,
},
{
name: i18n._(t`First Name`),
key: 'first_name__icontains',
},
{
name: i18n._(t`Last Name`),
key: 'last_name__icontains',
},
]}
toolbarSortColumns={[ toolbarSortColumns={[
{ {
name: i18n._(t`Username`), name: i18n._(t`Username`),

View File

@@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { import {
mountWithContexts, mountWithContexts,
waitForElement, waitForElement,
} from '../../../testUtils/enzymeHelpers'; } from '../../../testUtils/enzymeHelpers';
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../api'; import { OrganizationsAPI, TeamsAPI, UsersAPI, RolesAPI } from '../../api';
import ResourceAccessList from './ResourceAccessList'; import ResourceAccessList from './ResourceAccessList';
@@ -17,7 +18,24 @@ describe('<ResourceAccessList />', () => {
id: 1, id: 1,
name: 'Default', name: 'Default',
summary_fields: { summary_fields: {
object_roles: {}, object_roles: {
admin_role: {
description: 'Can manage all aspects of the organization',
name: 'Admin',
id: 2,
user_only: true,
},
execute_role: {
description: 'May run any executable resources in the organization',
name: 'Execute',
id: 3,
},
project_admin_role: {
description: 'Can manage all projects of the organization',
name: 'Project Admin',
id: 4,
},
},
user_capabilities: { user_capabilities: {
edit: true, edit: true,
}, },
@@ -87,12 +105,24 @@ describe('<ResourceAccessList />', () => {
}); });
TeamsAPI.disassociateRole.mockResolvedValue({}); TeamsAPI.disassociateRole.mockResolvedValue({});
UsersAPI.disassociateRole.mockResolvedValue({}); UsersAPI.disassociateRole.mockResolvedValue({});
RolesAPI.read.mockResolvedValue({
data: {
results: [
{ id: 1, name: 'System Administrator' },
{ id: 14, name: 'System Auditor' },
],
},
});
const history = createMemoryHistory({
initialEntries: ['/organizations/1/access'],
});
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<ResourceAccessList <ResourceAccessList
resource={organization} resource={organization}
apiModel={OrganizationsAPI} apiModel={OrganizationsAPI}
/> />,
{ context: { router: { history } } }
); );
}); });
wrapper.update(); wrapper.update();
@@ -168,4 +198,24 @@ describe('<ResourceAccessList />', () => {
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2); expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
done(); done();
}); });
test('should call api to get org details', async () => {
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(
wrapper.find('PaginatedDataList').prop('toolbarSearchColumns')
).toStrictEqual([
{ isDefault: true, key: 'username__icontains', name: 'Username' },
{ key: 'first_name__icontains', name: 'First Name' },
{ key: 'last_name__icontains', name: 'Last Name' },
{
key: 'or__roles__in',
name: 'Roles',
options: [
['2, 1', 'Admin'],
['3', 'Execute'],
['4', 'Project Admin'],
],
},
]);
});
}); });

View File

@@ -56,6 +56,8 @@ function ResourceAccessListItem({ accessRecord, onRoleDelete, i18n }) {
onRoleDelete(role, accessRecord); onRoleDelete(role, accessRecord);
}} }}
isReadOnly={!role.user_capabilities.unattach} isReadOnly={!role.user_capabilities.unattach}
ouiaId={`${role.name}-${role.id}`}
closeBtnAriaLabel={i18n._(t`Remove ${role.name} chip`)}
> >
{role.name} {role.name}
</Chip> </Chip>

View File

@@ -290,6 +290,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
} }
} }
paused={false} paused={false}
preventScrollOnDeactivate={false}
> >
<div <div
className="pf-l-bullseye" className="pf-l-bullseye"

View File

@@ -98,11 +98,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
> >
<Chip <Chip
className="" className=""
closeBtnAriaLabel="close" closeBtnAriaLabel="Remove Member chip"
component="div" component="div"
isOverflowChip={false} isOverflowChip={false}
isReadOnly={false} isReadOnly={false}
onClick={[Function]} onClick={[Function]}
ouiaId="Member-3"
tooltipPosition="top" tooltipPosition="top"
> >
Member Member
@@ -164,11 +165,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
> >
<Chip <Chip
className="" className=""
closeBtnAriaLabel="close" closeBtnAriaLabel="Remove Member chip"
component="div" component="div"
isOverflowChip={false} isOverflowChip={false}
isReadOnly={false} isReadOnly={false}
onClick={[Function]} onClick={[Function]}
ouiaId="Member-3"
tooltipPosition="top" tooltipPosition="top"
> >
Member Member
@@ -253,11 +255,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
> >
<Chip <Chip
className="" className=""
closeBtnAriaLabel="close" closeBtnAriaLabel="Remove Member chip"
component="div" component="div"
isOverflowChip={false} isOverflowChip={false}
isReadOnly={false} isReadOnly={false}
onClick={[Function]} onClick={[Function]}
ouiaId="Member-3"
tooltipPosition="top" tooltipPosition="top"
> >
Member Member
@@ -688,11 +691,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
> >
<Chip <Chip
className="" className=""
closeBtnAriaLabel="close" closeBtnAriaLabel="Remove Member chip"
component="div" component="div"
isOverflowChip={false} isOverflowChip={false}
isReadOnly={false} isReadOnly={false}
onClick={[Function]} onClick={[Function]}
ouiaId="Member-3"
tooltipPosition="top" tooltipPosition="top"
> >
Member Member
@@ -840,6 +844,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
isClosable={false} isClosable={false}
numChips={5} numChips={5}
onClick={[Function]} onClick={[Function]}
onOverflowChipClick={[Function]}
tooltipPosition="top" tooltipPosition="top"
> >
<GenerateId <GenerateId
@@ -864,12 +869,13 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
> >
<Chip <Chip
className="" className=""
closeBtnAriaLabel="close" closeBtnAriaLabel="Remove Member chip"
component="div" component="div"
isOverflowChip={false} isOverflowChip={false}
isReadOnly={false} isReadOnly={false}
key=".$3" key=".$3"
onClick={[Function]} onClick={[Function]}
ouiaId="Member-3"
tooltipPosition="top" tooltipPosition="top"
> >
<GenerateId <GenerateId
@@ -877,7 +883,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
> >
<div <div
className="pf-c-chip" className="pf-c-chip"
data-ouia-component-id="OUIA-Generated-Chip-1" data-ouia-component-id="Member-3"
data-ouia-component-type="PF4/Chip" data-ouia-component-type="PF4/Chip"
data-ouia-safe={true} data-ouia-safe={true}
> >
@@ -888,19 +894,19 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
Member Member
</span> </span>
<Button <Button
aria-label="close" aria-label="Remove Member chip"
aria-labelledby="remove_pf-random-id-1 pf-random-id-1" aria-labelledby="remove_pf-random-id-1 pf-random-id-1"
id="remove_pf-random-id-1" id="remove_pf-random-id-1"
onClick={[Function]} onClick={[Function]}
ouiaId="close" ouiaId="Member-3"
variant="plain" variant="plain"
> >
<button <button
aria-disabled={false} aria-disabled={false}
aria-label="close" aria-label="Remove Member chip"
aria-labelledby="remove_pf-random-id-1 pf-random-id-1" aria-labelledby="remove_pf-random-id-1 pf-random-id-1"
className="pf-c-button pf-m-plain" className="pf-c-button pf-m-plain"
data-ouia-component-id="close" data-ouia-component-id="Member-3"
data-ouia-component-type="PF4/Button" data-ouia-component-type="PF4/Button"
data-ouia-safe={true} data-ouia-safe={true}
disabled={false} disabled={false}

View File

@@ -140,10 +140,12 @@ describe('<Sort />', () => {
act(() => wrapper.find('Dropdown').invoke('onToggle')(true)); act(() => wrapper.find('Dropdown').invoke('onToggle')(true));
wrapper.update(); wrapper.update();
await waitForElement(wrapper, 'Dropdown', el => el.prop('isOpen') === true); await waitForElement(wrapper, 'Dropdown', el => el.prop('isOpen') === true);
wrapper act(() =>
.find('li') wrapper
.at(0) .find('li')
.prop('onClick')({ target: { innerText: 'Bar' } }); .at(0)
.prop('onClick')({ target: { innerText: 'Bar' } })
);
wrapper.update(); wrapper.update();
expect(onSort).toBeCalledWith('bar', 'ascending'); expect(onSort).toBeCalledWith('bar', 'ascending');
}); });

View File

@@ -18,7 +18,7 @@ import CredentialChip from '../CredentialChip';
import { timeOfDay, formatDateString } from '../../util/dates'; import { timeOfDay, formatDateString } from '../../util/dates';
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api'; import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api';
import LaunchButton from '../LaunchButton'; import { LaunchButton } from '../LaunchButton';
import Sparkline from '../Sparkline'; import Sparkline from '../Sparkline';
import { toTitleCase } from '../../util/strings'; import { toTitleCase } from '../../util/strings';
import CopyButton from '../CopyButton'; import CopyButton from '../CopyButton';
@@ -177,13 +177,13 @@ function TemplateListItem({
<PencilAltIcon /> <PencilAltIcon />
</Button> </Button>
</ActionItem> </ActionItem>
<ActionItem visible={template.summary_fields.user_capabilities.copy}> <ActionItem
tooltip={i18n._(t`Copy Template`)}
visible={template.summary_fields.user_capabilities.copy}
>
<CopyButton <CopyButton
id={`template-action-copy-${template.id}`} id={`template-action-copy-${template.id}`}
helperText={{ errorMessage={i18n._(t`Failed to copy template.`)}
errorMessage: i18n._(t`Failed to copy template.`),
tooltip: i18n._(t`Copy Template`),
}}
isDisabled={isDisabled} isDisabled={isDisabled}
onCopyStart={handleCopyStart} onCopyStart={handleCopyStart}
onCopyFinish={handleCopyFinish} onCopyFinish={handleCopyFinish}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { object } from 'prop-types'; import { object } from 'prop-types';
import { CardBody } from '../../../components/Card'; import { CardBody } from '../../../components/Card';
@@ -6,15 +6,20 @@ import {
CredentialsAPI, CredentialsAPI,
CredentialInputSourcesAPI, CredentialInputSourcesAPI,
CredentialTypesAPI, CredentialTypesAPI,
OrganizationsAPI,
UsersAPI,
} from '../../../api'; } from '../../../api';
import ContentError from '../../../components/ContentError'; import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading'; import ContentLoading from '../../../components/ContentLoading';
import CredentialForm from '../shared/CredentialForm'; import CredentialForm from '../shared/CredentialForm';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import { useConfig } from '../../../contexts/Config';
function CredentialEdit({ credential, me }) { function CredentialEdit({ credential }) {
const history = useHistory(); const history = useHistory();
const { id: credId } = useParams(); const { id: credId } = useParams();
const { me = {} } = useConfig();
const [isOrgLookupDisabled, setIsOrgLookupDisabled] = useState(false);
const { error: submitError, request: submitRequest, result } = useRequest( const { error: submitError, request: submitRequest, result } = useRequest(
useCallback( useCallback(
@@ -81,8 +86,11 @@ function CredentialEdit({ credential, me }) {
// can send only one of org, user, team // can send only one of org, user, team
if (organization?.id) { if (organization?.id) {
modifiedData.organization = organization.id; modifiedData.organization = organization.id;
} else if (me?.id) { } else {
modifiedData.user = me.id; modifiedData.organization = null;
if (me?.id) {
modifiedData.user = me.id;
}
} }
const [{ data }] = await Promise.all([ const [{ data }] = await Promise.all([
CredentialsAPI.update(credId, modifiedData), CredentialsAPI.update(credId, modifiedData),
@@ -114,10 +122,22 @@ function CredentialEdit({ credential, me }) {
{ {
data: { results }, data: { results },
}, },
{
data: { count: adminOrgCount },
},
{
data: { count: credentialAdminCount },
},
] = await Promise.all([ ] = await Promise.all([
CredentialTypesAPI.read({ page_size: 200 }), CredentialTypesAPI.read({ page_size: 200 }),
CredentialsAPI.readInputSources(credId, { page_size: 200 }), CredentialsAPI.readInputSources(credId),
UsersAPI.readAdminOfOrganizations(me.id),
OrganizationsAPI.read({
page_size: 1,
role_level: 'credential_admin_role',
}),
]); ]);
setIsOrgLookupDisabled(!(adminOrgCount || credentialAdminCount));
const credTypes = data.results; const credTypes = data.results;
if (data.next && data.next.includes('page=2')) { if (data.next && data.next.includes('page=2')) {
const { const {
@@ -137,7 +157,7 @@ function CredentialEdit({ credential, me }) {
return inputSourcesMap; return inputSourcesMap;
}, {}); }, {});
return { credentialTypes: creds, loadedInputSources: inputSources }; return { credentialTypes: creds, loadedInputSources: inputSources };
}, [credId]), }, [credId, me.id]),
{ credentialTypes: {}, loadedInputSources: {} } { credentialTypes: {}, loadedInputSources: {} }
); );
@@ -171,6 +191,7 @@ function CredentialEdit({ credential, me }) {
credentialTypes={credentialTypes} credentialTypes={credentialTypes}
inputSources={loadedInputSources} inputSources={loadedInputSources}
submitError={submitError} submitError={submitError}
isOrgLookupDisabled={isOrgLookupDisabled}
/> />
</CardBody> </CardBody>
); );

View File

@@ -10,6 +10,8 @@ import {
CredentialsAPI, CredentialsAPI,
CredentialInputSourcesAPI, CredentialInputSourcesAPI,
CredentialTypesAPI, CredentialTypesAPI,
OrganizationsAPI,
UsersAPI,
} from '../../../api'; } from '../../../api';
import CredentialEdit from './CredentialEdit'; import CredentialEdit from './CredentialEdit';
@@ -114,6 +116,25 @@ const mockCredential = {
kubernetes: false, kubernetes: false,
}; };
UsersAPI.readAdminOfOrganizations.mockResolvedValue({
data: {
count: 1,
results: [
{
id: 1,
name: 'org',
},
],
},
});
OrganizationsAPI.read.mockResolvedValue({
data: {
results: [{ id: 1 }],
count: 1,
},
});
CredentialTypesAPI.read.mockResolvedValue({ CredentialTypesAPI.read.mockResolvedValue({
data: { data: {
results: [ results: [
@@ -197,6 +218,58 @@ CredentialTypesAPI.read.mockResolvedValue({
}, },
injectors: {}, injectors: {},
}, },
{
id: 9,
type: 'credential_type',
url: '/api/v2/credential_types/9/',
related: {
credentials: '/api/v2/credential_types/9/credentials/',
activity_stream: '/api/v2/credential_types/9/activity_stream/',
},
summary_fields: {
user_capabilities: {
edit: true,
delete: true,
},
},
created: '2021-02-12T19:13:22.352791Z',
modified: '2021-02-12T19:14:15.578773Z',
name: 'Google Compute Engine',
description: '',
kind: 'cloud',
namespace: 'gce',
managed_by_tower: true,
inputs: {
fields: [
{
id: 'username',
label: 'Service Account Email Address',
type: 'string',
help_text:
'The email address assigned to the Google Compute Engine service account.',
},
{
id: 'project',
label: 'Project',
type: 'string',
help_text:
'The Project ID is the GCE assigned identification. It is often constructed as three words or two words followed by a three-digit number. Examples: project-id-000 and another-project-id',
},
{
id: 'ssh_key_data',
label: 'RSA Private Key',
type: 'string',
format: 'ssh_private_key',
secret: true,
multiline: true,
help_text:
'Paste the contents of the PEM file associated with the service account email.',
},
],
required: ['username', 'ssh_key_data'],
},
injectors: {},
},
], ],
}, },
}); });
@@ -258,7 +331,12 @@ describe('<CredentialEdit />', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<CredentialEdit credential={mockCredential} />, <CredentialEdit credential={mockCredential} />,
{ {
context: { router: { history } }, context: {
router: { history },
me: {
id: 1,
},
},
} }
); );
}); });
@@ -325,6 +403,7 @@ describe('<CredentialEdit />', () => {
expect(CredentialsAPI.update).toHaveBeenCalledWith(3, { expect(CredentialsAPI.update).toHaveBeenCalledWith(3, {
user: 1, user: 1,
name: 'foo', name: 'foo',
organization: null,
description: 'bar', description: 'bar',
credential_type: '1', credential_type: '1',
inputs: { inputs: {
@@ -354,7 +433,40 @@ describe('<CredentialEdit />', () => {
expect(CredentialInputSourcesAPI.destroy).toHaveBeenCalledWith(34); expect(CredentialInputSourcesAPI.destroy).toHaveBeenCalledWith(34);
expect(history.location.pathname).toBe('/credentials/3/details'); expect(history.location.pathname).toBe('/credentials/3/details');
}); });
test('inputs are properly rendered', async () => {
history = createMemoryHistory({ initialEntries: ['/credentials'] });
await act(async () => {
wrapper = mountWithContexts(
<CredentialEdit
credential={{
...mockCredential,
inputs: {
project: 'foo',
username: 'foo@ansible.com',
ssh_key_data: '$encrypted$',
},
kind: 'gce',
credential_type: 9,
}}
/>,
{
context: { router: { history } },
}
);
});
wrapper.update();
expect(wrapper.find('input#credential-username').prop('value')).toBe(
'foo@ansible.com'
);
expect(wrapper.find('input#credential-project').prop('value')).toBe(
'foo'
);
expect(
wrapper.find('textarea#credential-ssh_key_data').prop('value')
).toBe('$encrypted$');
});
}); });
describe('Initial GET request fails', () => { describe('Initial GET request fails', () => {
test('shows error when initial GET request fails', async () => { test('shows error when initial GET request fails', async () => {
CredentialTypesAPI.read.mockRejectedValue(new Error()); CredentialTypesAPI.read.mockRejectedValue(new Error());

View File

@@ -72,16 +72,16 @@ function CredentialListItem({
<PencilAltIcon /> <PencilAltIcon />
</Button> </Button>
</ActionItem> </ActionItem>
<ActionItem visible={credential.summary_fields.user_capabilities.copy}> <ActionItem
tooltip={i18n._(t`Copy Credential`)}
visible={credential.summary_fields.user_capabilities.copy}
>
<CopyButton <CopyButton
isDisabled={isDisabled} isDisabled={isDisabled}
onCopyStart={handleCopyStart} onCopyStart={handleCopyStart}
onCopyFinish={handleCopyFinish} onCopyFinish={handleCopyFinish}
copyItem={copyCredential} copyItem={copyCredential}
helperText={{ errorMessage={i18n._(t`Failed to copy credential.`)}
tooltip: i18n._(t`Copy Credential`),
errorMessage: i18n._(t`Failed to copy credential.`),
}}
/> />
</ActionItem> </ActionItem>
</ActionsTd> </ActionsTd>

View File

@@ -1,8 +1,8 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { func, shape } from 'prop-types';
import { Formik, useField, useFormikContext } from 'formik'; import { Formik, useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { arrayOf, func, object, shape } from 'prop-types';
import { import {
ActionGroup, ActionGroup,
Button, Button,
@@ -136,6 +136,7 @@ function CredentialFormFields({ i18n, credentialTypes }) {
touched={orgMeta.touched} touched={orgMeta.touched}
error={orgMeta.error} error={orgMeta.error}
required={isGalaxyCredential} required={isGalaxyCredential}
isDisabled={initialValues.isOrgLookupDisabled}
/> />
<FormGroup <FormGroup
fieldId="credential-Type" fieldId="credential-Type"
@@ -189,6 +190,7 @@ function CredentialForm({
onSubmit, onSubmit,
onCancel, onCancel,
submitError, submitError,
isOrgLookupDisabled,
...rest ...rest
}) { }) {
const [showExternalTestModal, setShowExternalTestModal] = useState(false); const [showExternalTestModal, setShowExternalTestModal] = useState(false);
@@ -197,8 +199,9 @@ function CredentialForm({
description: credential.description || '', description: credential.description || '',
organization: credential?.summary_fields?.organization || null, organization: credential?.summary_fields?.organization || null,
credential_type: credential?.credential_type || '', credential_type: credential?.credential_type || '',
inputs: {}, inputs: credential?.inputs || {},
passwordPrompts: {}, passwordPrompts: {},
isOrgLookupDisabled: isOrgLookupDisabled || false,
}; };
Object.values(credentialTypes).forEach(credentialType => { Object.values(credentialTypes).forEach(credentialType => {
@@ -311,18 +314,18 @@ function CredentialForm({
); );
} }
CredentialForm.proptype = { CredentialForm.propTypes = {
handleSubmit: func.isRequired, handleSubmit: func.isRequired,
handleCancel: func.isRequired, handleCancel: func.isRequired,
credentialTypes: shape({}).isRequired, credentialTypes: shape({}).isRequired,
credential: shape({}), credential: shape({}),
inputSources: arrayOf(object), inputSources: shape({}),
submitError: shape({}), submitError: shape({}),
}; };
CredentialForm.defaultProps = { CredentialForm.defaultProps = {
credential: {}, credential: {},
inputSources: [], inputSources: {},
submitError: null, submitError: null,
}; };

View File

@@ -327,6 +327,27 @@ describe('<CredentialForm />', () => {
machineFieldExpects(); machineFieldExpects();
}); });
test('organization lookup should be disabled', async () => {
await act(async () => {
wrapper = mountWithContexts(
<CredentialForm
onCancel={onCancel}
onSubmit={onSubmit}
credential={machineCredential}
credentialTypes={credentialTypes}
isOrgLookupDisabled
/>
);
});
expect(
wrapper
.find('CredentialFormFields')
.find('OrganizationLookup')
.prop('isDisabled')
).toBe(true);
});
test('should display form fields for source control credential properly', async () => { test('should display form fields for source control credential properly', async () => {
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(

View File

@@ -168,7 +168,7 @@ function ExternalTestModal({
); );
} }
ExternalTestModal.proptype = { ExternalTestModal.propType = {
credential: shape({}), credential: shape({}),
credentialType: shape({}).isRequired, credentialType: shape({}).isRequired,
credentialFormValues: shape({}).isRequired, credentialFormValues: shape({}).isRequired,

View File

@@ -276,7 +276,7 @@
"help_text": "OpenStack domains define administrative boundaries. It is only needed for Keystone v3 authentication URLs. Refer to Ansible Tower documentation for common scenarios." "help_text": "OpenStack domains define administrative boundaries. It is only needed for Keystone v3 authentication URLs. Refer to Ansible Tower documentation for common scenarios."
}, },
{ {
"id": "project_region_name", "id": "region",
"label": "Region Name", "label": "Region Name",
"type": "string" "type": "string"
}, },

View File

@@ -118,7 +118,7 @@ function InventoryEdit({ inventory }) {
); );
} }
InventoryEdit.proptype = { InventoryEdit.propType = {
inventory: object.isRequired, inventory: object.isRequired,
}; };

View File

@@ -28,7 +28,7 @@ function InventoryListItem({
isSelected: bool.isRequired, isSelected: bool.isRequired,
onSelect: func.isRequired, onSelect: func.isRequired,
}; };
const [isDisabled, setIsDisabled] = useState(false); const [isCopying, setIsCopying] = useState(false);
const copyInventory = useCallback(async () => { const copyInventory = useCallback(async () => {
await InventoriesAPI.copy(inventory.id, { await InventoriesAPI.copy(inventory.id, {
@@ -38,11 +38,11 @@ function InventoryListItem({
}, [inventory.id, inventory.name, fetchInventories]); }, [inventory.id, inventory.name, fetchInventories]);
const handleCopyStart = useCallback(() => { const handleCopyStart = useCallback(() => {
setIsDisabled(true); setIsCopying(true);
}, []); }, []);
const handleCopyFinish = useCallback(() => { const handleCopyFinish = useCallback(() => {
setIsDisabled(false); setIsCopying(false);
}, []); }, []);
const labelId = `check-action-${inventory.id}`; const labelId = `check-action-${inventory.id}`;
@@ -115,7 +115,7 @@ function InventoryListItem({
tooltip={i18n._(t`Edit Inventory`)} tooltip={i18n._(t`Edit Inventory`)}
> >
<Button <Button
isDisabled={isDisabled} isDisabled={isCopying}
aria-label={i18n._(t`Edit Inventory`)} aria-label={i18n._(t`Edit Inventory`)}
variant="plain" variant="plain"
component={Link} component={Link}
@@ -128,17 +128,18 @@ function InventoryListItem({
</ActionItem> </ActionItem>
<ActionItem <ActionItem
visible={inventory.summary_fields.user_capabilities.copy} visible={inventory.summary_fields.user_capabilities.copy}
tooltip={i18n._(t`Copy Inventory`)} tooltip={
inventory.has_inventory_sources
? i18n._(t`Inventories with sources cannot be copied`)
: i18n._(t`Copy Inventory`)
}
> >
<CopyButton <CopyButton
copyItem={copyInventory} copyItem={copyInventory}
isDisabled={isDisabled} isDisabled={isCopying || inventory.has_inventory_sources}
onCopyStart={handleCopyStart} onCopyStart={handleCopyStart}
onCopyFinish={handleCopyFinish} onCopyFinish={handleCopyFinish}
helperText={{ errorMessage={i18n._(t`Failed to copy inventory.`)}
tooltip: i18n._(t`Copy Inventory`),
errorMessage: i18n._(t`Failed to copy inventory.`),
}}
/> />
</ActionItem> </ActionItem>
</ActionsTd> </ActionsTd>

View File

@@ -138,7 +138,7 @@ function InventoryForm({
); );
} }
InventoryForm.proptype = { InventoryForm.propType = {
handleSubmit: func.isRequired, handleSubmit: func.isRequired,
handleCancel: func.isRequired, handleCancel: func.isRequired,
instanceGroups: shape(), instanceGroups: shape(),

View File

@@ -19,7 +19,10 @@ import CredentialChip from '../../../components/CredentialChip';
import { VariablesInput as _VariablesInput } from '../../../components/CodeMirrorInput'; import { VariablesInput as _VariablesInput } from '../../../components/CodeMirrorInput';
import DeleteButton from '../../../components/DeleteButton'; import DeleteButton from '../../../components/DeleteButton';
import ErrorDetail from '../../../components/ErrorDetail'; import ErrorDetail from '../../../components/ErrorDetail';
import LaunchButton from '../../../components/LaunchButton'; import {
LaunchButton,
ReLaunchDropDown,
} from '../../../components/LaunchButton';
import StatusIcon from '../../../components/StatusIcon'; import StatusIcon from '../../../components/StatusIcon';
import { toTitleCase } from '../../../util/strings'; import { toTitleCase } from '../../../util/strings';
import { formatDateString } from '../../../util/dates'; import { formatDateString } from '../../../util/dates';
@@ -346,7 +349,14 @@ function JobDetail({ job, i18n }) {
)} )}
<CardActionsRow> <CardActionsRow>
{job.type !== 'system_job' && {job.type !== 'system_job' &&
job.summary_fields.user_capabilities.start && ( job.summary_fields.user_capabilities.start &&
(job.status === 'failed' && job.type === 'job' ? (
<LaunchButton resource={job}>
{({ handleRelaunch }) => (
<ReLaunchDropDown isPrimary handleRelaunch={handleRelaunch} />
)}
</LaunchButton>
) : (
<LaunchButton resource={job} aria-label={i18n._(t`Relaunch`)}> <LaunchButton resource={job} aria-label={i18n._(t`Relaunch`)}>
{({ handleRelaunch }) => ( {({ handleRelaunch }) => (
<Button type="submit" onClick={handleRelaunch}> <Button type="submit" onClick={handleRelaunch}>
@@ -354,7 +364,7 @@ function JobDetail({ job, i18n }) {
</Button> </Button>
)} )}
</LaunchButton> </LaunchButton>
)} ))}
{job.summary_fields.user_capabilities.delete && ( {job.summary_fields.user_capabilities.delete && (
<DeleteButton <DeleteButton
name={job.name} name={job.name}

View File

@@ -11,7 +11,10 @@ import {
} from '@patternfly/react-icons'; } from '@patternfly/react-icons';
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core'; import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
import DeleteButton from '../../../../components/DeleteButton'; import DeleteButton from '../../../../components/DeleteButton';
import LaunchButton from '../../../../components/LaunchButton'; import {
LaunchButton,
ReLaunchDropDown,
} from '../../../../components/LaunchButton';
const BadgeGroup = styled.div` const BadgeGroup = styled.div`
margin-left: 20px; margin-left: 20px;
@@ -125,18 +128,32 @@ const OutputToolbar = ({ i18n, job, jobStatus, onDelete, onCancel }) => {
{job.type !== 'system_job' && {job.type !== 'system_job' &&
job.summary_fields.user_capabilities?.start && ( job.summary_fields.user_capabilities?.start && (
<Tooltip content={i18n._(t`Relaunch Job`)}> <Tooltip
<LaunchButton resource={job} aria-label={i18n._(t`Relaunch`)}> content={
{({ handleRelaunch }) => ( job.status === 'failed' && job.type === 'job'
<Button ? i18n._(t`Relaunch using host parameters`)
variant="plain" : i18n._(t`Relaunch Job`)
onClick={handleRelaunch} }
aria-label={i18n._(t`Relaunch`)} >
> {job.status === 'failed' && job.type === 'job' ? (
<RocketIcon /> <LaunchButton resource={job}>
</Button> {({ handleRelaunch }) => (
)} <ReLaunchDropDown handleRelaunch={handleRelaunch} />
</LaunchButton> )}
</LaunchButton>
) : (
<LaunchButton resource={job}>
{({ handleRelaunch }) => (
<Button
variant="plain"
onClick={handleRelaunch}
aria-label={i18n._(t`Relaunch`)}
>
<RocketIcon />
</Button>
)}
</LaunchButton>
)}
</Tooltip> </Tooltip>
)} )}

View File

@@ -159,16 +159,15 @@ function NotificationTemplateListItem({
<div /> <div />
)} )}
{template.summary_fields.user_capabilities.copy && ( {template.summary_fields.user_capabilities.copy && (
<CopyButton <Tooltip content={i18n._(t`Copy Notification Template`)}>
copyItem={copyTemplate} <CopyButton
isCopyDisabled={isCopyDisabled} copyItem={copyTemplate}
onCopyStart={handleCopyStart} isCopyDisabled={isCopyDisabled}
onCopyFinish={handleCopyFinish} onCopyStart={handleCopyStart}
helperText={{ onCopyFinish={handleCopyFinish}
tooltip: i18n._(t`Copy Notification Template`), errorMessage={i18n._(t`Failed to copy template.`)}
errorMessage: i18n._(t`Failed to copy template.`), />
}} </Tooltip>
/>
)} )}
</DataListAction> </DataListAction>
</DataListItemRow> </DataListItemRow>

View File

@@ -20,7 +20,7 @@ import {
import styled from 'styled-components'; import styled from 'styled-components';
import DataListCell from '../../../components/DataListCell'; import DataListCell from '../../../components/DataListCell';
import LaunchButton from '../../../components/LaunchButton'; import { LaunchButton } from '../../../components/LaunchButton';
import Sparkline from '../../../components/Sparkline'; import Sparkline from '../../../components/Sparkline';
import { toTitleCase } from '../../../util/strings'; import { toTitleCase } from '../../../util/strings';

View File

@@ -143,16 +143,16 @@ function ProjectListItem({
<PencilAltIcon /> <PencilAltIcon />
</Button> </Button>
</ActionItem> </ActionItem>
<ActionItem visible={project.summary_fields.user_capabilities.copy}> <ActionItem
tooltip={i18n._(t`Copy Project`)}
visible={project.summary_fields.user_capabilities.copy}
>
<CopyButton <CopyButton
copyItem={copyProject} copyItem={copyProject}
isDisabled={isDisabled} isDisabled={isDisabled}
onCopyStart={handleCopyStart} onCopyStart={handleCopyStart}
onCopyFinish={handleCopyFinish} onCopyFinish={handleCopyFinish}
helperText={{ errorMessage={i18n._(t`Failed to copy project.`)}
tooltip: i18n._(t`Copy Project`),
errorMessage: i18n._(t`Failed to copy project.`),
}}
/> />
</ActionItem> </ActionItem>
</ActionsTd> </ActionsTd>

View File

@@ -26,7 +26,7 @@ import {
} from '../../../components/DetailList'; } from '../../../components/DetailList';
import DeleteButton from '../../../components/DeleteButton'; import DeleteButton from '../../../components/DeleteButton';
import ErrorDetail from '../../../components/ErrorDetail'; import ErrorDetail from '../../../components/ErrorDetail';
import LaunchButton from '../../../components/LaunchButton'; import { LaunchButton } from '../../../components/LaunchButton';
import { VariablesDetail } from '../../../components/CodeMirrorInput'; import { VariablesDetail } from '../../../components/CodeMirrorInput';
import { JobTemplatesAPI } from '../../../api'; import { JobTemplatesAPI } from '../../../api';
import useRequest, { useDismissableError } from '../../../util/useRequest'; import useRequest, { useDismissableError } from '../../../util/useRequest';

View File

@@ -24,7 +24,7 @@ import {
UserDateDetail, UserDateDetail,
} from '../../../components/DetailList'; } from '../../../components/DetailList';
import ErrorDetail from '../../../components/ErrorDetail'; import ErrorDetail from '../../../components/ErrorDetail';
import LaunchButton from '../../../components/LaunchButton'; import { LaunchButton } from '../../../components/LaunchButton';
import Sparkline from '../../../components/Sparkline'; import Sparkline from '../../../components/Sparkline';
import { toTitleCase } from '../../../util/strings'; import { toTitleCase } from '../../../util/strings';
import useRequest, { useDismissableError } from '../../../util/useRequest'; import useRequest, { useDismissableError } from '../../../util/useRequest';

View File

@@ -18,7 +18,7 @@ import {
WrenchIcon, WrenchIcon,
} from '@patternfly/react-icons'; } from '@patternfly/react-icons';
import styled from 'styled-components'; import styled from 'styled-components';
import LaunchButton from '../../../components/LaunchButton'; import { LaunchButton } from '../../../components/LaunchButton';
import { import {
WorkflowDispatchContext, WorkflowDispatchContext,
WorkflowStateContext, WorkflowStateContext,

View File

@@ -118,7 +118,7 @@ def main():
limit=dict(), limit=dict(),
credential=dict(required=True), credential=dict(required=True),
module_name=dict(required=True), module_name=dict(required=True),
module_args=dict(default=""), module_args=dict(),
forks=dict(type='int'), forks=dict(type='int'),
verbosity=dict(type='int', choices=['0', '1', '2', '3', '4', '5']), verbosity=dict(type='int', choices=['0', '1', '2', '3', '4', '5']),
extra_vars=dict(type='dict'), extra_vars=dict(type='dict'),

View File

@@ -76,10 +76,10 @@ from ..module_utils.tower_api import TowerAPIModule
def main(): def main():
# Any additional arguments that are not fields of the item can be added here # Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
description=dict(default=''), description=dict(),
input_field_name=dict(required=True), input_field_name=dict(required=True),
target_credential=dict(required=True), target_credential=dict(required=True),
source_credential=dict(default=''), source_credential=dict(),
metadata=dict(type="dict"), metadata=dict(type="dict"),
state=dict(choices=['present', 'absent'], default='present'), state=dict(choices=['present', 'absent'], default='present'),
) )

View File

@@ -340,24 +340,24 @@ def main():
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
new_name=dict(), new_name=dict(),
description=dict(default=''), description=dict(),
organization=dict(), organization=dict(),
job_type=dict(choices=['run', 'check']), job_type=dict(choices=['run', 'check']),
inventory=dict(), inventory=dict(),
project=dict(), project=dict(),
playbook=dict(), playbook=dict(),
credential=dict(default=''), credential=dict(),
vault_credential=dict(default=''), vault_credential=dict(),
custom_virtualenv=dict(), custom_virtualenv=dict(),
credentials=dict(type='list', elements='str'), credentials=dict(type='list', elements='str'),
forks=dict(type='int'), forks=dict(type='int'),
limit=dict(default=''), limit=dict(),
verbosity=dict(type='int', choices=[0, 1, 2, 3, 4], default=0), verbosity=dict(type='int', choices=[0, 1, 2, 3, 4], default=0),
extra_vars=dict(type='dict'), extra_vars=dict(type='dict'),
job_tags=dict(default=''), job_tags=dict(),
force_handlers=dict(type='bool', default=False, aliases=['force_handlers_enabled']), force_handlers=dict(type='bool', default=False, aliases=['force_handlers_enabled']),
skip_tags=dict(default=''), skip_tags=dict(),
start_at_task=dict(default=''), start_at_task=dict(),
timeout=dict(type='int', default=0), timeout=dict(type='int', default=0),
use_fact_cache=dict(type='bool', aliases=['fact_caching_enabled']), use_fact_cache=dict(type='bool', aliases=['fact_caching_enabled']),
host_config_key=dict(), host_config_key=dict(),
@@ -399,11 +399,11 @@ def main():
credential = module.params.get('credential') credential = module.params.get('credential')
vault_credential = module.params.get('vault_credential') vault_credential = module.params.get('vault_credential')
credentials = module.params.get('credentials') credentials = module.params.get('credentials')
if vault_credential != '': if vault_credential:
if credentials is None: if credentials is None:
credentials = [] credentials = []
credentials.append(vault_credential) credentials.append(vault_credential)
if credential != '': if credential:
if credentials is None: if credentials is None:
credentials = [] credentials = []
credentials.append(credential) credentials.append(credential)

View File

@@ -229,8 +229,8 @@ def main():
scm_type=dict(choices=['manual', 'git', 'svn', 'insights'], default='manual'), scm_type=dict(choices=['manual', 'git', 'svn', 'insights'], default='manual'),
scm_url=dict(), scm_url=dict(),
local_path=dict(), local_path=dict(),
scm_branch=dict(default=''), scm_branch=dict(),
scm_refspec=dict(default=''), scm_refspec=dict(),
credential=dict(aliases=['scm_credential']), credential=dict(aliases=['scm_credential']),
scm_clean=dict(type='bool', default=False), scm_clean=dict(type='bool', default=False),
scm_delete_on_update=dict(type='bool', default=False), scm_delete_on_update=dict(type='bool', default=False),

View File

@@ -133,7 +133,7 @@ def main():
existing_settings = module.get_endpoint('settings/all')['json'] existing_settings = module.get_endpoint('settings/all')['json']
# Begin a json response # Begin a json response
json_response = {'changed': False, 'old_values': {}} json_output = {'changed': False, 'old_values': {}, 'new_values': {}}
# Check any of the settings to see if anything needs to be updated # Check any of the settings to see if anything needs to be updated
needs_update = False needs_update = False
@@ -141,18 +141,29 @@ def main():
if a_setting not in existing_settings or existing_settings[a_setting] != new_settings[a_setting]: if a_setting not in existing_settings or existing_settings[a_setting] != new_settings[a_setting]:
# At least one thing is different so we need to patch # At least one thing is different so we need to patch
needs_update = True needs_update = True
json_response['old_values'][a_setting] = existing_settings[a_setting] json_output['old_values'][a_setting] = existing_settings[a_setting]
json_output['new_values'][a_setting] = new_settings[a_setting]
if module._diff:
json_output['diff'] = {
'before': json_output['old_values'],
'after': json_output['new_values']
}
# If nothing needs an update we can simply exit with the response (as not changed) # If nothing needs an update we can simply exit with the response (as not changed)
if not needs_update: if not needs_update:
module.exit_json(**json_response) module.exit_json(**json_output)
if module.check_mode and module._diff:
json_output['changed'] = True
module.exit_json(**json_output)
# Make the call to update the settings # Make the call to update the settings
response = module.patch_endpoint('settings/all', **{'data': new_settings}) response = module.patch_endpoint('settings/all', **{'data': new_settings})
if response['status_code'] == 200: if response['status_code'] == 200:
# Set the changed response to True # Set the changed response to True
json_response['changed'] = True json_output['changed'] = True
# To deal with the old style values we need to return 'value' in the response # To deal with the old style values we need to return 'value' in the response
new_values = {} new_values = {}
@@ -161,11 +172,11 @@ def main():
# If we were using a name we will just add a value of a string, otherwise we will return an array in values # If we were using a name we will just add a value of a string, otherwise we will return an array in values
if name is not None: if name is not None:
json_response['value'] = new_values[name] json_output['value'] = new_values[name]
else: else:
json_response['values'] = new_values json_output['values'] = new_values
module.exit_json(**json_response) module.exit_json(**json_output)
elif 'json' in response and '__all__' in response['json']: elif 'json' in response and '__all__' in response['json']:
module.fail_json(msg=response['json']['__all__']) module.fail_json(msg=response['json']['__all__'])
else: else:

View File

@@ -48,6 +48,19 @@
- "result is changed" - "result is changed"
- "result.status == 'successful'" - "result.status == 'successful'"
- name: Launch an Ad Hoc Command without module argument
tower_ad_hoc_command:
inventory: "Demo Inventory"
credential: "{{ ssh_cred_name }}"
module_name: "ping"
wait: true
register: result
- assert:
that:
- "result is changed"
- "result.status == 'successful'"
- name: Check module fails with correct msg - name: Check module fails with correct msg
tower_ad_hoc_command: tower_ad_hoc_command:
inventory: "{{ inv_name }}" inventory: "{{ inv_name }}"

View File

@@ -0,0 +1,27 @@
Copyright (c) 2019 Jonas Krüger Svensson
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the authors nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -13,6 +13,7 @@ django-auth-ldap
django-cors-headers django-cors-headers
django-crum django-crum
django-extensions>=2.2.9 # https://github.com/ansible/awx/pull/6441 django-extensions>=2.2.9 # https://github.com/ansible/awx/pull/6441
django-guid==2.2.0 # pinned to match Django 2.2
django-jsonfield==1.2.0 # see UPGRADE BLOCKERs django-jsonfield==1.2.0 # see UPGRADE BLOCKERs
django-oauth-toolkit==1.1.3 # see UPGRADE BLOCKERs django-oauth-toolkit==1.1.3 # see UPGRADE BLOCKERs
django-polymorphic django-polymorphic

View File

@@ -19,7 +19,7 @@ channels-redis==3.1.0 # via -r /awx_devel/requirements/requirements.in
channels==2.4.0 # via -r /awx_devel/requirements/requirements.in, channels-redis channels==2.4.0 # via -r /awx_devel/requirements/requirements.in, channels-redis
chardet==3.0.4 # via aiohttp, requests chardet==3.0.4 # via aiohttp, requests
constantly==15.1.0 # via twisted constantly==15.1.0 # via twisted
cryptography==2.9.2 # via adal, autobahn, azure-keyvault, pyopenssl, service-identity, social-auth-core cryptography==2.9.2 # via -r /awx_devel/requirements/requirements.in, adal, autobahn, azure-keyvault, pyopenssl, service-identity, social-auth-core
daphne==2.4.1 # via -r /awx_devel/requirements/requirements.in, channels daphne==2.4.1 # via -r /awx_devel/requirements/requirements.in, channels
defusedxml==0.6.0 # via python3-openid, python3-saml, social-auth-core defusedxml==0.6.0 # via python3-openid, python3-saml, social-auth-core
dictdiffer==0.8.1 # via openshift dictdiffer==0.8.1 # via openshift
@@ -27,6 +27,7 @@ django-auth-ldap==2.1.0 # via -r /awx_devel/requirements/requirements.in
django-cors-headers==3.2.1 # via -r /awx_devel/requirements/requirements.in django-cors-headers==3.2.1 # via -r /awx_devel/requirements/requirements.in
django-crum==0.7.5 # via -r /awx_devel/requirements/requirements.in django-crum==0.7.5 # via -r /awx_devel/requirements/requirements.in
django-extensions==2.2.9 # via -r /awx_devel/requirements/requirements.in django-extensions==2.2.9 # via -r /awx_devel/requirements/requirements.in
django-guid==2.2.0 # via -r /awx_devel/requirements/requirements.in
django-jsonfield==1.2.0 # via -r /awx_devel/requirements/requirements.in django-jsonfield==1.2.0 # via -r /awx_devel/requirements/requirements.in
django-oauth-toolkit==1.1.3 # via -r /awx_devel/requirements/requirements.in django-oauth-toolkit==1.1.3 # via -r /awx_devel/requirements/requirements.in
django-pglocks==1.0.4 # via -r /awx_devel/requirements/requirements.in django-pglocks==1.0.4 # via -r /awx_devel/requirements/requirements.in
@@ -37,7 +38,7 @@ django-redis==4.5.0 # via -r /awx_devel/requirements/requirements.in
django-solo==1.1.3 # via -r /awx_devel/requirements/requirements.in django-solo==1.1.3 # via -r /awx_devel/requirements/requirements.in
django-split-settings==1.0.0 # via -r /awx_devel/requirements/requirements.in django-split-settings==1.0.0 # via -r /awx_devel/requirements/requirements.in
django-taggit==1.2.0 # via -r /awx_devel/requirements/requirements.in django-taggit==1.2.0 # via -r /awx_devel/requirements/requirements.in
django==2.2.16 # via -r /awx_devel/requirements/requirements.in, channels, django-auth-ldap, django-cors-headers, django-crum, django-jsonfield, django-oauth-toolkit, django-polymorphic, django-taggit, djangorestframework django==2.2.16 # via -r /awx_devel/requirements/requirements.in, channels, django-auth-ldap, django-cors-headers, django-crum, django-guid, django-jsonfield, django-oauth-toolkit, django-polymorphic, django-taggit, djangorestframework
djangorestframework-yaml==1.0.3 # via -r /awx_devel/requirements/requirements.in djangorestframework-yaml==1.0.3 # via -r /awx_devel/requirements/requirements.in
djangorestframework==3.12.1 # via -r /awx_devel/requirements/requirements.in djangorestframework==3.12.1 # via -r /awx_devel/requirements/requirements.in
docutils==0.16 # via python-daemon docutils==0.16 # via python-daemon

View File

@@ -21,4 +21,4 @@ backports.tempfile # support in unit tests for py32+ tempfile.TemporaryDirector
mockldap mockldap
sdb sdb
gprof2dot gprof2dot
atomicwrites==1.1.5 atomicwrites==1.4.0