Compare commits

..

1 Commits

Author SHA1 Message Date
Luiz Costa
7f25309078 WIP Makefile 2022-09-02 15:01:00 -03:00
110 changed files with 943 additions and 2639 deletions

103
Makefile
View File

@@ -54,45 +54,6 @@ I18N_FLAG_FILE = .i18n_built
VERSION PYTHON_VERSION docker-compose-sources \ VERSION PYTHON_VERSION docker-compose-sources \
.git/hooks/pre-commit .git/hooks/pre-commit
clean-tmp:
rm -rf tmp/
clean-venv:
rm -rf venv/
clean-dist:
rm -rf dist
clean-schema:
rm -rf swagger.json
rm -rf schema.json
rm -rf reference-schema.json
clean-languages:
rm -f $(I18N_FLAG_FILE)
find ./awx/locale/ -type f -regex ".*\.mo$" -delete
## Remove temporary build files, compiled Python files.
clean: clean-ui clean-api clean-awxkit clean-dist
rm -rf awx/public
rm -rf awx/lib/site-packages
rm -rf awx/job_status
rm -rf awx/job_output
rm -rf reports
rm -rf tmp
rm -rf $(I18N_FLAG_FILE)
mkdir tmp
clean-api:
rm -rf build $(NAME)-$(VERSION) *.egg-info
find . -type f -regex ".*\.py[co]$$" -delete
find . -type d -name "__pycache__" -delete
rm -f awx/awx_test.sqlite3*
rm -rf requirements/vendor
rm -rf awx/projects
clean-awxkit:
rm -rf awxkit/*.egg-info awxkit/.tox awxkit/build/*
## convenience target to assert environment variables are defined ## convenience target to assert environment variables are defined
guard-%: guard-%:
@@ -365,13 +326,75 @@ bulk_data:
fi; \ fi; \
$(PYTHON) tools/data_generators/rbac_dummy_data_generator.py --preset=$(DATA_GEN_PRESET) $(PYTHON) tools/data_generators/rbac_dummy_data_generator.py --preset=$(DATA_GEN_PRESET)
# CLEANUP COMMANDS
# --------------------------------------
## Clean everything. Including temporary build files, compiled Python files.
clean: clean-tmp clean-ui clean-api clean-awxkit clean-dist
rm -rf awx/public
rm -rf awx/lib/site-packages
rm -rf awx/job_status
rm -rf awx/job_output
rm -rf reports
rm -rf $(I18N_FLAG_FILE)
clean-tmp:
rm -rf tmp/
mkdir tmp
clean-venv:
rm -rf venv/
clean-dist:
rm -rf dist
clean-schema:
rm -rf swagger.json
rm -rf schema.json
rm -rf reference-schema.json
clean-languages:
rm -f $(I18N_FLAG_FILE)
find ./awx/locale/ -type f -regex ".*\.mo$" -delete
clean-api:
rm -rf build $(NAME)-$(VERSION) *.egg-info
find . -type f -regex ".*\.py[co]$$" -delete
find . -type d -name "__pycache__" -delete
rm -f awx/awx_test.sqlite3*
rm -rf requirements/vendor
rm -rf awx/projects
## Clean UI builded static files (alias for ui-clean)
clean-ui: ui-clean
## Clean temp build files from the awxkit
clean-awxkit:
rm -rf awxkit/*.egg-info awxkit/.tox awxkit/build/*
clean-docker-images:
IMAGES_TO_BE_DELETE=' \
quay.io/ansible/receptor \
quay.io/awx/awx_devel \
ansible/receptor \
postgres \
redis \
' && \
for IMAGE in $$IMAGES_TO_BE_DELETE; do \
echo "Removing image '$$IMAGE'" && \
IMAGE_IDS=$$(docker image ls -a | grep $$IMAGE | awk '{print $$3}') echo "oi" \
done
clean-docker-containers:
clean-docker-volumes:
# UI TASKS # UI TASKS
# -------------------------------------- # --------------------------------------
UI_BUILD_FLAG_FILE = awx/ui/.ui-built UI_BUILD_FLAG_FILE = awx/ui/.ui-built
clean-ui: ui-clean:
rm -rf node_modules rm -rf node_modules
rm -rf awx/ui/node_modules rm -rf awx/ui/node_modules
rm -rf awx/ui/build rm -rf awx/ui/build

View File

@@ -154,7 +154,6 @@ SUMMARIZABLE_FK_FIELDS = {
'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'),
'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed'), 'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed'),
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'kubernetes', 'credential_type_id'), 'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'kubernetes', 'credential_type_id'),
'signature_validation_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'credential_type_id'),
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed', 'type', 'canceled_on'), 'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed', 'type', 'canceled_on'),
'job_template': DEFAULT_SUMMARY_FIELDS, 'job_template': DEFAULT_SUMMARY_FIELDS,
'workflow_job_template': DEFAULT_SUMMARY_FIELDS, 'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
@@ -1471,7 +1470,6 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
'allow_override', 'allow_override',
'custom_virtualenv', 'custom_virtualenv',
'default_environment', 'default_environment',
'signature_validation_credential',
) + ( ) + (
'last_update_failed', 'last_update_failed',
'last_updated', 'last_updated',
@@ -4197,15 +4195,6 @@ class JobLaunchSerializer(BaseSerializer):
elif template.project.status in ('error', 'failed'): elif template.project.status in ('error', 'failed'):
errors['playbook'] = _("Missing a revision to run due to failed project update.") errors['playbook'] = _("Missing a revision to run due to failed project update.")
latest_update = template.project.project_updates.last()
if latest_update is not None and latest_update.failed:
failed_validation_tasks = latest_update.project_update_events.filter(
event='runner_on_failed',
play="Perform project signature/checksum verification",
)
if failed_validation_tasks:
errors['playbook'] = _("Last project update failed due to signature validation failure.")
# cannot run a playbook without an inventory # cannot run a playbook without an inventory
if template.inventory and template.inventory.pending_deletion is True: if template.inventory and template.inventory.pending_deletion is True:
errors['inventory'] = _("The inventory associated with this Job Template is being deleted.") errors['inventory'] = _("The inventory associated with this Job Template is being deleted.")

View File

@@ -37,24 +37,18 @@ class Control(object):
def running(self, *args, **kwargs): def running(self, *args, **kwargs):
return self.control_with_reply('running', *args, **kwargs) return self.control_with_reply('running', *args, **kwargs)
def cancel(self, task_ids, *args, **kwargs):
return self.control_with_reply('cancel', *args, extra_data={'task_ids': task_ids}, **kwargs)
@classmethod @classmethod
def generate_reply_queue_name(cls): def generate_reply_queue_name(cls):
return f"reply_to_{str(uuid.uuid4()).replace('-','_')}" return f"reply_to_{str(uuid.uuid4()).replace('-','_')}"
def control_with_reply(self, command, timeout=5, extra_data=None): def control_with_reply(self, command, timeout=5):
logger.warning('checking {} {} for {}'.format(self.service, command, self.queuename)) logger.warning('checking {} {} for {}'.format(self.service, command, self.queuename))
reply_queue = Control.generate_reply_queue_name() reply_queue = Control.generate_reply_queue_name()
self.result = None self.result = None
with pg_bus_conn(new_connection=True) as conn: with pg_bus_conn(new_connection=True) as conn:
conn.listen(reply_queue) conn.listen(reply_queue)
send_data = {'control': command, 'reply_to': reply_queue} conn.notify(self.queuename, json.dumps({'control': command, 'reply_to': reply_queue}))
if extra_data:
send_data.update(extra_data)
conn.notify(self.queuename, json.dumps(send_data))
for reply in conn.events(select_timeout=timeout, yield_timeouts=True): for reply in conn.events(select_timeout=timeout, yield_timeouts=True):
if reply is None: if reply is None:

View File

@@ -63,7 +63,7 @@ class AWXConsumerBase(object):
def control(self, body): def control(self, body):
logger.warning(f'Received control signal:\n{body}') logger.warning(f'Received control signal:\n{body}')
control = body.get('control') control = body.get('control')
if control in ('status', 'running', 'cancel'): if control in ('status', 'running'):
reply_queue = body['reply_to'] reply_queue = body['reply_to']
if control == 'status': if control == 'status':
msg = '\n'.join([self.listening_on, self.pool.debug()]) msg = '\n'.join([self.listening_on, self.pool.debug()])
@@ -72,17 +72,6 @@ class AWXConsumerBase(object):
for worker in self.pool.workers: for worker in self.pool.workers:
worker.calculate_managed_tasks() worker.calculate_managed_tasks()
msg.extend(worker.managed_tasks.keys()) msg.extend(worker.managed_tasks.keys())
elif control == 'cancel':
msg = []
task_ids = set(body['task_ids'])
for worker in self.pool.workers:
task = worker.current_task
if task and task['uuid'] in task_ids:
logger.warn(f'Sending SIGTERM to task id={task["uuid"]}, task={task.get("task")}, args={task.get("args")}')
os.kill(worker.pid, signal.SIGTERM)
msg.append(task['uuid'])
if task_ids and not msg:
logger.info(f'Could not locate running tasks to cancel with ids={task_ids}')
with pg_bus_conn() as conn: with pg_bus_conn() as conn:
conn.notify(reply_queue, json.dumps(msg)) conn.notify(reply_queue, json.dumps(msg))

View File

@@ -54,7 +54,7 @@ class Command(BaseCommand):
capacity = f' capacity={x.capacity}' if x.node_type != 'hop' else '' capacity = f' capacity={x.capacity}' if x.node_type != 'hop' else ''
version = f" version={x.version or '?'}" if x.node_type != 'hop' else '' version = f" version={x.version or '?'}" if x.node_type != 'hop' else ''
heartbeat = f' heartbeat="{x.last_seen:%Y-%m-%d %H:%M:%S}"' if x.capacity or x.node_type == 'hop' else '' heartbeat = f' heartbeat="{x.modified:%Y-%m-%d %H:%M:%S}"' if x.capacity or x.node_type == 'hop' else ''
print(f'\t{color}{x.hostname}{capacity} node_type={x.node_type}{version}{heartbeat}\033[0m') print(f'\t{color}{x.hostname}{capacity} node_type={x.node_type}{version}{heartbeat}\033[0m')
print() print()

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2015 Ansible, Inc. # Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved. # All Rights Reserved.
import logging import logging
import yaml
from django.conf import settings from django.conf import settings
from django.core.cache import cache as django_cache from django.core.cache import cache as django_cache
@@ -31,16 +30,7 @@ class Command(BaseCommand):
'--reload', '--reload',
dest='reload', dest='reload',
action='store_true', action='store_true',
help=('cause the dispatcher to recycle all of its worker processes; running jobs will run to completion first'), help=('cause the dispatcher to recycle all of its worker processes;' 'running jobs will run to completion first'),
)
parser.add_argument(
'--cancel',
dest='cancel',
help=(
'Cancel a particular task id. Takes either a single id string, or a JSON list of multiple ids. '
'Can take in output from the --running argument as input to cancel all tasks. '
'Only running tasks can be canceled, queued tasks must be started before they can be canceled.'
),
) )
def handle(self, *arg, **options): def handle(self, *arg, **options):
@@ -52,16 +42,6 @@ class Command(BaseCommand):
return return
if options.get('reload'): if options.get('reload'):
return Control('dispatcher').control({'control': 'reload'}) return Control('dispatcher').control({'control': 'reload'})
if options.get('cancel'):
cancel_str = options.get('cancel')
try:
cancel_data = yaml.safe_load(cancel_str)
except Exception:
cancel_data = [cancel_str]
if not isinstance(cancel_data, list):
cancel_data = [cancel_str]
print(Control('dispatcher').cancel(cancel_data))
return
# It's important to close these because we're _about_ to fork, and we # It's important to close these because we're _about_ to fork, and we
# don't want the forked processes to inherit the open sockets # don't want the forked processes to inherit the open sockets

View File

@@ -1,57 +0,0 @@
# Generated by Django 3.2.13 on 2022-08-24 14:02
from django.db import migrations, models
import django.db.models.deletion
from awx.main.models import CredentialType
from awx.main.utils.common import set_current_apps
def setup_tower_managed_defaults(apps, schema_editor):
set_current_apps(apps)
CredentialType.setup_tower_managed_defaults(apps)
class Migration(migrations.Migration):
dependencies = [
('main', '0166_alter_jobevent_host'),
]
operations = [
migrations.AddField(
model_name='project',
name='signature_validation_credential',
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='projects_signature_validation',
to='main.credential',
help_text='An optional credential used for validating files in the project against unexpected changes.',
),
),
migrations.AlterField(
model_name='credentialtype',
name='kind',
field=models.CharField(
choices=[
('ssh', 'Machine'),
('vault', 'Vault'),
('net', 'Network'),
('scm', 'Source Control'),
('cloud', 'Cloud'),
('registry', 'Container Registry'),
('token', 'Personal Access Token'),
('insights', 'Insights'),
('external', 'External'),
('kubernetes', 'Kubernetes'),
('galaxy', 'Galaxy/Automation Hub'),
('cryptography', 'Cryptography'),
],
max_length=32,
),
),
migrations.RunPython(setup_tower_managed_defaults),
]

View File

@@ -336,7 +336,6 @@ class CredentialType(CommonModelNameNotUnique):
('external', _('External')), ('external', _('External')),
('kubernetes', _('Kubernetes')), ('kubernetes', _('Kubernetes')),
('galaxy', _('Galaxy/Automation Hub')), ('galaxy', _('Galaxy/Automation Hub')),
('cryptography', _('Cryptography')),
) )
kind = models.CharField(max_length=32, choices=KIND_CHOICES) kind = models.CharField(max_length=32, choices=KIND_CHOICES)
@@ -1172,25 +1171,6 @@ ManagedCredentialType(
}, },
) )
ManagedCredentialType(
namespace='gpg_public_key',
kind='cryptography',
name=gettext_noop('GPG Public Key'),
inputs={
'fields': [
{
'id': 'gpg_public_key',
'label': gettext_noop('GPG Public Key'),
'type': 'string',
'secret': True,
'multiline': True,
'help_text': gettext_noop('GPG Public Key used to validate content signatures.'),
},
],
'required': ['gpg_public_key'],
},
)
class CredentialInputSource(PrimordialModel): class CredentialInputSource(PrimordialModel):
class Meta: class Meta:

View File

@@ -412,11 +412,6 @@ class TaskManagerJobMixin(TaskManagerUnifiedJobMixin):
class Meta: class Meta:
abstract = True abstract = True
def get_jobs_fail_chain(self):
if self.project_update_id:
return [self.project_update]
return []
class TaskManagerUpdateOnLaunchMixin(TaskManagerUnifiedJobMixin): class TaskManagerUpdateOnLaunchMixin(TaskManagerUnifiedJobMixin):
class Meta: class Meta:

View File

@@ -284,17 +284,6 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
help_text=_('Allow changing the SCM branch or revision in a job template ' 'that uses this project.'), help_text=_('Allow changing the SCM branch or revision in a job template ' 'that uses this project.'),
) )
# credential (keys) used to validate content signature
signature_validation_credential = models.ForeignKey(
'Credential',
related_name='%(class)ss_signature_validation',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
help_text=_('An optional credential used for validating files in the project against unexpected changes.'),
)
scm_revision = models.CharField( scm_revision = models.CharField(
max_length=1024, max_length=1024,
blank=True, blank=True,
@@ -631,10 +620,6 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
added_update_fields = [] added_update_fields = []
if not self.job_tags: if not self.job_tags:
job_tags = ['update_{}'.format(self.scm_type), 'install_roles', 'install_collections'] job_tags = ['update_{}'.format(self.scm_type), 'install_roles', 'install_collections']
if self.project.signature_validation_credential is not None:
credential_type = self.project.signature_validation_credential.credential_type.namespace
job_tags.append(f'validation_{credential_type}')
job_tags.append('validation_checksum_manifest')
self.job_tags = ','.join(job_tags) self.job_tags = ','.join(job_tags)
added_update_fields.append('job_tags') added_update_fields.append('job_tags')
if self.scm_delete_on_update and 'delete' not in self.job_tags and self.job_type == 'check': if self.scm_delete_on_update and 'delete' not in self.job_tags and self.job_type == 'check':

View File

@@ -1395,6 +1395,23 @@ class UnifiedJob(
# Done! # Done!
return True return True
@property
def actually_running(self):
# returns True if the job is running in the appropriate dispatcher process
running = False
if all([self.status == 'running', self.celery_task_id, self.execution_node]):
# If the job is marked as running, but the dispatcher
# doesn't know about it (or the dispatcher doesn't reply),
# then cancel the job
timeout = 5
try:
running = self.celery_task_id in ControlDispatcher('dispatcher', self.controller_node or self.execution_node).running(timeout=timeout)
except socket.timeout:
logger.error('could not reach dispatcher on {} within {}s'.format(self.execution_node, timeout))
except Exception:
logger.exception("error encountered when checking task status")
return running
@property @property
def can_cancel(self): def can_cancel(self):
return bool(self.status in CAN_CANCEL) return bool(self.status in CAN_CANCEL)
@@ -1404,61 +1421,27 @@ class UnifiedJob(
return 'Previous Task Canceled: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % (self.model_to_str(), self.name, self.id) return 'Previous Task Canceled: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % (self.model_to_str(), self.name, self.id)
return None return None
def fallback_cancel(self):
if not self.celery_task_id:
self.refresh_from_db(fields=['celery_task_id'])
self.cancel_dispatcher_process()
def cancel_dispatcher_process(self):
"""Returns True if dispatcher running this job acknowledged request and sent SIGTERM"""
if not self.celery_task_id:
return
canceled = []
try:
# Use control and reply mechanism to cancel and obtain confirmation
timeout = 5
canceled = ControlDispatcher('dispatcher', self.controller_node).cancel([self.celery_task_id])
except socket.timeout:
logger.error(f'could not reach dispatcher on {self.controller_node} within {timeout}s')
except Exception:
logger.exception("error encountered when checking task status")
return bool(self.celery_task_id in canceled) # True or False, whether confirmation was obtained
def cancel(self, job_explanation=None, is_chain=False): def cancel(self, job_explanation=None, is_chain=False):
if self.can_cancel: if self.can_cancel:
if not is_chain: if not is_chain:
for x in self.get_jobs_fail_chain(): for x in self.get_jobs_fail_chain():
x.cancel(job_explanation=self._build_job_explanation(), is_chain=True) x.cancel(job_explanation=self._build_job_explanation(), is_chain=True)
cancel_fields = []
if not self.cancel_flag: if not self.cancel_flag:
self.cancel_flag = True self.cancel_flag = True
self.start_args = '' # blank field to remove encrypted passwords self.start_args = '' # blank field to remove encrypted passwords
cancel_fields.extend(['cancel_flag', 'start_args']) cancel_fields = ['cancel_flag', 'start_args']
connection.on_commit(lambda: self.websocket_emit_status("canceled")) if self.status in ('pending', 'waiting', 'new'):
self.status = 'canceled'
cancel_fields.append('status')
if self.status == 'running' and not self.actually_running:
self.status = 'canceled'
cancel_fields.append('status')
if job_explanation is not None: if job_explanation is not None:
self.job_explanation = job_explanation self.job_explanation = job_explanation
cancel_fields.append('job_explanation') cancel_fields.append('job_explanation')
self.save(update_fields=cancel_fields)
controller_notified = False self.websocket_emit_status("canceled")
if self.celery_task_id:
controller_notified = self.cancel_dispatcher_process()
else:
# Avoid race condition where we have stale model from pending state but job has already started,
# its checking signal but not cancel_flag, so re-send signal after this database commit
connection.on_commit(self.fallback_cancel)
# If a SIGTERM signal was sent to the control process, and acked by the dispatcher
# then we want to let its own cleanup change status, otherwise change status now
if not controller_notified:
if self.status != 'canceled':
self.status = 'canceled'
cancel_fields.append('status')
self.save(update_fields=cancel_fields)
return self.cancel_flag return self.cancel_flag
@property @property

View File

@@ -723,10 +723,11 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
def preferred_instance_groups(self): def preferred_instance_groups(self):
return [] return []
def cancel_dispatcher_process(self): @property
def actually_running(self):
# WorkflowJobs don't _actually_ run anything in the dispatcher, so # WorkflowJobs don't _actually_ run anything in the dispatcher, so
# there's no point in asking the dispatcher if it knows about this task # there's no point in asking the dispatcher if it knows about this task
return True return self.status == 'running'
class WorkflowApprovalTemplate(UnifiedJobTemplate, RelatedJobsMixin): class WorkflowApprovalTemplate(UnifiedJobTemplate, RelatedJobsMixin):

View File

@@ -6,16 +6,17 @@ import os
import stat import stat
# Django # Django
from django.utils.timezone import now
from django.conf import settings from django.conf import settings
from django_guid import get_guid from django_guid import get_guid
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.db import connections
# AWX # AWX
from awx.main.redact import UriCleaner from awx.main.redact import UriCleaner
from awx.main.constants import MINIMAL_EVENTS, ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE from awx.main.constants import MINIMAL_EVENTS, ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE
from awx.main.utils.update_model import update_model from awx.main.utils.update_model import update_model
from awx.main.queue import CallbackQueueDispatcher from awx.main.queue import CallbackQueueDispatcher
from awx.main.tasks.signals import signal_callback
logger = logging.getLogger('awx.main.tasks.callback') logger = logging.getLogger('awx.main.tasks.callback')
@@ -174,6 +175,28 @@ class RunnerCallback:
return False return False
def cancel_callback(self):
"""
Ansible runner callback to tell the job when/if it is canceled
"""
unified_job_id = self.instance.pk
if signal_callback():
return True
try:
self.instance = self.update_model(unified_job_id)
except Exception:
logger.exception(f'Encountered error during cancel check for {unified_job_id}, canceling now')
return True
if not self.instance:
logger.error('unified job {} was deleted while running, canceling'.format(unified_job_id))
return True
if self.instance.cancel_flag or self.instance.status == 'canceled':
cancel_wait = (now() - self.instance.modified).seconds if self.instance.modified else 0
if cancel_wait > 5:
logger.warning('Request to cancel {} took {} seconds to complete.'.format(self.instance.log_format, cancel_wait))
return True
return False
def finished_callback(self, runner_obj): def finished_callback(self, runner_obj):
""" """
Ansible runner callback triggered on finished run Ansible runner callback triggered on finished run
@@ -204,8 +227,6 @@ class RunnerCallback:
with disable_activity_stream(): with disable_activity_stream():
self.instance = self.update_model(self.instance.pk, job_args=json.dumps(runner_config.command), job_cwd=runner_config.cwd, job_env=job_env) self.instance = self.update_model(self.instance.pk, job_args=json.dumps(runner_config.command), job_cwd=runner_config.cwd, job_env=job_env)
# We opened a connection just for that save, close it here now
connections.close_all()
elif status_data['status'] == 'failed': elif status_data['status'] == 'failed':
# For encrypted ssh_key_data, ansible-runner worker will open and write the # For encrypted ssh_key_data, ansible-runner worker will open and write the
# ssh_key_data to a named pipe. Then, once the podman container starts, ssh-agent will # ssh_key_data to a named pipe. Then, once the podman container starts, ssh-agent will

View File

@@ -487,7 +487,6 @@ class BaseTask(object):
self.instance.log_lifecycle("preparing_playbook") self.instance.log_lifecycle("preparing_playbook")
if self.instance.cancel_flag or signal_callback(): if self.instance.cancel_flag or signal_callback():
self.instance = self.update_model(self.instance.pk, status='canceled') self.instance = self.update_model(self.instance.pk, status='canceled')
if self.instance.status != 'running': if self.instance.status != 'running':
# Stop the task chain and prevent starting the job if it has # Stop the task chain and prevent starting the job if it has
# already been canceled. # already been canceled.
@@ -590,7 +589,7 @@ class BaseTask(object):
event_handler=self.runner_callback.event_handler, event_handler=self.runner_callback.event_handler,
finished_callback=self.runner_callback.finished_callback, finished_callback=self.runner_callback.finished_callback,
status_handler=self.runner_callback.status_handler, status_handler=self.runner_callback.status_handler,
cancel_callback=signal_callback, cancel_callback=self.runner_callback.cancel_callback,
**params, **params,
) )
else: else:
@@ -1271,10 +1270,6 @@ class RunProjectUpdate(BaseTask):
# for raw archive, prevent error moving files between volumes # for raw archive, prevent error moving files between volumes
extra_vars['ansible_remote_tmp'] = os.path.join(project_update.get_project_path(check_if_exists=False), '.ansible_awx', 'tmp') extra_vars['ansible_remote_tmp'] = os.path.join(project_update.get_project_path(check_if_exists=False), '.ansible_awx', 'tmp')
if project_update.project.signature_validation_credential is not None:
pubkey = project_update.project.signature_validation_credential.get_input('gpg_public_key')
extra_vars['gpg_pubkey'] = pubkey
self._write_extra_vars_file(private_data_dir, extra_vars) self._write_extra_vars_file(private_data_dir, extra_vars)
def build_playbook_path_relative_to_cwd(self, project_update, private_data_dir): def build_playbook_path_relative_to_cwd(self, project_update, private_data_dir):
@@ -1627,7 +1622,7 @@ class RunInventoryUpdate(SourceControlMixin, BaseTask):
handler = SpecialInventoryHandler( handler = SpecialInventoryHandler(
self.runner_callback.event_handler, self.runner_callback.event_handler,
signal_callback, self.runner_callback.cancel_callback,
verbosity=inventory_update.verbosity, verbosity=inventory_update.verbosity,
job_timeout=self.get_instance_timeout(self.instance), job_timeout=self.get_instance_timeout(self.instance),
start_time=inventory_update.started, start_time=inventory_update.started,

View File

@@ -12,7 +12,6 @@ import yaml
# Django # Django
from django.conf import settings from django.conf import settings
from django.db import connections
# Runner # Runner
import ansible_runner import ansible_runner
@@ -26,7 +25,6 @@ from awx.main.utils.common import (
cleanup_new_process, cleanup_new_process,
) )
from awx.main.constants import MAX_ISOLATED_PATH_COLON_DELIMITER from awx.main.constants import MAX_ISOLATED_PATH_COLON_DELIMITER
from awx.main.tasks.signals import signal_state, signal_callback, SignalExit
# Receptorctl # Receptorctl
from receptorctl.socket_interface import ReceptorControl from receptorctl.socket_interface import ReceptorControl
@@ -337,32 +335,24 @@ class AWXReceptorJob:
shutil.rmtree(artifact_dir) shutil.rmtree(artifact_dir)
resultsock, resultfile = receptor_ctl.get_work_results(self.unit_id, return_socket=True, return_sockfile=True) resultsock, resultfile = receptor_ctl.get_work_results(self.unit_id, return_socket=True, return_sockfile=True)
# Both "processor" and "cancel_watcher" are spawned in separate threads.
connections.close_all() # We wait for the first one to return. If cancel_watcher returns first,
# we yank the socket out from underneath the processor, which will cause it
# "processor" and the main thread will be separate threads. # to exit. A reference to the processor_future is passed into the cancel_watcher_future,
# If a cancel happens, the main thread will encounter an exception, in which case # Which exits if the job has finished normally. The context manager ensures we do not
# we yank the socket out from underneath the processor, which will cause it to exit. # leave any threads laying around.
# The ThreadPoolExecutor context manager ensures we do not leave any threads laying around. with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
processor_future = executor.submit(self.processor, resultfile) processor_future = executor.submit(self.processor, resultfile)
cancel_watcher_future = executor.submit(self.cancel_watcher, processor_future)
futures = [processor_future, cancel_watcher_future]
first_future = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
try: res = list(first_future.done)[0].result()
signal_state.raise_exception = True if res.status == 'canceled':
# address race condition where SIGTERM was issued after this dispatcher task started
if signal_callback():
raise SignalExit()
res = processor_future.result()
except SignalExit:
receptor_ctl.simple_command(f"work cancel {self.unit_id}") receptor_ctl.simple_command(f"work cancel {self.unit_id}")
resultsock.shutdown(socket.SHUT_RDWR) resultsock.shutdown(socket.SHUT_RDWR)
resultfile.close() resultfile.close()
result = namedtuple('result', ['status', 'rc']) elif res.status == 'error':
res = result('canceled', 1)
finally:
signal_state.raise_exception = False
if res.status == 'error':
# If ansible-runner ran, but an error occured at runtime, the traceback information # If ansible-runner ran, but an error occured at runtime, the traceback information
# is saved via the status_handler passed in to the processor. # is saved via the status_handler passed in to the processor.
if 'result_traceback' in self.task.runner_callback.extra_update_fields: if 'result_traceback' in self.task.runner_callback.extra_update_fields:
@@ -456,6 +446,18 @@ class AWXReceptorJob:
return 'local' return 'local'
return 'ansible-runner' return 'ansible-runner'
@cleanup_new_process
def cancel_watcher(self, processor_future):
while True:
if processor_future.done():
return processor_future.result()
if self.task.runner_callback.cancel_callback():
result = namedtuple('result', ['status', 'rc'])
return result('canceled', 1)
time.sleep(1)
@property @property
def pod_definition(self): def pod_definition(self):
ee = self.task.instance.execution_environment ee = self.task.instance.execution_environment

View File

@@ -9,17 +9,12 @@ logger = logging.getLogger('awx.main.tasks.signals')
__all__ = ['with_signal_handling', 'signal_callback'] __all__ = ['with_signal_handling', 'signal_callback']
class SignalExit(Exception):
pass
class SignalState: class SignalState:
def reset(self): def reset(self):
self.sigterm_flag = False self.sigterm_flag = False
self.is_active = False self.is_active = False
self.original_sigterm = None self.original_sigterm = None
self.original_sigint = None self.original_sigint = None
self.raise_exception = False
def __init__(self): def __init__(self):
self.reset() self.reset()
@@ -27,9 +22,6 @@ class SignalState:
def set_flag(self, *args): def set_flag(self, *args):
"""Method to pass into the python signal.signal method to receive signals""" """Method to pass into the python signal.signal method to receive signals"""
self.sigterm_flag = True self.sigterm_flag = True
if self.raise_exception:
self.raise_exception = False # so it is not raised a second time in error handling
raise SignalExit()
def connect_signals(self): def connect_signals(self):
self.original_sigterm = signal.getsignal(signal.SIGTERM) self.original_sigterm = signal.getsignal(signal.SIGTERM)

View File

@@ -74,37 +74,34 @@ GLqbpJyX2r3p/Rmo6mLY71SqpA==
@pytest.mark.django_db @pytest.mark.django_db
def test_default_cred_types(): def test_default_cred_types():
assert sorted(CredentialType.defaults.keys()) == sorted( assert sorted(CredentialType.defaults.keys()) == [
[ 'aim',
'aim', 'aws',
'aws', 'azure_kv',
'azure_kv', 'azure_rm',
'azure_rm', 'centrify_vault_kv',
'centrify_vault_kv', 'conjur',
'conjur', 'controller',
'controller', 'galaxy_api_token',
'galaxy_api_token', 'gce',
'gce', 'github_token',
'github_token', 'gitlab_token',
'gitlab_token', 'hashivault_kv',
'gpg_public_key', 'hashivault_ssh',
'hashivault_kv', 'insights',
'hashivault_ssh', 'kubernetes_bearer_token',
'insights', 'net',
'kubernetes_bearer_token', 'openstack',
'net', 'registry',
'openstack', 'rhv',
'registry', 'satellite6',
'rhv', 'scm',
'satellite6', 'ssh',
'scm', 'thycotic_dsv',
'ssh', 'thycotic_tss',
'thycotic_dsv', 'vault',
'thycotic_tss', 'vmware',
'vault', ]
'vmware',
]
)
for type_ in CredentialType.defaults.values(): for type_ in CredentialType.defaults.values():
assert type_().managed is True assert type_().managed is True

View File

@@ -244,7 +244,7 @@ class TestAutoScaling:
assert not self.pool.should_grow assert not self.pool.should_grow
alive_pid = self.pool.workers[1].pid alive_pid = self.pool.workers[1].pid
self.pool.workers[0].process.terminate() self.pool.workers[0].process.terminate()
time.sleep(2) # wait a moment for sigterm time.sleep(1) # wait a moment for sigterm
# clean up and the dead worker # clean up and the dead worker
self.pool.cleanup() self.pool.cleanup()

View File

@@ -22,10 +22,6 @@ def test_unified_job_workflow_attributes():
assert job.workflow_job_id == 1 assert job.workflow_job_id == 1
def mock_on_commit(f):
f()
@pytest.fixture @pytest.fixture
def unified_job(mocker): def unified_job(mocker):
mocker.patch.object(UnifiedJob, 'can_cancel', return_value=True) mocker.patch.object(UnifiedJob, 'can_cancel', return_value=True)
@@ -34,14 +30,12 @@ def unified_job(mocker):
j.cancel_flag = None j.cancel_flag = None
j.save = mocker.MagicMock() j.save = mocker.MagicMock()
j.websocket_emit_status = mocker.MagicMock() j.websocket_emit_status = mocker.MagicMock()
j.fallback_cancel = mocker.MagicMock()
return j return j
def test_cancel(unified_job): def test_cancel(unified_job):
with mock.patch('awx.main.models.unified_jobs.connection.on_commit', wraps=mock_on_commit): unified_job.cancel()
unified_job.cancel()
assert unified_job.cancel_flag is True assert unified_job.cancel_flag is True
assert unified_job.status == 'canceled' assert unified_job.status == 'canceled'
@@ -56,11 +50,10 @@ def test_cancel(unified_job):
def test_cancel_job_explanation(unified_job): def test_cancel_job_explanation(unified_job):
job_explanation = 'giggity giggity' job_explanation = 'giggity giggity'
with mock.patch('awx.main.models.unified_jobs.connection.on_commit'): unified_job.cancel(job_explanation=job_explanation)
unified_job.cancel(job_explanation=job_explanation)
assert unified_job.job_explanation == job_explanation assert unified_job.job_explanation == job_explanation
unified_job.save.assert_called_with(update_fields=['cancel_flag', 'start_args', 'job_explanation', 'status']) unified_job.save.assert_called_with(update_fields=['cancel_flag', 'start_args', 'status', 'job_explanation'])
def test_organization_copy_to_jobs(): def test_organization_copy_to_jobs():

View File

@@ -76,7 +76,7 @@ class SpecialInventoryHandler(logging.Handler):
def emit(self, record): def emit(self, record):
# check cancel and timeout status regardless of log level # check cancel and timeout status regardless of log level
this_time = now() this_time = now()
if (this_time - self.last_check).total_seconds() > 0.1: if (this_time - self.last_check).total_seconds() > 0.5: # cancel callback is expensive
self.last_check = this_time self.last_check = this_time
if self.cancel_callback(): if self.cancel_callback():
raise PostRunError('Inventory update has been canceled', status='canceled') raise PostRunError('Inventory update has been canceled', status='canceled')

View File

@@ -1,115 +0,0 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import gnupg
import os
import tempfile
from ansible.module_utils.basic import *
from ansible.plugins.action import ActionBase
from ansible.utils.display import Display
from ansible_sign.checksum import (
ChecksumFile,
ChecksumMismatch,
InvalidChecksumLine,
)
from ansible_sign.checksum.differ import DistlibManifestChecksumFileExistenceDiffer
from ansible_sign.signing import *
display = Display()
VALIDATION_TYPES = (
"checksum_manifest",
"gpg",
)
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
self._supports_check_mode = False
super(ActionModule, self).run(tmp, task_vars)
self.params = self._task.args
self.project_path = self.params.get("project_path")
if self.project_path is None:
return {
"failed": True,
"msg": "No project path (project_path) was supplied.",
}
validation_type = self.params.get("validation_type")
if validation_type is None or validation_type not in VALIDATION_TYPES:
return {"failed": True, "msg": "validation_type must be one of: " + ', '.join(VALIDATION_TYPES)}
validation_method = getattr(self, f"validate_{validation_type}")
return validation_method()
def validate_gpg(self):
gpg_pubkey = self.params.get("gpg_pubkey")
if gpg_pubkey is None:
return {
"failed": True,
"msg": "No GPG public key (gpg_pubkey) was supplied.",
}
signature_file = os.path.join(self.project_path, ".ansible-sign", "sha256sum.txt.sig")
manifest_file = os.path.join(self.project_path, ".ansible-sign", "sha256sum.txt")
for path in (signature_file, manifest_file):
if not os.path.exists(path):
return {
"failed": True,
"msg": f"Expected file not found: {path}",
}
with tempfile.TemporaryDirectory() as gpg_home:
gpg = gnupg.GPG(gnupghome=gpg_home)
gpg.import_keys(gpg_pubkey)
verifier = GPGVerifier(
manifest_path=manifest_file,
detached_signature_path=signature_file,
gpg_home=gpg_home,
)
result = verifier.verify()
return {
"failed": not result.success,
"msg": result.summary,
"gpg_details": result.extra_information,
}
def validate_checksum_manifest(self):
checksum = ChecksumFile(self.project_path, differ=DistlibManifestChecksumFileExistenceDiffer)
manifest_file = os.path.join(self.project_path, ".ansible-sign", "sha256sum.txt")
if not os.path.exists(manifest_file):
return {
"failed": True,
"msg": f"Expected file not found: {path}",
}
checksum_file_contents = open(manifest_file, "r").read()
try:
manifest = checksum.parse(checksum_file_contents)
except InvalidChecksumLine as e:
return {
"failed": True,
"msg": f"Invalid line in checksum manifest: {e}",
}
try:
checksum.verify(manifest)
except ChecksumMismatch as e:
return {
"failed": True,
"msg": str(e),
}
return {
"failed": False,
"msg": "Checksum manifest is valid.",
}

View File

@@ -1,65 +0,0 @@
ANSIBLE_METADATA = {"metadata_version": "1.0", "status": ["stableinterface"], "supported_by": "community"}
DOCUMENTATION = """
---
module: playbook_integrity
short_description: verify that files within a project have not been tampered with.
description:
- Makes use of the 'ansible-sign' project as a library for ensuring that an
Ansible project has not been tampered with.
- There are multiple types of validation that this action plugin supports,
currently: GPG public/private key signing of a checksum manifest file, and
checking the checksum manifest file itself against the checksum of each file
that is being verified.
- In the future, other types of validation may be supported.
options:
project_path:
description:
- Directory of the project being verified. Expected to contain a
C(.ansible-sign) directory with a generated checksum manifest file and a
detached signature for it. These files are produced by the
C(ansible-sign) command-line utility.
required: true
validation_type:
description:
- Describes the kind of validation to perform on the project.
- I(validation_type=gpg) means that a GPG Public Key credential is being
used to verify the integrity of the checksum manifest (and therefore the
project).
- 'checksum_manifest' means that the signed checksum manifest is validated
against all files in the project listed by its MANIFEST.in file. Just
running this plugin with I(validation_type=checksum_manifest) is
typically B(NOT) enough. It should also be run with a I(validation_type)
that ensures that the manifest file itself has not changed, such as
I(validation_type=gpg).
required: true
choices:
- gpg
- checksum_manifest
gpg_pubkey:
description:
- The public key to validate a checksum manifest against. Must match the
detached signature in the project's C(.ansible-sign) directory.
- Required when I(validation_type=gpg).
author:
- Ansible AWX Team
"""
EXAMPLES = """
- name: Verify project content using GPG signature
playbook_integrity:
project_path: /srv/projects/example
validation_type: gpg
gpg_pubkey: |
-----BEING PGP PUBLIC KEY BLOCK-----
mWINAFXMtjsACADIf/zJS0V3UO3c+KAUcpVAcChpliM31ICDWydfIfF3dzMzLcCd
Cj2kk1mPWtP/JHfk1V5czcWWWWGC2Tw4g4IS+LokAAuwk7VKTlI34eeMl8SiZCAI
[...]
- name: Verify project content against checksum manifest
playbook_integrity:
project_path: /srv/projects/example
validation_type: checksum_manifest
"""

View File

@@ -18,7 +18,6 @@
# galaxy_task_env: environment variables to use specifically for ansible-galaxy commands # galaxy_task_env: environment variables to use specifically for ansible-galaxy commands
# awx_version: Current running version of the awx or tower as a string # awx_version: Current running version of the awx or tower as a string
# awx_license_type: "open" for AWX; else presume Tower # awx_license_type: "open" for AWX; else presume Tower
# gpg_pubkey: the GPG public key to use for validation, when enabled
- hosts: localhost - hosts: localhost
gather_facts: false gather_facts: false
@@ -154,28 +153,6 @@
- update_insights - update_insights
- update_archive - update_archive
- hosts: localhost
gather_facts: false
connection: local
name: Perform project signature/checksum verification
tasks:
- name: Verify project content using GPG signature
playbook_integrity:
project_path: "{{ project_path | quote }}"
validation_type: gpg
gpg_pubkey: "{{ gpg_pubkey }}"
register: gpg_result
tags:
- validation_gpg_public_key
- name: Verify project content against checksum manifest
playbook_integrity:
project_path: "{{ project_path | quote }}"
validation_type: checksum_manifest
register: checksum_result
tags:
- validation_checksum_manifest
- hosts: localhost - hosts: localhost
gather_facts: false gather_facts: false
connection: local connection: local

View File

@@ -108,6 +108,10 @@ AWX_DISABLE_TASK_MANAGERS = False
if 'sqlite3' not in DATABASES['default']['ENGINE']: # noqa if 'sqlite3' not in DATABASES['default']['ENGINE']: # noqa
DATABASES['default'].setdefault('OPTIONS', dict()).setdefault('application_name', f'{CLUSTER_HOST_ID}-{os.getpid()}-{" ".join(sys.argv)}'[:63]) # noqa DATABASES['default'].setdefault('OPTIONS', dict()).setdefault('application_name', f'{CLUSTER_HOST_ID}-{os.getpid()}-{" ".join(sys.argv)}'[:63]) # noqa
# Everywhere else we use /var/lib/awx/public/static/ - but this requires running collectstatic.
# This makes the browsable API work in the dev env without any additional steps.
STATIC_ROOT = os.path.join(BASE_DIR, 'public', 'static')
# If any local_*.py files are present in awx/settings/, use them to override # If any local_*.py files are present in awx/settings/, use them to override
# default settings for development. If not present, we can still run using # default settings for development. If not present, we can still run using
# only the defaults. # only the defaults.

View File

@@ -44,7 +44,7 @@ Have questions about this document or anything not covered here? Feel free to re
- functions should adopt camelCase - functions should adopt camelCase
- constructors/classes should adopt PascalCase - constructors/classes should adopt PascalCase
- constants to be exported should adopt UPPERCASE - constants to be exported should adopt UPPERCASE
- For strings, we adopt the `sentence capitalization` since it is a [Patternfly style guide](https://www.patternfly.org/v4/ux-writing/capitalization). - For strings, we adopt the `sentence capitalization` since it is a [Patternfly style guide](https://www.patternfly.org/v4/design-guidelines/content/grammar-and-terminology#capitalization).
## Setting up your development environment ## Setting up your development environment

190
awx/ui/package-lock.json generated
View File

@@ -7,22 +7,22 @@
"name": "ui", "name": "ui",
"dependencies": { "dependencies": {
"@lingui/react": "3.14.0", "@lingui/react": "3.14.0",
"@patternfly/patternfly": "4.210.2", "@patternfly/patternfly": "4.202.1",
"@patternfly/react-core": "^4.221.3", "@patternfly/react-core": "^4.221.3",
"@patternfly/react-icons": "4.75.1", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-table": "4.100.8", "@patternfly/react-table": "4.93.1",
"ace-builds": "^1.10.1", "ace-builds": "^1.8.1",
"ansi-to-html": "0.7.2", "ansi-to-html": "0.7.2",
"axios": "0.27.2", "axios": "0.27.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"d3": "7.4.4", "d3": "7.4.4",
"dagre": "^0.8.4", "dagre": "^0.8.4",
"dompurify": "2.4.0", "dompurify": "2.3.10",
"formik": "2.2.9", "formik": "2.2.9",
"has-ansi": "5.0.1", "has-ansi": "5.0.1",
"html-entities": "2.3.2", "html-entities": "2.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"luxon": "^3.0.3", "luxon": "^3.0.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "17.0.2", "react": "17.0.2",
"react-ace": "^10.1.0", "react-ace": "^10.1.0",
@@ -3746,18 +3746,18 @@
"dev": true "dev": true
}, },
"node_modules/@patternfly/patternfly": { "node_modules/@patternfly/patternfly": {
"version": "4.210.2", "version": "4.202.1",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.210.2.tgz", "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.202.1.tgz",
"integrity": "sha512-aZiW24Bxi6uVmk5RyNTp+6q6ThtlJZotNRJfWVeGuwu1UlbBuV4DFa1bpjA6jfTZpfEpX2YL5+R+4ZVSCFAVdw==" "integrity": "sha512-cQiiPqmwJOm9onuTfLPQNRlpAZwDIJ/zVfDQeaFqMQyPJtxtKn3lkphz5xErY5dPs9rR4X94ytQ1I9pkVzaPJQ=="
}, },
"node_modules/@patternfly/react-core": { "node_modules/@patternfly/react-core": {
"version": "4.231.8", "version": "4.224.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.231.8.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.224.1.tgz",
"integrity": "sha512-2ClqlYCvSADppMfVfkUGIA/8XlO6jX8batoClXLxZDwqGoOfr61XyUgQ6SSlE4w60czoNeX4Nf6cfQKUH4RIKw==", "integrity": "sha512-v8wGGNoMGndAScAoE5jeOA5jVgymlLSwttPjQk/Idr0k7roSpOrsM39oXUR5DEgkZee45DW00WKTgmg50PP3FQ==",
"dependencies": { "dependencies": {
"@patternfly/react-icons": "^4.82.8", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-styles": "^4.81.8", "@patternfly/react-styles": "^4.74.1",
"@patternfly/react-tokens": "^4.83.8", "@patternfly/react-tokens": "^4.76.1",
"focus-trap": "6.9.2", "focus-trap": "6.9.2",
"react-dropzone": "9.0.0", "react-dropzone": "9.0.0",
"tippy.js": "5.1.2", "tippy.js": "5.1.2",
@@ -3768,15 +3768,6 @@
"react-dom": "^16.8.0 || ^17.0.0" "react-dom": "^16.8.0 || ^17.0.0"
} }
}, },
"node_modules/@patternfly/react-core/node_modules/@patternfly/react-icons": {
"version": "4.82.8",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.82.8.tgz",
"integrity": "sha512-cKixprTiMLZRe/+kmdZ5suvYb9ly9p1f/HjlcNiWBfsiA8ZDEPmxJnVdend/YsafelC8YC9QGcQf97ay5PNhcw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/@patternfly/react-core/node_modules/tslib": { "node_modules/@patternfly/react-core/node_modules/tslib": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@@ -3792,19 +3783,19 @@
} }
}, },
"node_modules/@patternfly/react-styles": { "node_modules/@patternfly/react-styles": {
"version": "4.81.8", "version": "4.74.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.81.8.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.74.1.tgz",
"integrity": "sha512-Q5FiureSSCMIuz+KLMcEm1317TzbXcwmg2q5iNDRKyf/K+5CT6tJp0Wbtk3FlfRvzli4u/7YfXipahia5TL+tA==" "integrity": "sha512-9eWvKrjtrJ3qhJkhY2GQKyYA13u/J0mU1befH49SYbvxZtkbuHdpKmXBAeQoHmcx1hcOKtiYXeKb+dVoRRNx0A=="
}, },
"node_modules/@patternfly/react-table": { "node_modules/@patternfly/react-table": {
"version": "4.100.8", "version": "4.93.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.100.8.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.93.1.tgz",
"integrity": "sha512-80XZCZzoYN9gsoufNdXUB/dk33SuWF9lUnOJs7ilezD6noTSD7ARqO1h532eaEPIbPBp4uIVkEUdfGSHd0HJtg==", "integrity": "sha512-N/zHkNsY3X3yUXPg6COwdZKAFmTCbWm25qCY2aHjrXlIlE2OKWaYvVag0CcTwPiQhIuCumztr9Y2Uw9uvv0Fsw==",
"dependencies": { "dependencies": {
"@patternfly/react-core": "^4.231.8", "@patternfly/react-core": "^4.224.1",
"@patternfly/react-icons": "^4.82.8", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-styles": "^4.81.8", "@patternfly/react-styles": "^4.74.1",
"@patternfly/react-tokens": "^4.83.8", "@patternfly/react-tokens": "^4.76.1",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
@@ -3813,24 +3804,15 @@
"react-dom": "^16.8.0 || ^17.0.0" "react-dom": "^16.8.0 || ^17.0.0"
} }
}, },
"node_modules/@patternfly/react-table/node_modules/@patternfly/react-icons": {
"version": "4.82.8",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.82.8.tgz",
"integrity": "sha512-cKixprTiMLZRe/+kmdZ5suvYb9ly9p1f/HjlcNiWBfsiA8ZDEPmxJnVdend/YsafelC8YC9QGcQf97ay5PNhcw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/@patternfly/react-table/node_modules/tslib": { "node_modules/@patternfly/react-table/node_modules/tslib": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}, },
"node_modules/@patternfly/react-tokens": { "node_modules/@patternfly/react-tokens": {
"version": "4.83.8", "version": "4.76.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.83.8.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.76.1.tgz",
"integrity": "sha512-Z/MHXNY8PQOuBFGUar2yzPVbz3BNJuhB+Dnk5RJcc/iIn3S+VlSru7g6v5jqoV/+a5wLqZtLGEBp8uhCZ7Xkig==" "integrity": "sha512-gLEezRSzQeflaPu3SCgYmWtuiqDIRtxNNFP1+ES7P2o56YHXJ5o1Pki7LpNCPk/VOzHy2+vRFE/7l+hBEweugw=="
}, },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": { "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.4", "version": "0.5.4",
@@ -5267,9 +5249,9 @@
} }
}, },
"node_modules/ace-builds": { "node_modules/ace-builds": {
"version": "1.10.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.10.1.tgz", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.8.1.tgz",
"integrity": "sha512-w8Xj6lZUtOYAquVYvdpZhb0GxXrZ+qpVfgj5LP2FwUbXE8fPrCmfu86FjwOiSphx/8PMbXXVldFLD2+RIXayyA==" "integrity": "sha512-wjEQ4khMQYg9FfdEDoOtqdoHwcwFL48H0VB3te5b5A3eqHwxsTw8IX6+xzfisgborIb8dYU+1y9tcmtGFrCPIg=="
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "7.4.1", "version": "7.4.1",
@@ -6466,20 +6448,14 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001393", "version": "1.0.30001300",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001393.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001300.tgz",
"integrity": "sha512-N/od11RX+Gsk+1qY/jbPa0R6zJupEa0lxeBG598EbrtblxVCTJsQwbRBm6+V+rxpc5lHKdsXb9RY83cZIPLseA==", "integrity": "sha512-cVjiJHWGcNlJi8TZVKNMnvMid3Z3TTdDHmLDzlOdIiZq138Exvo0G+G0wTdVYolxKb4AYwC+38pxodiInVtJSA==",
"dev": true, "dev": true,
"funding": [ "funding": {
{ "type": "opencollective",
"type": "opencollective", "url": "https://opencollective.com/browserslist"
"url": "https://opencollective.com/browserslist" }
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
}
]
}, },
"node_modules/case-sensitive-paths-webpack-plugin": { "node_modules/case-sensitive-paths-webpack-plugin": {
"version": "2.4.0", "version": "2.4.0",
@@ -8295,9 +8271,9 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "2.4.0", "version": "2.3.10",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.10.tgz",
"integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA==" "integrity": "sha512-o7Fg/AgC7p/XpKjf/+RC3Ok6k4St5F7Q6q6+Nnm3p2zGWioAY6dh0CbbuwOhH2UcSzKsdniE/YnE2/92JcsA+g=="
}, },
"node_modules/domutils": { "node_modules/domutils": {
"version": "2.8.0", "version": "2.8.0",
@@ -15472,9 +15448,9 @@
} }
}, },
"node_modules/luxon": { "node_modules/luxon": {
"version": "3.0.3", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.3.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.1.tgz",
"integrity": "sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w==", "integrity": "sha512-hF3kv0e5gwHQZKz4wtm4c+inDtyc7elkanAsBq+fundaCdUBNJB1dHEGUZIM6SfSBUlbVFduPwEtNjFK8wLtcw==",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@@ -25093,30 +25069,24 @@
"dev": true "dev": true
}, },
"@patternfly/patternfly": { "@patternfly/patternfly": {
"version": "4.210.2", "version": "4.202.1",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.210.2.tgz", "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.202.1.tgz",
"integrity": "sha512-aZiW24Bxi6uVmk5RyNTp+6q6ThtlJZotNRJfWVeGuwu1UlbBuV4DFa1bpjA6jfTZpfEpX2YL5+R+4ZVSCFAVdw==" "integrity": "sha512-cQiiPqmwJOm9onuTfLPQNRlpAZwDIJ/zVfDQeaFqMQyPJtxtKn3lkphz5xErY5dPs9rR4X94ytQ1I9pkVzaPJQ=="
}, },
"@patternfly/react-core": { "@patternfly/react-core": {
"version": "4.231.8", "version": "4.224.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.231.8.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.224.1.tgz",
"integrity": "sha512-2ClqlYCvSADppMfVfkUGIA/8XlO6jX8batoClXLxZDwqGoOfr61XyUgQ6SSlE4w60czoNeX4Nf6cfQKUH4RIKw==", "integrity": "sha512-v8wGGNoMGndAScAoE5jeOA5jVgymlLSwttPjQk/Idr0k7roSpOrsM39oXUR5DEgkZee45DW00WKTgmg50PP3FQ==",
"requires": { "requires": {
"@patternfly/react-icons": "^4.82.8", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-styles": "^4.81.8", "@patternfly/react-styles": "^4.74.1",
"@patternfly/react-tokens": "^4.83.8", "@patternfly/react-tokens": "^4.76.1",
"focus-trap": "6.9.2", "focus-trap": "6.9.2",
"react-dropzone": "9.0.0", "react-dropzone": "9.0.0",
"tippy.js": "5.1.2", "tippy.js": "5.1.2",
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"@patternfly/react-icons": {
"version": "4.82.8",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.82.8.tgz",
"integrity": "sha512-cKixprTiMLZRe/+kmdZ5suvYb9ly9p1f/HjlcNiWBfsiA8ZDEPmxJnVdend/YsafelC8YC9QGcQf97ay5PNhcw==",
"requires": {}
},
"tslib": { "tslib": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@@ -25131,29 +25101,23 @@
"requires": {} "requires": {}
}, },
"@patternfly/react-styles": { "@patternfly/react-styles": {
"version": "4.81.8", "version": "4.74.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.81.8.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.74.1.tgz",
"integrity": "sha512-Q5FiureSSCMIuz+KLMcEm1317TzbXcwmg2q5iNDRKyf/K+5CT6tJp0Wbtk3FlfRvzli4u/7YfXipahia5TL+tA==" "integrity": "sha512-9eWvKrjtrJ3qhJkhY2GQKyYA13u/J0mU1befH49SYbvxZtkbuHdpKmXBAeQoHmcx1hcOKtiYXeKb+dVoRRNx0A=="
}, },
"@patternfly/react-table": { "@patternfly/react-table": {
"version": "4.100.8", "version": "4.93.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.100.8.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.93.1.tgz",
"integrity": "sha512-80XZCZzoYN9gsoufNdXUB/dk33SuWF9lUnOJs7ilezD6noTSD7ARqO1h532eaEPIbPBp4uIVkEUdfGSHd0HJtg==", "integrity": "sha512-N/zHkNsY3X3yUXPg6COwdZKAFmTCbWm25qCY2aHjrXlIlE2OKWaYvVag0CcTwPiQhIuCumztr9Y2Uw9uvv0Fsw==",
"requires": { "requires": {
"@patternfly/react-core": "^4.231.8", "@patternfly/react-core": "^4.224.1",
"@patternfly/react-icons": "^4.82.8", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-styles": "^4.81.8", "@patternfly/react-styles": "^4.74.1",
"@patternfly/react-tokens": "^4.83.8", "@patternfly/react-tokens": "^4.76.1",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"@patternfly/react-icons": {
"version": "4.82.8",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.82.8.tgz",
"integrity": "sha512-cKixprTiMLZRe/+kmdZ5suvYb9ly9p1f/HjlcNiWBfsiA8ZDEPmxJnVdend/YsafelC8YC9QGcQf97ay5PNhcw==",
"requires": {}
},
"tslib": { "tslib": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
@@ -25162,9 +25126,9 @@
} }
}, },
"@patternfly/react-tokens": { "@patternfly/react-tokens": {
"version": "4.83.8", "version": "4.76.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.83.8.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.76.1.tgz",
"integrity": "sha512-Z/MHXNY8PQOuBFGUar2yzPVbz3BNJuhB+Dnk5RJcc/iIn3S+VlSru7g6v5jqoV/+a5wLqZtLGEBp8uhCZ7Xkig==" "integrity": "sha512-gLEezRSzQeflaPu3SCgYmWtuiqDIRtxNNFP1+ES7P2o56YHXJ5o1Pki7LpNCPk/VOzHy2+vRFE/7l+hBEweugw=="
}, },
"@pmmmwh/react-refresh-webpack-plugin": { "@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.4", "version": "0.5.4",
@@ -26343,9 +26307,9 @@
} }
}, },
"ace-builds": { "ace-builds": {
"version": "1.10.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.10.1.tgz", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.8.1.tgz",
"integrity": "sha512-w8Xj6lZUtOYAquVYvdpZhb0GxXrZ+qpVfgj5LP2FwUbXE8fPrCmfu86FjwOiSphx/8PMbXXVldFLD2+RIXayyA==" "integrity": "sha512-wjEQ4khMQYg9FfdEDoOtqdoHwcwFL48H0VB3te5b5A3eqHwxsTw8IX6+xzfisgborIb8dYU+1y9tcmtGFrCPIg=="
}, },
"acorn": { "acorn": {
"version": "7.4.1", "version": "7.4.1",
@@ -27300,9 +27264,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001393", "version": "1.0.30001300",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001393.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001300.tgz",
"integrity": "sha512-N/od11RX+Gsk+1qY/jbPa0R6zJupEa0lxeBG598EbrtblxVCTJsQwbRBm6+V+rxpc5lHKdsXb9RY83cZIPLseA==", "integrity": "sha512-cVjiJHWGcNlJi8TZVKNMnvMid3Z3TTdDHmLDzlOdIiZq138Exvo0G+G0wTdVYolxKb4AYwC+38pxodiInVtJSA==",
"dev": true "dev": true
}, },
"case-sensitive-paths-webpack-plugin": { "case-sensitive-paths-webpack-plugin": {
@@ -28697,9 +28661,9 @@
} }
}, },
"dompurify": { "dompurify": {
"version": "2.4.0", "version": "2.3.10",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.10.tgz",
"integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA==" "integrity": "sha512-o7Fg/AgC7p/XpKjf/+RC3Ok6k4St5F7Q6q6+Nnm3p2zGWioAY6dh0CbbuwOhH2UcSzKsdniE/YnE2/92JcsA+g=="
}, },
"domutils": { "domutils": {
"version": "2.8.0", "version": "2.8.0",
@@ -34219,9 +34183,9 @@
} }
}, },
"luxon": { "luxon": {
"version": "3.0.3", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.3.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.1.tgz",
"integrity": "sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w==" "integrity": "sha512-hF3kv0e5gwHQZKz4wtm4c+inDtyc7elkanAsBq+fundaCdUBNJB1dHEGUZIM6SfSBUlbVFduPwEtNjFK8wLtcw=="
}, },
"lz-string": { "lz-string": {
"version": "1.4.4", "version": "1.4.4",

View File

@@ -7,22 +7,22 @@
}, },
"dependencies": { "dependencies": {
"@lingui/react": "3.14.0", "@lingui/react": "3.14.0",
"@patternfly/patternfly": "4.210.2", "@patternfly/patternfly": "4.202.1",
"@patternfly/react-core": "^4.221.3", "@patternfly/react-core": "^4.221.3",
"@patternfly/react-icons": "4.75.1", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-table": "4.100.8", "@patternfly/react-table": "4.93.1",
"ace-builds": "^1.10.1", "ace-builds": "^1.8.1",
"ansi-to-html": "0.7.2", "ansi-to-html": "0.7.2",
"axios": "0.27.2", "axios": "0.27.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"d3": "7.4.4", "d3": "7.4.4",
"dagre": "^0.8.4", "dagre": "^0.8.4",
"dompurify": "2.4.0", "dompurify": "2.3.10",
"formik": "2.2.9", "formik": "2.2.9",
"has-ansi": "5.0.1", "has-ansi": "5.0.1",
"html-entities": "2.3.2", "html-entities": "2.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"luxon": "^3.0.3", "luxon": "^3.0.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "17.0.2", "react": "17.0.2",
"react-ace": "^10.1.0", "react-ace": "^10.1.0",

View File

@@ -7,15 +7,7 @@ class CredentialTypes extends Base {
} }
async loadAllTypes( async loadAllTypes(
acceptableKinds = [ acceptableKinds = ['machine', 'cloud', 'net', 'ssh', 'vault', 'kubernetes']
'machine',
'cloud',
'net',
'ssh',
'vault',
'kubernetes',
'cryptography',
]
) { ) {
const pageSize = 200; const pageSize = 200;
// The number of credential types a user can have is unlimited. In practice, it is unlikely for // The number of credential types a user can have is unlimited. In practice, it is unlikely for

View File

@@ -9,8 +9,6 @@ function CredentialChip({ credential, ...props }) {
let type; let type;
if (credential.cloud) { if (credential.cloud) {
type = t`Cloud`; type = t`Cloud`;
} else if (credential.kind === 'gpg_public_key') {
type = t`GPG Public Key`;
} else if (credential.kind === 'aws' || credential.kind === 'ssh') { } else if (credential.kind === 'aws' || credential.kind === 'ssh') {
type = credential.kind.toUpperCase(); type = credential.kind.toUpperCase();
} else { } else {

View File

@@ -29,8 +29,4 @@ export default styled(DetailList)`
--column-count: 3; --column-count: 3;
} }
`} `}
& + & {
margin-top: 20px;
}
`; `;

View File

@@ -125,21 +125,6 @@ function PromptProjectDetail({ resource }) {
} }
/> />
)} )}
{summary_fields?.signature_validation_credential?.id && (
<Detail
label={t`Content Signature Validation Credential`}
dataCy={`${prefixCy}-content-signature-validation-credential`}
value={
<CredentialChip
key={resource.summary_fields.signature_validation_credential.id}
credential={
resource.summary_fields.signature_validation_credential
}
isReadOnly
/>
}
/>
)}
{optionsList && ( {optionsList && (
<Detail <Detail
label={t`Enabled Options`} label={t`Enabled Options`}

View File

@@ -10,13 +10,7 @@ const Label = styled.div`
font-weight: var(--pf-global--FontWeight--bold); font-weight: var(--pf-global--FontWeight--bold);
`; `;
export default function FrequencyDetails({ export default function FrequencyDetails({ type, label, options, timezone }) {
type,
label,
options,
timezone,
isException,
}) {
const getRunEveryLabel = () => { const getRunEveryLabel = () => {
const { interval } = options; const { interval } = options;
switch (type) { switch (type) {
@@ -83,17 +77,11 @@ export default function FrequencyDetails({
6: t`Sunday`, 6: t`Sunday`,
}; };
const prefix = isException ? `exception-${type}` : `frequency-${type}`;
return ( return (
<div> <div>
<Label>{label}</Label> <Label>{label}</Label>
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail <Detail label={t`Run every`} value={getRunEveryLabel()} />
label={isException ? t`Skip every` : t`Run every`}
value={getRunEveryLabel()}
dataCy={`${prefix}-run-every`}
/>
{type === 'week' ? ( {type === 'week' ? (
<Detail <Detail
label={t`On days`} label={t`On days`}
@@ -101,15 +89,10 @@ export default function FrequencyDetails({
.sort(sortWeekday) .sort(sortWeekday)
.map((d) => weekdays[d.weekday]) .map((d) => weekdays[d.weekday])
.join(', ')} .join(', ')}
dataCy={`${prefix}-days-of-week`}
/> />
) : null} ) : null}
<RunOnDetail type={type} options={options} prefix={prefix} /> <RunOnDetail type={type} options={options} />
<Detail <Detail label={t`End`} value={getEndValue(type, options, timezone)} />
label={t`End`}
value={getEndValue(type, options, timezone)}
dataCy={`${prefix}-end`}
/>
</DetailList> </DetailList>
</div> </div>
); );
@@ -121,15 +104,11 @@ function sortWeekday(a, b) {
return a.weekday - b.weekday; return a.weekday - b.weekday;
} }
function RunOnDetail({ type, options, prefix }) { function RunOnDetail({ type, options }) {
if (type === 'month') { if (type === 'month') {
if (options.runOn === 'day') { if (options.runOn === 'day') {
return ( return (
<Detail <Detail label={t`Run on`} value={t`Day ${options.runOnDayNumber}`} />
label={t`Run on`}
value={t`Day ${options.runOnDayNumber}`}
dataCy={`${prefix}-run-on-day`}
/>
); );
} }
const dayOfWeek = options.runOnTheDay; const dayOfWeek = options.runOnTheDay;
@@ -150,7 +129,6 @@ function RunOnDetail({ type, options, prefix }) {
/> />
) )
} }
dataCy={`${prefix}-run-on-day`}
/> />
); );
} }
@@ -174,7 +152,6 @@ function RunOnDetail({ type, options, prefix }) {
<Detail <Detail
label={t`Run on`} label={t`Run on`}
value={`${months[options.runOnTheMonth]} ${options.runOnDayMonth}`} value={`${months[options.runOnTheMonth]} ${options.runOnDayMonth}`}
dataCy={`${prefix}-run-on-day`}
/> />
); );
} }
@@ -209,7 +186,6 @@ function RunOnDetail({ type, options, prefix }) {
/> />
) )
} }
dataCy={`${prefix}-run-on-day`}
/> />
); );
} }

View File

@@ -2,6 +2,7 @@ import 'styled-components/macro';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { Link, useHistory, useLocation } from 'react-router-dom'; import { Link, useHistory, useLocation } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Chip, Divider, Title, Button } from '@patternfly/react-core'; import { Chip, Divider, Title, Button } from '@patternfly/react-core';
import { Schedule } from 'types'; import { Schedule } from 'types';
@@ -25,7 +26,7 @@ import ErrorDetail from '../../ErrorDetail';
import ChipGroup from '../../ChipGroup'; import ChipGroup from '../../ChipGroup';
import { VariablesDetail } from '../../CodeEditor'; import { VariablesDetail } from '../../CodeEditor';
import { VERBOSITY } from '../../VerbositySelectField'; import { VERBOSITY } from '../../VerbositySelectField';
import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext'; import helpText from '../../../screens/Template/shared/JobTemplate.helptext';
const PromptDivider = styled(Divider)` const PromptDivider = styled(Divider)`
margin-top: var(--pf-global--spacer--lg); margin-top: var(--pf-global--spacer--lg);
@@ -59,10 +60,6 @@ const FrequencyDetailsContainer = styled.div`
padding-bottom: var(--pf-global--spacer--md); padding-bottom: var(--pf-global--spacer--md);
border-bottom: 1px solid var(--pf-global--palette--black-300); border-bottom: 1px solid var(--pf-global--palette--black-300);
} }
& + & {
margin-top: calc(var(--pf-global--spacer--lg) * -1);
}
`; `;
function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) { function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
@@ -88,7 +85,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
timezone, timezone,
verbosity, verbosity,
} = schedule; } = schedule;
const helpText = getHelpText();
const history = useHistory(); const history = useHistory();
const { pathname } = useLocation(); const { pathname } = useLocation();
const pathRoot = pathname.substr(0, pathname.indexOf('schedules')); const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
@@ -164,14 +161,10 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
month: t`Month`, month: t`Month`,
year: t`Year`, year: t`Year`,
}; };
const { frequency, frequencyOptions, exceptionFrequency, exceptionOptions } = const { frequency, frequencyOptions } = parseRuleObj(schedule);
parseRuleObj(schedule);
const repeatFrequency = frequency.length const repeatFrequency = frequency.length
? frequency.map((f) => frequencies[f]).join(', ') ? frequency.map((f) => frequencies[f]).join(', ')
: t`None (Run Once)`; : t`None (Run Once)`;
const exceptionRepeatFrequency = exceptionFrequency.length
? exceptionFrequency.map((f) => frequencies[f]).join(', ')
: t`None (Run Once)`;
const { const {
ask_credential_on_launch, ask_credential_on_launch,
@@ -278,84 +271,43 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
isDisabled={isDisabled} isDisabled={isDisabled}
/> />
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail label={t`Name`} value={name} dataCy="schedule-name" /> <Detail label={t`Name`} value={name} />
<Detail <Detail label={t`Description`} value={description} />
label={t`Description`}
value={description}
dataCy="schedule-description"
/>
<Detail <Detail
label={t`First Run`} label={t`First Run`}
value={formatDateString(dtstart, timezone)} value={formatDateString(dtstart, timezone)}
dataCy="schedule-first-run"
/> />
<Detail <Detail
label={t`Next Run`} label={t`Next Run`}
value={formatDateString(next_run, timezone)} value={formatDateString(next_run, timezone)}
dataCy="schedule-next-run"
/> />
<Detail label={t`Last Run`} value={formatDateString(dtend, timezone)} /> <Detail label={t`Last Run`} value={formatDateString(dtend, timezone)} />
<Detail <Detail
label={t`Local Time Zone`} label={t`Local Time Zone`}
value={timezone} value={timezone}
helpText={helpText.localTimeZone(config)} helpText={helpText.localTimeZone(config)}
dataCy="schedule-timezone"
/>
<Detail
label={t`Repeat Frequency`}
value={repeatFrequency}
dataCy="schedule-repeat-frequency"
/>
<Detail
label={t`Exception Frequency`}
value={exceptionRepeatFrequency}
dataCy="schedule-exception-frequency"
/> />
<Detail label={t`Repeat Frequency`} value={repeatFrequency} />
</DetailList> </DetailList>
{frequency.length ? ( {frequency.length ? (
<FrequencyDetailsContainer> <FrequencyDetailsContainer>
<div ouia-component-id="schedule-frequency-details"> <p>
<p> <strong>{t`Frequency Details`}</strong>
<strong>{t`Frequency Details`}</strong> </p>
</p> {frequency.map((freq) => (
{frequency.map((freq) => ( <FrequencyDetails
<FrequencyDetails key={freq}
key={freq} type={freq}
type={freq} label={frequencies[freq]}
label={frequencies[freq]} options={frequencyOptions[freq]}
options={frequencyOptions[freq]} timezone={timezone}
timezone={timezone} />
/> ))}
))}
</div>
</FrequencyDetailsContainer>
) : null}
{exceptionFrequency.length ? (
<FrequencyDetailsContainer>
<div ouia-component-id="schedule-exception-details">
<p css="border-top: 0">
<strong>{t`Frequency Exception Details`}</strong>
</p>
{exceptionFrequency.map((freq) => (
<FrequencyDetails
key={freq}
type={freq}
label={frequencies[freq]}
options={exceptionOptions[freq]}
timezone={timezone}
isException
/>
))}
</div>
</FrequencyDetailsContainer> </FrequencyDetailsContainer>
) : null} ) : null}
<DetailList gutter="sm"> <DetailList gutter="sm">
{hasDaysToKeepField ? ( {hasDaysToKeepField ? (
<Detail <Detail label={t`Days of Data to Keep`} value={daysToKeep} />
label={t`Days of Data to Keep`}
value={daysToKeep}
dataCy="schedule-days-to-keep"
/>
) : null} ) : null}
<ScheduleOccurrences preview={preview} tz={timezone} /> <ScheduleOccurrences preview={preview} tz={timezone} />
<UserDateDetail <UserDateDetail
@@ -375,11 +327,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
<PromptDivider /> <PromptDivider />
<PromptDetailList> <PromptDetailList>
{ask_job_type_on_launch && ( {ask_job_type_on_launch && (
<Detail <Detail label={t`Job Type`} value={job_type} />
label={t`Job Type`}
value={job_type}
dataCy="shedule-job-type"
/>
)} )}
{showInventoryDetail && ( {showInventoryDetail && (
<Detail <Detail
@@ -399,31 +347,19 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
' ' ' '
) )
} }
dataCy="schedule-inventory"
/> />
)} )}
{ask_verbosity_on_launch && ( {ask_verbosity_on_launch && (
<Detail <Detail label={t`Verbosity`} value={VERBOSITY()[verbosity]} />
label={t`Verbosity`}
value={VERBOSITY()[verbosity]}
dataCy="schedule-verbosity"
/>
)} )}
{ask_scm_branch_on_launch && ( {ask_scm_branch_on_launch && (
<Detail <Detail label={t`Source Control Branch`} value={scm_branch} />
label={t`Source Control Branch`}
value={scm_branch}
dataCy="schedule-scm-branch"
/>
)}
{ask_limit_on_launch && (
<Detail label={t`Limit`} value={limit} dataCy="schedule-limit" />
)} )}
{ask_limit_on_launch && <Detail label={t`Limit`} value={limit} />}
{showDiffModeDetail && ( {showDiffModeDetail && (
<Detail <Detail
label={t`Show Changes`} label={t`Show Changes`}
value={diff_mode ? t`On` : t`Off`} value={diff_mode ? t`On` : t`Off`}
dataCy="schedule-show-changes"
/> />
)} )}
{showCredentialsDetail && ( {showCredentialsDetail && (
@@ -446,7 +382,6 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
dataCy="schedule-credentials"
/> />
)} )}
{showTagsDetail && ( {showTagsDetail && (
@@ -470,7 +405,6 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
dataCy="schedule-job-tags"
/> />
)} )}
{showSkipTagsDetail && ( {showSkipTagsDetail && (
@@ -494,7 +428,6 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
dataCy="schedule-skip-tags"
/> />
)} )}
{showVariablesDetail && ( {showVariablesDetail && (

View File

@@ -45,7 +45,7 @@ const Checkbox = styled(_Checkbox)`
} }
`; `;
const FrequencyDetailSubform = ({ frequency, prefix, isException }) => { const FrequencyDetailSubform = ({ frequency, prefix }) => {
const id = prefix.replace('.', '-'); const id = prefix.replace('.', '-');
const [runOnDayMonth] = useField({ const [runOnDayMonth] = useField({
name: `${prefix}.runOnDayMonth`, name: `${prefix}.runOnDayMonth`,
@@ -220,7 +220,7 @@ const FrequencyDetailSubform = ({ frequency, prefix, isException }) => {
validated={ validated={
!intervalMeta.touched || !intervalMeta.error ? 'default' : 'error' !intervalMeta.touched || !intervalMeta.error ? 'default' : 'error'
} }
label={isException ? t`Skip every` : t`Run every`} label={t`Run every`}
> >
<div css="display: flex"> <div css="display: flex">
<TextInput <TextInput

View File

@@ -20,7 +20,6 @@ import ScheduleFormFields from './ScheduleFormFields';
import UnsupportedScheduleForm from './UnsupportedScheduleForm'; import UnsupportedScheduleForm from './UnsupportedScheduleForm';
import parseRuleObj, { UnsupportedRRuleError } from './parseRuleObj'; import parseRuleObj, { UnsupportedRRuleError } from './parseRuleObj';
import buildRuleObj from './buildRuleObj'; import buildRuleObj from './buildRuleObj';
import buildRuleSet from './buildRuleSet';
const NUM_DAYS_PER_FREQUENCY = { const NUM_DAYS_PER_FREQUENCY = {
week: 7, week: 7,
@@ -412,10 +411,6 @@ function ScheduleForm({
} }
}); });
if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) {
errors.exceptionFrequency = t`This schedule has no occurrences due to the selected exceptions.`;
}
return errors; return errors;
}; };
@@ -523,24 +518,3 @@ ScheduleForm.defaultProps = {
}; };
export default ScheduleForm; export default ScheduleForm;
function scheduleHasInstances(values) {
let rangeToCheck = 1;
values.frequency.forEach((freq) => {
if (NUM_DAYS_PER_FREQUENCY[freq] > rangeToCheck) {
rangeToCheck = NUM_DAYS_PER_FREQUENCY[freq];
}
});
const ruleSet = buildRuleSet(values, true);
const startDate = DateTime.fromISO(values.startDate);
const endDate = startDate.plus({ days: rangeToCheck });
const instances = ruleSet.between(
startDate.toJSDate(),
endDate.toJSDate(),
true,
(date, i) => i === 0
);
return instances.length > 0;
}

View File

@@ -86,7 +86,7 @@ const mockSchedule = {
let wrapper; let wrapper;
const defaultFieldsVisible = (isExceptionsVisible) => { const defaultFieldsVisible = () => {
expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Start date/time"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Start date/time"]').length).toBe(1);
@@ -94,11 +94,7 @@ const defaultFieldsVisible = (isExceptionsVisible) => {
expect( expect(
wrapper.find('FormGroup[label="Local time zone"]').find('HelpIcon').length wrapper.find('FormGroup[label="Local time zone"]').find('HelpIcon').length
).toBe(1); ).toBe(1);
if (isExceptionsVisible) { expect(wrapper.find('FrequencySelect').length).toBe(1);
expect(wrapper.find('FrequencySelect').length).toBe(2);
} else {
expect(wrapper.find('FrequencySelect').length).toBe(1);
}
}; };
const nonRRuleValuesMatch = () => { const nonRRuleValuesMatch = () => {
@@ -517,7 +513,7 @@ describe('<ScheduleForm />', () => {
runFrequencySelect.invoke('onChange')(['minute']); runFrequencySelect.invoke('onChange')(['minute']);
}); });
wrapper.update(); wrapper.update();
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0); expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
@@ -551,7 +547,7 @@ describe('<ScheduleForm />', () => {
runFrequencySelect.invoke('onChange')(['hour']); runFrequencySelect.invoke('onChange')(['hour']);
}); });
wrapper.update(); wrapper.update();
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0); expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
@@ -583,7 +579,7 @@ describe('<ScheduleForm />', () => {
runFrequencySelect.invoke('onChange')(['day']); runFrequencySelect.invoke('onChange')(['day']);
}); });
wrapper.update(); wrapper.update();
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0); expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
@@ -615,7 +611,7 @@ describe('<ScheduleForm />', () => {
runFrequencySelect.invoke('onChange')(['week']); runFrequencySelect.invoke('onChange')(['week']);
}); });
wrapper.update(); wrapper.update();
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(1); expect(wrapper.find('FormGroup[label="On days"]').length).toBe(1);
@@ -647,7 +643,7 @@ describe('<ScheduleForm />', () => {
runFrequencySelect.invoke('onChange')(['month']); runFrequencySelect.invoke('onChange')(['month']);
}); });
wrapper.update(); wrapper.update();
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0); expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
@@ -696,7 +692,7 @@ describe('<ScheduleForm />', () => {
runFrequencySelect.invoke('onChange')(['year']); runFrequencySelect.invoke('onChange')(['year']);
}); });
wrapper.update(); wrapper.update();
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0); expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
@@ -1062,7 +1058,7 @@ describe('<ScheduleForm />', () => {
wrapper.update(); wrapper.update();
expect(wrapper.find('ScheduleForm').length).toBe(1); expect(wrapper.find('ScheduleForm').length).toBe(1);
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0); expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
@@ -1117,7 +1113,7 @@ describe('<ScheduleForm />', () => {
wrapper.update(); wrapper.update();
expect(wrapper.find('ScheduleForm').length).toBe(1); expect(wrapper.find('ScheduleForm').length).toBe(1);
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(1);
@@ -1175,7 +1171,7 @@ describe('<ScheduleForm />', () => {
wrapper.update(); wrapper.update();
expect(wrapper.find('ScheduleForm').length).toBe(1); expect(wrapper.find('ScheduleForm').length).toBe(1);
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0); expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
@@ -1228,7 +1224,7 @@ describe('<ScheduleForm />', () => {
wrapper.update(); wrapper.update();
expect(wrapper.find('ScheduleForm').length).toBe(1); expect(wrapper.find('ScheduleForm').length).toBe(1);
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(1);
@@ -1322,7 +1318,10 @@ describe('<ScheduleForm />', () => {
wrapper.update(); wrapper.update();
expect(wrapper.find('ScheduleForm').length).toBe(1); expect(wrapper.find('ScheduleForm').length).toBe(1);
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('ScheduleForm').length).toBe(1);
defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(1);
@@ -1395,7 +1394,7 @@ describe('<ScheduleForm />', () => {
wrapper.update(); wrapper.update();
expect(wrapper.find('ScheduleForm').length).toBe(1); expect(wrapper.find('ScheduleForm').length).toBe(1);
defaultFieldsVisible(true); defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1); expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(1);

View File

@@ -3,14 +3,13 @@ import { useField } from 'formik';
import { FormGroup, Title } from '@patternfly/react-core'; import { FormGroup, Title } from '@patternfly/react-core';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import styled from 'styled-components'; import styled from 'styled-components';
import 'styled-components/macro';
import FormField from 'components/FormField'; import FormField from 'components/FormField';
import { required } from 'util/validators'; import { required } from 'util/validators';
import { useConfig } from 'contexts/Config'; import { useConfig } from 'contexts/Config';
import Popover from '../../Popover'; import Popover from '../../Popover';
import AnsibleSelect from '../../AnsibleSelect'; import AnsibleSelect from '../../AnsibleSelect';
import FrequencySelect, { SelectOption } from './FrequencySelect'; import FrequencySelect, { SelectOption } from './FrequencySelect';
import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext'; import helpText from '../../../screens/Template/shared/JobTemplate.helptext';
import { SubFormLayout, FormColumnLayout } from '../../FormLayout'; import { SubFormLayout, FormColumnLayout } from '../../FormLayout';
import FrequencyDetailSubform from './FrequencyDetailSubform'; import FrequencyDetailSubform from './FrequencyDetailSubform';
import DateTimePicker from './DateTimePicker'; import DateTimePicker from './DateTimePicker';
@@ -27,7 +26,6 @@ export default function ScheduleFormFields({
zoneOptions, zoneOptions,
zoneLinks, zoneLinks,
}) { }) {
const helpText = getHelpText();
const [timezone, timezoneMeta] = useField({ const [timezone, timezoneMeta] = useField({
name: 'timezone', name: 'timezone',
validate: required(t`Select a value for this field`), validate: required(t`Select a value for this field`),
@@ -55,11 +53,11 @@ export default function ScheduleFormFields({
} }
const config = useConfig(); const config = useConfig();
const [exceptionFrequency, exceptionFrequencyMeta, exceptionFrequencyHelper] = // const [exceptionFrequency, exceptionFrequencyMeta, exceptionFrequencyHelper] =
useField({ // useField({
name: 'exceptionFrequency', // name: 'exceptionFrequency',
validate: required(t`Select a value for this field`), // validate: required(t`Select a value for this field`),
}); // });
const updateFrequency = (setFrequency) => (values) => { const updateFrequency = (setFrequency) => (values) => {
setFrequency(values.sort(sortFrequencies)); setFrequency(values.sort(sortFrequencies));
@@ -153,53 +151,42 @@ export default function ScheduleFormFields({
/> />
</FormColumnLayout> </FormColumnLayout>
))} ))}
<Title {/* <Title size="md" headingLevel="h4">{t`Exceptions`}</Title>
size="md" <FormGroup
headingLevel="h4" name="exceptions"
css="margin-top: var(--pf-c-card--child--PaddingRight)" fieldId="exception-frequency"
>{t`Exceptions`}</Title> helperTextInvalid={exceptionFrequencyMeta.error}
<FormColumnLayout stacked> validated={
<FormGroup !exceptionFrequencyMeta.touched || !exceptionFrequencyMeta.error
name="exceptions" ? 'default'
fieldId="exception-frequency" : 'error'
helperTextInvalid={exceptionFrequencyMeta.error} }
validated={ label={t`Add exceptions`}
!exceptionFrequencyMeta.touched || !exceptionFrequencyMeta.error >
? 'default' <FrequencySelect
: 'error' variant={SelectVariant.checkbox}
} onChange={exceptionFrequencyHelper.setValue}
label={t`Add exceptions`} value={exceptionFrequency.value}
placeholderText={t`None`}
onBlur={exceptionFrequencyHelper.setTouched}
> >
<FrequencySelect <SelectClearOption value="none">{t`None`}</SelectClearOption>
id="exception-frequency" <SelectOption value="minute">{t`Minute`}</SelectOption>
onChange={updateFrequency(exceptionFrequencyHelper.setValue)} <SelectOption value="hour">{t`Hour`}</SelectOption>
value={exceptionFrequency.value} <SelectOption value="day">{t`Day`}</SelectOption>
placeholderText={ <SelectOption value="week">{t`Week`}</SelectOption>
exceptionFrequency.value.length <SelectOption value="month">{t`Month`}</SelectOption>
? t`Select frequency` <SelectOption value="year">{t`Year`}</SelectOption>
: t`None` </FrequencySelect>
} </FormGroup>
onBlur={exceptionFrequencyHelper.setTouched}
>
<SelectClearOption value="none">{t`None`}</SelectClearOption>
<SelectOption value="minute">{t`Minute`}</SelectOption>
<SelectOption value="hour">{t`Hour`}</SelectOption>
<SelectOption value="day">{t`Day`}</SelectOption>
<SelectOption value="week">{t`Week`}</SelectOption>
<SelectOption value="month">{t`Month`}</SelectOption>
<SelectOption value="year">{t`Year`}</SelectOption>
</FrequencySelect>
</FormGroup>
</FormColumnLayout>
{exceptionFrequency.value.map((val) => ( {exceptionFrequency.value.map((val) => (
<FormColumnLayout key={val} stacked> <FormColumnLayout key={val} stacked>
<FrequencyDetailSubform <FrequencyDetailSubform
frequency={val} frequency={val}
prefix={`exceptionOptions.${val}`} prefix={`exceptionOptions.${val}`}
isException
/> />
</FormColumnLayout> </FormColumnLayout>
))} ))} */}
</SubFormLayout> </SubFormLayout>
) : null} ) : null}
</> </>

