mirror of
https://github.com/ansible/awx.git
synced 2026-04-10 12:39:22 -02:30
Compare commits
1 Commits
21.11.0
...
13089-Sche
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26a947ed31 |
11
.github/workflows/promote.yml
vendored
11
.github/workflows/promote.yml
vendored
@@ -38,13 +38,9 @@ jobs:
|
|||||||
- name: Build collection and publish to galaxy
|
- name: Build collection and publish to galaxy
|
||||||
run: |
|
run: |
|
||||||
COLLECTION_TEMPLATE_VERSION=true COLLECTION_NAMESPACE=${{ env.collection_namespace }} make build_collection
|
COLLECTION_TEMPLATE_VERSION=true COLLECTION_NAMESPACE=${{ env.collection_namespace }} make build_collection
|
||||||
if [ "$(curl --head -sw '%{http_code}' https://galaxy.ansible.com/download/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz | tail -1)" == "302" ] ; then \
|
|
||||||
echo "Galaxy release already done"; \
|
|
||||||
else \
|
|
||||||
ansible-galaxy collection publish \
|
ansible-galaxy collection publish \
|
||||||
--token=${{ secrets.GALAXY_TOKEN }} \
|
--token=${{ secrets.GALAXY_TOKEN }} \
|
||||||
awx_collection_build/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz; \
|
awx_collection_build/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set official pypi info
|
- name: Set official pypi info
|
||||||
run: echo pypi_repo=pypi >> $GITHUB_ENV
|
run: echo pypi_repo=pypi >> $GITHUB_ENV
|
||||||
@@ -56,7 +52,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Build awxkit and upload to pypi
|
- name: Build awxkit and upload to pypi
|
||||||
run: |
|
run: |
|
||||||
git reset --hard
|
|
||||||
cd awxkit && python3 setup.py bdist_wheel
|
cd awxkit && python3 setup.py bdist_wheel
|
||||||
twine upload \
|
twine upload \
|
||||||
-r ${{ env.pypi_repo }} \
|
-r ${{ env.pypi_repo }} \
|
||||||
@@ -79,6 +74,4 @@ jobs:
|
|||||||
docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:latest
|
docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:latest
|
||||||
docker push quay.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
docker push quay.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
||||||
docker push quay.io/${{ github.repository }}:latest
|
docker push quay.io/${{ github.repository }}:latest
|
||||||
docker pull ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
|
||||||
docker tag ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }} quay.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
|
||||||
docker push quay.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
|
||||||
|
|||||||
15
.github/workflows/stage.yml
vendored
15
.github/workflows/stage.yml
vendored
@@ -84,20 +84,6 @@ jobs:
|
|||||||
-e push=yes \
|
-e push=yes \
|
||||||
-e awx_official=yes
|
-e awx_official=yes
|
||||||
|
|
||||||
- name: Log in to GHCR
|
|
||||||
run: |
|
|
||||||
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Log in to Quay
|
|
||||||
run: |
|
|
||||||
echo ${{ secrets.QUAY_TOKEN }} | docker login quay.io -u ${{ secrets.QUAY_USER }} --password-stdin
|
|
||||||
|
|
||||||
- name: tag awx-ee:latest with version input
|
|
||||||
run: |
|
|
||||||
docker pull quay.io/ansible/awx-ee:latest
|
|
||||||
docker tag quay.io/ansible/awx-ee:latest ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
|
||||||
docker push ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
|
||||||
|
|
||||||
- name: Build and stage awx-operator
|
- name: Build and stage awx-operator
|
||||||
working-directory: awx-operator
|
working-directory: awx-operator
|
||||||
run: |
|
run: |
|
||||||
@@ -117,7 +103,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
AWX_TEST_IMAGE: ${{ github.repository }}
|
AWX_TEST_IMAGE: ${{ github.repository }}
|
||||||
AWX_TEST_VERSION: ${{ github.event.inputs.version }}
|
AWX_TEST_VERSION: ${{ github.event.inputs.version }}
|
||||||
AWX_EE_TEST_IMAGE: ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
|
||||||
|
|
||||||
- name: Create draft release for AWX
|
- name: Create draft release for AWX
|
||||||
working-directory: awx
|
working-directory: awx
|
||||||
|
|||||||
@@ -96,15 +96,6 @@ register(
|
|||||||
category=_('Authentication'),
|
category=_('Authentication'),
|
||||||
category_slug='authentication',
|
category_slug='authentication',
|
||||||
)
|
)
|
||||||
register(
|
|
||||||
'ALLOW_METRICS_FOR_ANONYMOUS_USERS',
|
|
||||||
field_class=fields.BooleanField,
|
|
||||||
default=False,
|
|
||||||
label=_('Allow anonymous users to poll metrics'),
|
|
||||||
help_text=_('If true, anonymous users are allowed to poll metrics.'),
|
|
||||||
category=_('Authentication'),
|
|
||||||
category_slug='authentication',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def authentication_validate(serializer, attrs):
|
def authentication_validate(serializer, attrs):
|
||||||
|
|||||||
@@ -5,11 +5,9 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
from rest_framework.permissions import AllowAny
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
@@ -33,14 +31,9 @@ class MetricsView(APIView):
|
|||||||
|
|
||||||
renderer_classes = [renderers.PlainTextRenderer, renderers.PrometheusJSONRenderer, renderers.BrowsableAPIRenderer]
|
renderer_classes = [renderers.PlainTextRenderer, renderers.PrometheusJSONRenderer, renderers.BrowsableAPIRenderer]
|
||||||
|
|
||||||
def initialize_request(self, request, *args, **kwargs):
|
|
||||||
if settings.ALLOW_METRICS_FOR_ANONYMOUS_USERS:
|
|
||||||
self.permission_classes = (AllowAny,)
|
|
||||||
return super(APIView, self).initialize_request(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
'''Show Metrics Details'''
|
'''Show Metrics Details'''
|
||||||
if settings.ALLOW_METRICS_FOR_ANONYMOUS_USERS or request.user.is_superuser or request.user.is_system_auditor:
|
if request.user.is_superuser or request.user.is_system_auditor:
|
||||||
metrics_to_show = ''
|
metrics_to_show = ''
|
||||||
if not request.query_params.get('subsystemonly', "0") == "1":
|
if not request.query_params.get('subsystemonly', "0") == "1":
|
||||||
metrics_to_show += metrics().decode('UTF-8')
|
metrics_to_show += metrics().decode('UTF-8')
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import time
|
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from .plugin import CredentialPlugin, CertFiles, raise_for_status
|
from .plugin import CredentialPlugin, CertFiles, raise_for_status
|
||||||
@@ -248,15 +247,7 @@ def kv_backend(**kwargs):
|
|||||||
request_url = urljoin(url, '/'.join(['v1'] + path_segments)).rstrip('/')
|
request_url = urljoin(url, '/'.join(['v1'] + path_segments)).rstrip('/')
|
||||||
with CertFiles(cacert) as cert:
|
with CertFiles(cacert) as cert:
|
||||||
request_kwargs['verify'] = cert
|
request_kwargs['verify'] = cert
|
||||||
request_retries = 0
|
|
||||||
while request_retries < 5:
|
|
||||||
response = sess.get(request_url, **request_kwargs)
|
response = sess.get(request_url, **request_kwargs)
|
||||||
# https://developer.hashicorp.com/vault/docs/enterprise/consistency
|
|
||||||
if response.status_code == 412:
|
|
||||||
request_retries += 1
|
|
||||||
time.sleep(1)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
raise_for_status(response)
|
raise_for_status(response)
|
||||||
|
|
||||||
json = response.json()
|
json = response.json()
|
||||||
@@ -298,15 +289,8 @@ def ssh_backend(**kwargs):
|
|||||||
|
|
||||||
with CertFiles(cacert) as cert:
|
with CertFiles(cacert) as cert:
|
||||||
request_kwargs['verify'] = cert
|
request_kwargs['verify'] = cert
|
||||||
request_retries = 0
|
|
||||||
while request_retries < 5:
|
|
||||||
resp = sess.post(request_url, **request_kwargs)
|
resp = sess.post(request_url, **request_kwargs)
|
||||||
# https://developer.hashicorp.com/vault/docs/enterprise/consistency
|
|
||||||
if resp.status_code == 412:
|
|
||||||
request_retries += 1
|
|
||||||
time.sleep(1)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
raise_for_status(resp)
|
raise_for_status(resp)
|
||||||
return resp.json()['data']['signed_key']
|
return resp.json()['data']['signed_key']
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import now as tz_now
|
from django.utils.timezone import now as tz_now
|
||||||
from django.db import transaction, connection as django_connection
|
from django.db import DatabaseError, OperationalError, transaction, connection as django_connection
|
||||||
|
from django.db.utils import InterfaceError, InternalError
|
||||||
from django_guid import set_guid
|
from django_guid import set_guid
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
@@ -62,7 +64,6 @@ class CallbackBrokerWorker(BaseWorker):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
MAX_RETRIES = 2
|
MAX_RETRIES = 2
|
||||||
INDIVIDUAL_EVENT_RETRIES = 3
|
|
||||||
last_stats = time.time()
|
last_stats = time.time()
|
||||||
last_flush = time.time()
|
last_flush = time.time()
|
||||||
total = 0
|
total = 0
|
||||||
@@ -163,48 +164,38 @@ class CallbackBrokerWorker(BaseWorker):
|
|||||||
else: # only calculate the seconds if the created time already has been set
|
else: # only calculate the seconds if the created time already has been set
|
||||||
metrics_total_job_event_processing_seconds += e.modified - e.created
|
metrics_total_job_event_processing_seconds += e.modified - e.created
|
||||||
metrics_duration_to_save = time.perf_counter()
|
metrics_duration_to_save = time.perf_counter()
|
||||||
saved_events = []
|
|
||||||
try:
|
try:
|
||||||
cls.objects.bulk_create(events)
|
cls.objects.bulk_create(events)
|
||||||
metrics_bulk_events_saved += len(events)
|
metrics_bulk_events_saved += len(events)
|
||||||
saved_events = events
|
|
||||||
self.buff[cls] = []
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# If the database is flaking, let ensure_connection throw a general exception
|
logger.warning(f'Error in events bulk_create, will try indiviually up to 5 errors, error {str(exc)}')
|
||||||
# will be caught by the outer loop, which goes into a proper sleep and retry loop
|
|
||||||
django_connection.ensure_connection()
|
|
||||||
logger.warning(f'Error in events bulk_create, will try indiviually, error: {str(exc)}')
|
|
||||||
# if an exception occurs, we should re-attempt to save the
|
# if an exception occurs, we should re-attempt to save the
|
||||||
# events one-by-one, because something in the list is
|
# events one-by-one, because something in the list is
|
||||||
# broken/stale
|
# broken/stale
|
||||||
|
consecutive_errors = 0
|
||||||
|
events_saved = 0
|
||||||
metrics_events_batch_save_errors += 1
|
metrics_events_batch_save_errors += 1
|
||||||
for e in events.copy():
|
for e in events:
|
||||||
try:
|
try:
|
||||||
e.save()
|
e.save()
|
||||||
metrics_singular_events_saved += 1
|
events_saved += 1
|
||||||
events.remove(e)
|
consecutive_errors = 0
|
||||||
saved_events.append(e) # Importantly, remove successfully saved events from the buffer
|
|
||||||
except Exception as exc_indv:
|
except Exception as exc_indv:
|
||||||
retry_count = getattr(e, '_retry_count', 0) + 1
|
consecutive_errors += 1
|
||||||
e._retry_count = retry_count
|
logger.info(f'Database Error Saving individual Job Event, error {str(exc_indv)}')
|
||||||
|
if consecutive_errors >= 5:
|
||||||
# special sanitization logic for postgres treatment of NUL 0x00 char
|
raise
|
||||||
if (retry_count == 1) and isinstance(exc_indv, ValueError) and ("\x00" in e.stdout):
|
metrics_singular_events_saved += events_saved
|
||||||
e.stdout = e.stdout.replace("\x00", "")
|
if events_saved == 0:
|
||||||
|
raise
|
||||||
if retry_count >= self.INDIVIDUAL_EVENT_RETRIES:
|
|
||||||
logger.error(f'Hit max retries ({retry_count}) saving individual Event error: {str(exc_indv)}\ndata:\n{e.__dict__}')
|
|
||||||
events.remove(e)
|
|
||||||
else:
|
|
||||||
logger.info(f'Database Error Saving individual Event uuid={e.uuid} try={retry_count}, error: {str(exc_indv)}')
|
|
||||||
|
|
||||||
metrics_duration_to_save = time.perf_counter() - metrics_duration_to_save
|
metrics_duration_to_save = time.perf_counter() - metrics_duration_to_save
|
||||||
for e in saved_events:
|
for e in events:
|
||||||
if not getattr(e, '_skip_websocket_message', False):
|
if not getattr(e, '_skip_websocket_message', False):
|
||||||
metrics_events_broadcast += 1
|
metrics_events_broadcast += 1
|
||||||
emit_event_detail(e)
|
emit_event_detail(e)
|
||||||
if getattr(e, '_notification_trigger_event', False):
|
if getattr(e, '_notification_trigger_event', False):
|
||||||
job_stats_wrapup(getattr(e, e.JOB_REFERENCE), event=e)
|
job_stats_wrapup(getattr(e, e.JOB_REFERENCE), event=e)
|
||||||
|
self.buff = {}
|
||||||
self.last_flush = time.time()
|
self.last_flush = time.time()
|
||||||
# only update metrics if we saved events
|
# only update metrics if we saved events
|
||||||
if (metrics_bulk_events_saved + metrics_singular_events_saved) > 0:
|
if (metrics_bulk_events_saved + metrics_singular_events_saved) > 0:
|
||||||
@@ -276,16 +267,20 @@ class CallbackBrokerWorker(BaseWorker):
|
|||||||
try:
|
try:
|
||||||
self.flush(force=flush)
|
self.flush(force=flush)
|
||||||
break
|
break
|
||||||
except Exception as exc:
|
except (OperationalError, InterfaceError, InternalError) as exc:
|
||||||
# Aside form bugs, exceptions here are assumed to be due to database flake
|
|
||||||
if retries >= self.MAX_RETRIES:
|
if retries >= self.MAX_RETRIES:
|
||||||
logger.exception('Worker could not re-establish database connectivity, giving up on one or more events.')
|
logger.exception('Worker could not re-establish database connectivity, giving up on one or more events.')
|
||||||
self.buff = {}
|
|
||||||
return
|
return
|
||||||
delay = 60 * retries
|
delay = 60 * retries
|
||||||
logger.warning(f'Database Error Flushing Job Events, retry #{retries + 1} in {delay} seconds: {str(exc)}')
|
logger.warning(f'Database Error Flushing Job Events, retry #{retries + 1} in {delay} seconds: {str(exc)}')
|
||||||
django_connection.close()
|
django_connection.close()
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
retries += 1
|
retries += 1
|
||||||
except Exception:
|
except DatabaseError:
|
||||||
logger.exception(f'Callback Task Processor Raised Unexpected Exception processing event data:\n{body}')
|
logger.exception('Database Error Flushing Job Events')
|
||||||
|
django_connection.close()
|
||||||
|
break
|
||||||
|
except Exception as exc:
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
logger.error('Callback Task Processor Raised Exception: %r', exc)
|
||||||
|
logger.error('Detail: {}'.format(tb))
|
||||||
|
|||||||
@@ -390,7 +390,6 @@ class BaseTask(object):
|
|||||||
logger.error("I/O error({0}) while trying to open lock file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
|
logger.error("I/O error({0}) while trying to open lock file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
emitted_lockfile_log = False
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -402,9 +401,6 @@ class BaseTask(object):
|
|||||||
logger.error("I/O error({0}) while trying to aquire lock on file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
|
logger.error("I/O error({0}) while trying to aquire lock on file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
if not emitted_lockfile_log:
|
|
||||||
logger.info(f"exception acquiring lock {lock_path}: {e}")
|
|
||||||
emitted_lockfile_log = True
|
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
self.instance.refresh_from_db(fields=['cancel_flag'])
|
self.instance.refresh_from_db(fields=['cancel_flag'])
|
||||||
if self.instance.cancel_flag or signal_callback():
|
if self.instance.cancel_flag or signal_callback():
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import time
|
|
||||||
from unittest import mock
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django.test import TransactionTestCase
|
|
||||||
|
|
||||||
from awx.main.dispatch.worker.callback import job_stats_wrapup, CallbackBrokerWorker
|
|
||||||
|
|
||||||
|
from awx.main.dispatch.worker.callback import job_stats_wrapup
|
||||||
from awx.main.models.jobs import Job
|
from awx.main.models.jobs import Job
|
||||||
from awx.main.models.inventory import InventoryUpdate, InventorySource
|
|
||||||
from awx.main.models.events import InventoryUpdateEvent
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -32,108 +24,3 @@ def test_wrapup_does_send_notifications(mocker):
|
|||||||
job.refresh_from_db()
|
job.refresh_from_db()
|
||||||
assert job.host_status_counts == {}
|
assert job.host_status_counts == {}
|
||||||
mock.assert_called_once_with('succeeded')
|
mock.assert_called_once_with('succeeded')
|
||||||
|
|
||||||
|
|
||||||
class FakeRedis:
|
|
||||||
def keys(self, *args, **kwargs):
|
|
||||||
return []
|
|
||||||
|
|
||||||
def set(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_url(cls, *args, **kwargs):
|
|
||||||
return cls()
|
|
||||||
|
|
||||||
def pipeline(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class TestCallbackBrokerWorker(TransactionTestCase):
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def turn_off_websockets(self):
|
|
||||||
with mock.patch('awx.main.dispatch.worker.callback.emit_event_detail', lambda *a, **kw: None):
|
|
||||||
yield
|
|
||||||
|
|
||||||
def get_worker(self):
|
|
||||||
with mock.patch('redis.Redis', new=FakeRedis): # turn off redis stuff
|
|
||||||
return CallbackBrokerWorker()
|
|
||||||
|
|
||||||
def event_create_kwargs(self):
|
|
||||||
inventory_update = InventoryUpdate.objects.create(source='file', inventory_source=InventorySource.objects.create(source='file'))
|
|
||||||
return dict(inventory_update=inventory_update, created=inventory_update.created)
|
|
||||||
|
|
||||||
def test_flush_with_valid_event(self):
|
|
||||||
worker = self.get_worker()
|
|
||||||
events = [InventoryUpdateEvent(uuid=str(uuid4()), **self.event_create_kwargs())]
|
|
||||||
worker.buff = {InventoryUpdateEvent: events}
|
|
||||||
worker.flush()
|
|
||||||
assert worker.buff.get(InventoryUpdateEvent, []) == []
|
|
||||||
assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 1
|
|
||||||
|
|
||||||
def test_flush_with_invalid_event(self):
|
|
||||||
worker = self.get_worker()
|
|
||||||
kwargs = self.event_create_kwargs()
|
|
||||||
events = [
|
|
||||||
InventoryUpdateEvent(uuid=str(uuid4()), stdout='good1', **kwargs),
|
|
||||||
InventoryUpdateEvent(uuid=str(uuid4()), stdout='bad', counter=-2, **kwargs),
|
|
||||||
InventoryUpdateEvent(uuid=str(uuid4()), stdout='good2', **kwargs),
|
|
||||||
]
|
|
||||||
worker.buff = {InventoryUpdateEvent: events.copy()}
|
|
||||||
worker.flush()
|
|
||||||
assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 1
|
|
||||||
assert InventoryUpdateEvent.objects.filter(uuid=events[1].uuid).count() == 0
|
|
||||||
assert InventoryUpdateEvent.objects.filter(uuid=events[2].uuid).count() == 1
|
|
||||||
assert worker.buff == {InventoryUpdateEvent: [events[1]]}
|
|
||||||
|
|
||||||
def test_duplicate_key_not_saved_twice(self):
|
|
||||||
worker = self.get_worker()
|
|
||||||
events = [InventoryUpdateEvent(uuid=str(uuid4()), **self.event_create_kwargs())]
|
|
||||||
worker.buff = {InventoryUpdateEvent: events.copy()}
|
|
||||||
worker.flush()
|
|
||||||
|
|
||||||
# put current saved event in buffer (error case)
|
|
||||||
worker.buff = {InventoryUpdateEvent: [InventoryUpdateEvent.objects.get(uuid=events[0].uuid)]}
|
|
||||||
worker.last_flush = time.time() - 2.0
|
|
||||||
# here, the bulk_create will fail with UNIQUE constraint violation, but individual saves should resolve it
|
|
||||||
worker.flush()
|
|
||||||
assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 1
|
|
||||||
assert worker.buff.get(InventoryUpdateEvent, []) == []
|
|
||||||
|
|
||||||
def test_give_up_on_bad_event(self):
|
|
||||||
worker = self.get_worker()
|
|
||||||
events = [InventoryUpdateEvent(uuid=str(uuid4()), counter=-2, **self.event_create_kwargs())]
|
|
||||||
worker.buff = {InventoryUpdateEvent: events.copy()}
|
|
||||||
|
|
||||||
for i in range(5):
|
|
||||||
worker.last_flush = time.time() - 2.0
|
|
||||||
worker.flush()
|
|
||||||
|
|
||||||
# Could not save, should be logged, and buffer should be cleared
|
|
||||||
assert worker.buff.get(InventoryUpdateEvent, []) == []
|
|
||||||
assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 0 # sanity
|
|
||||||
|
|
||||||
def test_postgres_invalid_NUL_char(self):
|
|
||||||
# In postgres, text fields reject NUL character, 0x00
|
|
||||||
# tests use sqlite3 which will not raise an error
|
|
||||||
# but we can still test that it is sanitized before saving
|
|
||||||
worker = self.get_worker()
|
|
||||||
kwargs = self.event_create_kwargs()
|
|
||||||
events = [InventoryUpdateEvent(uuid=str(uuid4()), stdout="\x00", **kwargs)]
|
|
||||||
assert "\x00" in events[0].stdout # sanity
|
|
||||||
worker.buff = {InventoryUpdateEvent: events.copy()}
|
|
||||||
|
|
||||||
with mock.patch.object(InventoryUpdateEvent.objects, 'bulk_create', side_effect=ValueError):
|
|
||||||
with mock.patch.object(events[0], 'save', side_effect=ValueError):
|
|
||||||
worker.flush()
|
|
||||||
|
|
||||||
assert "\x00" not in events[0].stdout
|
|
||||||
|
|
||||||
worker.last_flush = time.time() - 2.0
|
|
||||||
worker.flush()
|
|
||||||
|
|
||||||
event = InventoryUpdateEvent.objects.get(uuid=events[0].uuid)
|
|
||||||
assert "\x00" not in event.stdout
|
|
||||||
|
|||||||
@@ -103,10 +103,6 @@ ColorHandler = logging.StreamHandler
|
|||||||
if settings.COLOR_LOGS is True:
|
if settings.COLOR_LOGS is True:
|
||||||
try:
|
try:
|
||||||
from logutils.colorize import ColorizingStreamHandler
|
from logutils.colorize import ColorizingStreamHandler
|
||||||
import colorama
|
|
||||||
|
|
||||||
colorama.deinit()
|
|
||||||
colorama.init(wrap=False, convert=False, strip=False)
|
|
||||||
|
|
||||||
class ColorHandler(ColorizingStreamHandler):
|
class ColorHandler(ColorizingStreamHandler):
|
||||||
def colorize(self, line, record):
|
def colorize(self, line, record):
|
||||||
|
|||||||
@@ -418,9 +418,6 @@ AUTH_BASIC_ENABLED = True
|
|||||||
# when trying to access a UI page that requries authentication.
|
# when trying to access a UI page that requries authentication.
|
||||||
LOGIN_REDIRECT_OVERRIDE = ''
|
LOGIN_REDIRECT_OVERRIDE = ''
|
||||||
|
|
||||||
# Note: This setting may be overridden by database settings.
|
|
||||||
ALLOW_METRICS_FOR_ANONYMOUS_USERS = False
|
|
||||||
|
|
||||||
DEVSERVER_DEFAULT_ADDR = '0.0.0.0'
|
DEVSERVER_DEFAULT_ADDR = '0.0.0.0'
|
||||||
DEVSERVER_DEFAULT_PORT = '8013'
|
DEVSERVER_DEFAULT_PORT = '8013'
|
||||||
|
|
||||||
|
|||||||
@@ -452,10 +452,7 @@ def on_populate_user(sender, **kwargs):
|
|||||||
remove = bool(team_opts.get('remove', True))
|
remove = bool(team_opts.get('remove', True))
|
||||||
state = _update_m2m_from_groups(ldap_user, users_opts, remove)
|
state = _update_m2m_from_groups(ldap_user, users_opts, remove)
|
||||||
if state is not None:
|
if state is not None:
|
||||||
organization = team_opts['organization']
|
desired_team_states[team_name] = {'member_role': state}
|
||||||
if organization not in desired_team_states:
|
|
||||||
desired_team_states[organization] = {}
|
|
||||||
desired_team_states[organization][team_name] = {'member_role': state}
|
|
||||||
|
|
||||||
# Check if user.profile is available, otherwise force user.save()
|
# Check if user.profile is available, otherwise force user.save()
|
||||||
try:
|
try:
|
||||||
@@ -476,28 +473,16 @@ def on_populate_user(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def reconcile_users_org_team_mappings(user, desired_org_states, desired_team_states, source):
|
def reconcile_users_org_team_mappings(user, desired_org_states, desired_team_states, source):
|
||||||
#
|
|
||||||
# Arguments:
|
|
||||||
# user - a user object
|
|
||||||
# desired_org_states: { '<org_name>': { '<role>': <boolean> or None } }
|
|
||||||
# desired_team_states: { '<org_name>': { '<team name>': { '<role>': <boolean> or None } } }
|
|
||||||
# source - a text label indicating the "authentication adapter" for debug messages
|
|
||||||
#
|
|
||||||
# This function will load the users existing roles and then based on the deisred states modify the users roles
|
|
||||||
# True indicates the user needs to be a member of the role
|
|
||||||
# False indicates the user should not be a member of the role
|
|
||||||
# None means this function should not change the users membership of a role
|
|
||||||
#
|
|
||||||
from awx.main.models import Organization, Team
|
from awx.main.models import Organization, Team
|
||||||
|
|
||||||
content_types = []
|
content_types = []
|
||||||
reconcile_items = []
|
reconcile_items = []
|
||||||
if desired_org_states:
|
if desired_org_states:
|
||||||
content_types.append(ContentType.objects.get_for_model(Organization))
|
content_types.append(ContentType.objects.get_for_model(Organization))
|
||||||
reconcile_items.append(('organization', desired_org_states))
|
reconcile_items.append(('organization', desired_org_states, Organization))
|
||||||
if desired_team_states:
|
if desired_team_states:
|
||||||
content_types.append(ContentType.objects.get_for_model(Team))
|
content_types.append(ContentType.objects.get_for_model(Team))
|
||||||
reconcile_items.append(('team', desired_team_states))
|
reconcile_items.append(('team', desired_team_states, Team))
|
||||||
|
|
||||||
if not content_types:
|
if not content_types:
|
||||||
# If both desired states were empty we can simply return because there is nothing to reconcile
|
# If both desired states were empty we can simply return because there is nothing to reconcile
|
||||||
@@ -506,39 +491,24 @@ def reconcile_users_org_team_mappings(user, desired_org_states, desired_team_sta
|
|||||||
# users_roles is a flat set of IDs
|
# users_roles is a flat set of IDs
|
||||||
users_roles = set(user.roles.filter(content_type__in=content_types).values_list('pk', flat=True))
|
users_roles = set(user.roles.filter(content_type__in=content_types).values_list('pk', flat=True))
|
||||||
|
|
||||||
for object_type, desired_states in reconcile_items:
|
for object_type, desired_states, model in reconcile_items:
|
||||||
|
# Get all of the roles in the desired states for efficient DB extraction
|
||||||
roles = []
|
roles = []
|
||||||
# Get a set of named tuples for the org/team name plus all of the roles we got above
|
|
||||||
if object_type == 'organization':
|
|
||||||
for sub_dict in desired_states.values():
|
for sub_dict in desired_states.values():
|
||||||
for role_name in sub_dict:
|
for role_name in sub_dict:
|
||||||
if sub_dict[role_name] is None:
|
if sub_dict[role_name] is None:
|
||||||
continue
|
continue
|
||||||
if role_name not in roles:
|
if role_name not in roles:
|
||||||
roles.append(role_name)
|
roles.append(role_name)
|
||||||
model_roles = Organization.objects.filter(name__in=desired_states.keys()).values_list('name', *roles, named=True)
|
|
||||||
else:
|
|
||||||
team_names = []
|
|
||||||
for teams_dict in desired_states.values():
|
|
||||||
team_names.extend(teams_dict.keys())
|
|
||||||
for sub_dict in teams_dict.values():
|
|
||||||
for role_name in sub_dict:
|
|
||||||
if sub_dict[role_name] is None:
|
|
||||||
continue
|
|
||||||
if role_name not in roles:
|
|
||||||
roles.append(role_name)
|
|
||||||
model_roles = Team.objects.filter(name__in=team_names).values_list('name', 'organization__name', *roles, named=True)
|
|
||||||
|
|
||||||
|
# Get a set of named tuples for the org/team name plus all of the roles we got above
|
||||||
|
model_roles = model.objects.filter(name__in=desired_states.keys()).values_list('name', *roles, named=True)
|
||||||
for row in model_roles:
|
for row in model_roles:
|
||||||
for role_name in roles:
|
for role_name in roles:
|
||||||
if object_type == 'organization':
|
|
||||||
desired_state = desired_states.get(row.name, {})
|
desired_state = desired_states.get(row.name, {})
|
||||||
else:
|
if desired_state[role_name] is None:
|
||||||
desired_state = desired_states.get(row.organization__name, {}).get(row.name, {})
|
|
||||||
|
|
||||||
if desired_state.get(role_name, None) is None:
|
|
||||||
# The mapping was not defined for this [org/team]/role so we can just pass
|
# The mapping was not defined for this [org/team]/role so we can just pass
|
||||||
continue
|
pass
|
||||||
|
|
||||||
# If somehow the auth adapter knows about an items role but that role is not defined in the DB we are going to print a pretty error
|
# If somehow the auth adapter knows about an items role but that role is not defined in the DB we are going to print a pretty error
|
||||||
# This is your classic safety net that we should never hit; but here you are reading this comment... good luck and Godspeed.
|
# This is your classic safety net that we should never hit; but here you are reading this comment... good luck and Godspeed.
|
||||||
|
|||||||
@@ -34,14 +34,8 @@ const QS_CONFIG = getQSConfig('template', {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
const resources = {
|
function RelatedTemplateList({ searchParams, projectName = null }) {
|
||||||
projects: 'project',
|
const { id: projectId } = useParams();
|
||||||
inventories: 'inventory',
|
|
||||||
credentials: 'credentials',
|
|
||||||
};
|
|
||||||
|
|
||||||
function RelatedTemplateList({ searchParams, resourceName = null }) {
|
|
||||||
const { id } = useParams();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { addToast, Toast, toastProps } = useToast();
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
@@ -135,19 +129,12 @@ function RelatedTemplateList({ searchParams, resourceName = null }) {
|
|||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
|
|
||||||
let linkTo = '';
|
let linkTo = '';
|
||||||
if (resourceName) {
|
|
||||||
const queryString = {
|
if (projectName) {
|
||||||
resource_id: id,
|
const qs = encodeQueryString({
|
||||||
resource_name: resourceName,
|
project_id: projectId,
|
||||||
resource_type: resources[location.pathname.split('/')[1]],
|
project_name: projectName,
|
||||||
resource_kind: null,
|
});
|
||||||
};
|
|
||||||
if (Array.isArray(resourceName)) {
|
|
||||||
const [name, kind] = resourceName;
|
|
||||||
queryString.resource_name = name;
|
|
||||||
queryString.resource_kind = kind;
|
|
||||||
}
|
|
||||||
const qs = encodeQueryString(queryString);
|
|
||||||
linkTo = `/templates/job_template/add/?${qs}`;
|
linkTo = `/templates/job_template/add/?${qs}`;
|
||||||
} else {
|
} else {
|
||||||
linkTo = '/templates/job_template/add';
|
linkTo = '/templates/job_template/add';
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
@@ -55,7 +55,6 @@ function DateTimePicker({ dateFieldName, timeFieldName, label }) {
|
|||||||
onChange={onDateChange}
|
onChange={onDateChange}
|
||||||
/>
|
/>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
placeholder="hh:mm AM/PM"
|
|
||||||
stepMinutes={15}
|
stepMinutes={15}
|
||||||
aria-label={
|
aria-label={
|
||||||
timeFieldName.startsWith('start') ? t`Start time` : t`End time`
|
timeFieldName.startsWith('start') ? t`Start time` : t`End time`
|
||||||
|
|||||||
93
awx/ui/src/components/Schedule/shared/FrequenciesList.js
Normal file
93
awx/ui/src/components/Schedule/shared/FrequenciesList.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Switch,
|
||||||
|
Toolbar,
|
||||||
|
ToolbarContent,
|
||||||
|
ToolbarItem,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||||
|
import {
|
||||||
|
TableComposable,
|
||||||
|
Tbody,
|
||||||
|
Thead,
|
||||||
|
Th,
|
||||||
|
Tr,
|
||||||
|
Td,
|
||||||
|
} from '@patternfly/react-table';
|
||||||
|
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import ContentEmpty from 'components/ContentEmpty';
|
||||||
|
|
||||||
|
function FrequenciesList({ openWizard }) {
|
||||||
|
const [isShowingRules, setIsShowingRules] = useState(true);
|
||||||
|
const [frequencies] = useField('frequencies');
|
||||||
|
const list = (freq) => (
|
||||||
|
<Tr key={freq.rrule}>
|
||||||
|
<Td>{freq.frequency}</Td>
|
||||||
|
<Td>{freq.rrule}</Td>
|
||||||
|
<Td>{t`End`}</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
aria-label={t`Click to toggle default value`}
|
||||||
|
ouiaId={freq ? `${freq}-button` : 'new-freq-button'}
|
||||||
|
onClick={() => {
|
||||||
|
openWizard(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PencilAltIcon />
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Toolbar>
|
||||||
|
<ToolbarContent>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
openWizard(true);
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
{isShowingRules ? t`Add RRules` : t`Add Exception`}
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Switch
|
||||||
|
label={t`Occurances`}
|
||||||
|
labelOff={t`Exceptions`}
|
||||||
|
isChecked={isShowingRules}
|
||||||
|
onChange={(isChecked) => {
|
||||||
|
setIsShowingRules(isChecked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarContent>
|
||||||
|
</Toolbar>
|
||||||
|
<div css="overflow: auto">
|
||||||
|
{frequencies.value[0].frequency === '' &&
|
||||||
|
frequencies.value.length < 2 ? (
|
||||||
|
<ContentEmpty title={t`RRules`} message={t`Add RRules`} />
|
||||||
|
) : (
|
||||||
|
<TableComposable aria-label={t`RRules`} ouiaId="rrules-list">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{t`Frequency`}</Th>
|
||||||
|
<Th>{t`RRule`}</Th>
|
||||||
|
<Th>{t`Ending`}</Th>
|
||||||
|
<Th>{t`Actions`}</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>{frequencies.value.map((freq, i) => list(freq, i))}</Tbody>
|
||||||
|
</TableComposable>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FrequenciesList;
|
||||||
@@ -1,568 +0,0 @@
|
|||||||
import 'styled-components/macro';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { useField } from 'formik';
|
|
||||||
|
|
||||||
import { t, Trans, Plural } from '@lingui/macro';
|
|
||||||
import { RRule } from 'rrule';
|
|
||||||
import {
|
|
||||||
Checkbox as _Checkbox,
|
|
||||||
FormGroup,
|
|
||||||
Radio,
|
|
||||||
TextInput,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { required, requiredPositiveInteger } from 'util/validators';
|
|
||||||
import AnsibleSelect from '../../AnsibleSelect';
|
|
||||||
import FormField from '../../FormField';
|
|
||||||
import DateTimePicker from './DateTimePicker';
|
|
||||||
|
|
||||||
const RunOnRadio = styled(Radio)`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(:last-of-type) {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
select:not(:first-of-type) {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const RunEveryLabel = styled.p`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Checkbox = styled(_Checkbox)`
|
|
||||||
:not(:last-of-type) {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FrequencyDetailSubform = ({ frequency, prefix, isException }) => {
|
|
||||||
const id = prefix.replace('.', '-');
|
|
||||||
const [runOnDayMonth] = useField({
|
|
||||||
name: `${prefix}.runOnDayMonth`,
|
|
||||||
});
|
|
||||||
const [runOnDayNumber] = useField({
|
|
||||||
name: `${prefix}.runOnDayNumber`,
|
|
||||||
});
|
|
||||||
const [runOnTheOccurrence] = useField({
|
|
||||||
name: `${prefix}.runOnTheOccurrence`,
|
|
||||||
});
|
|
||||||
const [runOnTheDay] = useField({
|
|
||||||
name: `${prefix}.runOnTheDay`,
|
|
||||||
});
|
|
||||||
const [runOnTheMonth] = useField({
|
|
||||||
name: `${prefix}.runOnTheMonth`,
|
|
||||||
});
|
|
||||||
const [startDate] = useField(`${prefix}.startDate`);
|
|
||||||
|
|
||||||
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField({
|
|
||||||
name: `${prefix}.daysOfWeek`,
|
|
||||||
validate: (val) => {
|
|
||||||
if (frequency === 'week') {
|
|
||||||
return required(t`Select a value for this field`)(val?.length > 0);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [end, endMeta] = useField({
|
|
||||||
name: `${prefix}.end`,
|
|
||||||
validate: required(t`Select a value for this field`),
|
|
||||||
});
|
|
||||||
const [interval, intervalMeta] = useField({
|
|
||||||
name: `${prefix}.interval`,
|
|
||||||
validate: requiredPositiveInteger(),
|
|
||||||
});
|
|
||||||
const [runOn, runOnMeta] = useField({
|
|
||||||
name: `${prefix}.runOn`,
|
|
||||||
validate: (val) => {
|
|
||||||
if (frequency === 'month' || frequency === 'year') {
|
|
||||||
return required(t`Select a value for this field`)(val);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const monthOptions = [
|
|
||||||
{
|
|
||||||
key: 'january',
|
|
||||||
value: 1,
|
|
||||||
label: t`January`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'february',
|
|
||||||
value: 2,
|
|
||||||
label: t`February`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'march',
|
|
||||||
value: 3,
|
|
||||||
label: t`March`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'april',
|
|
||||||
value: 4,
|
|
||||||
label: t`April`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'may',
|
|
||||||
value: 5,
|
|
||||||
label: t`May`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'june',
|
|
||||||
value: 6,
|
|
||||||
label: t`June`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'july',
|
|
||||||
value: 7,
|
|
||||||
label: t`July`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'august',
|
|
||||||
value: 8,
|
|
||||||
label: t`August`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'september',
|
|
||||||
value: 9,
|
|
||||||
label: t`September`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'october',
|
|
||||||
value: 10,
|
|
||||||
label: t`October`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'november',
|
|
||||||
value: 11,
|
|
||||||
label: t`November`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'december',
|
|
||||||
value: 12,
|
|
||||||
label: t`December`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const updateDaysOfWeek = (day, checked) => {
|
|
||||||
const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : [];
|
|
||||||
daysOfWeekHelpers.setTouched(true);
|
|
||||||
if (checked) {
|
|
||||||
newDaysOfWeek.push(day);
|
|
||||||
daysOfWeekHelpers.setValue(newDaysOfWeek);
|
|
||||||
} else {
|
|
||||||
daysOfWeekHelpers.setValue(
|
|
||||||
newDaysOfWeek.filter((selectedDay) => selectedDay !== day)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPeriodLabel = () => {
|
|
||||||
switch (frequency) {
|
|
||||||
case 'minute':
|
|
||||||
return t`Minute`;
|
|
||||||
case 'hour':
|
|
||||||
return t`Hour`;
|
|
||||||
case 'day':
|
|
||||||
return t`Day`;
|
|
||||||
case 'week':
|
|
||||||
return t`Week`;
|
|
||||||
case 'month':
|
|
||||||
return t`Month`;
|
|
||||||
case 'year':
|
|
||||||
return t`Year`;
|
|
||||||
default:
|
|
||||||
throw new Error(t`Frequency did not match an expected value`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRunEveryLabel = () => {
|
|
||||||
const intervalValue = interval.value;
|
|
||||||
|
|
||||||
switch (frequency) {
|
|
||||||
case 'minute':
|
|
||||||
return <Plural value={intervalValue} one="minute" other="minutes" />;
|
|
||||||
case 'hour':
|
|
||||||
return <Plural value={intervalValue} one="hour" other="hours" />;
|
|
||||||
case 'day':
|
|
||||||
return <Plural value={intervalValue} one="day" other="days" />;
|
|
||||||
case 'week':
|
|
||||||
return <Plural value={intervalValue} one="week" other="weeks" />;
|
|
||||||
case 'month':
|
|
||||||
return <Plural value={intervalValue} one="month" other="months" />;
|
|
||||||
case 'year':
|
|
||||||
return <Plural value={intervalValue} one="year" other="years" />;
|
|
||||||
default:
|
|
||||||
throw new Error(t`Frequency did not match an expected value`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p css="grid-column: 1/-1">
|
|
||||||
<b>{getPeriodLabel()}</b>
|
|
||||||
</p>
|
|
||||||
<FormGroup
|
|
||||||
name={`${prefix}.interval`}
|
|
||||||
fieldId={`schedule-run-every-${id}`}
|
|
||||||
helperTextInvalid={intervalMeta.error}
|
|
||||||
isRequired
|
|
||||||
validated={
|
|
||||||
!intervalMeta.touched || !intervalMeta.error ? 'default' : 'error'
|
|
||||||
}
|
|
||||||
label={isException ? t`Skip every` : t`Run every`}
|
|
||||||
>
|
|
||||||
<div css="display: flex">
|
|
||||||
<TextInput
|
|
||||||
css="margin-right: 10px;"
|
|
||||||
id={`schedule-run-every-${id}`}
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
step="1"
|
|
||||||
{...interval}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
interval.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<RunEveryLabel>{getRunEveryLabel()}</RunEveryLabel>
|
|
||||||
</div>
|
|
||||||
</FormGroup>
|
|
||||||
{frequency === 'week' && (
|
|
||||||
<FormGroup
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
fieldId={`schedule-days-of-week-${id}`}
|
|
||||||
helperTextInvalid={daysOfWeekMeta.error}
|
|
||||||
isRequired
|
|
||||||
validated={
|
|
||||||
!daysOfWeekMeta.touched || !daysOfWeekMeta.error
|
|
||||||
? 'default'
|
|
||||||
: 'error'
|
|
||||||
}
|
|
||||||
label={t`On days`}
|
|
||||||
>
|
|
||||||
<div css="display: flex">
|
|
||||||
<Checkbox
|
|
||||||
label={t`Sun`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.SU)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.SU, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Sunday`}
|
|
||||||
id={`schedule-days-of-week-sun-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-sun-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Mon`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.MO)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.MO, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Monday`}
|
|
||||||
id={`schedule-days-of-week-mon-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-mon-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Tue`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.TU)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.TU, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Tuesday`}
|
|
||||||
id={`schedule-days-of-week-tue-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-tue-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Wed`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.WE)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.WE, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Wednesday`}
|
|
||||||
id={`schedule-days-of-week-wed-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-wed-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Thu`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.TH)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.TH, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Thursday`}
|
|
||||||
id={`schedule-days-of-week-thu-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-thu-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Fri`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.FR)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.FR, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Friday`}
|
|
||||||
id={`schedule-days-of-week-fri-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-fri-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t`Sat`}
|
|
||||||
isChecked={daysOfWeek.value?.includes(RRule.SA)}
|
|
||||||
onChange={(checked) => {
|
|
||||||
updateDaysOfWeek(RRule.SA, checked);
|
|
||||||
}}
|
|
||||||
aria-label={t`Saturday`}
|
|
||||||
id={`schedule-days-of-week-sat-${id}`}
|
|
||||||
ouiaId={`schedule-days-of-week-sat-${id}`}
|
|
||||||
name={`${prefix}.daysOfWeek`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
{(frequency === 'month' || frequency === 'year') &&
|
|
||||||
!Number.isNaN(new Date(startDate.value)) && (
|
|
||||||
<FormGroup
|
|
||||||
name={`${prefix}.runOn`}
|
|
||||||
fieldId={`schedule-run-on-${id}`}
|
|
||||||
helperTextInvalid={runOnMeta.error}
|
|
||||||
isRequired
|
|
||||||
validated={
|
|
||||||
!runOnMeta.touched || !runOnMeta.error ? 'default' : 'error'
|
|
||||||
}
|
|
||||||
label={t`Run on`}
|
|
||||||
>
|
|
||||||
<RunOnRadio
|
|
||||||
id={`schedule-run-on-day-${id}`}
|
|
||||||
name={`${prefix}.runOn`}
|
|
||||||
label={
|
|
||||||
<div css="display: flex;align-items: center;">
|
|
||||||
{frequency === 'month' && (
|
|
||||||
<span
|
|
||||||
id="radio-schedule-run-on-day"
|
|
||||||
css="margin-right: 10px;"
|
|
||||||
>
|
|
||||||
<Trans>Day</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{frequency === 'year' && (
|
|
||||||
<AnsibleSelect
|
|
||||||
id={`schedule-run-on-day-month-${id}`}
|
|
||||||
css="margin-right: 10px"
|
|
||||||
isDisabled={runOn.value !== 'day'}
|
|
||||||
data={monthOptions}
|
|
||||||
{...runOnDayMonth}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TextInput
|
|
||||||
id={`schedule-run-on-day-number-${id}`}
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="31"
|
|
||||||
step="1"
|
|
||||||
isDisabled={runOn.value !== 'day'}
|
|
||||||
{...runOnDayNumber}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
runOnDayNumber.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
value="day"
|
|
||||||
isChecked={runOn.value === 'day'}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
event.target.value = 'day';
|
|
||||||
runOn.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<RunOnRadio
|
|
||||||
id={`schedule-run-on-the-${id}`}
|
|
||||||
name={`${prefix}.runOn`}
|
|
||||||
label={
|
|
||||||
<div css="display: flex;align-items: center;">
|
|
||||||
<span
|
|
||||||
id={`radio-schedule-run-on-the-${id}`}
|
|
||||||
css="margin-right: 10px;"
|
|
||||||
>
|
|
||||||
<Trans>The</Trans>
|
|
||||||
</span>
|
|
||||||
<AnsibleSelect
|
|
||||||
id={`schedule-run-on-the-occurrence-${id}`}
|
|
||||||
isDisabled={runOn.value !== 'the'}
|
|
||||||
data={[
|
|
||||||
{ value: 1, key: 'first', label: t`First` },
|
|
||||||
{
|
|
||||||
value: 2,
|
|
||||||
key: 'second',
|
|
||||||
label: t`Second`,
|
|
||||||
},
|
|
||||||
{ value: 3, key: 'third', label: t`Third` },
|
|
||||||
{
|
|
||||||
value: 4,
|
|
||||||
key: 'fourth',
|
|
||||||
label: t`Fourth`,
|
|
||||||
},
|
|
||||||
{ value: 5, key: 'fifth', label: t`Fifth` },
|
|
||||||
{ value: -1, key: 'last', label: t`Last` },
|
|
||||||
]}
|
|
||||||
{...runOnTheOccurrence}
|
|
||||||
/>
|
|
||||||
<AnsibleSelect
|
|
||||||
id={`schedule-run-on-the-day-${id}`}
|
|
||||||
isDisabled={runOn.value !== 'the'}
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
value: 'sunday',
|
|
||||||
key: 'sunday',
|
|
||||||
label: t`Sunday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'monday',
|
|
||||||
key: 'monday',
|
|
||||||
label: t`Monday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'tuesday',
|
|
||||||
key: 'tuesday',
|
|
||||||
label: t`Tuesday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'wednesday',
|
|
||||||
key: 'wednesday',
|
|
||||||
label: t`Wednesday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'thursday',
|
|
||||||
key: 'thursday',
|
|
||||||
label: t`Thursday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'friday',
|
|
||||||
key: 'friday',
|
|
||||||
label: t`Friday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'saturday',
|
|
||||||
key: 'saturday',
|
|
||||||
label: t`Saturday`,
|
|
||||||
},
|
|
||||||
{ value: 'day', key: 'day', label: t`Day` },
|
|
||||||
{
|
|
||||||
value: 'weekday',
|
|
||||||
key: 'weekday',
|
|
||||||
label: t`Weekday`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'weekendDay',
|
|
||||||
key: 'weekendDay',
|
|
||||||
label: t`Weekend day`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
{...runOnTheDay}
|
|
||||||
/>
|
|
||||||
{frequency === 'year' && (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
id={`of-schedule-run-on-the-month-${id}`}
|
|
||||||
css="margin-left: 10px;"
|
|
||||||
>
|
|
||||||
<Trans>of</Trans>
|
|
||||||
</span>
|
|
||||||
<AnsibleSelect
|
|
||||||
id={`schedule-run-on-the-month-${id}`}
|
|
||||||
isDisabled={runOn.value !== 'the'}
|
|
||||||
data={monthOptions}
|
|
||||||
{...runOnTheMonth}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
value="the"
|
|
||||||
isChecked={runOn.value === 'the'}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
event.target.value = 'the';
|
|
||||||
runOn.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
<FormGroup
|
|
||||||
name={`${prefix}.end`}
|
|
||||||
fieldId={`schedule-end-${id}`}
|
|
||||||
helperTextInvalid={endMeta.error}
|
|
||||||
isRequired
|
|
||||||
validated={!endMeta.touched || !endMeta.error ? 'default' : 'error'}
|
|
||||||
label={t`End`}
|
|
||||||
>
|
|
||||||
<Radio
|
|
||||||
id={`end-never-${id}`}
|
|
||||||
name={`${prefix}.end`}
|
|
||||||
label={t`Never`}
|
|
||||||
value="never"
|
|
||||||
isChecked={end.value === 'never'}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
event.target.value = 'never';
|
|
||||||
end.onChange(event);
|
|
||||||
}}
|
|
||||||
ouiaId={`end-never-radio-button-${id}`}
|
|
||||||
/>
|
|
||||||
<Radio
|
|
||||||
id={`end-after-${id}`}
|
|
||||||
name={`${prefix}.end`}
|
|
||||||
label={t`After number of occurrences`}
|
|
||||||
value="after"
|
|
||||||
isChecked={end.value === 'after'}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
event.target.value = 'after';
|
|
||||||
end.onChange(event);
|
|
||||||
}}
|
|
||||||
ouiaId={`end-after-radio-button-${id}`}
|
|
||||||
/>
|
|
||||||
<Radio
|
|
||||||
id={`end-on-date-${id}`}
|
|
||||||
name={`${prefix}.end`}
|
|
||||||
label={t`On date`}
|
|
||||||
value="onDate"
|
|
||||||
isChecked={end.value === 'onDate'}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
event.target.value = 'onDate';
|
|
||||||
end.onChange(event);
|
|
||||||
}}
|
|
||||||
ouiaId={`end-on-radio-button-${id}`}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
{end?.value === 'after' && (
|
|
||||||
<FormField
|
|
||||||
id={`schedule-occurrences-${id}`}
|
|
||||||
label={t`Occurrences`}
|
|
||||||
name={`${prefix}.occurrences`}
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
step="1"
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{end?.value === 'onDate' && (
|
|
||||||
<DateTimePicker
|
|
||||||
dateFieldName={`${prefix}.endDate`}
|
|
||||||
timeFieldName={`${prefix}.endTime`}
|
|
||||||
label={t`End date/time`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FrequencyDetailSubform;
|
|
||||||
@@ -1,30 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { arrayOf, string } from 'prop-types';
|
import { t } from '@lingui/macro';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { RRule } from 'rrule';
|
||||||
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
||||||
|
|
||||||
export default function FrequencySelect({
|
export default function FrequencySelect({ id, onBlur, placeholderText }) {
|
||||||
id,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onBlur,
|
|
||||||
placeholderText,
|
|
||||||
children,
|
|
||||||
}) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [frequency, , frequencyHelpers] = useField('freq');
|
||||||
const onSelect = (event, selectedValue) => {
|
|
||||||
if (selectedValue === 'none') {
|
|
||||||
onChange([]);
|
|
||||||
setIsOpen(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const index = value.indexOf(selectedValue);
|
|
||||||
if (index === -1) {
|
|
||||||
onChange(value.concat(selectedValue));
|
|
||||||
} else {
|
|
||||||
onChange(value.slice(0, index).concat(value.slice(index + 1)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggle = (val) => {
|
const onToggle = (val) => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
@@ -35,21 +17,26 @@ export default function FrequencySelect({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
variant={SelectVariant.checkbox}
|
onSelect={(e, v) => {
|
||||||
onSelect={onSelect}
|
frequencyHelpers.setValue(v);
|
||||||
selections={value}
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
selections={frequency.value}
|
||||||
placeholderText={placeholderText}
|
placeholderText={placeholderText}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
|
value={frequency.value}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
ouiaId={`frequency-select-${id}`}
|
ouiaId={`frequency-select-${id}`}
|
||||||
|
onBlur={() => frequencyHelpers.setTouched(true)}
|
||||||
>
|
>
|
||||||
{children}
|
<SelectOption value={RRule.MINUTELY}>{t`Minute`}</SelectOption>
|
||||||
|
<SelectOption value={RRule.HOURLY}>{t`Hour`}</SelectOption>
|
||||||
|
<SelectOption value={RRule.DAILY}>{t`Day`}</SelectOption>
|
||||||
|
<SelectOption value={RRule.WEEKLY}>{t`Week`}</SelectOption>
|
||||||
|
<SelectOption value={RRule.MONTHLY}>{t`Month`}</SelectOption>
|
||||||
|
<SelectOption value={RRule.YEARLY}>{t`Year`}</SelectOption>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
FrequencySelect.propTypes = {
|
|
||||||
value: arrayOf(string).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { SelectOption, SelectVariant };
|
export { SelectOption, SelectVariant };
|
||||||
|
|||||||
77
awx/ui/src/components/Schedule/shared/MonthandYearForm.js
Normal file
77
awx/ui/src/components/Schedule/shared/MonthandYearForm.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import AnsibleSelect from 'components/AnsibleSelect';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
Checkbox as _Checkbox,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { bysetposOptions, monthOptions } from './scheduleFormHelpers';
|
||||||
|
|
||||||
|
const GroupWrapper = styled(FormGroup)`
|
||||||
|
&& .pf-c-form__group-control {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&& .pf-c-form__group-label {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const Checkbox = styled(_Checkbox)`
|
||||||
|
:not(:last-of-type) {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
function MonthandYearForm({ id }) {
|
||||||
|
const [bySetPos, , bySetPosHelpers] = useField('bysetpos');
|
||||||
|
const [byMonth, , byMonthHelpers] = useField('bymonth');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GroupWrapper
|
||||||
|
fieldId={`schedule-run-on-${id}`}
|
||||||
|
label={<b>{t`Run on a specific month`}</b>}
|
||||||
|
>
|
||||||
|
<Grid hasGutter>
|
||||||
|
{monthOptions.map((month) => (
|
||||||
|
<GridItem key={month.label} span={2} rowSpan={2}>
|
||||||
|
<Checkbox
|
||||||
|
label={month.label}
|
||||||
|
isChecked={byMonth.value?.includes(month.value)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
byMonthHelpers.setValue([...byMonth.value, month.value]);
|
||||||
|
} else {
|
||||||
|
const removed = byMonth.value.filter(
|
||||||
|
(i) => i !== month.value
|
||||||
|
);
|
||||||
|
byMonthHelpers.setValue(removed);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
id={`bymonth-${month.label}`}
|
||||||
|
ouiaId={`bymonth-${month.label}`}
|
||||||
|
name="bymonth"
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</GroupWrapper>
|
||||||
|
<GroupWrapper
|
||||||
|
label={<b>{t`Run on a specific week day at monthly intervals`}</b>}
|
||||||
|
>
|
||||||
|
<AnsibleSelect
|
||||||
|
id={`schedule-run-on-the-occurrence-${id}`}
|
||||||
|
data={bysetposOptions}
|
||||||
|
{...bySetPos}
|
||||||
|
onChange={(e, v) => {
|
||||||
|
bySetPosHelpers.setValue(v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GroupWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default MonthandYearForm;
|
||||||
45
awx/ui/src/components/Schedule/shared/OrdinalDayForm.js
Normal file
45
awx/ui/src/components/Schedule/shared/OrdinalDayForm.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { FormGroup, TextInput } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
const GroupWrapper = styled(FormGroup)`
|
||||||
|
&& .pf-c-form__group-control {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&& .pf-c-form__group-label {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function OrdinalDayForm() {
|
||||||
|
const [byMonthDay] = useField('bymonthday');
|
||||||
|
const [byYearDay] = useField('byyearday');
|
||||||
|
return (
|
||||||
|
<GroupWrapper
|
||||||
|
label={<b>{t`On a specific number day`}</b>}
|
||||||
|
name="ordinalDay"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder={t`Run on a day of month`}
|
||||||
|
aria-label={t`Type a numbered day`}
|
||||||
|
type="number"
|
||||||
|
onChange={(value, event) => {
|
||||||
|
byMonthDay.onChange(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder={t`Run on a day of year`}
|
||||||
|
aria-label={t`Type a numbered day`}
|
||||||
|
type="number"
|
||||||
|
onChange={(value, event) => {
|
||||||
|
byYearDay.onChange(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GroupWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrdinalDayForm;
|
||||||
67
awx/ui/src/components/Schedule/shared/ScheduleEndForm.js
Normal file
67
awx/ui/src/components/Schedule/shared/ScheduleEndForm.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { FormGroup, Radio } from '@patternfly/react-core';
|
||||||
|
import FormField from 'components/FormField';
|
||||||
|
import DateTimePicker from './DateTimePicker';
|
||||||
|
|
||||||
|
function ScheduleEndForm() {
|
||||||
|
const [endType, , { setValue }] = useField('endingType');
|
||||||
|
const [count] = useField('count');
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup name="end" label={t`End`}>
|
||||||
|
<Radio
|
||||||
|
id="endNever"
|
||||||
|
name={t`Never End`}
|
||||||
|
label={t`Never`}
|
||||||
|
value="never"
|
||||||
|
isChecked={endType.value === 'never'}
|
||||||
|
onChange={() => {
|
||||||
|
setValue('never');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
name="Count"
|
||||||
|
id="after"
|
||||||
|
label={t`After number of occurrences`}
|
||||||
|
value="after"
|
||||||
|
isChecked={endType.value === 'after'}
|
||||||
|
onChange={() => {
|
||||||
|
setValue('after');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
name="End Date"
|
||||||
|
label={t`On date`}
|
||||||
|
value="onDate"
|
||||||
|
id="endDate"
|
||||||
|
isChecked={endType.value === 'onDate'}
|
||||||
|
onChange={() => {
|
||||||
|
setValue('onDate');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
{endType.value === 'after' && (
|
||||||
|
<FormField
|
||||||
|
label={t`Occurrences`}
|
||||||
|
name="count"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
isRequired
|
||||||
|
{...count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{endType.value === 'onDate' && (
|
||||||
|
<DateTimePicker
|
||||||
|
dateFieldName="endDate"
|
||||||
|
timeFieldName="endTime"
|
||||||
|
label={t`End date/time`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScheduleEndForm;
|
||||||
@@ -18,14 +18,9 @@ import SchedulePromptableFields from './SchedulePromptableFields';
|
|||||||
import ScheduleFormFields from './ScheduleFormFields';
|
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 ScheduleFormWizard from './ScheduleFormWizard';
|
||||||
import buildRuleSet from './buildRuleSet';
|
import FrequenciesList from './FrequenciesList';
|
||||||
|
// import { validateSchedule } from './scheduleFormHelpers';
|
||||||
const NUM_DAYS_PER_FREQUENCY = {
|
|
||||||
week: 7,
|
|
||||||
month: 31,
|
|
||||||
year: 365,
|
|
||||||
};
|
|
||||||
|
|
||||||
function ScheduleForm({
|
function ScheduleForm({
|
||||||
hasDaysToKeepField,
|
hasDaysToKeepField,
|
||||||
@@ -40,15 +35,16 @@ function ScheduleForm({
|
|||||||
}) {
|
}) {
|
||||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||||
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
||||||
|
const [isScheduleWizardOpen, setIsScheduleWizardOpen] = useState(false);
|
||||||
const originalLabels = useRef([]);
|
const originalLabels = useRef([]);
|
||||||
const originalInstanceGroups = useRef([]);
|
const originalInstanceGroups = useRef([]);
|
||||||
|
|
||||||
let rruleError;
|
let rruleError;
|
||||||
const now = DateTime.now();
|
const now = DateTime.now();
|
||||||
|
|
||||||
const closestQuarterHour = DateTime.fromMillis(
|
const closestQuarterHour = DateTime.fromMillis(
|
||||||
Math.ceil(now.ts / 900000) * 900000
|
Math.ceil(now.ts / 900000) * 900000
|
||||||
);
|
);
|
||||||
const tomorrow = closestQuarterHour.plus({ days: 1 });
|
|
||||||
const isTemplate =
|
const isTemplate =
|
||||||
resource.type === 'workflow_job_template' ||
|
resource.type === 'workflow_job_template' ||
|
||||||
resource.type === 'job_template';
|
resource.type === 'job_template';
|
||||||
@@ -283,69 +279,10 @@ function ScheduleForm({
|
|||||||
}
|
}
|
||||||
const [currentDate, time] = dateToInputDateTime(closestQuarterHour.toISO());
|
const [currentDate, time] = dateToInputDateTime(closestQuarterHour.toISO());
|
||||||
|
|
||||||
const [tomorrowDate] = dateToInputDateTime(tomorrow.toISO());
|
|
||||||
const initialFrequencyOptions = {
|
|
||||||
minute: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
},
|
|
||||||
hour: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
},
|
|
||||||
day: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
},
|
|
||||||
week: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
daysOfWeek: [],
|
|
||||||
},
|
|
||||||
month: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
runOn: 'day',
|
|
||||||
runOnTheOccurrence: 1,
|
|
||||||
runOnTheDay: 'sunday',
|
|
||||||
runOnDayNumber: 1,
|
|
||||||
},
|
|
||||||
year: {
|
|
||||||
interval: 1,
|
|
||||||
end: 'never',
|
|
||||||
occurrences: 1,
|
|
||||||
endDate: tomorrowDate,
|
|
||||||
endTime: time,
|
|
||||||
runOn: 'day',
|
|
||||||
runOnTheOccurrence: 1,
|
|
||||||
runOnTheDay: 'sunday',
|
|
||||||
runOnTheMonth: 1,
|
|
||||||
runOnDayMonth: 1,
|
|
||||||
runOnDayNumber: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
description: schedule.description || '',
|
description: schedule.description || '',
|
||||||
frequency: [],
|
frequencies: [],
|
||||||
exceptionFrequency: [],
|
exceptionFrequency: [],
|
||||||
frequencyOptions: initialFrequencyOptions,
|
|
||||||
exceptionOptions: initialFrequencyOptions,
|
|
||||||
name: schedule.name || '',
|
name: schedule.name || '',
|
||||||
startDate: currentDate,
|
startDate: currentDate,
|
||||||
startTime: time,
|
startTime: time,
|
||||||
@@ -367,11 +304,9 @@ function ScheduleForm({
|
|||||||
}
|
}
|
||||||
initialValues.daysToKeep = initialDaysToKeep;
|
initialValues.daysToKeep = initialDaysToKeep;
|
||||||
}
|
}
|
||||||
|
|
||||||
let overriddenValues = {};
|
|
||||||
if (schedule.rrule) {
|
if (schedule.rrule) {
|
||||||
try {
|
try {
|
||||||
overriddenValues = parseRuleObj(schedule);
|
parseRuleObj(schedule);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof UnsupportedRRuleError) {
|
if (error instanceof UnsupportedRRuleError) {
|
||||||
return (
|
return (
|
||||||
@@ -394,89 +329,33 @@ function ScheduleForm({
|
|||||||
if (contentLoading) {
|
if (contentLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
const frequencies = [];
|
||||||
const validate = (values) => {
|
frequencies.push(parseRuleObj(schedule));
|
||||||
const errors = {};
|
|
||||||
|
|
||||||
values.frequency.forEach((freq) => {
|
|
||||||
const options = values.frequencyOptions[freq];
|
|
||||||
const freqErrors = {};
|
|
||||||
|
|
||||||
if (
|
|
||||||
(freq === 'month' || freq === 'year') &&
|
|
||||||
options.runOn === 'day' &&
|
|
||||||
(options.runOnDayNumber < 1 || options.runOnDayNumber > 31)
|
|
||||||
) {
|
|
||||||
freqErrors.runOn = t`Please select a day number between 1 and 31.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.end === 'after' && !options.occurrences) {
|
|
||||||
freqErrors.occurrences = t`Please enter a number of occurrences.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.end === 'onDate') {
|
|
||||||
if (
|
|
||||||
DateTime.fromFormat(
|
|
||||||
`${values.startDate} ${values.startTime}`,
|
|
||||||
'yyyy-LL-dd h:mm a'
|
|
||||||
).toMillis() >=
|
|
||||||
DateTime.fromFormat(
|
|
||||||
`${options.endDate} ${options.endTime}`,
|
|
||||||
'yyyy-LL-dd h:mm a'
|
|
||||||
).toMillis()
|
|
||||||
) {
|
|
||||||
freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
DateTime.fromISO(options.endDate)
|
|
||||||
.diff(DateTime.fromISO(values.startDate), 'days')
|
|
||||||
.toObject().days < NUM_DAYS_PER_FREQUENCY[freq]
|
|
||||||
) {
|
|
||||||
const rule = new RRule(
|
|
||||||
buildRuleObj({
|
|
||||||
startDate: values.startDate,
|
|
||||||
startTime: values.startTime,
|
|
||||||
frequency: freq,
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (rule.all().length === 0) {
|
|
||||||
errors.startDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
|
||||||
freqErrors.endDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Object.keys(freqErrors).length > 0) {
|
|
||||||
if (!errors.frequencyOptions) {
|
|
||||||
errors.frequencyOptions = {};
|
|
||||||
}
|
|
||||||
errors.frequencyOptions[freq] = freqErrors;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) {
|
|
||||||
errors.exceptionFrequency = t`This schedule has no occurrences due to the selected exceptions.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Config>
|
<Config>
|
||||||
{() => (
|
{() => (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
...initialValues,
|
name: schedule.name || '',
|
||||||
...overriddenValues,
|
description: schedule.description || '',
|
||||||
frequencyOptions: {
|
frequencies: frequencies || [],
|
||||||
...initialValues.frequencyOptions,
|
freq: RRule.DAILY,
|
||||||
...overriddenValues.frequencyOptions,
|
interval: 1,
|
||||||
},
|
wkst: RRule.SU,
|
||||||
exceptionOptions: {
|
byweekday: [],
|
||||||
...initialValues.exceptionOptions,
|
byweekno: [],
|
||||||
...overriddenValues.exceptionOptions,
|
bymonth: [],
|
||||||
},
|
bymonthday: '',
|
||||||
|
byyearday: '',
|
||||||
|
bysetpos: '',
|
||||||
|
until: schedule.until || null,
|
||||||
|
endDate: currentDate,
|
||||||
|
endTime: time,
|
||||||
|
count: 1,
|
||||||
|
endingType: 'never',
|
||||||
|
timezone: schedule.timezone || now.zoneName,
|
||||||
|
startDate: currentDate,
|
||||||
|
startTime: time,
|
||||||
}}
|
}}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
submitSchedule(
|
submitSchedule(
|
||||||
@@ -488,9 +367,10 @@ function ScheduleForm({
|
|||||||
credentials
|
credentials
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
validate={validate}
|
validate={() => {}}
|
||||||
>
|
>
|
||||||
{(formik) => (
|
{(formik) => (
|
||||||
|
<>
|
||||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||||
<FormColumnLayout>
|
<FormColumnLayout>
|
||||||
<ScheduleFormFields
|
<ScheduleFormFields
|
||||||
@@ -517,6 +397,9 @@ function ScheduleForm({
|
|||||||
instanceGroups={originalInstanceGroups.current}
|
instanceGroups={originalInstanceGroups.current}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<FormFullWidthLayout>
|
||||||
|
<FrequenciesList openWizard={setIsScheduleWizardOpen} />
|
||||||
|
</FormFullWidthLayout>
|
||||||
<FormSubmitError error={submitError} />
|
<FormSubmitError error={submitError} />
|
||||||
<FormFullWidthLayout>
|
<FormFullWidthLayout>
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
@@ -531,6 +414,10 @@ function ScheduleForm({
|
|||||||
{t`Save`}
|
{t`Save`}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {}}
|
||||||
|
>{t`Preview occurances`}</Button>
|
||||||
|
|
||||||
{isTemplate && showPromptButton && (
|
{isTemplate && showPromptButton && (
|
||||||
<Button
|
<Button
|
||||||
ouiaId="schedule-form-prompt-button"
|
ouiaId="schedule-form-prompt-button"
|
||||||
@@ -555,6 +442,15 @@ function ScheduleForm({
|
|||||||
</FormFullWidthLayout>
|
</FormFullWidthLayout>
|
||||||
</FormColumnLayout>
|
</FormColumnLayout>
|
||||||
</Form>
|
</Form>
|
||||||
|
{isScheduleWizardOpen && (
|
||||||
|
<ScheduleFormWizard
|
||||||
|
staticFormFormkik={formik}
|
||||||
|
isOpen={isScheduleWizardOpen}
|
||||||
|
handleSave={() => {}}
|
||||||
|
setIsOpen={setIsScheduleWizardOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
)}
|
)}
|
||||||
@@ -575,24 +471,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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,41 +1,27 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import { FormGroup, Title } from '@patternfly/react-core';
|
import { FormGroup } from '@patternfly/react-core';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
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 getHelpText from '../../../screens/Template/shared/JobTemplate.helptext';
|
import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext';
|
||||||
import { SubFormLayout, FormColumnLayout } from '../../FormLayout';
|
|
||||||
import FrequencyDetailSubform from './FrequencyDetailSubform';
|
|
||||||
import DateTimePicker from './DateTimePicker';
|
import DateTimePicker from './DateTimePicker';
|
||||||
import sortFrequencies from './sortFrequencies';
|
|
||||||
|
|
||||||
const SelectClearOption = styled(SelectOption)`
|
|
||||||
& > input[type='checkbox'] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function ScheduleFormFields({
|
export default function ScheduleFormFields({
|
||||||
hasDaysToKeepField,
|
hasDaysToKeepField,
|
||||||
zoneOptions,
|
zoneOptions,
|
||||||
zoneLinks,
|
zoneLinks,
|
||||||
|
setTimeZone,
|
||||||
}) {
|
}) {
|
||||||
const helpText = getHelpText();
|
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`),
|
||||||
});
|
});
|
||||||
const [frequency, frequencyMeta, frequencyHelper] = useField({
|
|
||||||
name: 'frequency',
|
|
||||||
validate: required(t`Select a value for this field`),
|
|
||||||
});
|
|
||||||
const [timezoneMessage, setTimezoneMessage] = useState('');
|
const [timezoneMessage, setTimezoneMessage] = useState('');
|
||||||
const warnLinkedTZ = (event, selectedValue) => {
|
const warnLinkedTZ = (event, selectedValue) => {
|
||||||
if (zoneLinks[selectedValue]) {
|
if (zoneLinks[selectedValue]) {
|
||||||
@@ -46,6 +32,7 @@ export default function ScheduleFormFields({
|
|||||||
setTimezoneMessage('');
|
setTimezoneMessage('');
|
||||||
}
|
}
|
||||||
timezone.onChange(event, selectedValue);
|
timezone.onChange(event, selectedValue);
|
||||||
|
setTimeZone(zoneLinks(selectedValue));
|
||||||
};
|
};
|
||||||
let timezoneValidatedStatus = 'default';
|
let timezoneValidatedStatus = 'default';
|
||||||
if (timezoneMeta.touched && timezoneMeta.error) {
|
if (timezoneMeta.touched && timezoneMeta.error) {
|
||||||
@@ -55,16 +42,6 @@ export default function ScheduleFormFields({
|
|||||||
}
|
}
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
|
||||||
const [exceptionFrequency, exceptionFrequencyMeta, exceptionFrequencyHelper] =
|
|
||||||
useField({
|
|
||||||
name: 'exceptionFrequency',
|
|
||||||
validate: required(t`Select a value for this field`),
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateFrequency = (setFrequency) => (values) => {
|
|
||||||
setFrequency(values.sort(sortFrequencies));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -103,33 +80,7 @@ export default function ScheduleFormFields({
|
|||||||
onChange={warnLinkedTZ}
|
onChange={warnLinkedTZ}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup
|
|
||||||
name="frequency"
|
|
||||||
fieldId="schedule-frequency"
|
|
||||||
helperTextInvalid={frequencyMeta.error}
|
|
||||||
validated={
|
|
||||||
!frequencyMeta.touched || !frequencyMeta.error ? 'default' : 'error'
|
|
||||||
}
|
|
||||||
label={t`Repeat frequency`}
|
|
||||||
>
|
|
||||||
<FrequencySelect
|
|
||||||
id="schedule-frequency"
|
|
||||||
onChange={updateFrequency(frequencyHelper.setValue)}
|
|
||||||
value={frequency.value}
|
|
||||||
placeholderText={
|
|
||||||
frequency.value.length ? t`Select frequency` : t`None (run once)`
|
|
||||||
}
|
|
||||||
onBlur={frequencyHelper.setTouched}
|
|
||||||
>
|
|
||||||
<SelectClearOption value="none">{t`None (run once)`}</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>
|
|
||||||
{hasDaysToKeepField ? (
|
{hasDaysToKeepField ? (
|
||||||
<FormField
|
<FormField
|
||||||
id="schedule-days-to-keep"
|
id="schedule-days-to-keep"
|
||||||
@@ -140,68 +91,6 @@ export default function ScheduleFormFields({
|
|||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{frequency.value.length ? (
|
|
||||||
<SubFormLayout>
|
|
||||||
<Title size="md" headingLevel="h4">
|
|
||||||
{t`Frequency Details`}
|
|
||||||
</Title>
|
|
||||||
{frequency.value.map((val) => (
|
|
||||||
<FormColumnLayout key={val} stacked>
|
|
||||||
<FrequencyDetailSubform
|
|
||||||
frequency={val}
|
|
||||||
prefix={`frequencyOptions.${val}`}
|
|
||||||
/>
|
|
||||||
</FormColumnLayout>
|
|
||||||
))}
|
|
||||||
<Title
|
|
||||||
size="md"
|
|
||||||
headingLevel="h4"
|
|
||||||
css="margin-top: var(--pf-c-card--child--PaddingRight)"
|
|
||||||
>{t`Exceptions`}</Title>
|
|
||||||
<FormColumnLayout stacked>
|
|
||||||
<FormGroup
|
|
||||||
name="exceptions"
|
|
||||||
fieldId="exception-frequency"
|
|
||||||
helperTextInvalid={exceptionFrequencyMeta.error}
|
|
||||||
validated={
|
|
||||||
!exceptionFrequencyMeta.touched || !exceptionFrequencyMeta.error
|
|
||||||
? 'default'
|
|
||||||
: 'error'
|
|
||||||
}
|
|
||||||
label={t`Add exceptions`}
|
|
||||||
>
|
|
||||||
<FrequencySelect
|
|
||||||
id="exception-frequency"
|
|
||||||
onChange={updateFrequency(exceptionFrequencyHelper.setValue)}
|
|
||||||
value={exceptionFrequency.value}
|
|
||||||
placeholderText={
|
|
||||||
exceptionFrequency.value.length
|
|
||||||
? t`Select frequency`
|
|
||||||
: t`None`
|
|
||||||
}
|
|
||||||
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) => (
|
|
||||||
<FormColumnLayout key={val} stacked>
|
|
||||||
<FrequencyDetailSubform
|
|
||||||
frequency={val}
|
|
||||||
prefix={`exceptionOptions.${val}`}
|
|
||||||
isException
|
|
||||||
/>
|
|
||||||
</FormColumnLayout>
|
|
||||||
))}
|
|
||||||
</SubFormLayout>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
199
awx/ui/src/components/Schedule/shared/ScheduleFormWizard.js
Normal file
199
awx/ui/src/components/Schedule/shared/ScheduleFormWizard.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Wizard,
|
||||||
|
WizardContextConsumer,
|
||||||
|
WizardFooter,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { RRule } from 'rrule';
|
||||||
|
import { useField, useFormikContext } from 'formik';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { formatDateString } from 'util/dates';
|
||||||
|
import FrequencySelect from './FrequencySelect';
|
||||||
|
import MonthandYearForm from './MonthandYearForm';
|
||||||
|
import OrdinalDayForm from './OrdinalDayForm';
|
||||||
|
import WeekdayForm from './WeekdayForm';
|
||||||
|
import ScheduleEndForm from './ScheduleEndForm';
|
||||||
|
import parseRuleObj from './parseRuleObj';
|
||||||
|
import { buildDtStartObj } from './buildRuleObj';
|
||||||
|
|
||||||
|
const GroupWrapper = styled(FormGroup)`
|
||||||
|
&& .pf-c-form__group-control {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&& .pf-c-form__group-label {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function ScheduleFormWizard({ isOpen, setIsOpen }) {
|
||||||
|
const { values, resetForm, initialValues } = useFormikContext();
|
||||||
|
const [freq, freqMeta] = useField('freq');
|
||||||
|
const [{ value: frequenciesValue }] = useField('frequencies');
|
||||||
|
const [interval, , intervalHelpers] = useField('interval');
|
||||||
|
|
||||||
|
const handleSubmit = (goToStepById) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
endingType,
|
||||||
|
endTime,
|
||||||
|
endDate,
|
||||||
|
timezone,
|
||||||
|
startDate,
|
||||||
|
startTime,
|
||||||
|
frequencies,
|
||||||
|
...rest
|
||||||
|
} = values;
|
||||||
|
if (endingType === 'onDate') {
|
||||||
|
const dt = DateTime.fromFormat(
|
||||||
|
`${endDate} ${endTime}`,
|
||||||
|
'yyyy-MM-dd h:mm a',
|
||||||
|
{
|
||||||
|
zone: timezone,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
rest.until = formatDateString(dt, timezone);
|
||||||
|
|
||||||
|
delete rest.count;
|
||||||
|
}
|
||||||
|
if (endingType === 'never') delete rest.count;
|
||||||
|
|
||||||
|
const rule = new RRule(rest);
|
||||||
|
|
||||||
|
const start = buildDtStartObj({
|
||||||
|
startDate: values.startDate,
|
||||||
|
startTime: values.startTime,
|
||||||
|
timezone: values.timezone,
|
||||||
|
frequency: values.freq,
|
||||||
|
});
|
||||||
|
const newFrequency = parseRuleObj({
|
||||||
|
timezone,
|
||||||
|
frequency: freq.value,
|
||||||
|
rrule: rule.toString(),
|
||||||
|
dtstart: start,
|
||||||
|
});
|
||||||
|
if (goToStepById) {
|
||||||
|
goToStepById(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm({
|
||||||
|
values: {
|
||||||
|
...initialValues,
|
||||||
|
description: values.description,
|
||||||
|
name: values.name,
|
||||||
|
startDate: values.startDate,
|
||||||
|
startTime: values.startTime,
|
||||||
|
timezone: values.timezone,
|
||||||
|
frequencies: frequenciesValue[0].frequency.length
|
||||||
|
? [...frequenciesValue, newFrequency]
|
||||||
|
: [newFrequency],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const CustomFooter = (
|
||||||
|
<WizardFooter>
|
||||||
|
<WizardContextConsumer>
|
||||||
|
{({ activeStep, onNext, onBack, goToStepById }) => (
|
||||||
|
<>
|
||||||
|
{activeStep.id === 2 ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
handleSubmit(true, goToStepById);
|
||||||
|
}}
|
||||||
|
>{t`Finish and create new`}</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
handleSubmit(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>{t`Finish and close`}</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button variant="primary" onClick={onNext}>{t`Next`}</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button variant="secondary" onClick={onBack}>{t`Back`}</Button>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
resetForm({
|
||||||
|
values: {
|
||||||
|
...initialValues,
|
||||||
|
description: values.description,
|
||||||
|
name: values.name,
|
||||||
|
startDate: values.startDate,
|
||||||
|
startTime: values.startTime,
|
||||||
|
timezone: values.timezone,
|
||||||
|
frequencies: values.frequencies,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>{t`Cancel`}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</WizardContextConsumer>
|
||||||
|
</WizardFooter>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wizard
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
isOpen={isOpen}
|
||||||
|
footer={CustomFooter}
|
||||||
|
steps={[
|
||||||
|
{
|
||||||
|
key: 'frequency',
|
||||||
|
name: 'Frequency',
|
||||||
|
id: 1,
|
||||||
|
component: (
|
||||||
|
<>
|
||||||
|
<Title size="md" headingLevel="h4">{t`Repeat frequency`}</Title>
|
||||||
|
<GroupWrapper
|
||||||
|
name="freq"
|
||||||
|
fieldId="schedule-frequency"
|
||||||
|
isRequired
|
||||||
|
helperTextInvalid={freqMeta.error}
|
||||||
|
validated={
|
||||||
|
!freqMeta.touched || !freqMeta.error ? 'default' : 'error'
|
||||||
|
}
|
||||||
|
label={<b>{t`Frequency`}</b>}
|
||||||
|
>
|
||||||
|
<FrequencySelect />
|
||||||
|
</GroupWrapper>
|
||||||
|
<GroupWrapper isRequired label={<b>{t`Interval`}</b>}>
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
value={interval.value}
|
||||||
|
placeholder={t`Choose an interval for the schedule`}
|
||||||
|
aria-label={t`Choose an interval for the schedule`}
|
||||||
|
onChange={(v) => intervalHelpers.setValue(v)}
|
||||||
|
/>
|
||||||
|
</GroupWrapper>
|
||||||
|
<WeekdayForm />
|
||||||
|
<MonthandYearForm />
|
||||||
|
<OrdinalDayForm />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'End',
|
||||||
|
key: 'end',
|
||||||
|
id: 2,
|
||||||
|
component: <ScheduleEndForm />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default ScheduleFormWizard;
|
||||||
164
awx/ui/src/components/Schedule/shared/WeekdayForm.js
Normal file
164
awx/ui/src/components/Schedule/shared/WeekdayForm.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Checkbox as _Checkbox,
|
||||||
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { RRule } from 'rrule';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { weekdayOptions } from './scheduleFormHelpers';
|
||||||
|
|
||||||
|
const Checkbox = styled(_Checkbox)`
|
||||||
|
:not(:last-of-type) {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const GroupWrapper = styled(FormGroup)`
|
||||||
|
&& .pf-c-form__group-control {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&& .pf-c-form__group-label {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function WeekdayForm({ id }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField('byweekday');
|
||||||
|
const [weekStartDay, , weekStartDayHelpers] = useField('wkst');
|
||||||
|
const updateDaysOfWeek = (day, checked) => {
|
||||||
|
const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : [];
|
||||||
|
daysOfWeekHelpers.setTouched(true);
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
newDaysOfWeek.push(day);
|
||||||
|
daysOfWeekHelpers.setValue(newDaysOfWeek);
|
||||||
|
} else {
|
||||||
|
daysOfWeekHelpers.setValue(
|
||||||
|
newDaysOfWeek.filter((selectedDay) => selectedDay !== day)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GroupWrapper
|
||||||
|
name="wkst"
|
||||||
|
label={<b>{t`Select the first day of the week`}</b>}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
onSelect={(e, value) => {
|
||||||
|
weekStartDayHelpers.setValue(value);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
onBlur={() => setIsOpen(false)}
|
||||||
|
selections={weekStartDay.value}
|
||||||
|
onToggle={(isopen) => setIsOpen(isopen)}
|
||||||
|
isOpen={isOpen}
|
||||||
|
id={`schedule-run-on-the-day-${id}`}
|
||||||
|
onChange={(e, v) => {
|
||||||
|
weekStartDayHelpers.setValue(v);
|
||||||
|
}}
|
||||||
|
{...weekStartDay}
|
||||||
|
>
|
||||||
|
{weekdayOptions.map(({ key, value, label }) => (
|
||||||
|
<SelectOption key={key} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</GroupWrapper>
|
||||||
|
<GroupWrapper
|
||||||
|
name="byweekday"
|
||||||
|
fieldId={`schedule-days-of-week-${id}`}
|
||||||
|
helperTextInvalid={daysOfWeekMeta.error}
|
||||||
|
validated={
|
||||||
|
!daysOfWeekMeta.touched || !daysOfWeekMeta.error ? 'default' : 'error'
|
||||||
|
}
|
||||||
|
label={<b>{t`On selected day(s) of the week`}</b>}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Sun`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.SU)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.SU, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Sunday`}
|
||||||
|
id={`schedule-days-of-week-sun-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-sun-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Mon`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.MO)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.MO, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Monday`}
|
||||||
|
id={`schedule-days-of-week-mon-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-mon-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Tue`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.TU)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.TU, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Tuesday`}
|
||||||
|
id={`schedule-days-of-week-tue-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-tue-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Wed`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.WE)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.WE, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Wednesday`}
|
||||||
|
id={`schedule-days-of-week-wed-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-wed-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Thu`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.TH)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.TH, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Thursday`}
|
||||||
|
id={`schedule-days-of-week-thu-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-thu-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Fri`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.FR)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.FR, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Friday`}
|
||||||
|
id={`schedule-days-of-week-fri-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-fri-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t`Sat`}
|
||||||
|
isChecked={daysOfWeek.value?.includes(RRule.SA)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
updateDaysOfWeek(RRule.SA, checked);
|
||||||
|
}}
|
||||||
|
aria-label={t`Saturday`}
|
||||||
|
id={`schedule-days-of-week-sat-${id}`}
|
||||||
|
ouiaId={`schedule-days-of-week-sat-${id}`}
|
||||||
|
name="daysOfWeek"
|
||||||
|
/>
|
||||||
|
</GroupWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default WeekdayForm;
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
|
||||||
import { RRule } from 'rrule';
|
import { RRule } from 'rrule';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { getRRuleDayConstants } from 'util/dates';
|
|
||||||
|
|
||||||
window.RRule = RRule;
|
window.RRule = RRule;
|
||||||
window.DateTime = DateTime;
|
window.DateTime = DateTime;
|
||||||
@@ -22,7 +20,7 @@ export function buildDtStartObj(values) {
|
|||||||
startHour
|
startHour
|
||||||
)}${pad(startMinute)}00`;
|
)}${pad(startMinute)}00`;
|
||||||
const rruleString = values.timezone
|
const rruleString = values.timezone
|
||||||
? `DTSTART;TZID=${values.timezone}:${dateString}`
|
? `DTSTART;TZID=${values.timezone}${dateString}`
|
||||||
: `DTSTART:${dateString}Z`;
|
: `DTSTART:${dateString}Z`;
|
||||||
const rule = RRule.fromString(rruleString);
|
const rule = RRule.fromString(rruleString);
|
||||||
|
|
||||||
@@ -38,7 +36,8 @@ function pad(num) {
|
|||||||
|
|
||||||
export default function buildRuleObj(values, includeStart) {
|
export default function buildRuleObj(values, includeStart) {
|
||||||
const ruleObj = {
|
const ruleObj = {
|
||||||
interval: values.interval,
|
interval: values.interval || 1,
|
||||||
|
freq: values.freq,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeStart) {
|
if (includeStart) {
|
||||||
@@ -49,68 +48,6 @@ export default function buildRuleObj(values, includeStart) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (values.frequency) {
|
|
||||||
case 'none':
|
|
||||||
ruleObj.count = 1;
|
|
||||||
ruleObj.freq = RRule.MINUTELY;
|
|
||||||
break;
|
|
||||||
case 'minute':
|
|
||||||
ruleObj.freq = RRule.MINUTELY;
|
|
||||||
break;
|
|
||||||
case 'hour':
|
|
||||||
ruleObj.freq = RRule.HOURLY;
|
|
||||||
break;
|
|
||||||
case 'day':
|
|
||||||
ruleObj.freq = RRule.DAILY;
|
|
||||||
break;
|
|
||||||
case 'week':
|
|
||||||
ruleObj.freq = RRule.WEEKLY;
|
|
||||||
ruleObj.byweekday = values.daysOfWeek;
|
|
||||||
break;
|
|
||||||
case 'month':
|
|
||||||
ruleObj.freq = RRule.MONTHLY;
|
|
||||||
if (values.runOn === 'day') {
|
|
||||||
ruleObj.bymonthday = values.runOnDayNumber;
|
|
||||||
} else if (values.runOn === 'the') {
|
|
||||||
ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10);
|
|
||||||
ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'year':
|
|
||||||
ruleObj.freq = RRule.YEARLY;
|
|
||||||
if (values.runOn === 'day') {
|
|
||||||
ruleObj.bymonth = parseInt(values.runOnDayMonth, 10);
|
|
||||||
ruleObj.bymonthday = values.runOnDayNumber;
|
|
||||||
} else if (values.runOn === 'the') {
|
|
||||||
ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10);
|
|
||||||
ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay);
|
|
||||||
ruleObj.bymonth = parseInt(values.runOnTheMonth, 10);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(t`Frequency did not match an expected value`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.frequency !== 'none') {
|
|
||||||
switch (values.end) {
|
|
||||||
case 'never':
|
|
||||||
break;
|
|
||||||
case 'after':
|
|
||||||
ruleObj.count = values.occurrences;
|
|
||||||
break;
|
|
||||||
case 'onDate': {
|
|
||||||
ruleObj.until = buildDateTime(
|
|
||||||
values.endDate,
|
|
||||||
values.endTime,
|
|
||||||
values.timezone
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(t`End did not match an expected value (${values.end})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ruleObj;
|
return ruleObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { RRule, RRuleSet } from 'rrule';
|
import { RRule, RRuleSet } from 'rrule';
|
||||||
import buildRuleObj, { buildDtStartObj } from './buildRuleObj';
|
import buildRuleObj, { buildDtStartObj } from './buildRuleObj';
|
||||||
|
import { FREQUENCIESCONSTANTS } from './scheduleFormHelpers';
|
||||||
|
|
||||||
window.RRuleSet = RRuleSet;
|
window.RRuleSet = RRuleSet;
|
||||||
|
|
||||||
@@ -12,42 +13,31 @@ export default function buildRuleSet(values, useUTCStart) {
|
|||||||
startDate: values.startDate,
|
startDate: values.startDate,
|
||||||
startTime: values.startTime,
|
startTime: values.startTime,
|
||||||
timezone: values.timezone,
|
timezone: values.timezone,
|
||||||
|
frequency: values.freq,
|
||||||
});
|
});
|
||||||
set.rrule(startRule);
|
set.rrule(startRule);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.frequency.length === 0) {
|
values.frequencies.forEach(({ frequency, rrule }) => {
|
||||||
const rule = buildRuleObj(
|
if (!frequencies.includes(frequency)) {
|
||||||
{
|
|
||||||
startDate: values.startDate,
|
|
||||||
startTime: values.startTime,
|
|
||||||
timezone: values.timezone,
|
|
||||||
frequency: 'none',
|
|
||||||
interval: 1,
|
|
||||||
},
|
|
||||||
useUTCStart
|
|
||||||
);
|
|
||||||
set.rrule(new RRule(rule));
|
|
||||||
}
|
|
||||||
|
|
||||||
frequencies.forEach((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,
|
freq: FREQUENCIESCONSTANTS[frequency],
|
||||||
...values.frequencyOptions[frequency],
|
rrule,
|
||||||
},
|
},
|
||||||
useUTCStart
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
set.rrule(new RRule(rule));
|
set.rrule(new RRule(rule));
|
||||||
});
|
});
|
||||||
|
|
||||||
frequencies.forEach((frequency) => {
|
values.exceptions?.forEach(({ frequency, rrule }) => {
|
||||||
if (!values.exceptionFrequency?.includes(frequency)) {
|
if (!values.exceptionFrequency?.includes(frequency)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -56,8 +46,8 @@ export default function buildRuleSet(values, useUTCStart) {
|
|||||||
startDate: values.startDate,
|
startDate: values.startDate,
|
||||||
startTime: values.startTime,
|
startTime: values.startTime,
|
||||||
timezone: values.timezone,
|
timezone: values.timezone,
|
||||||
frequency,
|
freq: FREQUENCIESCONSTANTS[frequency],
|
||||||
...values.exceptionOptions[frequency],
|
rrule,
|
||||||
},
|
},
|
||||||
useUTCStart
|
useUTCStart
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ export class UnsupportedRRuleError extends Error {
|
|||||||
|
|
||||||
export default function parseRuleObj(schedule) {
|
export default function parseRuleObj(schedule) {
|
||||||
let values = {
|
let values = {
|
||||||
frequency: [],
|
frequency: '',
|
||||||
frequencyOptions: {},
|
rrules: '',
|
||||||
exceptionFrequency: [],
|
|
||||||
exceptionOptions: {},
|
|
||||||
timezone: schedule.timezone,
|
timezone: schedule.timezone,
|
||||||
};
|
};
|
||||||
|
if (Object.values(schedule).length === 0) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
const ruleset = rrulestr(schedule.rrule.replace(' ', '\n'), {
|
const ruleset = rrulestr(schedule.rrule.replace(' ', '\n'), {
|
||||||
forceset: true,
|
forceset: true,
|
||||||
});
|
});
|
||||||
@@ -40,25 +42,9 @@ export default function parseRuleObj(schedule) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isSingleOccurrence(values)) {
|
|
||||||
values.frequency = [];
|
|
||||||
values.frequencyOptions = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSingleOccurrence(values) {
|
|
||||||
if (values.frequency.length > 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (values.frequency[0] !== 'minute') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const options = values.frequencyOptions.minute;
|
|
||||||
return options.end === 'after' && options.occurrences === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseDtstart(schedule, values) {
|
function parseDtstart(schedule, values) {
|
||||||
// TODO: should this rely on DTSTART in rruleset rather than schedule.dtstart?
|
// TODO: should this rely on DTSTART in rruleset rather than schedule.dtstart?
|
||||||
const [startDate, startTime] = dateToInputDateTime(
|
const [startDate, startTime] = dateToInputDateTime(
|
||||||
@@ -81,27 +67,12 @@ const frequencyTypes = {
|
|||||||
[RRule.YEARLY]: 'year',
|
[RRule.YEARLY]: 'year',
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseRrule(rruleString, schedule, values) {
|
function parseRrule(rruleString, schedule) {
|
||||||
const { frequency, options } = parseRule(
|
const { frequency } = parseRule(rruleString, schedule);
|
||||||
rruleString,
|
|
||||||
schedule,
|
|
||||||
values.exceptionFrequency
|
|
||||||
);
|
|
||||||
|
|
||||||
if (values.frequencyOptions[frequency]) {
|
const freq = { frequency, rrule: rruleString };
|
||||||
throw new UnsupportedRRuleError(
|
|
||||||
'Duplicate exception frequency types not supported'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return freq;
|
||||||
...values,
|
|
||||||
frequency: [...values.frequency, frequency].sort(sortFrequencies),
|
|
||||||
frequencyOptions: {
|
|
||||||
...values.frequencyOptions,
|
|
||||||
[frequency]: options,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseExRule(exruleString, schedule, values) {
|
function parseExRule(exruleString, schedule, values) {
|
||||||
@@ -129,20 +100,10 @@ function parseExRule(exruleString, schedule, values) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseRule(ruleString, schedule, frequencies) {
|
function parseRule(ruleString, schedule) {
|
||||||
const {
|
const {
|
||||||
origOptions: {
|
origOptions: { count, freq, interval, until, ...rest },
|
||||||
bymonth,
|
|
||||||
bymonthday,
|
|
||||||
bysetpos,
|
|
||||||
byweekday,
|
|
||||||
count,
|
|
||||||
freq,
|
|
||||||
interval,
|
|
||||||
until,
|
|
||||||
},
|
|
||||||
} = RRule.fromString(ruleString);
|
} = RRule.fromString(ruleString);
|
||||||
|
|
||||||
const now = DateTime.now();
|
const now = DateTime.now();
|
||||||
const closestQuarterHour = DateTime.fromMillis(
|
const closestQuarterHour = DateTime.fromMillis(
|
||||||
Math.ceil(now.ts / 900000) * 900000
|
Math.ceil(now.ts / 900000) * 900000
|
||||||
@@ -156,17 +117,17 @@ function parseRule(ruleString, schedule, frequencies) {
|
|||||||
endTime: time,
|
endTime: time,
|
||||||
occurrences: 1,
|
occurrences: 1,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
end: 'never',
|
endingType: 'never',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (until) {
|
if (until?.length) {
|
||||||
options.end = 'onDate';
|
options.endingType = 'onDate';
|
||||||
const end = DateTime.fromISO(until.toISOString());
|
const end = DateTime.fromISO(until.toISOString());
|
||||||
const [endDate, endTime] = dateToInputDateTime(end, schedule.timezone);
|
const [endDate, endTime] = dateToInputDateTime(end, schedule.timezone);
|
||||||
options.endDate = endDate;
|
options.endDate = endDate;
|
||||||
options.endTime = endTime;
|
options.endTime = endTime;
|
||||||
} else if (count) {
|
} else if (count) {
|
||||||
options.end = 'after';
|
options.endingType = 'after';
|
||||||
options.occurrences = count;
|
options.occurrences = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,101 +139,10 @@ 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)) {
|
|
||||||
throw new Error(`Duplicate frequency types not supported (${frequency})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (freq === RRule.WEEKLY && byweekday) {
|
|
||||||
options.daysOfWeek = byweekday;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (freq === RRule.MONTHLY) {
|
|
||||||
options.runOn = 'day';
|
|
||||||
options.runOnTheOccurrence = 1;
|
|
||||||
options.runOnTheDay = 'sunday';
|
|
||||||
options.runOnDayNumber = 1;
|
|
||||||
|
|
||||||
if (bymonthday) {
|
|
||||||
options.runOnDayNumber = bymonthday;
|
|
||||||
}
|
|
||||||
if (bysetpos) {
|
|
||||||
options.runOn = 'the';
|
|
||||||
options.runOnTheOccurrence = bysetpos;
|
|
||||||
options.runOnTheDay = generateRunOnTheDay(byweekday);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (freq === RRule.YEARLY) {
|
|
||||||
options.runOn = 'day';
|
|
||||||
options.runOnTheOccurrence = 1;
|
|
||||||
options.runOnTheDay = 'sunday';
|
|
||||||
options.runOnTheMonth = 1;
|
|
||||||
options.runOnDayMonth = 1;
|
|
||||||
options.runOnDayNumber = 1;
|
|
||||||
|
|
||||||
if (bymonthday) {
|
|
||||||
options.runOnDayNumber = bymonthday;
|
|
||||||
options.runOnDayMonth = bymonth;
|
|
||||||
}
|
|
||||||
if (bysetpos) {
|
|
||||||
options.runOn = 'the';
|
|
||||||
options.runOnTheOccurrence = bysetpos;
|
|
||||||
options.runOnTheDay = generateRunOnTheDay(byweekday);
|
|
||||||
options.runOnTheMonth = bymonth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
frequency,
|
frequency,
|
||||||
options,
|
...options,
|
||||||
|
...rest,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateRunOnTheDay(days = []) {
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
RRule.MO,
|
|
||||||
RRule.TU,
|
|
||||||
RRule.WE,
|
|
||||||
RRule.TH,
|
|
||||||
RRule.FR,
|
|
||||||
RRule.SA,
|
|
||||||
RRule.SU,
|
|
||||||
].every((element) => days.indexOf(element) > -1)
|
|
||||||
) {
|
|
||||||
return 'day';
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
[RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR].every(
|
|
||||||
(element) => days.indexOf(element) > -1
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return 'weekday';
|
|
||||||
}
|
|
||||||
if ([RRule.SA, RRule.SU].every((element) => days.indexOf(element) > -1)) {
|
|
||||||
return 'weekendDay';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.MO) > -1) {
|
|
||||||
return 'monday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.TU) > -1) {
|
|
||||||
return 'tuesday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.WE) > -1) {
|
|
||||||
return 'wednesday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.TH) > -1) {
|
|
||||||
return 'thursday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.FR) > -1) {
|
|
||||||
return 'friday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.SA) > -1) {
|
|
||||||
return 'saturday';
|
|
||||||
}
|
|
||||||
if (days.indexOf(RRule.SU) > -1) {
|
|
||||||
return 'sunday';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|||||||
232
awx/ui/src/components/Schedule/shared/scheduleFormHelpers.js
Normal file
232
awx/ui/src/components/Schedule/shared/scheduleFormHelpers.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { RRule } from 'rrule';
|
||||||
|
import buildRuleObj from './buildRuleObj';
|
||||||
|
import buildRuleSet from './buildRuleSet';
|
||||||
|
|
||||||
|
// const NUM_DAYS_PER_FREQUENCY = {
|
||||||
|
// week: 7,
|
||||||
|
// month: 31,
|
||||||
|
// year: 365,
|
||||||
|
// };
|
||||||
|
// const validateSchedule = () =>
|
||||||
|
// const errors = {};
|
||||||
|
|
||||||
|
// values.frequencies.forEach((freq) => {
|
||||||
|
// const options = values.frequencyOptions[freq];
|
||||||
|
// const freqErrors = {};
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// (freq === 'month' || freq === 'year') &&
|
||||||
|
// options.runOn === 'day' &&
|
||||||
|
// (options.runOnDayNumber < 1 || options.runOnDayNumber > 31)
|
||||||
|
// ) {
|
||||||
|
// freqErrors.runOn = t`Please select a day number between 1 and 31.`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (options.end === 'after' && !options.occurrences) {
|
||||||
|
// freqErrors.occurrences = t`Please enter a number of occurrences.`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (options.end === 'onDate') {
|
||||||
|
// if (
|
||||||
|
// DateTime.fromFormat(
|
||||||
|
// `${values.startDate} ${values.startTime}`,
|
||||||
|
// 'yyyy-LL-dd h:mm a'
|
||||||
|
// ).toMillis() >=
|
||||||
|
// DateTime.fromFormat(
|
||||||
|
// `${options.endDate} ${options.endTime}`,
|
||||||
|
// 'yyyy-LL-dd h:mm a'
|
||||||
|
// ).toMillis()
|
||||||
|
// ) {
|
||||||
|
// freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// DateTime.fromISO(options.endDate)
|
||||||
|
// .diff(DateTime.fromISO(values.startDate), 'days')
|
||||||
|
// .toObject().days < NUM_DAYS_PER_FREQUENCY[freq]
|
||||||
|
// ) {
|
||||||
|
// const rule = new RRule(
|
||||||
|
// buildRuleObj({
|
||||||
|
// startDate: values.startDate,
|
||||||
|
// startTime: values.startTime,
|
||||||
|
// frequencies: freq,
|
||||||
|
// ...options,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// if (rule.all().length === 0) {
|
||||||
|
// errors.startDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
||||||
|
// freqErrors.endDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (Object.keys(freqErrors).length > 0) {
|
||||||
|
// if (!errors.frequencyOptions) {
|
||||||
|
// errors.frequencyOptions = {};
|
||||||
|
// }
|
||||||
|
// errors.frequencyOptions[freq] = freqErrors;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) {
|
||||||
|
// errors.exceptionFrequency = t`This schedule has no occurrences due to the
|
||||||
|
// selected exceptions.`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ({});
|
||||||
|
// function scheduleHasInstances(values) {
|
||||||
|
// let rangeToCheck = 1;
|
||||||
|
// values.frequencies.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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const bysetposOptions = [
|
||||||
|
{ value: '', key: 'none', label: 'None' },
|
||||||
|
{ value: 1, key: 'first', label: t`First` },
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
key: 'second',
|
||||||
|
label: t`Second`,
|
||||||
|
},
|
||||||
|
{ value: 3, key: 'third', label: t`Third` },
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
key: 'fourth',
|
||||||
|
label: t`Fourth`,
|
||||||
|
},
|
||||||
|
{ value: 5, key: 'fifth', label: t`Fifth` },
|
||||||
|
{ value: -1, key: 'last', label: t`Last` },
|
||||||
|
];
|
||||||
|
|
||||||
|
const monthOptions = [
|
||||||
|
{
|
||||||
|
key: 'january',
|
||||||
|
value: 1,
|
||||||
|
label: t`January`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'february',
|
||||||
|
value: 2,
|
||||||
|
label: t`February`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'march',
|
||||||
|
value: 3,
|
||||||
|
label: t`March`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'april',
|
||||||
|
value: 4,
|
||||||
|
label: t`April`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'may',
|
||||||
|
value: 5,
|
||||||
|
label: t`May`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'june',
|
||||||
|
value: 6,
|
||||||
|
label: t`June`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'july',
|
||||||
|
value: 7,
|
||||||
|
label: t`July`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'august',
|
||||||
|
value: 8,
|
||||||
|
label: t`August`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'september',
|
||||||
|
value: 9,
|
||||||
|
label: t`September`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'october',
|
||||||
|
value: 10,
|
||||||
|
label: t`October`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'november',
|
||||||
|
value: 11,
|
||||||
|
label: t`November`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'december',
|
||||||
|
value: 12,
|
||||||
|
label: t`December`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const weekdayOptions = [
|
||||||
|
{
|
||||||
|
value: RRule.SU,
|
||||||
|
key: 'sunday',
|
||||||
|
label: t`Sunday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.MO,
|
||||||
|
key: 'monday',
|
||||||
|
label: t`Monday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.TU,
|
||||||
|
key: 'tuesday',
|
||||||
|
label: t`Tuesday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.WE,
|
||||||
|
key: 'wednesday',
|
||||||
|
label: t`Wednesday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.TH,
|
||||||
|
key: 'thursday',
|
||||||
|
label: t`Thursday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.FR,
|
||||||
|
key: 'friday',
|
||||||
|
label: t`Friday`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RRule.SA,
|
||||||
|
key: 'saturday',
|
||||||
|
label: t`Saturday`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const FREQUENCIESCONSTANTS = {
|
||||||
|
minute: RRule.MINUTELY,
|
||||||
|
hour: RRule.HOURLY,
|
||||||
|
day: RRule.DAILY,
|
||||||
|
week: RRule.WEEKLY,
|
||||||
|
month: RRule.MONTHLY,
|
||||||
|
year: RRule.YEARLY,
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
monthOptions,
|
||||||
|
weekdayOptions,
|
||||||
|
bysetposOptions,
|
||||||
|
// validateSchedule,
|
||||||
|
FREQUENCIESCONSTANTS,
|
||||||
|
};
|
||||||
@@ -420,7 +420,7 @@ describe('<AdvancedSearch />', () => {
|
|||||||
const selectOptions = wrapper.find(
|
const selectOptions = wrapper.find(
|
||||||
'Select[aria-label="Related search type"] SelectOption'
|
'Select[aria-label="Related search type"] SelectOption'
|
||||||
);
|
);
|
||||||
expect(selectOptions).toHaveLength(3);
|
expect(selectOptions).toHaveLength(2);
|
||||||
expect(
|
expect(
|
||||||
selectOptions.find('SelectOption[id="name-option-select"]').prop('value')
|
selectOptions.find('SelectOption[id="name-option-select"]').prop('value')
|
||||||
).toBe('name__icontains');
|
).toBe('name__icontains');
|
||||||
|
|||||||
@@ -31,12 +31,6 @@ function RelatedLookupTypeInput({
|
|||||||
value="name__icontains"
|
value="name__icontains"
|
||||||
description={t`Fuzzy search on name field.`}
|
description={t`Fuzzy search on name field.`}
|
||||||
/>
|
/>
|
||||||
<SelectOption
|
|
||||||
id="name-exact-option-select"
|
|
||||||
key="name"
|
|
||||||
value="name"
|
|
||||||
description={t`Exact search on name field.`}
|
|
||||||
/>
|
|
||||||
<SelectOption
|
<SelectOption
|
||||||
id="id-option-select"
|
id="id-option-select"
|
||||||
key="id"
|
key="id"
|
||||||
|
|||||||
@@ -22,16 +22,6 @@ import { CredentialsAPI } from 'api';
|
|||||||
import CredentialDetail from './CredentialDetail';
|
import CredentialDetail from './CredentialDetail';
|
||||||
import CredentialEdit from './CredentialEdit';
|
import CredentialEdit from './CredentialEdit';
|
||||||
|
|
||||||
const jobTemplateCredentialTypes = [
|
|
||||||
'machine',
|
|
||||||
'cloud',
|
|
||||||
'net',
|
|
||||||
'ssh',
|
|
||||||
'vault',
|
|
||||||
'kubernetes',
|
|
||||||
'cryptography',
|
|
||||||
];
|
|
||||||
|
|
||||||
function Credential({ setBreadcrumb }) {
|
function Credential({ setBreadcrumb }) {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
@@ -85,14 +75,13 @@ function Credential({ setBreadcrumb }) {
|
|||||||
link: `/credentials/${id}/access`,
|
link: `/credentials/${id}/access`,
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
if (jobTemplateCredentialTypes.includes(credential?.kind)) {
|
|
||||||
tabsArray.push({
|
|
||||||
name: t`Job Templates`,
|
name: t`Job Templates`,
|
||||||
link: `/credentials/${id}/job_templates`,
|
link: `/credentials/${id}/job_templates`,
|
||||||
id: 2,
|
id: 2,
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
let showCardHeader = true;
|
let showCardHeader = true;
|
||||||
|
|
||||||
if (pathname.endsWith('edit') || pathname.endsWith('add')) {
|
if (pathname.endsWith('edit') || pathname.endsWith('add')) {
|
||||||
@@ -144,7 +133,6 @@ function Credential({ setBreadcrumb }) {
|
|||||||
<Route key="job_templates" path="/credentials/:id/job_templates">
|
<Route key="job_templates" path="/credentials/:id/job_templates">
|
||||||
<RelatedTemplateList
|
<RelatedTemplateList
|
||||||
searchParams={{ credentials__id: credential.id }}
|
searchParams={{ credentials__id: credential.id }}
|
||||||
resourceName={[credential.name, credential.kind]}
|
|
||||||
/>
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
<Route key="not-found" path="*">
|
<Route key="not-found" path="*">
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
import mockMachineCredential from './shared/data.machineCredential.json';
|
import mockCredential from './shared/data.scmCredential.json';
|
||||||
import mockSCMCredential from './shared/data.scmCredential.json';
|
|
||||||
import Credential from './Credential';
|
import Credential from './Credential';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
@@ -22,10 +21,13 @@ jest.mock('react-router-dom', () => ({
|
|||||||
describe('<Credential />', () => {
|
describe('<Credential />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
test('initially renders user-based machine credential successfully', async () => {
|
beforeEach(() => {
|
||||||
CredentialsAPI.readDetail.mockResolvedValueOnce({
|
CredentialsAPI.readDetail.mockResolvedValueOnce({
|
||||||
data: mockMachineCredential,
|
data: mockCredential,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initially renders user-based credential successfully', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
||||||
});
|
});
|
||||||
@@ -34,18 +36,6 @@ describe('<Credential />', () => {
|
|||||||
expect(wrapper.find('RoutedTabs li').length).toBe(4);
|
expect(wrapper.find('RoutedTabs li').length).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders user-based SCM credential successfully', async () => {
|
|
||||||
CredentialsAPI.readDetail.mockResolvedValueOnce({
|
|
||||||
data: mockSCMCredential,
|
|
||||||
});
|
|
||||||
await act(async () => {
|
|
||||||
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
|
||||||
});
|
|
||||||
wrapper.update();
|
|
||||||
expect(wrapper.find('Credential').length).toBe(1);
|
|
||||||
expect(wrapper.find('RoutedTabs li').length).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render expected tabs', async () => {
|
test('should render expected tabs', async () => {
|
||||||
const expectedTabs = [
|
const expectedTabs = [
|
||||||
'Back to Credentials',
|
'Back to Credentials',
|
||||||
|
|||||||
@@ -81,30 +81,35 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
const {
|
const {
|
||||||
data: { results },
|
data: { results },
|
||||||
} = await InstanceGroupsAPI.readInstances(instanceGroup.id);
|
} = await InstanceGroupsAPI.readInstances(instanceGroup.id);
|
||||||
|
let instanceDetails;
|
||||||
const isAssociated = results.some(
|
const isAssociated = results.some(
|
||||||
({ id: instId }) => instId === parseInt(instanceId, 10)
|
({ id: instId }) => instId === parseInt(instanceId, 10)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isAssociated) {
|
if (isAssociated) {
|
||||||
const { data: details } = await InstancesAPI.readDetail(instanceId);
|
const [{ data: details }, { data: healthCheckData }] =
|
||||||
if (details.node_type === 'execution') {
|
await Promise.all([
|
||||||
const { data: healthCheckData } =
|
InstancesAPI.readDetail(instanceId),
|
||||||
await InstancesAPI.readHealthCheckDetail(instanceId);
|
InstancesAPI.readHealthCheckDetail(instanceId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
instanceDetails = details;
|
||||||
setHealthCheck(healthCheckData);
|
setHealthCheck(healthCheckData);
|
||||||
}
|
} else {
|
||||||
setBreadcrumb(instanceGroup, details);
|
|
||||||
setForks(
|
|
||||||
computeForks(
|
|
||||||
details.mem_capacity,
|
|
||||||
details.cpu_capacity,
|
|
||||||
details.capacity_adjustment
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return { instance: details };
|
|
||||||
}
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`This instance is not associated with this instance group`
|
`This instance is not associated with this instance group`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBreadcrumb(instanceGroup, instanceDetails);
|
||||||
|
setForks(
|
||||||
|
computeForks(
|
||||||
|
instanceDetails.mem_capacity,
|
||||||
|
instanceDetails.cpu_capacity,
|
||||||
|
instanceDetails.capacity_adjustment
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return { instance: instanceDetails };
|
||||||
}, [instanceId, setBreadcrumb, instanceGroup]),
|
}, [instanceId, setBreadcrumb, instanceGroup]),
|
||||||
{ instance: {}, isLoading: true }
|
{ instance: {}, isLoading: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -181,7 +181,6 @@ function Inventory({ setBreadcrumb }) {
|
|||||||
>
|
>
|
||||||
<RelatedTemplateList
|
<RelatedTemplateList
|
||||||
searchParams={{ inventory__id: inventory.id }}
|
searchParams={{ inventory__id: inventory.id }}
|
||||||
resourceName={inventory.name}
|
|
||||||
/>
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
<Route path="*" key="not-found">
|
<Route path="*" key="not-found">
|
||||||
|
|||||||
@@ -187,9 +187,7 @@ 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
|
||||||
if (location.search) {
|
|
||||||
setIsFollowModeEnabled(false);
|
setIsFollowModeEnabled(false);
|
||||||
}
|
|
||||||
Promise.allSettled(pendingRequests).then(() => {
|
Promise.allSettled(pendingRequests).then(() => {
|
||||||
setRemoteRowCount(0);
|
setRemoteRowCount(0);
|
||||||
clearLoadedEvents();
|
clearLoadedEvents();
|
||||||
@@ -253,9 +251,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
});
|
});
|
||||||
const updated = oldWsEvents.concat(newEvents);
|
const updated = oldWsEvents.concat(newEvents);
|
||||||
jobSocketCounter.current = updated.length;
|
jobSocketCounter.current = updated.length;
|
||||||
if (!oldWsEvents.length && min > remoteRowCount + 1) {
|
|
||||||
loadJobEvents(min);
|
|
||||||
}
|
|
||||||
return updated.sort((a, b) => a.counter - b.counter);
|
return updated.sort((a, b) => a.counter - b.counter);
|
||||||
});
|
});
|
||||||
setCssMap((prevCssMap) => ({
|
setCssMap((prevCssMap) => ({
|
||||||
@@ -363,7 +358,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadJobEvents = async (firstWsCounter = null) => {
|
const loadJobEvents = async () => {
|
||||||
const [params, loadRange] = getEventRequestParams(job, 50, [1, 50]);
|
const [params, loadRange] = getEventRequestParams(job, 50, [1, 50]);
|
||||||
|
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
@@ -376,9 +371,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
if (isFlatMode) {
|
if (isFlatMode) {
|
||||||
params.not__stdout = '';
|
params.not__stdout = '';
|
||||||
}
|
}
|
||||||
if (firstWsCounter) {
|
|
||||||
params.counter__lt = firstWsCounter;
|
|
||||||
}
|
|
||||||
const qsParams = parseQueryString(QS_CONFIG, location.search);
|
const qsParams = parseQueryString(QS_CONFIG, location.search);
|
||||||
const eventPromise = getJobModel(job.type).readEvents(job.id, {
|
const eventPromise = getJobModel(job.type).readEvents(job.id, {
|
||||||
...params,
|
...params,
|
||||||
@@ -443,7 +435,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
if (getEvent(counter)) {
|
if (getEvent(counter)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (index >= remoteRowCount && index < remoteRowCount + wsEvents.length) {
|
if (index > remoteRowCount && index < remoteRowCount + wsEvents.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return currentlyLoading.includes(counter);
|
return currentlyLoading.includes(counter);
|
||||||
@@ -470,7 +462,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!event &&
|
!event &&
|
||||||
index >= remoteRowCount &&
|
index > remoteRowCount &&
|
||||||
index < remoteRowCount + wsEvents.length
|
index < remoteRowCount + wsEvents.length
|
||||||
) {
|
) {
|
||||||
event = wsEvents[index - remoteRowCount];
|
event = wsEvents[index - remoteRowCount];
|
||||||
@@ -637,14 +629,10 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
setIsFollowModeEnabled(false);
|
setIsFollowModeEnabled(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToEnd = useCallback(() => {
|
const scrollToEnd = () => {
|
||||||
scrollToRow(-1);
|
scrollToRow(-1);
|
||||||
let timeout;
|
|
||||||
if (isFollowModeEnabled) {
|
|
||||||
setTimeout(() => scrollToRow(-1), 100);
|
setTimeout(() => scrollToRow(-1), 100);
|
||||||
}
|
};
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, [isFollowModeEnabled]);
|
|
||||||
|
|
||||||
const handleScrollLast = () => {
|
const handleScrollLast = () => {
|
||||||
scrollToEnd();
|
scrollToEnd();
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ function Project({ setBreadcrumb }) {
|
|||||||
searchParams={{
|
searchParams={{
|
||||||
project__id: project.id,
|
project__id: project.id,
|
||||||
}}
|
}}
|
||||||
resourceName={project.name}
|
projectName={project.name}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
{project?.scm_type && project.scm_type !== '' && (
|
{project?.scm_type && project.scm_type !== '' && (
|
||||||
|
|||||||
@@ -141,14 +141,14 @@ function JobsEdit() {
|
|||||||
<FormColumnLayout>
|
<FormColumnLayout>
|
||||||
<InputField
|
<InputField
|
||||||
name="AWX_ISOLATION_BASE_PATH"
|
name="AWX_ISOLATION_BASE_PATH"
|
||||||
config={jobs.AWX_ISOLATION_BASE_PATH ?? null}
|
config={jobs.AWX_ISOLATION_BASE_PATH}
|
||||||
isRequired={Boolean(options?.AWX_ISOLATION_BASE_PATH)}
|
isRequired
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="SCHEDULE_MAX_JOBS"
|
name="SCHEDULE_MAX_JOBS"
|
||||||
config={jobs.SCHEDULE_MAX_JOBS ?? null}
|
config={jobs.SCHEDULE_MAX_JOBS}
|
||||||
type={options?.SCHEDULE_MAX_JOBS ? 'number' : undefined}
|
type="number"
|
||||||
isRequired={Boolean(options?.SCHEDULE_MAX_JOBS)}
|
isRequired
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="DEFAULT_JOB_TIMEOUT"
|
name="DEFAULT_JOB_TIMEOUT"
|
||||||
|
|||||||
@@ -122,22 +122,4 @@ describe('<JobsEdit />', () => {
|
|||||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||||
expect(wrapper.find('ContentError').length).toBe(1);
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Form input fields that are invisible (due to being set manually via a settings file) should not prevent submitting the form', async () => {
|
|
||||||
const mockOptions = Object.assign({}, mockAllOptions);
|
|
||||||
// If AWX_ISOLATION_BASE_PATH has been set in a settings file it will be absent in the PUT options
|
|
||||||
delete mockOptions['actions']['PUT']['AWX_ISOLATION_BASE_PATH'];
|
|
||||||
await act(async () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<SettingsProvider value={mockOptions.actions}>
|
|
||||||
<JobsEdit />
|
|
||||||
</SettingsProvider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
|
||||||
await act(async () => {
|
|
||||||
wrapper.find('Form').invoke('onSubmit')();
|
|
||||||
});
|
|
||||||
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -397,10 +397,7 @@ const InputField = ({ name, config, type = 'text', isRequired = false }) => {
|
|||||||
};
|
};
|
||||||
InputField.propTypes = {
|
InputField.propTypes = {
|
||||||
name: string.isRequired,
|
name: string.isRequired,
|
||||||
config: shape({}),
|
config: shape({}).isRequired,
|
||||||
};
|
|
||||||
InputField.defaultProps = {
|
|
||||||
config: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TextAreaField = ({ name, config, isRequired = false }) => {
|
const TextAreaField = ({ name, config, isRequired = false }) => {
|
||||||
|
|||||||
@@ -9,31 +9,29 @@ function JobTemplateAdd() {
|
|||||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const resourceParams = {
|
const projectParams = {
|
||||||
resource_id: null,
|
project_id: null,
|
||||||
resource_name: null,
|
project_name: null,
|
||||||
resource_type: null,
|
|
||||||
resource_kind: null,
|
|
||||||
};
|
};
|
||||||
history.location.search
|
history.location.search
|
||||||
.replace(/^\?/, '')
|
.replace(/^\?/, '')
|
||||||
.split('&')
|
.split('&')
|
||||||
.map((s) => s.split('='))
|
.map((s) => s.split('='))
|
||||||
.forEach(([key, val]) => {
|
.forEach(([key, val]) => {
|
||||||
if (!(key in resourceParams)) {
|
if (!(key in projectParams)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resourceParams[key] = decodeURIComponent(val);
|
projectParams[key] = decodeURIComponent(val);
|
||||||
});
|
});
|
||||||
|
|
||||||
let resourceValues = null;
|
let projectValues = null;
|
||||||
|
|
||||||
if (history.location.search.includes('resource_id' && 'resource_name')) {
|
if (
|
||||||
resourceValues = {
|
Object.values(projectParams).filter((item) => item !== null).length === 2
|
||||||
id: resourceParams.resource_id,
|
) {
|
||||||
name: resourceParams.resource_name,
|
projectValues = {
|
||||||
type: resourceParams.resource_type,
|
id: projectParams.project_id,
|
||||||
kind: resourceParams.resource_kind, // refers to credential kind
|
name: projectParams.project_name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +122,7 @@ function JobTemplateAdd() {
|
|||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
submitError={formSubmitError}
|
submitError={formSubmitError}
|
||||||
resourceValues={resourceValues}
|
projectValues={projectValues}
|
||||||
isOverrideDisabledLookup
|
isOverrideDisabledLookup
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -274,14 +274,9 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
test('should parse and pre-fill project field from query params', async () => {
|
test('should parse and pre-fill project field from query params', async () => {
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: [
|
initialEntries: [
|
||||||
'/templates/job_template/add?resource_id=6&resource_name=Demo%20Project&resource_type=project',
|
'/templates/job_template/add/add?project_id=6&project_name=Demo%20Project',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
ProjectsAPI.read.mockResolvedValueOnce({
|
|
||||||
count: 1,
|
|
||||||
results: [{ name: 'foo', id: 1, allow_override: true, organization: 1 }],
|
|
||||||
});
|
|
||||||
ProjectsAPI.readOptions.mockResolvedValueOnce({});
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<JobTemplateAdd />, {
|
wrapper = mountWithContexts(<JobTemplateAdd />, {
|
||||||
@@ -289,9 +284,8 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0);
|
await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0);
|
||||||
|
|
||||||
expect(wrapper.find('input#project').prop('value')).toEqual('Demo Project');
|
expect(wrapper.find('input#project').prop('value')).toEqual('Demo Project');
|
||||||
expect(ProjectsAPI.readPlaybooks).toBeCalledWith(6);
|
expect(ProjectsAPI.readPlaybooks).toBeCalledWith('6');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
|
test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
|
||||||
|
|||||||
@@ -690,7 +690,7 @@ JobTemplateForm.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FormikApp = withFormik({
|
const FormikApp = withFormik({
|
||||||
mapPropsToValues({ resourceValues = null, template = {} }) {
|
mapPropsToValues({ projectValues = {}, template = {} }) {
|
||||||
const {
|
const {
|
||||||
summary_fields = {
|
summary_fields = {
|
||||||
labels: { results: [] },
|
labels: { results: [] },
|
||||||
@@ -698,7 +698,7 @@ const FormikApp = withFormik({
|
|||||||
},
|
},
|
||||||
} = template;
|
} = template;
|
||||||
|
|
||||||
const initialValues = {
|
return {
|
||||||
allow_callbacks: template.allow_callbacks || false,
|
allow_callbacks: template.allow_callbacks || false,
|
||||||
allow_simultaneous: template.allow_simultaneous || false,
|
allow_simultaneous: template.allow_simultaneous || false,
|
||||||
ask_credential_on_launch: template.ask_credential_on_launch || false,
|
ask_credential_on_launch: template.ask_credential_on_launch || false,
|
||||||
@@ -739,7 +739,7 @@ const FormikApp = withFormik({
|
|||||||
playbook: template.playbook || '',
|
playbook: template.playbook || '',
|
||||||
prevent_instance_group_fallback:
|
prevent_instance_group_fallback:
|
||||||
template.prevent_instance_group_fallback || false,
|
template.prevent_instance_group_fallback || false,
|
||||||
project: summary_fields?.project || null,
|
project: summary_fields?.project || projectValues || null,
|
||||||
scm_branch: template.scm_branch || '',
|
scm_branch: template.scm_branch || '',
|
||||||
skip_tags: template.skip_tags || '',
|
skip_tags: template.skip_tags || '',
|
||||||
timeout: template.timeout || 0,
|
timeout: template.timeout || 0,
|
||||||
@@ -756,24 +756,6 @@ const FormikApp = withFormik({
|
|||||||
execution_environment:
|
execution_environment:
|
||||||
template.summary_fields?.execution_environment || null,
|
template.summary_fields?.execution_environment || null,
|
||||||
};
|
};
|
||||||
if (resourceValues !== null) {
|
|
||||||
if (resourceValues.type === 'credentials') {
|
|
||||||
initialValues[resourceValues.type] = [
|
|
||||||
{
|
|
||||||
id: parseInt(resourceValues.id, 10),
|
|
||||||
name: resourceValues.name,
|
|
||||||
kind: resourceValues.kind,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
initialValues[resourceValues.type] = {
|
|
||||||
id: parseInt(resourceValues.id, 10),
|
|
||||||
name: resourceValues.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return initialValues;
|
|
||||||
},
|
},
|
||||||
handleSubmit: async (values, { props, setErrors }) => {
|
handleSubmit: async (values, { props, setErrors }) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -46,216 +46,90 @@ action_groups:
|
|||||||
plugin_routing:
|
plugin_routing:
|
||||||
inventory:
|
inventory:
|
||||||
tower:
|
tower:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* plugins have been deprecated, use awx.awx.controller instead.
|
|
||||||
redirect: awx.awx.controller
|
redirect: awx.awx.controller
|
||||||
lookup:
|
lookup:
|
||||||
tower_api:
|
tower_api:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* plugins have been deprecated, use awx.awx.controller_api instead.
|
|
||||||
redirect: awx.awx.controller_api
|
redirect: awx.awx.controller_api
|
||||||
tower_schedule_rrule:
|
tower_schedule_rrule:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* plugins have been deprecated, use awx.awx.schedule_rrule instead.
|
|
||||||
redirect: awx.awx.schedule_rrule
|
redirect: awx.awx.schedule_rrule
|
||||||
modules:
|
modules:
|
||||||
tower_ad_hoc_command_cancel:
|
tower_ad_hoc_command_cancel:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.ad_hoc_command_cancel instead.
|
|
||||||
redirect: awx.awx.ad_hoc_command_cancel
|
redirect: awx.awx.ad_hoc_command_cancel
|
||||||
tower_ad_hoc_command_wait:
|
tower_ad_hoc_command_wait:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.ad_hoc_command_wait instead.
|
|
||||||
redirect: awx.awx.ad_hoc_command_wait
|
redirect: awx.awx.ad_hoc_command_wait
|
||||||
tower_ad_hoc_command:
|
tower_ad_hoc_command:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.ad_hoc_command instead.
|
|
||||||
redirect: awx.awx.ad_hoc_command
|
redirect: awx.awx.ad_hoc_command
|
||||||
tower_application:
|
tower_application:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.application instead.
|
|
||||||
redirect: awx.awx.application
|
redirect: awx.awx.application
|
||||||
tower_meta:
|
tower_meta:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.controller_meta instead.
|
|
||||||
redirect: awx.awx.controller_meta
|
redirect: awx.awx.controller_meta
|
||||||
tower_credential_input_source:
|
tower_credential_input_source:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.credential_input_source instead.
|
|
||||||
redirect: awx.awx.credential_input_source
|
redirect: awx.awx.credential_input_source
|
||||||
tower_credential_type:
|
tower_credential_type:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.credential_type instead.
|
|
||||||
redirect: awx.awx.credential_type
|
redirect: awx.awx.credential_type
|
||||||
tower_credential:
|
tower_credential:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.credential instead.
|
|
||||||
redirect: awx.awx.credential
|
redirect: awx.awx.credential
|
||||||
tower_execution_environment:
|
tower_execution_environment:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.execution_environment instead.
|
|
||||||
redirect: awx.awx.execution_environment
|
redirect: awx.awx.execution_environment
|
||||||
tower_export:
|
tower_export:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.export instead.
|
|
||||||
redirect: awx.awx.export
|
redirect: awx.awx.export
|
||||||
tower_group:
|
tower_group:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.group instead.
|
|
||||||
redirect: awx.awx.group
|
redirect: awx.awx.group
|
||||||
tower_host:
|
tower_host:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.host instead.
|
|
||||||
redirect: awx.awx.host
|
redirect: awx.awx.host
|
||||||
tower_import:
|
tower_import:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.import instead.
|
|
||||||
redirect: awx.awx.import
|
redirect: awx.awx.import
|
||||||
tower_instance_group:
|
tower_instance_group:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.instance_group instead.
|
|
||||||
redirect: awx.awx.instance_group
|
redirect: awx.awx.instance_group
|
||||||
tower_inventory_source_update:
|
tower_inventory_source_update:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.inventory_source_update instead.
|
|
||||||
redirect: awx.awx.inventory_source_update
|
redirect: awx.awx.inventory_source_update
|
||||||
tower_inventory_source:
|
tower_inventory_source:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.inventory_source instead.
|
|
||||||
redirect: awx.awx.inventory_source
|
redirect: awx.awx.inventory_source
|
||||||
tower_inventory:
|
tower_inventory:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.inventory instead.
|
|
||||||
redirect: awx.awx.inventory
|
redirect: awx.awx.inventory
|
||||||
tower_job_cancel:
|
tower_job_cancel:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.job_cancel instead.
|
|
||||||
redirect: awx.awx.job_cancel
|
redirect: awx.awx.job_cancel
|
||||||
tower_job_launch:
|
tower_job_launch:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.job_launch instead.
|
|
||||||
redirect: awx.awx.job_launch
|
redirect: awx.awx.job_launch
|
||||||
tower_job_list:
|
tower_job_list:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.job_list instead.
|
|
||||||
redirect: awx.awx.job_list
|
redirect: awx.awx.job_list
|
||||||
tower_job_template:
|
tower_job_template:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.job_template instead.
|
|
||||||
redirect: awx.awx.job_template
|
redirect: awx.awx.job_template
|
||||||
tower_job_wait:
|
tower_job_wait:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.job_wait instead.
|
|
||||||
redirect: awx.awx.job_wait
|
redirect: awx.awx.job_wait
|
||||||
tower_label:
|
tower_label:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.label instead.
|
|
||||||
redirect: awx.awx.label
|
redirect: awx.awx.label
|
||||||
tower_license:
|
tower_license:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.license instead.
|
|
||||||
redirect: awx.awx.license
|
redirect: awx.awx.license
|
||||||
tower_notification_template:
|
tower_notification_template:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.notification_template instead.
|
|
||||||
redirect: awx.awx.notification_template
|
redirect: awx.awx.notification_template
|
||||||
tower_notification:
|
tower_notification:
|
||||||
redirect: awx.awx.notification_template
|
redirect: awx.awx.notification_template
|
||||||
tower_organization:
|
tower_organization:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.organization instead.
|
|
||||||
redirect: awx.awx.organization
|
redirect: awx.awx.organization
|
||||||
tower_project_update:
|
tower_project_update:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.project_update instead.
|
|
||||||
redirect: awx.awx.project_update
|
redirect: awx.awx.project_update
|
||||||
tower_project:
|
tower_project:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.project instead.
|
|
||||||
redirect: awx.awx.project
|
redirect: awx.awx.project
|
||||||
tower_role:
|
tower_role:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.role instead.
|
|
||||||
redirect: awx.awx.role
|
redirect: awx.awx.role
|
||||||
tower_schedule:
|
tower_schedule:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.schedule instead.
|
|
||||||
redirect: awx.awx.schedule
|
redirect: awx.awx.schedule
|
||||||
tower_settings:
|
tower_settings:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.settings instead.
|
|
||||||
redirect: awx.awx.settings
|
redirect: awx.awx.settings
|
||||||
tower_team:
|
tower_team:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.team instead.
|
|
||||||
redirect: awx.awx.team
|
redirect: awx.awx.team
|
||||||
tower_token:
|
tower_token:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.token instead.
|
|
||||||
redirect: awx.awx.token
|
redirect: awx.awx.token
|
||||||
tower_user:
|
tower_user:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.user instead.
|
|
||||||
redirect: awx.awx.user
|
redirect: awx.awx.user
|
||||||
tower_workflow_approval:
|
tower_workflow_approval:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_approval instead.
|
|
||||||
redirect: awx.awx.workflow_approval
|
redirect: awx.awx.workflow_approval
|
||||||
tower_workflow_job_template_node:
|
tower_workflow_job_template_node:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_job_template_node instead.
|
|
||||||
redirect: awx.awx.workflow_job_template_node
|
redirect: awx.awx.workflow_job_template_node
|
||||||
tower_workflow_job_template:
|
tower_workflow_job_template:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_job_template instead.
|
|
||||||
redirect: awx.awx.workflow_job_template
|
redirect: awx.awx.workflow_job_template
|
||||||
tower_workflow_launch:
|
tower_workflow_launch:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_launch instead.
|
|
||||||
redirect: awx.awx.workflow_launch
|
redirect: awx.awx.workflow_launch
|
||||||
tower_workflow_node_wait:
|
tower_workflow_node_wait:
|
||||||
deprecation:
|
|
||||||
removal_date: '2022-01-23'
|
|
||||||
warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_node_wait instead.
|
|
||||||
redirect: awx.awx.workflow_node_wait
|
redirect: awx.awx.workflow_node_wait
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ def main():
|
|||||||
description = module.params.get('description')
|
description = module.params.get('description')
|
||||||
state = module.params.pop('state')
|
state = module.params.pop('state')
|
||||||
preserve_existing_hosts = module.params.get('preserve_existing_hosts')
|
preserve_existing_hosts = module.params.get('preserve_existing_hosts')
|
||||||
preserve_existing_children = module.params.get('preserve_existing_children')
|
preserve_existing_children = module.params.get('preserve_existing_groups')
|
||||||
variables = module.params.get('variables')
|
variables = module.params.get('variables')
|
||||||
|
|
||||||
# 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)
|
||||||
|
|||||||
1
awx_collection/plugins/modules/tower_ad_hoc_command.py
Symbolic link
1
awx_collection/plugins/modules/tower_ad_hoc_command.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
ad_hoc_command.py
|
||||||
1
awx_collection/plugins/modules/tower_ad_hoc_command_cancel.py
Symbolic link
1
awx_collection/plugins/modules/tower_ad_hoc_command_cancel.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
ad_hoc_command_cancel.py
|
||||||
1
awx_collection/plugins/modules/tower_ad_hoc_command_wait.py
Symbolic link
1
awx_collection/plugins/modules/tower_ad_hoc_command_wait.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
ad_hoc_command_wait.py
|
||||||
1
awx_collection/plugins/modules/tower_application.py
Symbolic link
1
awx_collection/plugins/modules/tower_application.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
application.py
|
||||||
1
awx_collection/plugins/modules/tower_controller_meta.py
Symbolic link
1
awx_collection/plugins/modules/tower_controller_meta.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
controller_meta.py
|
||||||
1
awx_collection/plugins/modules/tower_credential.py
Symbolic link
1
awx_collection/plugins/modules/tower_credential.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
credential.py
|
||||||
1
awx_collection/plugins/modules/tower_credential_input_source.py
Symbolic link
1
awx_collection/plugins/modules/tower_credential_input_source.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
credential_input_source.py
|
||||||
1
awx_collection/plugins/modules/tower_credential_type.py
Symbolic link
1
awx_collection/plugins/modules/tower_credential_type.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
credential_type.py
|
||||||
1
awx_collection/plugins/modules/tower_execution_environment.py
Symbolic link
1
awx_collection/plugins/modules/tower_execution_environment.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
execution_environment.py
|
||||||
1
awx_collection/plugins/modules/tower_export.py
Symbolic link
1
awx_collection/plugins/modules/tower_export.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
export.py
|
||||||
1
awx_collection/plugins/modules/tower_group.py
Symbolic link
1
awx_collection/plugins/modules/tower_group.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
group.py
|
||||||
1
awx_collection/plugins/modules/tower_host.py
Symbolic link
1
awx_collection/plugins/modules/tower_host.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
host.py
|
||||||
1
awx_collection/plugins/modules/tower_import.py
Symbolic link
1
awx_collection/plugins/modules/tower_import.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
import.py
|
||||||
1
awx_collection/plugins/modules/tower_instance_group.py
Symbolic link
1
awx_collection/plugins/modules/tower_instance_group.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
instance_group.py
|
||||||
1
awx_collection/plugins/modules/tower_inventory.py
Symbolic link
1
awx_collection/plugins/modules/tower_inventory.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
inventory.py
|
||||||
1
awx_collection/plugins/modules/tower_inventory_source.py
Symbolic link
1
awx_collection/plugins/modules/tower_inventory_source.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
inventory_source.py
|
||||||
1
awx_collection/plugins/modules/tower_inventory_source_update.py
Symbolic link
1
awx_collection/plugins/modules/tower_inventory_source_update.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
inventory_source_update.py
|
||||||
1
awx_collection/plugins/modules/tower_job_cancel.py
Symbolic link
1
awx_collection/plugins/modules/tower_job_cancel.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
job_cancel.py
|
||||||
1
awx_collection/plugins/modules/tower_job_launch.py
Symbolic link
1
awx_collection/plugins/modules/tower_job_launch.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
job_launch.py
|
||||||
1
awx_collection/plugins/modules/tower_job_list.py
Symbolic link
1
awx_collection/plugins/modules/tower_job_list.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
job_list.py
|
||||||
1
awx_collection/plugins/modules/tower_job_template.py
Symbolic link
1
awx_collection/plugins/modules/tower_job_template.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
job_template.py
|
||||||
1
awx_collection/plugins/modules/tower_job_wait.py
Symbolic link
1
awx_collection/plugins/modules/tower_job_wait.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
job_wait.py
|
||||||
1
awx_collection/plugins/modules/tower_label.py
Symbolic link
1
awx_collection/plugins/modules/tower_label.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
label.py
|
||||||
1
awx_collection/plugins/modules/tower_license.py
Symbolic link
1
awx_collection/plugins/modules/tower_license.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
license.py
|
||||||
1
awx_collection/plugins/modules/tower_notification_template.py
Symbolic link
1
awx_collection/plugins/modules/tower_notification_template.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
notification_template.py
|
||||||
1
awx_collection/plugins/modules/tower_organization.py
Symbolic link
1
awx_collection/plugins/modules/tower_organization.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
organization.py
|
||||||
1
awx_collection/plugins/modules/tower_project.py
Symbolic link
1
awx_collection/plugins/modules/tower_project.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
project.py
|
||||||
1
awx_collection/plugins/modules/tower_project_update.py
Symbolic link
1
awx_collection/plugins/modules/tower_project_update.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
project_update.py
|
||||||
1
awx_collection/plugins/modules/tower_role.py
Symbolic link
1
awx_collection/plugins/modules/tower_role.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
role.py
|
||||||
1
awx_collection/plugins/modules/tower_schedule.py
Symbolic link
1
awx_collection/plugins/modules/tower_schedule.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
schedule.py
|
||||||
1
awx_collection/plugins/modules/tower_settings.py
Symbolic link
1
awx_collection/plugins/modules/tower_settings.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
settings.py
|
||||||
1
awx_collection/plugins/modules/tower_team.py
Symbolic link
1
awx_collection/plugins/modules/tower_team.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
team.py
|
||||||
1
awx_collection/plugins/modules/tower_token.py
Symbolic link
1
awx_collection/plugins/modules/tower_token.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
token.py
|
||||||
1
awx_collection/plugins/modules/tower_user.py
Symbolic link
1
awx_collection/plugins/modules/tower_user.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
user.py
|
||||||
1
awx_collection/plugins/modules/tower_workflow_approval.py
Symbolic link
1
awx_collection/plugins/modules/tower_workflow_approval.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
workflow_approval.py
|
||||||
1
awx_collection/plugins/modules/tower_workflow_job_template.py
Symbolic link
1
awx_collection/plugins/modules/tower_workflow_job_template.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
workflow_job_template.py
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
workflow_job_template_node.py
|
||||||
1
awx_collection/plugins/modules/tower_workflow_launch.py
Symbolic link
1
awx_collection/plugins/modules/tower_workflow_launch.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
workflow_launch.py
|
||||||
1
awx_collection/plugins/modules/tower_workflow_node_wait.py
Symbolic link
1
awx_collection/plugins/modules/tower_workflow_node_wait.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
workflow_node_wait.py
|
||||||
@@ -19,6 +19,7 @@ author: "John Westcott IV (@john-westcott-iv)"
|
|||||||
short_description: create, update, or destroy Automation Platform Controller workflow job templates.
|
short_description: create, update, or destroy Automation Platform Controller workflow job templates.
|
||||||
description:
|
description:
|
||||||
- Create, update, or destroy Automation Platform Controller workflow job templates.
|
- Create, update, or destroy Automation Platform Controller workflow job templates.
|
||||||
|
- Replaces the deprecated tower_workflow_template module.
|
||||||
- Use workflow_job_template_node after this, or use the workflow_nodes parameter to build the workflow's graph
|
- Use workflow_job_template_node after this, or use the workflow_nodes parameter to build the workflow's graph
|
||||||
options:
|
options:
|
||||||
name:
|
name:
|
||||||
@@ -613,10 +614,6 @@ def create_workflow_nodes(module, response, workflow_nodes, workflow_id):
|
|||||||
if workflow_node['unified_job_template']['type'] != 'workflow_approval':
|
if workflow_node['unified_job_template']['type'] != 'workflow_approval':
|
||||||
module.fail_json(msg="Unable to Find unified_job_template: {0}".format(search_fields))
|
module.fail_json(msg="Unable to Find unified_job_template: {0}".format(search_fields))
|
||||||
|
|
||||||
inventory = workflow_node.get('inventory')
|
|
||||||
if inventory:
|
|
||||||
workflow_node_fields['inventory'] = module.resolve_name_to_id('inventories', inventory)
|
|
||||||
|
|
||||||
# Lookup Values for other fields
|
# Lookup Values for other fields
|
||||||
|
|
||||||
for field_name in (
|
for field_name in (
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ short_description: create, update, or destroy Automation Platform Controller wor
|
|||||||
description:
|
description:
|
||||||
- Create, update, or destroy Automation Platform Controller workflow job template nodes.
|
- Create, update, or destroy Automation Platform Controller workflow job template nodes.
|
||||||
- Use this to build a graph for a workflow, which dictates what the workflow runs.
|
- Use this to build a graph for a workflow, which dictates what the workflow runs.
|
||||||
|
- Replaces the deprecated tower_workflow_template module schema command.
|
||||||
- You can create nodes first, and link them afterwards, and not worry about ordering.
|
- You can create nodes first, and link them afterwards, and not worry about ordering.
|
||||||
For failsafe referencing of a node, specify identifier, WFJT, and organization.
|
For failsafe referencing of a node, specify identifier, WFJT, and organization.
|
||||||
With those specified, you can choose to modify or not modify any other parameter.
|
With those specified, you can choose to modify or not modify any other parameter.
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ Notable releases of the `{{ collection_namespace }}.{{ collection_package }}` co
|
|||||||
- 7.0.0 is intended to be identical to the content prior to the migration, aside from changes necessary to function as a collection.
|
- 7.0.0 is intended to be identical to the content prior to the migration, aside from changes necessary to function as a collection.
|
||||||
- 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/).
|
- 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/).
|
||||||
- 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names
|
- 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names
|
||||||
- 21.11.0 "tower" modules deprecated and symlinks removed.
|
|
||||||
- 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable.
|
- 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable.
|
||||||
{% else %}
|
{% else %}
|
||||||
- 3.7.0 initial release
|
- 3.7.0 initial release
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
awxkit
|
awxkit
|
||||||
======
|
======
|
||||||
|
|
||||||
A Python library that backs the provided `awx` command line client.
|
Python library that backs the provided `awx` command line client.
|
||||||
|
|
||||||
It can be installed by running `pip install awxkit`.
|
|
||||||
|
|
||||||
The PyPI respository can be found [here](https://pypi.org/project/awxkit/).
|
|
||||||
|
|
||||||
For more information on installing the CLI and building the docs on how to use it, look [here](./awxkit/cli/docs).
|
For more information on installing the CLI and building the docs on how to use it, look [here](./awxkit/cli/docs).
|
||||||
Reference in New Issue
Block a user