View File

@@ -36,19 +36,11 @@ function pad(num) {
return num < 10 ? `0${num}` : num; return num < 10 ? `0${num}` : num;
} }
export default function buildRuleObj(values, includeStart) { export default function buildRuleObj(values) {
const ruleObj = { const ruleObj = {
interval: values.interval, interval: values.interval,
}; };
if (includeStart) {
ruleObj.dtstart = buildDateTime(
values.startDate,
values.startTime,
values.timezone
);
}
switch (values.frequency) { switch (values.frequency) {
case 'none': case 'none':
ruleObj.count = 1; ruleObj.count = 1;
@@ -99,11 +91,16 @@ export default function buildRuleObj(values, includeStart) {
ruleObj.count = values.occurrences; ruleObj.count = values.occurrences;
break; break;
case 'onDate': { case 'onDate': {
ruleObj.until = buildDateTime( const [endHour, endMinute] = parseTime(values.endTime);
values.endDate, const localEndDate = DateTime.fromISO(`${values.endDate}T000000`, {
values.endTime, zone: values.timezone,
values.timezone });
); const localEndTime = localEndDate.set({
hour: endHour,
minute: endMinute,
second: 0,
});
ruleObj.until = localEndTime.toJSDate();
break; break;
} }
default: default:
@@ -113,16 +110,3 @@ export default function buildRuleObj(values, includeStart) {
return ruleObj; return ruleObj;
} }
function buildDateTime(dateString, timeString, timezone) {
const localDate = DateTime.fromISO(`${dateString}T000000`, {
zone: timezone,
});
const [hour, minute] = parseTime(timeString);
const localTime = localDate.set({
hour,
minute,
second: 0,
});
return localTime.toJSDate();
}

View File

@@ -4,29 +4,24 @@ import buildRuleObj, { buildDtStartObj } from './buildRuleObj';
window.RRuleSet = RRuleSet; window.RRuleSet = RRuleSet;
const frequencies = ['minute', 'hour', 'day', 'week', 'month', 'year']; const frequencies = ['minute', 'hour', 'day', 'week', 'month', 'year'];
export default function buildRuleSet(values, useUTCStart) { export default function buildRuleSet(values) {
const set = new RRuleSet(); const set = new RRuleSet();
if (!useUTCStart) { const startRule = buildDtStartObj({
const startRule = buildDtStartObj({ startDate: values.startDate,
startTime: values.startTime,
timezone: values.timezone,
});
set.rrule(startRule);
if (values.frequency.length === 0) {
const rule = buildRuleObj({
startDate: values.startDate, startDate: values.startDate,
startTime: values.startTime, startTime: values.startTime,
timezone: values.timezone, timezone: values.timezone,
frequency: 'none',
interval: 1,
}); });
set.rrule(startRule);
}
if (values.frequency.length === 0) {
const rule = buildRuleObj(
{
startDate: values.startDate,
startTime: values.startTime,
timezone: values.timezone,
frequency: 'none',
interval: 1,
},
useUTCStart
);
set.rrule(new RRule(rule)); set.rrule(new RRule(rule));
} }
@@ -34,35 +29,17 @@ export default function buildRuleSet(values, useUTCStart) {
if (!values.frequency.includes(frequency)) { if (!values.frequency.includes(frequency)) {
return; return;
} }
const rule = buildRuleObj( const rule = buildRuleObj({
{ startDate: values.startDate,
startDate: values.startDate, startTime: values.startTime,
startTime: values.startTime, timezone: values.timezone,
timezone: values.timezone, frequency,
frequency, ...values.frequencyOptions[frequency],
...values.frequencyOptions[frequency], });
},
useUTCStart
);
set.rrule(new RRule(rule)); set.rrule(new RRule(rule));
}); });
frequencies.forEach((frequency) => { // TODO: exclusions
if (!values.exceptionFrequency?.includes(frequency)) {
return;
}
const rule = buildRuleObj(
{
startDate: values.startDate,
startTime: values.startTime,
timezone: values.timezone,
frequency,
...values.exceptionOptions[frequency],
},
useUTCStart
);
set.exrule(new RRule(rule));
});
return set; return set;
} }

View File

@@ -243,257 +243,4 @@ RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=2;BYDAY=MO;UNTIL=20260602T170000Z`);
expect(ruleSet.toString()).toEqual(`DTSTART:20220613T123000Z expect(ruleSet.toString()).toEqual(`DTSTART:20220613T123000Z
RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY`); RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY`);
}); });
test('should build minutely exception', () => {
const values = {
startDate: '2022-06-13',
startTime: '12:30 PM',
frequency: ['minute'],
frequencyOptions: {
minute: {
interval: 1,
end: 'never',
},
},
exceptionFrequency: ['minute'],
exceptionOptions: {
minute: {
interval: 3,
end: 'never',
},
},
};
const ruleSet = buildRuleSet(values);
expect(ruleSet.toString()).toEqual(
[
'DTSTART:20220613T123000Z',
'RRULE:INTERVAL=1;FREQ=MINUTELY',
'EXRULE:INTERVAL=3;FREQ=MINUTELY',
].join('\n')
);
});
test('should build hourly exception', () => {
const values = {
startDate: '2022-06-13',
startTime: '12:30 PM',
frequency: ['minute'],
frequencyOptions: {
minute: {
interval: 1,
end: 'never',
},
},
exceptionFrequency: ['hour'],
exceptionOptions: {
hour: {
interval: 3,
end: 'never',
},
},
};
const ruleSet = buildRuleSet(values);
expect(ruleSet.toString()).toEqual(
[
'DTSTART:20220613T123000Z',
'RRULE:INTERVAL=1;FREQ=MINUTELY',
'EXRULE:INTERVAL=3;FREQ=HOURLY',
].join('\n')
);
});
test('should build daily exception', () => {
const values = {
startDate: '2022-06-13',
startTime: '12:30 PM',
frequency: ['minute'],
frequencyOptions: {
minute: {
interval: 1,
end: 'never',
},
},
exceptionFrequency: ['day'],
exceptionOptions: {
day: {
interval: 3,
end: 'never',
},
},
};
const ruleSet = buildRuleSet(values);
expect(ruleSet.toString()).toEqual(
[
'DTSTART:20220613T123000Z',
'RRULE:INTERVAL=1;FREQ=MINUTELY',
'EXRULE:INTERVAL=3;FREQ=DAILY',
].join('\n')
);
});
test('should build weekly exception', () => {
const values = {
startDate: '2022-06-13',
startTime: '12:30 PM',
frequency: ['minute'],
frequencyOptions: {
minute: {
interval: 1,
end: 'never',
},
},
exceptionFrequency: ['week'],
exceptionOptions: {
week: {
interval: 3,
end: 'never',
daysOfWeek: [RRule.SU],
},
},
};
const ruleSet = buildRuleSet(values);
expect(ruleSet.toString()).toEqual(
[
'DTSTART:20220613T123000Z',
'RRULE:INTERVAL=1;FREQ=MINUTELY',
'EXRULE:INTERVAL=3;FREQ=WEEKLY;BYDAY=SU',
].join('\n')
);
});
test('should build monthly exception by day', () => {
const values = {
startDate: '2022-06-13',
startTime: '12:30 PM',
frequency: ['minute'],
frequencyOptions: {
minute: {
interval: 1,
end: 'never',
},
},
exceptionFrequency: ['month'],
exceptionOptions: {
month: {
interval: 3,
end: 'never',
runOn: 'day',
runOnDayNumber: 15,
},
},
};
const ruleSet = buildRuleSet(values);
expect(ruleSet.toString()).toEqual(
[
'DTSTART:20220613T123000Z',
'RRULE:INTERVAL=1;FREQ=MINUTELY',
'EXRULE:INTERVAL=3;FREQ=MONTHLY;BYMONTHDAY=15',
].join('\n')
);
});
test('should build monthly exception by weekday', () => {
const values = {
startDate: '2022-06-13',
startTime: '12:30 PM',
frequency: ['minute'],
frequencyOptions: {
minute: {
interval: 1,
end: 'never',
},
},
exceptionFrequency: ['month'],
exceptionOptions: {
month: {
interval: 3,
end: 'never',
runOn: 'the',
runOnTheOccurrence: 2,
runOnTheDay: 'monday',
},
},
};
const ruleSet = buildRuleSet(values);
expect(ruleSet.toString()).toEqual(
[
'DTSTART:20220613T123000Z',
'RRULE:INTERVAL=1;FREQ=MINUTELY',
'EXRULE:INTERVAL=3;FREQ=MONTHLY;BYSETPOS=2;BYDAY=MO',
].join('\n')
);
});
test('should build annual exception by day', () => {
const values = {
startDate: '2022-06-13',
startTime: '12:30 PM',
frequency: ['minute'],
frequencyOptions: {
minute: {
interval: 1,
end: 'never',
},
},
exceptionFrequency: ['year'],
exceptionOptions: {
year: {
interval: 1,
end: 'never',
runOn: 'day',
runOnDayMonth: 3,
runOnDayNumber: 15,
},
},
};
const ruleSet = buildRuleSet(values);
expect(ruleSet.toString()).toEqual(
[
'DTSTART:20220613T123000Z',
'RRULE:INTERVAL=1;FREQ=MINUTELY',
'EXRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15',
].join('\n')
);
});
test('should build annual exception by weekday', () => {
const values = {
startDate: '2022-06-13',
startTime: '12:30 PM',
frequency: ['minute'],
frequencyOptions: {
minute: {
interval: 1,
end: 'never',
},
},
exceptionFrequency: ['year'],
exceptionOptions: {
year: {
interval: 1,
end: 'never',
runOn: 'the',
runOnTheOccurrence: 4,
runOnTheDay: 'monday',
runOnTheMonth: 6,
},
},
};
const ruleSet = buildRuleSet(values);
expect(ruleSet.toString()).toEqual(
[
'DTSTART:20220613T123000Z',
'RRULE:INTERVAL=1;FREQ=MINUTELY',
'EXRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=4;BYDAY=MO;BYMONTH=6',
].join('\n')
);
});
}); });

View File

@@ -32,9 +32,6 @@ export default function parseRuleObj(schedule) {
case 'RRULE': case 'RRULE':
values = parseRrule(ruleString, schedule, values); values = parseRrule(ruleString, schedule, values);
break; break;
case 'EXRULE':
values = parseExRule(ruleString, schedule, values);
break;
default: default:
throw new UnsupportedRRuleError(`Unsupported rrule type: ${type}`); throw new UnsupportedRRuleError(`Unsupported rrule type: ${type}`);
} }
@@ -82,54 +79,6 @@ const frequencyTypes = {
}; };
function parseRrule(rruleString, schedule, values) { function parseRrule(rruleString, schedule, values) {
const { frequency, options } = parseRule(
rruleString,
schedule,
values.exceptionFrequency
);
if (values.frequencyOptions[frequency]) {
throw new UnsupportedRRuleError(
'Duplicate exception frequency types not supported'
);
}
return {
...values,
frequency: [...values.frequency, frequency].sort(sortFrequencies),
frequencyOptions: {
...values.frequencyOptions,
[frequency]: options,
},
};
}
function parseExRule(exruleString, schedule, values) {
const { frequency, options } = parseRule(
exruleString,
schedule,
values.exceptionFrequency
);
if (values.exceptionOptions[frequency]) {
throw new UnsupportedRRuleError(
'Duplicate exception frequency types not supported'
);
}
return {
...values,
exceptionFrequency: [...values.exceptionFrequency, frequency].sort(
sortFrequencies
),
exceptionOptions: {
...values.exceptionOptions,
[frequency]: options,
},
};
}
function parseRule(ruleString, schedule, frequencies) {
const { const {
origOptions: { origOptions: {
bymonth, bymonth,
@@ -141,7 +90,7 @@ function parseRule(ruleString, schedule, frequencies) {
interval, interval,
until, until,
}, },
} = RRule.fromString(ruleString); } = RRule.fromString(rruleString);
const now = DateTime.now(); const now = DateTime.now();
const closestQuarterHour = DateTime.fromMillis( const closestQuarterHour = DateTime.fromMillis(
@@ -178,7 +127,7 @@ function parseRule(ruleString, schedule, frequencies) {
throw new Error(`Unexpected rrule frequency: ${freq}`); throw new Error(`Unexpected rrule frequency: ${freq}`);
} }
const frequency = frequencyTypes[freq]; const frequency = frequencyTypes[freq];
if (frequencies.includes(frequency)) { if (values.frequency.includes(frequency)) {
throw new Error(`Duplicate frequency types not supported (${frequency})`); throw new Error(`Duplicate frequency types not supported (${frequency})`);
} }
@@ -222,9 +171,17 @@ function parseRule(ruleString, schedule, frequencies) {
} }
} }
if (values.frequencyOptions.frequency) {
throw new UnsupportedRRuleError('Duplicate frequency types not supported');
}
return { return {
frequency, ...values,
options, frequency: [...values.frequency, frequency].sort(sortFrequencies),
frequencyOptions: {
...values.frequencyOptions,
[frequency]: options,
},
}; };
} }

View File

@@ -241,51 +241,4 @@ RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=2;BYDAY=MO;UNTIL=20260602T170000Z`;
expect(parsed).toEqual(values); expect(parsed).toEqual(values);
}); });
test('should parse exemptions', () => {
const schedule = {
rrule: [
'DTSTART;TZID=US/Eastern:20220608T123000',
'RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO',
'EXRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=1;BYDAY=MO',
].join(' '),
dtstart: '2022-06-13T16:30:00Z',
timezone: 'US/Eastern',
until: '',
dtend: null,
};
const parsed = parseRuleObj(schedule);
expect(parsed).toEqual({
startDate: '2022-06-13',
startTime: '12:30 PM',
timezone: 'US/Eastern',
frequency: ['week'],
frequencyOptions: {
week: {
interval: 1,
end: 'never',
occurrences: 1,
endDate: '2022-06-02',
endTime: '1:00 PM',
daysOfWeek: [RRule.MO],
},
},
exceptionFrequency: ['month'],
exceptionOptions: {
month: {
interval: 1,
end: 'never',
endDate: '2022-06-02',
endTime: '1:00 PM',
occurrences: 1,
runOn: 'the',
runOnDayNumber: 1,
runOnTheOccurrence: 1,
runOnTheDay: 'monday',
},
},
});
});
}); });

View File

@@ -11,14 +11,13 @@ import { Detail, DetailList, UserDateDetail } from 'components/DetailList';
import { ApplicationsAPI } from 'api'; import { ApplicationsAPI } from 'api';
import DeleteButton from 'components/DeleteButton'; import DeleteButton from 'components/DeleteButton';
import ErrorDetail from 'components/ErrorDetail'; import ErrorDetail from 'components/ErrorDetail';
import getApplicationHelpTextStrings from '../shared/Application.helptext'; import applicationHelpTextStrings from '../shared/Application.helptext';
function ApplicationDetails({ function ApplicationDetails({
application, application,
authorizationOptions, authorizationOptions,
clientTypeOptions, clientTypeOptions,
}) { }) {
const applicationHelpTextStrings = getApplicationHelpTextStrings();
const history = useHistory(); const history = useHistory();
const { const {
isLoading: deleteLoading, isLoading: deleteLoading,

View File

@@ -1,9 +1,9 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
const applicationHelpTextStrings = () => ({ const applicationHelpTextStrings = {
authorizationGrantType: t`The Grant type the user must use to acquire tokens for this application`, authorizationGrantType: t`The Grant type the user must use to acquire tokens for this application`,
clientType: t`Set to Public or Confidential depending on how secure the client device is.`, clientType: t`Set to Public or Confidential depending on how secure the client device is.`,
redirectURIS: t`Allowed URIs list, space separated`, redirectURIS: t`Allowed URIs list, space separated`,
}); };
export default applicationHelpTextStrings; export default applicationHelpTextStrings;

View File

@@ -13,14 +13,13 @@ import FormActionGroup from 'components/FormActionGroup/FormActionGroup';
import OrganizationLookup from 'components/Lookup/OrganizationLookup'; import OrganizationLookup from 'components/Lookup/OrganizationLookup';
import AnsibleSelect from 'components/AnsibleSelect'; import AnsibleSelect from 'components/AnsibleSelect';
import Popover from 'components/Popover'; import Popover from 'components/Popover';
import getApplicationHelpTextStrings from './Application.helptext'; import applicationHelpTextStrings from './Application.helptext';
function ApplicationFormFields({ function ApplicationFormFields({
application, application,
authorizationOptions, authorizationOptions,
clientTypeOptions, clientTypeOptions,
}) { }) {
const applicationHelpTextStrings = getApplicationHelpTextStrings();
const match = useRouteMatch(); const match = useRouteMatch();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [organizationField, organizationMeta, organizationHelpers] = const [organizationField, organizationMeta, organizationHelpers] =

View File

@@ -12,10 +12,9 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
import { toTitleCase } from 'util/strings'; import { toTitleCase } from 'util/strings';
import { ExecutionEnvironmentsAPI } from 'api'; import { ExecutionEnvironmentsAPI } from 'api';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails'; import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
import getHelpText from '../shared/ExecutionEnvironment.helptext'; import helpText from '../shared/ExecutionEnvironment.helptext';
function ExecutionEnvironmentDetails({ executionEnvironment }) { function ExecutionEnvironmentDetails({ executionEnvironment }) {
const helpText = getHelpText();
const history = useHistory(); const history = useHistory();
const { const {
id, id,

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
const executionEnvironmentHelpTextStrings = () => ({ const executionEnvironmentHelpTextStrings = {
image: ( image: (
<span> <span>
{t`The full image location, including the container registry, image name, and version tag.`} {t`The full image location, including the container registry, image name, and version tag.`}
@@ -19,6 +19,6 @@ const executionEnvironmentHelpTextStrings = () => ({
</span> </span>
), ),
registryCredential: t`Credential to authenticate with a protected container registry.`, registryCredential: t`Credential to authenticate with a protected container registry.`,
}); };
export default executionEnvironmentHelpTextStrings; export default executionEnvironmentHelpTextStrings;

View File

@@ -14,7 +14,7 @@ import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading'; import ContentLoading from 'components/ContentLoading';
import { required } from 'util/validators'; import { required } from 'util/validators';
import useRequest from 'hooks/useRequest'; import useRequest from 'hooks/useRequest';
import getHelpText from './ExecutionEnvironment.helptext'; import helpText from './ExecutionEnvironment.helptext';
function ExecutionEnvironmentFormFields({ function ExecutionEnvironmentFormFields({
me, me,
@@ -22,7 +22,6 @@ function ExecutionEnvironmentFormFields({
executionEnvironment, executionEnvironment,
isOrgLookupDisabled, isOrgLookupDisabled,
}) { }) {
const helpText = getHelpText();
const [credentialField, credentialMeta, credentialHelpers] = const [credentialField, credentialMeta, credentialHelpers] =
useField('credential'); useField('credential');
const [organizationField, organizationMeta, organizationHelpers] = const [organizationField, organizationMeta, organizationHelpers] =

View File

@@ -16,11 +16,10 @@ import { InventoriesAPI } from 'api';
import useRequest, { useDismissableError } from 'hooks/useRequest'; import useRequest, { useDismissableError } from 'hooks/useRequest';
import { Inventory } from 'types'; import { Inventory } from 'types';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails'; import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
import getHelpText from '../shared/Inventory.helptext'; import helpText from '../shared/Inventory.helptext';
function InventoryDetail({ inventory }) { function InventoryDetail({ inventory }) {
const history = useHistory(); const history = useHistory();
const helpText = getHelpText();
const { const {
result: instanceGroups, result: instanceGroups,
isLoading, isLoading,

View File

@@ -32,10 +32,9 @@ import Popover from 'components/Popover';
import { VERBOSITY } from 'components/VerbositySelectField'; import { VERBOSITY } from 'components/VerbositySelectField';
import InventorySourceSyncButton from '../shared/InventorySourceSyncButton'; import InventorySourceSyncButton from '../shared/InventorySourceSyncButton';
import useWsInventorySourcesDetails from '../InventorySources/useWsInventorySourcesDetails'; import useWsInventorySourcesDetails from '../InventorySources/useWsInventorySourcesDetails';
import getHelpText from '../shared/Inventory.helptext'; import helpText from '../shared/Inventory.helptext';
function InventorySourceDetail({ inventorySource }) { function InventorySourceDetail({ inventorySource }) {
const helpText = getHelpText();
const { const {
created, created,
custom_virtualenv, custom_virtualenv,

View File

@@ -21,7 +21,7 @@ const ansibleDocUrls = {
'https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_vm_inventory_inventory.html', 'https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_vm_inventory_inventory.html',
}; };
const getInventoryHelpTextStrings = () => ({ const getInventoryHelpTextStrings = {
labels: t`Optional labels that describe this inventory, labels: t`Optional labels that describe this inventory,
such as 'dev' or 'test'. Labels can be used to group and filter such as 'dev' or 'test'. Labels can be used to group and filter
inventories and completed jobs.`, inventories and completed jobs.`,
@@ -191,6 +191,6 @@ const getInventoryHelpTextStrings = () => ({
sourcePath: t`The inventory file sourcePath: t`The inventory file
to be synced by this source. You can select from to be synced by this source. You can select from
the dropdown or enter a file within the input.`, the dropdown or enter a file within the input.`,
}); };
export default getInventoryHelpTextStrings; export default getInventoryHelpTextStrings;

View File

@@ -13,10 +13,9 @@ import InstanceGroupsLookup from 'components/Lookup/InstanceGroupsLookup';
import OrganizationLookup from 'components/Lookup/OrganizationLookup'; import OrganizationLookup from 'components/Lookup/OrganizationLookup';
import ContentError from 'components/ContentError'; import ContentError from 'components/ContentError';
import { FormColumnLayout, FormFullWidthLayout } from 'components/FormLayout'; import { FormColumnLayout, FormFullWidthLayout } from 'components/FormLayout';
import getHelpText from './Inventory.helptext'; import helpText from './Inventory.helptext';
function InventoryFormFields({ inventory }) { function InventoryFormFields({ inventory }) {
const helpText = getHelpText();
const [contentError, setContentError] = useState(false); const [contentError, setContentError] = useState(false);
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [organizationField, organizationMeta, organizationHelpers] = const [organizationField, organizationMeta, organizationHelpers] =

View File

@@ -13,10 +13,9 @@ import {
EnabledValueField, EnabledValueField,
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
const AzureSubForm = ({ autoPopulateCredential }) => { const AzureSubForm = ({ autoPopulateCredential }) => {
const helpText = getHelpText();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = const [credentialField, credentialMeta, credentialHelpers] =
useField('credential'); useField('credential');

View File

@@ -14,10 +14,9 @@ import {
HostFilterField, HostFilterField,
SourceVarsField, SourceVarsField,
} from './SharedFields'; } from './SharedFields';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
const ControllerSubForm = ({ autoPopulateCredential }) => { const ControllerSubForm = ({ autoPopulateCredential }) => {
const helpText = getHelpText();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = const [credentialField, credentialMeta, credentialHelpers] =
useField('credential'); useField('credential');

View File

@@ -12,10 +12,9 @@ import {
EnabledValueField, EnabledValueField,
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
const EC2SubForm = () => { const EC2SubForm = () => {
const helpText = getHelpText();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField, credentialMeta] = useField('credential'); const [credentialField, credentialMeta] = useField('credential');
const config = useConfig(); const config = useConfig();

View File

@@ -13,10 +13,9 @@ import {
HostFilterField, HostFilterField,
SourceVarsField, SourceVarsField,
} from './SharedFields'; } from './SharedFields';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
const GCESubForm = ({ autoPopulateCredential }) => { const GCESubForm = ({ autoPopulateCredential }) => {
const helpText = getHelpText();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = const [credentialField, credentialMeta, credentialHelpers] =
useField('credential'); useField('credential');

View File

@@ -14,10 +14,9 @@ import {
HostFilterField, HostFilterField,
SourceVarsField, SourceVarsField,
} from './SharedFields'; } from './SharedFields';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
const InsightsSubForm = ({ autoPopulateCredential }) => { const InsightsSubForm = ({ autoPopulateCredential }) => {
const helpText = getHelpText();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = const [credentialField, credentialMeta, credentialHelpers] =
useField('credential'); useField('credential');

View File

@@ -13,10 +13,9 @@ import {
EnabledValueField, EnabledValueField,
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
const OpenStackSubForm = ({ autoPopulateCredential }) => { const OpenStackSubForm = ({ autoPopulateCredential }) => {
const helpText = getHelpText();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = const [credentialField, credentialMeta, credentialHelpers] =
useField('credential'); useField('credential');

View File

@@ -21,10 +21,9 @@ import {
EnabledValueField, EnabledValueField,
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
const SCMSubForm = ({ autoPopulateProject }) => { const SCMSubForm = ({ autoPopulateProject }) => {
const helpText = getHelpText();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [sourcePath, setSourcePath] = useState([]); const [sourcePath, setSourcePath] = useState([]);
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();

View File

@@ -13,10 +13,9 @@ import {
EnabledValueField, EnabledValueField,
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
const SatelliteSubForm = ({ autoPopulateCredential }) => { const SatelliteSubForm = ({ autoPopulateCredential }) => {
const helpText = getHelpText();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = const [credentialField, credentialMeta, credentialHelpers] =
useField('credential'); useField('credential');

View File

@@ -9,29 +9,25 @@ import { VariablesField } from 'components/CodeEditor';
import FormField, { CheckboxField } from 'components/FormField'; import FormField, { CheckboxField } from 'components/FormField';
import { FormFullWidthLayout, FormCheckboxLayout } from 'components/FormLayout'; import { FormFullWidthLayout, FormCheckboxLayout } from 'components/FormLayout';
import Popover from 'components/Popover'; import Popover from 'components/Popover';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
export const SourceVarsField = ({ popoverContent }) => { export const SourceVarsField = ({ popoverContent }) => (
const helpText = getHelpText(); <FormFullWidthLayout>
return ( <VariablesField
<FormFullWidthLayout> id="source_vars"
<VariablesField name="source_vars"
id="source_vars" label={t`Source variables`}
name="source_vars" tooltip={
label={t`Source variables`} <>
tooltip={ {popoverContent}
<> {helpText.variables()}
{popoverContent} </>
{helpText.variables()} }
</> />
} </FormFullWidthLayout>
/> );
</FormFullWidthLayout>
);
};
export const VerbosityField = () => { export const VerbosityField = () => {
const helpText = getHelpText();
const [field, meta, helpers] = useField('verbosity'); const [field, meta, helpers] = useField('verbosity');
const isValid = !(meta.touched && meta.error); const isValid = !(meta.touched && meta.error);
const options = [ const options = [
@@ -58,7 +54,6 @@ export const VerbosityField = () => {
}; };
export const OptionsField = () => { export const OptionsField = () => {
const helpText = getHelpText();
const [updateOnLaunchField] = useField('update_on_launch'); const [updateOnLaunchField] = useField('update_on_launch');
const [, , updateCacheTimeoutHelper] = useField('update_cache_timeout'); const [, , updateCacheTimeoutHelper] = useField('update_cache_timeout');
const [projectField] = useField('source_project'); const [projectField] = useField('source_project');
@@ -111,42 +106,33 @@ export const OptionsField = () => {
); );
}; };
export const EnabledVarField = () => { export const EnabledVarField = () => (
const helpText = getHelpText(); <FormField
return ( id="inventory-enabled-var"
<FormField label={t`Enabled Variable`}
id="inventory-enabled-var" tooltip={helpText.enabledVariableField}
label={t`Enabled Variable`} name="enabled_var"
tooltip={helpText.enabledVariableField} type="text"
name="enabled_var" />
type="text" );
/>
);
};
export const EnabledValueField = () => { export const EnabledValueField = () => (
const helpText = getHelpText(); <FormField
return ( id="inventory-enabled-value"
<FormField label={t`Enabled Value`}
id="inventory-enabled-value" tooltip={helpText.enabledValue}
label={t`Enabled Value`} name="enabled_value"
tooltip={helpText.enabledValue} type="text"
name="enabled_value" />
type="text" );
/>
);
};
export const HostFilterField = () => { export const HostFilterField = () => (
const helpText = getHelpText(); <FormField
return ( id="host-filter"
<FormField label={t`Host Filter`}
id="host-filter" tooltip={helpText.hostFilter}
label={t`Host Filter`} name="host_filter"
tooltip={helpText.hostFilter} type="text"
name="host_filter" validate={regExp()}
type="text" />
validate={regExp()} );
/>
);
};

View File

@@ -13,10 +13,9 @@ import {
EnabledValueField, EnabledValueField,
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
const VMwareSubForm = ({ autoPopulateCredential }) => { const VMwareSubForm = ({ autoPopulateCredential }) => {
const helpText = getHelpText();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = const [credentialField, credentialMeta, credentialHelpers] =
useField('credential'); useField('credential');

View File

@@ -13,10 +13,9 @@ import {
HostFilterField, HostFilterField,
SourceVarsField, SourceVarsField,
} from './SharedFields'; } from './SharedFields';
import getHelpText from '../Inventory.helptext'; import helpText from '../Inventory.helptext';
const VirtualizationSubForm = ({ autoPopulateCredential }) => { const VirtualizationSubForm = ({ autoPopulateCredential }) => {
const helpText = getHelpText();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = const [credentialField, credentialMeta, credentialHelpers] =
useField('credential'); useField('credential');

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
const jobHelpText = () => ({ const jobHelpText = {
jobType: t`For job templates, select run to execute the playbook. Select check to only check playbook syntax, test environment setup, and report problems without executing the playbook.`, jobType: t`For job templates, select run to execute the playbook. Select check to only check playbook syntax, test environment setup, and report problems without executing the playbook.`,
inventory: t`Select the inventory containing the hosts you want this job to manage.`, inventory: t`Select the inventory containing the hosts you want this job to manage.`,
project: t`Select the project containing the playbook you want this job to execute.`, project: t`Select the project containing the playbook you want this job to execute.`,
@@ -41,6 +41,6 @@ const jobHelpText = () => ({
) : ( ) : (
t`These arguments are used with the specified module.` t`These arguments are used with the specified module.`
), ),
}); };
export default jobHelpText; export default jobHelpText;

View File

@@ -29,7 +29,7 @@ import { VERBOSITY } from 'components/VerbositySelectField';
import { getJobModel, isJobRunning } from 'util/jobs'; import { getJobModel, isJobRunning } from 'util/jobs';
import { formatDateString } from 'util/dates'; import { formatDateString } from 'util/dates';
import { Job } from 'types'; import { Job } from 'types';
import getJobHelpText from '../Job.helptext'; import jobHelpText from '../Job.helptext';
const StatusDetailValue = styled.div` const StatusDetailValue = styled.div`
align-items: center; align-items: center;
@@ -39,7 +39,6 @@ const StatusDetailValue = styled.div`
`; `;
function JobDetail({ job, inventorySourceLabels }) { function JobDetail({ job, inventorySourceLabels }) {
const jobHelpText = getJobHelpText();
const { me } = useConfig(); const { me } = useConfig();
const { const {
created_by, created_by,

View File

@@ -187,7 +187,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
useEffect(() => { useEffect(() => {
const pendingRequests = Object.values(eventByUuidRequests.current || {}); const pendingRequests = Object.values(eventByUuidRequests.current || {});
setHasContentLoading(true); // prevents "no content found" screen from flashing setHasContentLoading(true); // prevents "no content found" screen from flashing
setIsFollowModeEnabled(false);
Promise.allSettled(pendingRequests).then(() => { Promise.allSettled(pendingRequests).then(() => {
setRemoteRowCount(0); setRemoteRowCount(0);
clearLoadedEvents(); clearLoadedEvents();
@@ -225,45 +224,39 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
} }
let batchTimeout; let batchTimeout;
let batchedEvents = []; let batchedEvents = [];
const addBatchedEvents = () => {
let min;
let max;
let newCssMap;
batchedEvents.forEach((event) => {
if (!min || event.counter < min) {
min = event.counter;
}
if (!max || event.counter > max) {
max = event.counter;
}
const { lineCssMap } = getLineTextHtml(event);
newCssMap = {
...newCssMap,
...lineCssMap,
};
});
setWsEvents((oldWsEvents) => {
const newEvents = [];
batchedEvents.forEach((event) => {
if (!oldWsEvents.find((e) => e.id === event.id)) {
newEvents.push(event);
}
});
const updated = oldWsEvents.concat(newEvents);
jobSocketCounter.current = updated.length;
return updated.sort((a, b) => a.counter - b.counter);
});
setCssMap((prevCssMap) => ({
...prevCssMap,
...newCssMap,
}));
if (max > jobSocketCounter.current) {
jobSocketCounter.current = max;
}
batchedEvents = [];
};
connectJobSocket(job, (data) => { connectJobSocket(job, (data) => {
const addBatchedEvents = () => {
let min;
let max;
let newCssMap;
batchedEvents.forEach((event) => {
if (!min || event.counter < min) {
min = event.counter;
}
if (!max || event.counter > max) {
max = event.counter;
}
const { lineCssMap } = getLineTextHtml(event);
newCssMap = {
...newCssMap,
...lineCssMap,
};
});
setWsEvents((oldWsEvents) => {
const updated = oldWsEvents.concat(batchedEvents);
jobSocketCounter.current = updated.length;
return updated.sort((a, b) => a.counter - b.counter);
});
setCssMap((prevCssMap) => ({
...prevCssMap,
...newCssMap,
}));
if (max > jobSocketCounter.current) {
jobSocketCounter.current = max;
}
batchedEvents = [];
};
if (data.group_name === `${job.type}_events`) { if (data.group_name === `${job.type}_events`) {
batchedEvents.push(data); batchedEvents.push(data);
clearTimeout(batchTimeout); clearTimeout(batchTimeout);

View File

@@ -25,13 +25,12 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
import StatusLabel from 'components/StatusLabel'; import StatusLabel from 'components/StatusLabel';
import hasCustomMessages from '../shared/hasCustomMessages'; import hasCustomMessages from '../shared/hasCustomMessages';
import { NOTIFICATION_TYPES } from '../constants'; import { NOTIFICATION_TYPES } from '../constants';
import getHelpText from '../shared/Notifications.helptext'; import helpText from '../shared/Notifications.helptext';
const NUM_RETRIES = 25; const NUM_RETRIES = 25;
const RETRY_TIMEOUT = 5000; const RETRY_TIMEOUT = 5000;
function NotificationTemplateDetail({ template, defaultMessages }) { function NotificationTemplateDetail({ template, defaultMessages }) {
const helpText = getHelpText();
const history = useHistory(); const history = useHistory();
const [testStatus, setTestStatus] = useState( const [testStatus, setTestStatus] = useState(
template.summary_fields?.recent_notifications[0]?.status ?? undefined template.summary_fields?.recent_notifications[0]?.status ?? undefined

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
const helpText = () => ({ const helpText = {
emailRecepients: t`Use one email address per line to create a recipient list for this type of notification.`, emailRecepients: t`Use one email address per line to create a recipient list for this type of notification.`,
emailTimeout: t`The amount of time (in seconds) before the email emailTimeout: t`The amount of time (in seconds) before the email
notification stops trying to reach the host and times out. Ranges notification stops trying to reach the host and times out. Ranges
@@ -40,6 +40,6 @@ const helpText = () => ({
<span>{t`for more information.`}</span> <span>{t`for more information.`}</span>
</> </>
), ),
}); };
export default helpText; export default helpText;

View File

@@ -26,7 +26,7 @@ import {
} from 'util/validators'; } from 'util/validators';
import { NotificationType } from 'types'; import { NotificationType } from 'types';
import Popover from '../../../components/Popover/Popover'; import Popover from '../../../components/Popover/Popover';
import getHelpText from './Notifications.helptext'; import helpText from './Notifications.helptext';
const TypeFields = { const TypeFields = {
email: EmailFields, email: EmailFields,
@@ -59,7 +59,6 @@ TypeInputsSubForm.propTypes = {
export default TypeInputsSubForm; export default TypeInputsSubForm;
function EmailFields() { function EmailFields() {
const helpText = getHelpText();
return ( return (
<> <>
<FormField <FormField
@@ -143,7 +142,6 @@ function EmailFields() {
} }
function GrafanaFields() { function GrafanaFields() {
const helpText = getHelpText();
return ( return (
<> <>
<FormField <FormField
@@ -192,8 +190,6 @@ function GrafanaFields() {
} }
function IRCFields() { function IRCFields() {
const helpText = getHelpText();
return ( return (
<> <>
<PasswordField <PasswordField
@@ -355,8 +351,6 @@ function RocketChatFields() {
} }
function SlackFields() { function SlackFields() {
const helpText = getHelpText();
return ( return (
<> <>
<ArrayTextField <ArrayTextField
@@ -387,8 +381,6 @@ function SlackFields() {
} }
function TwilioFields() { function TwilioFields() {
const helpText = getHelpText();
return ( return (
<> <>
<PasswordField <PasswordField
@@ -429,8 +421,6 @@ function TwilioFields() {
} }
function WebhookFields() { function WebhookFields() {
const helpText = getHelpText();
const [methodField, methodMeta] = useField({ const [methodField, methodMeta] = useField({
name: 'notification_configuration.http_method', name: 'notification_configuration.http_method',
validate: required(t`Select a value for this field`), validate: required(t`Select a value for this field`),

View File

@@ -18,16 +18,10 @@ function ProjectAdd() {
// the API might throw an unexpected error if our creation request // the API might throw an unexpected error if our creation request
// has a zero-length string as its credential field. As a work-around, // has a zero-length string as its credential field. As a work-around,
// normalize falsey credential fields by deleting them. // normalize falsey credential fields by deleting them.
values.credential = null; delete values.credential;
} else if (typeof values.credential.id === 'number') { } else {
values.credential = values.credential.id; values.credential = values.credential.id;
} }
if (!values.signature_validation_credential) {
values.signature_validation_credential = null;
} else if (typeof values.signature_validation_credential.id === 'number') {
values.signature_validation_credential =
values.signature_validation_credential.id;
}
setFormSubmitError(null); setFormSubmitError(null);
try { try {
const { const {

View File

@@ -20,7 +20,6 @@ describe('<ProjectAdd />', () => {
scm_clean: true, scm_clean: true,
scm_track_submodules: false, scm_track_submodules: false,
credential: 100, credential: 100,
signature_validation_credential: 200,
local_path: '', local_path: '',
organization: { id: 2, name: 'Bar' }, organization: { id: 2, name: 'Bar' },
scm_update_on_launch: true, scm_update_on_launch: true,
@@ -74,32 +73,16 @@ describe('<ProjectAdd />', () => {
}, },
}; };
const cryptographyCredentialResolve = {
data: {
results: [
{
id: 6,
name: 'GPG Public Key',
kind: 'cryptography',
},
],
count: 1,
},
};
beforeEach(async () => { beforeEach(async () => {
await ProjectsAPI.readOptions.mockImplementation( await ProjectsAPI.readOptions.mockImplementation(
() => projectOptionsResolve () => projectOptionsResolve
); );
await CredentialTypesAPI.read.mockImplementation( await CredentialTypesAPI.read.mockImplementationOnce(
() => scmCredentialResolve () => scmCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementation( await CredentialTypesAPI.read.mockImplementationOnce(
() => insightsCredentialResolve () => insightsCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementation(
() => cryptographyCredentialResolve
);
}); });
afterEach(() => { afterEach(() => {
@@ -127,7 +110,6 @@ describe('<ProjectAdd />', () => {
...projectData, ...projectData,
organization: 2, organization: 2,
default_environment: 1, default_environment: 1,
signature_validation_credential: 200,
}); });
}); });

View File

@@ -31,7 +31,7 @@ import { formatDateString } from 'util/dates';
import Popover from 'components/Popover'; import Popover from 'components/Popover';
import getDocsBaseUrl from 'util/getDocsBaseUrl'; import getDocsBaseUrl from 'util/getDocsBaseUrl';
import ProjectSyncButton from '../shared/ProjectSyncButton'; import ProjectSyncButton from '../shared/ProjectSyncButton';
import getProjectHelpText from '../shared/Project.helptext'; import projectHelpText from '../shared/Project.helptext';
import useWsProject from './useWsProject'; import useWsProject from './useWsProject';
const Label = styled.span` const Label = styled.span`
@@ -39,7 +39,6 @@ const Label = styled.span`
`; `;
function ProjectDetail({ project }) { function ProjectDetail({ project }) {
const projectHelpText = getProjectHelpText();
const { const {
allow_override, allow_override,
created, created,
@@ -125,6 +124,7 @@ function ProjectDetail({ project }) {
</TextList> </TextList>
); );
} }
const generateLastJobTooltip = (job) => ( const generateLastJobTooltip = (job) => (
<> <>
<div>{t`MOST RECENT SYNC`}</div> <div>{t`MOST RECENT SYNC`}</div>
@@ -149,7 +149,6 @@ function ProjectDetail({ project }) {
} else if (summary_fields?.last_job) { } else if (summary_fields?.last_job) {
job = summary_fields.last_job; job = summary_fields.last_job;
} }
const getSourceControlUrlHelpText = () => const getSourceControlUrlHelpText = () =>
scm_type === 'git' scm_type === 'git'
? projectHelpText.githubSourceControlUrl ? projectHelpText.githubSourceControlUrl
@@ -235,22 +234,6 @@ function ProjectDetail({ project }) {
label={t`Source Control Refspec`} label={t`Source Control Refspec`}
value={scm_refspec} value={scm_refspec}
/> />
{summary_fields.signature_validation_credential && (
<Detail
label={t`Content Signature Validation Credential`}
helpText={projectHelpText.signatureValidation}
value={
<CredentialChip
key={summary_fields.signature_validation_credential.id}
credential={summary_fields.signature_validation_credential}
isReadOnly
/>
}
isEmpty={
summary_fields.signature_validation_credential.length === 0
}
/>
)}
{summary_fields.credential && ( {summary_fields.credential && (
<Detail <Detail
label={t`Source Control Credential`} label={t`Source Control Credential`}
@@ -261,7 +244,6 @@ function ProjectDetail({ project }) {
isReadOnly isReadOnly
/> />
} }
isEmpty={summary_fields.credential.length === 0}
/> />
)} )}
<Detail <Detail

View File

@@ -46,11 +46,6 @@ describe('<ProjectDetail />', () => {
name: 'qux', name: 'qux',
kind: 'scm', kind: 'scm',
}, },
signature_validation_credential: {
id: 2000,
name: 'svc',
kind: 'cryptography',
},
last_job: { last_job: {
id: 9000, id: 9000,
status: 'successful', status: 'successful',
@@ -83,7 +78,6 @@ describe('<ProjectDetail />', () => {
scm_delete_on_update: true, scm_delete_on_update: true,
scm_track_submodules: true, scm_track_submodules: true,
credential: 100, credential: 100,
signature_validation_credential: 200,
status: 'successful', status: 'successful',
organization: 10, organization: 10,
scm_update_on_launch: true, scm_update_on_launch: true,
@@ -114,10 +108,6 @@ describe('<ProjectDetail />', () => {
'Source Control Credential', 'Source Control Credential',
`Scm: ${mockProject.summary_fields.credential.name}` `Scm: ${mockProject.summary_fields.credential.name}`
); );
assertDetail(
'Content Signature Validation Credential',
`Cryptography: ${mockProject.summary_fields.signature_validation_credential.name}`
);
assertDetail( assertDetail(
'Cache Timeout', 'Cache Timeout',
`${mockProject.scm_update_cache_timeout} Seconds` `${mockProject.scm_update_cache_timeout} Seconds`

View File

@@ -18,17 +18,10 @@ function ProjectEdit({ project }) {
// the API might throw an unexpected error if our creation request // the API might throw an unexpected error if our creation request
// has a zero-length string as its credential field. As a work-around, // has a zero-length string as its credential field. As a work-around,
// normalize falsey credential fields by deleting them. // normalize falsey credential fields by deleting them.
values.credential = null; delete values.credential;
} else if (typeof values.credential.id === 'number') { } else {
values.credential = values.credential.id; values.credential = values.credential.id;
} }
if (!values.signature_validation_credential) {
values.signature_validation_credential = null;
} else if (typeof values.signature_validation_credential.id === 'number') {
values.signature_validation_credential =
values.signature_validation_credential.id;
}
try { try {
const { const {
data: { id }, data: { id },

View File

@@ -21,7 +21,6 @@ describe('<ProjectEdit />', () => {
scm_clean: true, scm_clean: true,
scm_track_submodules: false, scm_track_submodules: false,
credential: 100, credential: 100,
signature_validation_credential: 200,
local_path: 'bar', local_path: 'bar',
organization: 2, organization: 2,
scm_update_on_launch: true, scm_update_on_launch: true,
@@ -34,12 +33,6 @@ describe('<ProjectEdit />', () => {
credential_type_id: 5, credential_type_id: 5,
kind: 'insights', kind: 'insights',
}, },
signature_validation_credential: {
id: 200,
credential_type_id: 6,
kind: 'cryptography',
name: 'foo',
},
organization: { organization: {
id: 2, id: 2,
name: 'Default', name: 'Default',
@@ -67,7 +60,6 @@ describe('<ProjectEdit />', () => {
const scmCredentialResolve = { const scmCredentialResolve = {
data: { data: {
count: 1,
results: [ results: [
{ {
id: 4, id: 4,
@@ -80,7 +72,6 @@ describe('<ProjectEdit />', () => {
const insightsCredentialResolve = { const insightsCredentialResolve = {
data: { data: {
count: 1,
results: [ results: [
{ {
id: 5, id: 5,
@@ -91,19 +82,6 @@ describe('<ProjectEdit />', () => {
}, },
}; };
const cryptographyCredentialResolve = {
data: {
count: 1,
results: [
{
id: 6,
name: 'GPG Public Key',
kind: 'cryptography',
},
],
},
};
beforeEach(async () => { beforeEach(async () => {
RootAPI.readAssetVariables.mockResolvedValue({ RootAPI.readAssetVariables.mockResolvedValue({
data: { data: {
@@ -113,15 +91,12 @@ describe('<ProjectEdit />', () => {
await ProjectsAPI.readOptions.mockImplementation( await ProjectsAPI.readOptions.mockImplementation(
() => projectOptionsResolve () => projectOptionsResolve
); );
await CredentialTypesAPI.read.mockImplementation( await CredentialTypesAPI.read.mockImplementationOnce(
() => scmCredentialResolve () => scmCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementation( await CredentialTypesAPI.read.mockImplementationOnce(
() => insightsCredentialResolve () => insightsCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementation(
() => cryptographyCredentialResolve
);
}); });
afterEach(() => { afterEach(() => {

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
const projectHelpTextStrings = () => ({ const projectHelpTextStrings = {
executionEnvironment: t`The execution environment that will be used for jobs that use this project. This will be used as fallback when an execution environment has not been explicitly assigned at the job template or workflow level.`, executionEnvironment: t`The execution environment that will be used for jobs that use this project. This will be used as fallback when an execution environment has not been explicitly assigned at the job template or workflow level.`,
projectBasePath: (brandName = '') => ( projectBasePath: (brandName = '') => (
<span> <span>
@@ -105,10 +105,6 @@ const projectHelpTextStrings = () => ({
you can input tags, commit hashes, and arbitrary refs. Some you can input tags, commit hashes, and arbitrary refs. Some
commit hashes and refs may not be available unless you also commit hashes and refs may not be available unless you also
provide a custom refspec.`, provide a custom refspec.`,
signatureValidation: t`Enable content signing to verify that the content
has remained secure when a project is synced.
If the content has been tampered with, the
job will not run.`,
options: { options: {
clean: t`Remove any local modifications prior to performing an update.`, clean: t`Remove any local modifications prior to performing an update.`,
delete: t`Delete the local repository in its entirety prior to delete: t`Delete the local repository in its entirety prior to
@@ -132,6 +128,6 @@ const projectHelpTextStrings = () => ({
considered current, and a new project update will be considered current, and a new project update will be
performed.`, performed.`,
}, },
}); };
export default projectHelpTextStrings; export default projectHelpTextStrings;

View File

@@ -9,7 +9,6 @@ import { useConfig } from 'contexts/Config';
import AnsibleSelect from 'components/AnsibleSelect'; import AnsibleSelect from 'components/AnsibleSelect';
import ContentError from 'components/ContentError'; import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading'; import ContentLoading from 'components/ContentLoading';
import CredentialLookup from 'components/Lookup/CredentialLookup';
import FormActionGroup from 'components/FormActionGroup/FormActionGroup'; import FormActionGroup from 'components/FormActionGroup/FormActionGroup';
import FormField, { FormSubmitError } from 'components/FormField'; import FormField, { FormSubmitError } from 'components/FormField';
import OrganizationLookup from 'components/Lookup/OrganizationLookup'; import OrganizationLookup from 'components/Lookup/OrganizationLookup';
@@ -17,7 +16,7 @@ import ExecutionEnvironmentLookup from 'components/Lookup/ExecutionEnvironmentLo
import { CredentialTypesAPI, ProjectsAPI } from 'api'; import { CredentialTypesAPI, ProjectsAPI } from 'api';
import { required } from 'util/validators'; import { required } from 'util/validators';
import { FormColumnLayout, SubFormLayout } from 'components/FormLayout'; import { FormColumnLayout, SubFormLayout } from 'components/FormLayout';
import getProjectHelpText from './Project.helptext'; import projectHelpText from './Project.helptext';
import { import {
GitSubForm, GitSubForm,
SvnSubForm, SvnSubForm,
@@ -38,22 +37,15 @@ const fetchCredentials = async (credential) => {
results: [insightsCredentialType], results: [insightsCredentialType],
}, },
}, },
{
data: {
results: [cryptographyCredentialType],
},
},
] = await Promise.all([ ] = await Promise.all([
CredentialTypesAPI.read({ kind: 'scm' }), CredentialTypesAPI.read({ kind: 'scm' }),
CredentialTypesAPI.read({ name: 'Insights' }), CredentialTypesAPI.read({ name: 'Insights' }),
CredentialTypesAPI.read({ kind: 'cryptography' }),
]); ]);
if (!credential) { if (!credential) {
return { return {
scm: { typeId: scmCredentialType.id }, scm: { typeId: scmCredentialType.id },
insights: { typeId: insightsCredentialType.id }, insights: { typeId: insightsCredentialType.id },
cryptography: { typeId: cryptographyCredentialType.id },
}; };
} }
@@ -68,13 +60,6 @@ const fetchCredentials = async (credential) => {
value: value:
credential_type_id === insightsCredentialType.id ? credential : null, credential_type_id === insightsCredentialType.id ? credential : null,
}, },
cryptography: {
typeId: cryptographyCredentialType.id,
value:
credential_type_id === cryptographyCredentialType.id
? credential
: null,
},
}; };
}; };
@@ -84,20 +69,16 @@ function ProjectFormFields({
project_local_paths, project_local_paths,
formik, formik,
setCredentials, setCredentials,
setSignatureValidationCredentials,
credentials, credentials,
signatureValidationCredentials,
scmTypeOptions, scmTypeOptions,
setScmSubFormState, setScmSubFormState,
scmSubFormState, scmSubFormState,
}) { }) {
const projectHelpText = getProjectHelpText();
const scmFormFields = { const scmFormFields = {
scm_url: '', scm_url: '',
scm_branch: '', scm_branch: '',
scm_refspec: '', scm_refspec: '',
credential: '', credential: '',
signature_validation_credential: '',
scm_clean: false, scm_clean: false,
scm_delete_on_update: false, scm_delete_on_update: false,
scm_track_submodules: false, scm_track_submodules: false,
@@ -105,6 +86,7 @@ function ProjectFormFields({
allow_override: false, allow_override: false,
scm_update_cache_timeout: 0, scm_update_cache_timeout: 0,
}; };
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [scmTypeField, scmTypeMeta, scmTypeHelpers] = useField({ const [scmTypeField, scmTypeMeta, scmTypeHelpers] = useField({
@@ -165,32 +147,6 @@ function ProjectFormFields({
[credentials, setCredentials] [credentials, setCredentials]
); );
const handleSignatureValidationCredentialSelection = useCallback(
(type, value) => {
setSignatureValidationCredentials({
...signatureValidationCredentials,
[type]: {
...signatureValidationCredentials[type],
value,
},
});
},
[signatureValidationCredentials, setSignatureValidationCredentials]
);
const handleSignatureValidationCredentialChange = useCallback(
(value) => {
handleSignatureValidationCredentialSelection('cryptography', value);
setFieldValue('signature_validation_credential', value);
setFieldTouched('signature_validation_credential', true, false);
},
[
handleSignatureValidationCredentialSelection,
setFieldValue,
setFieldTouched,
]
);
const handleOrganizationUpdate = useCallback( const handleOrganizationUpdate = useCallback(
(value) => { (value) => {
setFieldValue('organization', value); setFieldValue('organization', value);
@@ -285,13 +241,6 @@ function ProjectFormFields({
}} }}
/> />
</FormGroup> </FormGroup>
<CredentialLookup
credentialTypeId={signatureValidationCredentials.cryptography.typeId}
label={t`Content Signature Validation Credential`}
onChange={handleSignatureValidationCredentialChange}
value={signatureValidationCredentials.cryptography.value}
tooltip={projectHelpText.signatureValidation}
/>
{formik.values.scm_type !== '' && ( {formik.values.scm_type !== '' && (
<SubFormLayout> <SubFormLayout>
<Title size="md" headingLevel="h4"> <Title size="md" headingLevel="h4">
@@ -346,6 +295,7 @@ function ProjectFormFields({
</> </>
); );
} }
function ProjectForm({ project, submitError, ...props }) { function ProjectForm({ project, submitError, ...props }) {
const { handleCancel, handleSubmit } = props; const { handleCancel, handleSubmit } = props;
const { summary_fields = {} } = project; const { summary_fields = {} } = project;
@@ -357,7 +307,6 @@ function ProjectForm({ project, submitError, ...props }) {
scm_branch: '', scm_branch: '',
scm_refspec: '', scm_refspec: '',
credential: '', credential: '',
signature_validation_credential: '',
scm_clean: false, scm_clean: false,
scm_delete_on_update: false, scm_delete_on_update: false,
scm_track_submodules: false, scm_track_submodules: false,
@@ -369,22 +318,12 @@ function ProjectForm({ project, submitError, ...props }) {
const [credentials, setCredentials] = useState({ const [credentials, setCredentials] = useState({
scm: { typeId: null, value: null }, scm: { typeId: null, value: null },
insights: { typeId: null, value: null }, insights: { typeId: null, value: null },
cryptography: { typeId: null, value: null },
}); });
const [signatureValidationCredentials, setSignatureValidationCredentials] =
useState({
scm: { typeId: null, value: null },
insights: { typeId: null, value: null },
cryptography: { typeId: null, value: null },
});
useEffect(() => { useEffect(() => {
async function fetchData() { async function fetchData() {
try { try {
const credentialResponse = fetchCredentials(summary_fields.credential); const credentialResponse = fetchCredentials(summary_fields.credential);
const signatureValidationCredentialResponse = fetchCredentials(
summary_fields.signature_validation_credential
);
const { const {
data: { data: {
actions: { actions: {
@@ -396,9 +335,6 @@ function ProjectForm({ project, submitError, ...props }) {
} = await ProjectsAPI.readOptions(); } = await ProjectsAPI.readOptions();
setCredentials(await credentialResponse); setCredentials(await credentialResponse);
setSignatureValidationCredentials(
await signatureValidationCredentialResponse
);
setScmTypeOptions(choices); setScmTypeOptions(choices);
} catch (error) { } catch (error) {
setContentError(error); setContentError(error);
@@ -408,10 +344,7 @@ function ProjectForm({ project, submitError, ...props }) {
} }
fetchData(); fetchData();
}, [ }, [summary_fields.credential]);
summary_fields.credential,
summary_fields.signature_validation_credential,
]);
if (isLoading) { if (isLoading) {
return <ContentLoading />; return <ContentLoading />;
@@ -445,8 +378,6 @@ function ProjectForm({ project, submitError, ...props }) {
scm_update_cache_timeout: project.scm_update_cache_timeout || 0, scm_update_cache_timeout: project.scm_update_cache_timeout || 0,
scm_update_on_launch: project.scm_update_on_launch || false, scm_update_on_launch: project.scm_update_on_launch || false,
scm_url: project.scm_url || '', scm_url: project.scm_url || '',
signature_validation_credential:
project.signature_validation_credential || '',
default_environment: default_environment:
project.summary_fields?.default_environment || null, project.summary_fields?.default_environment || null,
}} }}
@@ -461,11 +392,7 @@ function ProjectForm({ project, submitError, ...props }) {
project_local_paths={project_local_paths} project_local_paths={project_local_paths}
formik={formik} formik={formik}
setCredentials={setCredentials} setCredentials={setCredentials}
setSignatureValidationCredentials={
setSignatureValidationCredentials
}
credentials={credentials} credentials={credentials}
signatureValidationCredentials={signatureValidationCredentials}
scmTypeOptions={scmTypeOptions} scmTypeOptions={scmTypeOptions}
setScmSubFormState={setScmSubFormState} setScmSubFormState={setScmSubFormState}
scmSubFormState={scmSubFormState} scmSubFormState={scmSubFormState}

View File

@@ -19,7 +19,6 @@ describe('<ProjectForm />', () => {
scm_clean: true, scm_clean: true,
scm_track_submodules: false, scm_track_submodules: false,
credential: 100, credential: 100,
signature_validation_credential: 200,
organization: 2, organization: 2,
scm_update_on_launch: true, scm_update_on_launch: true,
scm_update_cache_timeout: 3, scm_update_cache_timeout: 3,
@@ -36,12 +35,6 @@ describe('<ProjectForm />', () => {
id: 2, id: 2,
name: 'Default', name: 'Default',
}, },
signature_validation_credential: {
id: 200,
credential_type_id: 6,
kind: 'cryptography',
name: 'Svc',
},
}, },
}; };
@@ -65,7 +58,6 @@ describe('<ProjectForm />', () => {
const scmCredentialResolve = { const scmCredentialResolve = {
data: { data: {
count: 1,
results: [ results: [
{ {
id: 4, id: 4,
@@ -78,7 +70,6 @@ describe('<ProjectForm />', () => {
const insightsCredentialResolve = { const insightsCredentialResolve = {
data: { data: {
count: 1,
results: [ results: [
{ {
id: 5, id: 5,
@@ -89,19 +80,6 @@ describe('<ProjectForm />', () => {
}, },
}; };
const cryptographyCredentialResolve = {
data: {
count: 1,
results: [
{
id: 6,
name: 'GPG Public Key',
kind: 'cryptography',
},
],
},
};
beforeEach(async () => { beforeEach(async () => {
RootAPI.readAssetVariables.mockResolvedValue({ RootAPI.readAssetVariables.mockResolvedValue({
data: { data: {
@@ -111,15 +89,12 @@ describe('<ProjectForm />', () => {
await ProjectsAPI.readOptions.mockImplementation( await ProjectsAPI.readOptions.mockImplementation(
() => projectOptionsResolve () => projectOptionsResolve
); );
await CredentialTypesAPI.read.mockImplementation( await CredentialTypesAPI.read.mockImplementationOnce(
() => scmCredentialResolve () => scmCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementation( await CredentialTypesAPI.read.mockImplementationOnce(
() => insightsCredentialResolve () => insightsCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementation(
() => cryptographyCredentialResolve
);
}); });
afterEach(() => { afterEach(() => {
@@ -178,17 +153,9 @@ describe('<ProjectForm />', () => {
expect( expect(
wrapper.find('FormGroup[label="Source Control Refspec"]').length wrapper.find('FormGroup[label="Source Control Refspec"]').length
).toBe(1); ).toBe(1);
expect(
wrapper.find('FormGroup[label="Content Signature Validation Credential"]')
.length
).toBe(1);
expect( expect(
wrapper.find('FormGroup[label="Source Control Credential"]').length wrapper.find('FormGroup[label="Source Control Credential"]').length
).toBe(1); ).toBe(1);
expect(
wrapper.find('FormGroup[label="Content Signature Validation Credential"]')
.length
).toBe(1);
expect(wrapper.find('FormGroup[label="Options"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Options"]').length).toBe(1);
}); });
@@ -210,52 +177,21 @@ describe('<ProjectForm />', () => {
id: 1, id: 1,
name: 'organization', name: 'organization',
}); });
wrapper wrapper.find('CredentialLookup').invoke('onBlur')();
.find('CredentialLookup[label="Source Control Credential"]') wrapper.find('CredentialLookup').invoke('onChange')({
.invoke('onBlur')();
wrapper
.find('CredentialLookup[label="Source Control Credential"]')
.invoke('onChange')({
id: 10, id: 10,
name: 'credential', name: 'credential',
}); });
wrapper
.find(
'CredentialLookup[label="Content Signature Validation Credential"]'
)
.invoke('onBlur')();
wrapper
.find(
'CredentialLookup[label="Content Signature Validation Credential"]'
)
.invoke('onChange')({
id: 20,
name: 'signature_validation_credential',
});
}); });
wrapper.update(); wrapper.update();
expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({ expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({
id: 1, id: 1,
name: 'organization', name: 'organization',
}); });
expect( expect(wrapper.find('CredentialLookup').prop('value')).toEqual({
wrapper
.find('CredentialLookup[label="Source Control Credential"]')
.prop('value')
).toEqual({
id: 10, id: 10,
name: 'credential', name: 'credential',
}); });
expect(
wrapper
.find(
'CredentialLookup[label="Content Signature Validation Credential"]'
)
.prop('value')
).toEqual({
id: 20,
name: 'signature_validation_credential',
});
}); });
test('should display insights credential lookup when source control type is "insights"', async () => { test('should display insights credential lookup when source control type is "insights"', async () => {
@@ -276,22 +212,14 @@ describe('<ProjectForm />', () => {
1 1
); );
await act(async () => { await act(async () => {
wrapper wrapper.find('CredentialLookup').invoke('onBlur')();
.find('CredentialLookup[label="Insights Credential"]') wrapper.find('CredentialLookup').invoke('onChange')({
.invoke('onBlur')();
wrapper
.find('CredentialLookup[label="Insights Credential"]')
.invoke('onChange')({
id: 123, id: 123,
name: 'credential', name: 'credential',
}); });
}); });
wrapper.update(); wrapper.update();
expect( expect(wrapper.find('CredentialLookup').prop('value')).toEqual({
wrapper
.find('CredentialLookup[label="Insights Credential"]')
.prop('value')
).toEqual({
id: 123, id: 123,
name: 'credential', name: 'credential',
}); });
@@ -430,9 +358,7 @@ describe('<ProjectForm />', () => {
}); });
test('should display ContentError on throw', async () => { test('should display ContentError on throw', async () => {
CredentialTypesAPI.read.mockImplementationOnce(() => CredentialTypesAPI.read = () => Promise.reject(new Error());
Promise.reject(new Error())
);
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} /> <ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />

View File

@@ -1,6 +1,6 @@
import 'styled-components/macro'; import 'styled-components/macro';
import React from 'react'; import React from 'react';
import getProjectHelpText from '../Project.helptext'; import projectHelpText from '../Project.helptext';
import { import {
UrlFormField, UrlFormField,
@@ -12,18 +12,15 @@ const ArchiveSubForm = ({
credential, credential,
onCredentialSelection, onCredentialSelection,
scmUpdateOnLaunch, scmUpdateOnLaunch,
}) => { }) => (
const projectHelpText = getProjectHelpText(); <>
return ( <UrlFormField tooltip={projectHelpText.archiveUrl} />
<> <ScmCredentialFormField
<UrlFormField tooltip={projectHelpText.archiveUrl} /> credential={credential}
<ScmCredentialFormField onCredentialSelection={onCredentialSelection}
credential={credential} />
onCredentialSelection={onCredentialSelection} <ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} />
/> </>
<ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} /> );
</>
);
};
export default ArchiveSubForm; export default ArchiveSubForm;

View File

@@ -11,7 +11,8 @@ import {
ScmCredentialFormField, ScmCredentialFormField,
ScmTypeOptions, ScmTypeOptions,
} from './SharedFields'; } from './SharedFields';
import getProjectHelpStrings from '../Project.helptext';
import projectHelpStrings from '../Project.helptext';
const GitSubForm = ({ const GitSubForm = ({
credential, credential,
@@ -21,7 +22,6 @@ const GitSubForm = ({
const docsURL = `${getDocsBaseUrl( const docsURL = `${getDocsBaseUrl(
useConfig() useConfig()
)}/html/userguide/projects.html#manage-playbooks-using-source-control`; )}/html/userguide/projects.html#manage-playbooks-using-source-control`;
const projectHelpStrings = getProjectHelpStrings();
return ( return (
<> <>

View File

@@ -8,14 +8,13 @@ import AnsibleSelect from 'components/AnsibleSelect';
import FormField from 'components/FormField'; import FormField from 'components/FormField';
import Popover from 'components/Popover'; import Popover from 'components/Popover';
import useBrandName from 'hooks/useBrandName'; import useBrandName from 'hooks/useBrandName';
import getProjectHelpStrings from '../Project.helptext'; import projectHelpStrings from '../Project.helptext';
const ManualSubForm = ({ const ManualSubForm = ({
localPath, localPath,
project_base_dir, project_base_dir,
project_local_paths, project_local_paths,
}) => { }) => {
const projectHelpStrings = getProjectHelpStrings();
const brandName = useBrandName(); const brandName = useBrandName();
const localPaths = [...new Set([...project_local_paths, localPath])]; const localPaths = [...new Set([...project_local_paths, localPath])];
const options = [ const options = [

View File

@@ -7,7 +7,7 @@ import CredentialLookup from 'components/Lookup/CredentialLookup';
import FormField, { CheckboxField } from 'components/FormField'; import FormField, { CheckboxField } from 'components/FormField';
import { required } from 'util/validators'; import { required } from 'util/validators';
import { FormCheckboxLayout, FormFullWidthLayout } from 'components/FormLayout'; import { FormCheckboxLayout, FormFullWidthLayout } from 'components/FormLayout';
import getProjectHelpStrings from '../Project.helptext'; import projectHelpStrings from '../Project.helptext';
export const UrlFormField = ({ tooltip }) => ( export const UrlFormField = ({ tooltip }) => (
<FormField <FormField
@@ -22,18 +22,15 @@ export const UrlFormField = ({ tooltip }) => (
/> />
); );
export const BranchFormField = ({ label }) => { export const BranchFormField = ({ label }) => (
const projectHelpStrings = getProjectHelpStrings(); <FormField
return ( id="project-scm-branch"
<FormField name="scm_branch"
id="project-scm-branch" type="text"
name="scm_branch" label={label}
type="text" tooltip={projectHelpStrings.branchFormField}
label={label} />
tooltip={projectHelpStrings.branchFormField} );
/>
);
};
export const ScmCredentialFormField = ({ export const ScmCredentialFormField = ({
credential, credential,
@@ -62,7 +59,6 @@ export const ScmCredentialFormField = ({
export const ScmTypeOptions = ({ scmUpdateOnLaunch, hideAllowOverride }) => { export const ScmTypeOptions = ({ scmUpdateOnLaunch, hideAllowOverride }) => {
const { values } = useFormikContext(); const { values } = useFormikContext();
const projectHelpStrings = getProjectHelpStrings();
return ( return (
<FormFullWidthLayout> <FormFullWidthLayout>

View File

@@ -1,7 +1,7 @@
import 'styled-components/macro'; import 'styled-components/macro';
import React from 'react'; import React from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import getProjectHelpStrings from '../Project.helptext'; import projectHelpStrings from '../Project.helptext';
import { import {
UrlFormField, UrlFormField,
@@ -14,19 +14,16 @@ const SvnSubForm = ({
credential, credential,
onCredentialSelection, onCredentialSelection,
scmUpdateOnLaunch, scmUpdateOnLaunch,
}) => { }) => (
const projectHelpStrings = getProjectHelpStrings(); <>
return ( <UrlFormField tooltip={projectHelpStrings.svnSourceControlUrl} />
<> <BranchFormField label={t`Revision #`} />
<UrlFormField tooltip={projectHelpStrings.svnSourceControlUrl} /> <ScmCredentialFormField
<BranchFormField label={t`Revision #`} /> credential={credential}
<ScmCredentialFormField onCredentialSelection={onCredentialSelection}
credential={credential} />
onCredentialSelection={onCredentialSelection} <ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} />
/> </>
<ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} /> );
</>
);
};
export default SvnSubForm; export default SvnSubForm;

View File

@@ -11,10 +11,9 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
import AlertModal from 'components/AlertModal'; import AlertModal from 'components/AlertModal';
import ErrorDetail from 'components/ErrorDetail'; import ErrorDetail from 'components/ErrorDetail';
import { ProjectsAPI } from 'api'; import { ProjectsAPI } from 'api';
import getProjectHelpStrings from './Project.helptext'; import projectHelpStrings from './Project.helptext';
function ProjectSyncButton({ projectId, lastJobStatus = null }) { function ProjectSyncButton({ projectId, lastJobStatus = null }) {
const projectHelpStrings = getProjectHelpStrings();
const match = useRouteMatch(); const match = useRouteMatch();
const { request: handleSync, error: syncError } = useRequest( const { request: handleSync, error: syncError } = useRequest(

View File

@@ -34,7 +34,7 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
import useBrandName from 'hooks/useBrandName'; import useBrandName from 'hooks/useBrandName';
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail'; import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails'; import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
import getHelpText from '../shared/JobTemplate.helptext'; import helpText from '../shared/JobTemplate.helptext';
function JobTemplateDetail({ template }) { function JobTemplateDetail({ template }) {
const { const {
@@ -68,7 +68,7 @@ function JobTemplateDetail({ template }) {
const { id: templateId } = useParams(); const { id: templateId } = useParams();
const history = useHistory(); const history = useHistory();
const brandName = useBrandName(); const brandName = useBrandName();
const helpText = getHelpText();
const { const {
isLoading: isLoadingInstanceGroups, isLoading: isLoadingInstanceGroups,
request: fetchInstanceGroups, request: fetchInstanceGroups,

View File

@@ -25,7 +25,7 @@ import Sparkline from 'components/Sparkline';
import { toTitleCase } from 'util/strings'; import { toTitleCase } from 'util/strings';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails'; import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
import useRequest, { useDismissableError } from 'hooks/useRequest'; import useRequest, { useDismissableError } from 'hooks/useRequest';
import getHelpText from '../shared/WorkflowJobTemplate.helptext'; import helpText from '../shared/WorkflowJobTemplate.helptext';
function WorkflowJobTemplateDetail({ template }) { function WorkflowJobTemplateDetail({ template }) {
const { const {
@@ -44,7 +44,7 @@ function WorkflowJobTemplateDetail({ template }) {
scm_branch: scmBranch, scm_branch: scmBranch,
limit, limit,
} = template; } = template;
const helpText = getHelpText();
const urlOrigin = window.location.origin; const urlOrigin = window.location.origin;
const history = useHistory(); const history = useHistory();

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import getDocsBaseUrl from 'util/getDocsBaseUrl'; import getDocsBaseUrl from 'util/getDocsBaseUrl';
const jtHelpTextStrings = () => ({ const jtHelpTextStrings = {
jobType: t`For job templates, select run to execute the playbook. Select check to only check playbook syntax, test environment setup, and report problems without executing the playbook.`, jobType: t`For job templates, select run to execute the playbook. Select check to only check playbook syntax, test environment setup, and report problems without executing the playbook.`,
inventory: t`Select the inventory containing the hosts you want this job to manage.`, inventory: t`Select the inventory containing the hosts you want this job to manage.`,
project: t`Select the project containing the playbook you want this job to execute.`, project: t`Select the project containing the playbook you want this job to execute.`,
@@ -60,6 +60,6 @@ const jtHelpTextStrings = () => ({
{t`for more information.`} {t`for more information.`}
</span> </span>
), ),
}); };
export default jtHelpTextStrings; export default jtHelpTextStrings;

View File

@@ -46,7 +46,7 @@ import LabelSelect from 'components/LabelSelect';
import { VerbositySelectField } from 'components/VerbositySelectField'; import { VerbositySelectField } from 'components/VerbositySelectField';
import PlaybookSelect from './PlaybookSelect'; import PlaybookSelect from './PlaybookSelect';
import WebhookSubForm from './WebhookSubForm'; import WebhookSubForm from './WebhookSubForm';
import getHelpText from './JobTemplate.helptext'; import helpText from './JobTemplate.helptext';
const { origin } = document.location; const { origin } = document.location;
@@ -60,7 +60,6 @@ function JobTemplateForm({
validateField, validateField,
isOverrideDisabledLookup, // TODO: this is a confusing variable name isOverrideDisabledLookup, // TODO: this is a confusing variable name
}) { }) {
const helpText = getHelpText();
const [contentError, setContentError] = useState(false); const [contentError, setContentError] = useState(false);
const [allowCallbacks, setAllowCallbacks] = useState( const [allowCallbacks, setAllowCallbacks] = useState(
Boolean(template?.host_config_key) Boolean(template?.host_config_key)

View File

@@ -22,10 +22,9 @@ import {
WorkflowJobTemplatesAPI, WorkflowJobTemplatesAPI,
CredentialTypesAPI, CredentialTypesAPI,
} from 'api'; } from 'api';
import getHelpText from './WorkflowJobTemplate.helptext'; import helpText from './WorkflowJobTemplate.helptext';
function WebhookSubForm({ templateType }) { function WebhookSubForm({ templateType }) {
const helpText = getHelpText();
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
const { id } = useParams(); const { id } = useParams();
const { pathname } = useLocation(); const { pathname } = useLocation();

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
const wfHelpTextStrings = () => ({ const wfHelpTextStrings = {
inventory: t`Select an inventory for the workflow. This inventory is applied to all workflow nodes that prompt for an inventory.`, inventory: t`Select an inventory for the workflow. This inventory is applied to all workflow nodes that prompt for an inventory.`,
limit: t`Provide a host pattern to further constrain limit: t`Provide a host pattern to further constrain
the list of hosts that will be managed or affected by the the list of hosts that will be managed or affected by the
@@ -24,6 +24,6 @@ const wfHelpTextStrings = () => ({
<p>{t`Webhooks: Enable Webhook for this workflow job template.`}</p> <p>{t`Webhooks: Enable Webhook for this workflow job template.`}</p>
</> </>
), ),
}); };
export default wfHelpTextStrings; export default wfHelpTextStrings;

View File

@@ -28,7 +28,7 @@ import Popover from 'components/Popover';
import { WorkFlowJobTemplate } from 'types'; import { WorkFlowJobTemplate } from 'types';
import LabelSelect from 'components/LabelSelect'; import LabelSelect from 'components/LabelSelect';
import WebhookSubForm from './WebhookSubForm'; import WebhookSubForm from './WebhookSubForm';
import getHelpText from './WorkflowJobTemplate.helptext'; import helpText from './WorkflowJobTemplate.helptext';
const urlOrigin = window.location.origin; const urlOrigin = window.location.origin;
@@ -39,7 +39,6 @@ function WorkflowJobTemplateForm({
submitError, submitError,
isOrgAdmin, isOrgAdmin,
}) { }) {
const helpText = getHelpText();
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [enableWebhooks, setEnableWebhooks] = useState( const [enableWebhooks, setEnableWebhooks] = useState(
Boolean(template.webhook_service) Boolean(template.webhook_service)

View File

@@ -12,10 +12,9 @@ import { TokensAPI } from 'api';
import { formatDateString } from 'util/dates'; import { formatDateString } from 'util/dates';
import useRequest, { useDismissableError } from 'hooks/useRequest'; import useRequest, { useDismissableError } from 'hooks/useRequest';
import { toTitleCase } from 'util/strings'; import { toTitleCase } from 'util/strings';
import getHelptext from '../shared/User.helptext'; import helptext from '../shared/User.helptext';
function UserTokenDetail({ token }) { function UserTokenDetail({ token }) {
const helptext = getHelptext();
const { scope, description, created, modified, expires, summary_fields } = const { scope, description, created, modified, expires, summary_fields } =
token; token;
const history = useHistory(); const history = useHistory();

View File

@@ -1,8 +1,8 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
const userHelpTextStrings = () => ({ const userHelpTextStrings = {
application: t`The application that this token belongs to, or leave this field empty to create a Personal Access Token.`, application: t`The application that this token belongs to, or leave this field empty to create a Personal Access Token.`,
scope: t`Scope for the token's access`, scope: t`Scope for the token's access`,
}); };
export default userHelpTextStrings; export default userHelpTextStrings;

View File

@@ -145,7 +145,6 @@ export const Project = shape({
summary_fields: shape({ summary_fields: shape({
organization: Organization, organization: Organization,
credential: Credential, credential: Credential,
signature_validation_credential: Credential,
last_job: shape({}), last_job: shape({}),
last_update: shape({}), last_update: shape({}),
created_by: shape({}), created_by: shape({}),
@@ -164,7 +163,6 @@ export const Project = shape({
scm_delete_on_update: bool, scm_delete_on_update: bool,
scm_track_submodules: bool, scm_track_submodules: bool,
credential: number, credential: number,
signature_validation_credential: number,
status: oneOf([ status: oneOf([
'new', 'new',
'pending', 'pending',

View File

@@ -53,7 +53,7 @@ options:
- Can be a built-in credential type such as "Machine", or a custom credential type such as "My Credential Type" - Can be a built-in credential type such as "Machine", or a custom credential type such as "My Credential Type"
- Choices include Amazon Web Services, Ansible Galaxy/Automation Hub API Token, Centrify Vault Credential Provider Lookup, - Choices include Amazon Web Services, Ansible Galaxy/Automation Hub API Token, Centrify Vault Credential Provider Lookup,
Container Registry, CyberArk AIM Central Credential Provider Lookup, CyberArk Conjur Secret Lookup, Google Compute Engine, Container Registry, CyberArk AIM Central Credential Provider Lookup, CyberArk Conjur Secret Lookup, Google Compute Engine,
GitHub Personal Access Token, GitLab Personal Access Token, GPG Public Key, HashiCorp Vault Secret Lookup, HashiCorp Vault Signed SSH, GitHub Personal Access Token, GitLab Personal Access Token, HashiCorp Vault Secret Lookup, HashiCorp Vault Signed SSH,
Insights, Machine, Microsoft Azure Key Vault, Microsoft Azure Resource Manager, Network, OpenShift or Kubernetes API Insights, Machine, Microsoft Azure Key Vault, Microsoft Azure Resource Manager, Network, OpenShift or Kubernetes API
Bearer Token, OpenStack, Red Hat Ansible Automation Platform, Red Hat Satellite 6, Red Hat Virtualization, Source Control, Bearer Token, OpenStack, Red Hat Ansible Automation Platform, Red Hat Satellite 6, Red Hat Virtualization, Source Control,
Thycotic DevOps Secrets Vault, Thycotic Secret Server, Vault, VMware vCenter, or a custom credential type Thycotic DevOps Secrets Vault, Thycotic Secret Server, Vault, VMware vCenter, or a custom credential type
@@ -82,7 +82,6 @@ options:
- ssh_key_data (SSH private key content; to extract the content from a file path, use the lookup function (see examples)) - ssh_key_data (SSH private key content; to extract the content from a file path, use the lookup function (see examples))
- vault_id (the vault identifier; this parameter is only valid if C(kind) is specified as C(vault).) - vault_id (the vault identifier; this parameter is only valid if C(kind) is specified as C(vault).)
- ssh_key_unlock (unlock password for ssh_key; use "ASK" and launch job to be prompted) - ssh_key_unlock (unlock password for ssh_key; use "ASK" and launch job to be prompted)
- gpg_public_key (GPG Public Key used for signature validation)
type: dict type: dict
update_secrets: update_secrets:
description: description:

View File

@@ -170,7 +170,7 @@ def main():
id_list.append(sub_obj['id']) id_list.append(sub_obj['id'])
# Preserve existing objects # Preserve existing objects
if (preserve_existing_hosts and relationship == 'hosts') or (preserve_existing_children and relationship == 'children'): if (preserve_existing_hosts and relationship == 'hosts') or (preserve_existing_children and relationship == 'children'):
preserve_existing_check = module.get_all_endpoint(group['related'][relationship]) preserve_existing_check = module.get_endpoint(group['related'][relationship])
for sub_obj in preserve_existing_check['json']['results']: for sub_obj in preserve_existing_check['json']['results']:
id_list.append(sub_obj['id']) id_list.append(sub_obj['id'])
if id_list: if id_list:

View File

@@ -169,12 +169,6 @@ options:
required: False required: False
default: 2 default: 2
type: float type: float
signature_validation_credential:
description:
- Name of the credential to use for signature validation.
- If signature validation credential is provided, signature validation will be enabled.
type: str
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''
@@ -285,13 +279,10 @@ def main():
wait=dict(type='bool', default=True), wait=dict(type='bool', default=True),
update_project=dict(default=False, type='bool'), update_project=dict(default=False, type='bool'),
interval=dict(default=2.0, type='float'), interval=dict(default=2.0, type='float'),
signature_validation_credential=dict(type='str'),
) )
# Create a module for ourselves # Create a module for ourselves
module = ControllerAPIModule( module = ControllerAPIModule(argument_spec=argument_spec)
argument_spec=argument_spec,
)
# Extract our parameters # Extract our parameters
name = module.params.get('name') name = module.params.get('name')
@@ -310,8 +301,6 @@ def main():
wait = module.params.get('wait') wait = module.params.get('wait')
update_project = module.params.get('update_project') update_project = module.params.get('update_project')
signature_validation_credential = module.params.get('signature_validation_credential')
# Attempt to look up the related items the user specified (these will fail the module if not found) # Attempt to look up the related items the user specified (these will fail the module if not found)
lookup_data = {} lookup_data = {}
org_id = None org_id = None
@@ -341,9 +330,6 @@ def main():
if credential is not None: if credential is not None:
credential = module.resolve_name_to_id('credentials', credential) credential = module.resolve_name_to_id('credentials', credential)
if signature_validation_credential is not None:
signature_validation_credential = module.resolve_name_to_id('credentials', signature_validation_credential)
# Attempt to look up associated field items the user specified. # Attempt to look up associated field items the user specified.
association_fields = {} association_fields = {}
@@ -372,7 +358,6 @@ def main():
'organization': org_id, 'organization': org_id,
'scm_update_on_launch': scm_update_on_launch, 'scm_update_on_launch': scm_update_on_launch,
'scm_update_cache_timeout': scm_update_cache_timeout, 'scm_update_cache_timeout': scm_update_cache_timeout,
'signature_validation_credential': signature_validation_credential,
} }
for field_name in ( for field_name in (

View File

@@ -45,7 +45,6 @@ credential_input_fields = (
'username', 'username',
'vault_password', 'vault_password',
'vault_id', 'vault_id',
'gpg_public_key',
) )
@@ -95,7 +94,6 @@ credential_type_name_to_config_kind_map = {
'machine': 'ssh', 'machine': 'ssh',
'vault': 'vault', 'vault': 'vault',
'vmware vcenter': 'vmware', 'vmware vcenter': 'vmware',
'gpg public key': 'gpg_public_key',
} }
config_kind_to_credential_type_name_map = {kind: name for name, kind in credential_type_name_to_config_kind_map.items()} config_kind_to_credential_type_name_map = {kind: name for name, kind in credential_type_name_to_config_kind_map.items()}

View File

@@ -40,7 +40,6 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
'scm_update_on_launch', 'scm_update_on_launch',
'scm_refspec', 'scm_refspec',
'allow_override', 'allow_override',
'signature_validation_credential',
) )
update_payload(payload, fields, kwargs) update_payload(payload, fields, kwargs)

View File

@@ -8,7 +8,7 @@ In AWX, a task of a certain job type is kicked off (_i.e._, RunJob, RunProjectUp
The callbacks and handlers are: The callbacks and handlers are:
* `event_handler`: Called each time a new event is created in `ansible-runner`. AWX will dispatch the event to `redis` to be processed on the other end by the callback receiver. * `event_handler`: Called each time a new event is created in `ansible-runner`. AWX will dispatch the event to `redis` to be processed on the other end by the callback receiver.
* `cancel_callback`: Called periodically by `ansible-runner`; this is so that AWX can inform `ansible-runner` if the job should be canceled or not. Only applies for system jobs now, and other jobs are canceled via receptor. * `cancel_callback`: Called periodically by `ansible-runner`; this is so that AWX can inform `ansible-runner` if the job should be canceled or not.
* `finished_callback`: Called once by `ansible-runner` to denote that the process that was asked to run is finished. AWX will construct the special control event, `EOF`, with the associated total number of events that it observed. * `finished_callback`: Called once by `ansible-runner` to denote that the process that was asked to run is finished. AWX will construct the special control event, `EOF`, with the associated total number of events that it observed.
* `status_handler`: Called by `ansible-runner` as the process transitions state internally. AWX uses the `starting` status to know that `ansible-runner` has made all of its decisions around the process that it will launch. AWX gathers and associates these decisions with the Job for historical observation. * `status_handler`: Called by `ansible-runner` as the process transitions state internally. AWX uses the `starting` status to know that `ansible-runner` has made all of its decisions around the process that it will launch. AWX gathers and associates these decisions with the Job for historical observation.

Some files were not shown because too many files have changed in this diff Show More