diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a53e50174..7c929aefc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/`. +## 14.1.0 (Aug 25, 2020) +- AWX images can now be built on ARM64 - https://github.com/ansible/awx/pull/7607 +- Added the Remote Archive SCM Type to support using immutable artifacts and releases (such as tarballs and zip files) as projects - https://github.com/ansible/awx/issues/7954 +- Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 +- Added resource import/export support to the official AWX collection - https://github.com/ansible/awx/issues/7329 +- Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 +- Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847 +- Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 +- Upgraded git-python to fix a bug that caused workflows to sometimes fail - https://github.com/ansible/awx/issues/6119 +- Worked around a bug in the channels_redis library that slowly causes Daphne processes to leak memory over time - https://github.com/django/channels_redis/issues/212 +- Fixed a bug in the AWX CLI that prevented Workflow nodes from importing properly - https://github.com/ansible/awx/issues/7793 +- Fixed a bug in the awx.awx collection release process that templated the wrong version - https://github.com/ansible/awx/issues/7870 +- Fixed a bug that caused errors rendering stdout that contained UTF-16 surrogate pairs - https://github.com/ansible/awx/pull/7918 + ## 14.0.0 (Aug 6, 2020) - As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to: * https://github.com/ansible/awx/commit/78229f58715fbfbf88177e54031f532543b57acc diff --git a/VERSION b/VERSION index 4b964e9654..7b3b6e02bb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -14.0.0 +14.1.0 diff --git a/awx/api/renderers.py b/awx/api/renderers.py index bd4136a76a..92d59e2c7b 100644 --- a/awx/api/renderers.py +++ b/awx/api/renderers.py @@ -7,6 +7,24 @@ from prometheus_client.parser import text_string_to_metric_families # Django REST Framework from rest_framework import renderers from rest_framework.request import override_method +from rest_framework.utils import encoders + + +class SurrogateEncoder(encoders.JSONEncoder): + + def encode(self, obj): + ret = super(SurrogateEncoder, self).encode(obj) + try: + ret.encode() + except UnicodeEncodeError as e: + if 'surrogates not allowed' in e.reason: + ret = ret.encode('utf-8', 'replace').decode() + return ret + + +class DefaultJSONRenderer(renderers.JSONRenderer): + + encoder_class = SurrogateEncoder class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer): diff --git a/awx/api/serializers.py b/awx/api/serializers.py index be6a9d640b..429bf512be 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1336,6 +1336,8 @@ class ProjectOptionsSerializer(BaseSerializer): attrs.pop('local_path', None) if 'local_path' in attrs and attrs['local_path'] not in valid_local_paths: errors['local_path'] = _('This path is already being used by another manual project.') + if attrs.get('scm_branch') and scm_type == 'archive': + errors['scm_branch'] = _('SCM branch cannot be used with archive projects.') if attrs.get('scm_refspec') and scm_type != 'git': errors['scm_refspec'] = _('SCM refspec can only be used with git projects.') diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index f6378f5282..c5b22d105a 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -242,6 +242,8 @@ class DashboardView(APIView): svn_failed_projects = svn_projects.filter(last_job_failed=True) hg_projects = user_projects.filter(scm_type='hg') hg_failed_projects = hg_projects.filter(last_job_failed=True) + archive_projects = user_projects.filter(scm_type='archive') + archive_failed_projects = archive_projects.filter(last_job_failed=True) data['scm_types'] = {} data['scm_types']['git'] = {'url': reverse('api:project_list', request=request) + "?scm_type=git", 'label': 'Git', @@ -258,6 +260,11 @@ class DashboardView(APIView): 'failures_url': reverse('api:project_list', request=request) + "?scm_type=hg&last_job_failed=True", 'total': hg_projects.count(), 'failed': hg_failed_projects.count()} + data['scm_types']['archive'] = {'url': reverse('api:project_list', request=request) + "?scm_type=archive", + 'label': 'Remote Archive', + 'failures_url': reverse('api:project_list', request=request) + "?scm_type=archive&last_job_failed=True", + 'total': archive_projects.count(), + 'failed': archive_failed_projects.count()} user_list = get_user_queryset(request.user, models.User) team_list = get_user_queryset(request.user, models.Team) diff --git a/awx/api/views/inventory.py b/awx/api/views/inventory.py index 987f5467b4..607a71c6d5 100644 --- a/awx/api/views/inventory.py +++ b/awx/api/views/inventory.py @@ -134,7 +134,8 @@ class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, Retri # Do not allow changes to an Inventory kind. if kind is not None and obj.kind != kind: - return self.http_method_not_allowed(request, *args, **kwargs) + return Response(dict(error=_('You cannot turn a regular inventory into a "smart" inventory.')), + status=status.HTTP_405_METHOD_NOT_ALLOWED) return super(InventoryDetail, self).update(request, *args, **kwargs) def destroy(self, request, *args, **kwargs): diff --git a/awx/main/consumers.py b/awx/main/consumers.py index b6d8872ebd..d32219b3ac 100644 --- a/awx/main/consumers.py +++ b/awx/main/consumers.py @@ -1,3 +1,5 @@ +import collections +import functools import json import logging import time @@ -12,12 +14,40 @@ from django.contrib.auth.models import User from channels.generic.websocket import AsyncJsonWebsocketConsumer from channels.layers import get_channel_layer from channels.db import database_sync_to_async +from channels_redis.core import RedisChannelLayer logger = logging.getLogger('awx.main.consumers') XRF_KEY = '_auth_user_xrf' +class BoundedQueue(asyncio.Queue): + + def put_nowait(self, item): + if self.full(): + # dispose the oldest item + # if we actually get into this code block, it likely means that + # this specific consumer has stopped reading + # unfortunately, channels_redis will just happily continue to + # queue messages specific to their channel until the heat death + # of the sun: https://github.com/django/channels_redis/issues/212 + # this isn't a huge deal for browser clients that disconnect, + # but it *does* cause a problem for our global broadcast topic + # that's used to broadcast messages to peers in a cluster + # if we get into this code block, it's better to drop messages + # than to continue to malloc() forever + self.get_nowait() + return super(BoundedQueue, self).put_nowait(item) + + +class ExpiringRedisChannelLayer(RedisChannelLayer): + def __init__(self, *args, **kw): + super(ExpiringRedisChannelLayer, self).__init__(*args, **kw) + self.receive_buffer = collections.defaultdict( + functools.partial(BoundedQueue, self.capacity) + ) + + class WebsocketSecretAuthHelper: """ Middlewareish for websockets to verify node websocket broadcast interconnect. diff --git a/awx/main/isolated/manager.py b/awx/main/isolated/manager.py index 551867986f..623bb5700e 100644 --- a/awx/main/isolated/manager.py +++ b/awx/main/isolated/manager.py @@ -58,7 +58,7 @@ class IsolatedManager(object): os.chmod(temp.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) for host in hosts: inventory['all']['hosts'][host] = { - "ansible_connection": "community.kubernetes.kubectl", + "ansible_connection": "kubectl", "ansible_kubectl_config": path, } else: diff --git a/awx/main/management/commands/remove_from_queue.py b/awx/main/management/commands/remove_from_queue.py index df7530992c..b249749219 100644 --- a/awx/main/management/commands/remove_from_queue.py +++ b/awx/main/management/commands/remove_from_queue.py @@ -32,4 +32,7 @@ class Command(BaseCommand): sys.exit(1) i = i.first() ig.instances.remove(i) + if i.hostname in ig.policy_instance_list: + ig.policy_instance_list.remove(i.hostname) + ig.save() print("Instance removed from instance group") diff --git a/awx/main/migrations/0118_add_remote_archive_scm_type.py b/awx/main/migrations/0118_add_remote_archive_scm_type.py new file mode 100644 index 0000000000..246ca4c823 --- /dev/null +++ b/awx/main/migrations/0118_add_remote_archive_scm_type.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.11 on 2020-08-18 22:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0117_v400_remove_cloudforms_inventory'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='scm_type', + field=models.CharField(blank=True, choices=[('', 'Manual'), ('git', 'Git'), ('hg', 'Mercurial'), ('svn', 'Subversion'), ('insights', 'Red Hat Insights'), ('archive', 'Remote Archive')], default='', help_text='Specifies the source control system used to store the project.', max_length=8, verbose_name='SCM Type'), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_type', + field=models.CharField(blank=True, choices=[('', 'Manual'), ('git', 'Git'), ('hg', 'Mercurial'), ('svn', 'Subversion'), ('insights', 'Red Hat Insights'), ('archive', 'Remote Archive')], default='', help_text='Specifies the source control system used to store the project.', max_length=8, verbose_name='SCM Type'), + ), + ] diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 5d8cfd5290..bc52d4269c 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -55,6 +55,7 @@ class ProjectOptions(models.Model): ('hg', _('Mercurial')), ('svn', _('Subversion')), ('insights', _('Red Hat Insights')), + ('archive', _('Remote Archive')), ] class Meta: diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index c16a1d1fa0..cba6161d83 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -14,8 +14,10 @@ from django.utils.translation import ugettext_lazy as _, gettext_noop from django.utils.timezone import now as tz_now # AWX +from awx.main.dispatch.reaper import reap_job from awx.main.models import ( AdHocCommand, + Instance, InstanceGroup, InventorySource, InventoryUpdate, @@ -515,6 +517,20 @@ class TaskManager(): task.job_explanation = timeout_message task.save(update_fields=['status', 'job_explanation', 'timed_out']) + def reap_jobs_from_orphaned_instances(self): + # discover jobs that are in running state but aren't on an execution node + # that we know about; this is a fairly rare event, but it can occur if you, + # for example, SQL backup an awx install with running jobs and restore it + # elsewhere + for j in UnifiedJob.objects.filter( + status__in=['pending', 'waiting', 'running'], + ).exclude( + execution_node__in=Instance.objects.values_list('hostname', flat=True) + ): + if j.execution_node and not j.is_containerized: + logger.error(f'{j.execution_node} is not a registered instance; reaping {j.log_format}') + reap_job(j, 'failed') + def calculate_capacity_consumed(self, tasks): self.graph = InstanceGroup.objects.capacity_values(tasks=tasks, graph=self.graph) @@ -567,6 +583,7 @@ class TaskManager(): self.spawn_workflow_graph_jobs(running_workflow_tasks) self.timeout_approval_node() + self.reap_jobs_from_orphaned_instances() self.process_tasks(all_sorted_tasks) return finished_wfjs diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 06c740c129..3e70f93c19 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2105,7 +2105,7 @@ class RunProjectUpdate(BaseTask): scm_username = False elif scm_url_parts.scheme.endswith('ssh'): scm_password = False - elif scm_type == 'insights': + elif scm_type in ('insights', 'archive'): extra_vars['scm_username'] = scm_username extra_vars['scm_password'] = scm_password scm_url = update_scm_url(scm_type, scm_url, scm_username, diff --git a/awx/main/tests/functional/analytics/test_projects_by_scm_type.py b/awx/main/tests/functional/analytics/test_projects_by_scm_type.py index 29ffbd6283..1590b5d3bb 100644 --- a/awx/main/tests/functional/analytics/test_projects_by_scm_type.py +++ b/awx/main/tests/functional/analytics/test_projects_by_scm_type.py @@ -12,7 +12,8 @@ def test_empty(): 'git': 0, 'svn': 0, 'hg': 0, - 'insights': 0 + 'insights': 0, + 'archive': 0, } @@ -24,7 +25,8 @@ def test_multiple(scm_type): 'git': 0, 'svn': 0, 'hg': 0, - 'insights': 0 + 'insights': 0, + 'archive': 0, } for i in range(random.randint(0, 10)): Project(scm_type=scm_type).save() diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 71bcd8d03c..7dcea33ccd 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -1792,16 +1792,19 @@ class TestProjectUpdateCredentials(TestJobExecution): dict(scm_type='git'), dict(scm_type='hg'), dict(scm_type='svn'), + dict(scm_type='archive'), ], 'test_ssh_key_auth': [ dict(scm_type='git'), dict(scm_type='hg'), dict(scm_type='svn'), + dict(scm_type='archive'), ], 'test_awx_task_env': [ dict(scm_type='git'), dict(scm_type='hg'), dict(scm_type='svn'), + dict(scm_type='archive'), ] } diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 75fcd306ef..f34cf4e4d8 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -257,7 +257,7 @@ def update_scm_url(scm_type, url, username=True, password=True, # git: https://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS # hg: http://www.selenic.com/mercurial/hg.1.html#url-paths # svn: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.advanced.reposurls - if scm_type not in ('git', 'hg', 'svn', 'insights'): + if scm_type not in ('git', 'hg', 'svn', 'insights', 'archive'): raise ValueError(_('Unsupported SCM type "%s"') % str(scm_type)) if not url.strip(): return '' @@ -303,7 +303,8 @@ def update_scm_url(scm_type, url, username=True, password=True, 'git': ('ssh', 'git', 'git+ssh', 'http', 'https', 'ftp', 'ftps', 'file'), 'hg': ('http', 'https', 'ssh', 'file'), 'svn': ('http', 'https', 'svn', 'svn+ssh', 'file'), - 'insights': ('http', 'https') + 'insights': ('http', 'https'), + 'archive': ('http', 'https'), } if parts.scheme not in scm_type_schemes.get(scm_type, ()): raise ValueError(_('Unsupported %s URL') % scm_type) @@ -339,7 +340,7 @@ def update_scm_url(scm_type, url, username=True, password=True, #raise ValueError('Password not supported for SSH with Mercurial.') netloc_password = '' - if netloc_username and parts.scheme != 'file' and scm_type != "insights": + if netloc_username and parts.scheme != 'file' and scm_type not in ("insights", "archive"): netloc = u':'.join([urllib.parse.quote(x,safe='') for x in (netloc_username, netloc_password) if x]) else: netloc = u'' diff --git a/awx/playbooks/action_plugins/hg_deprecation.py b/awx/playbooks/action_plugins/hg_deprecation.py new file mode 100644 index 0000000000..d4593b2360 --- /dev/null +++ b/awx/playbooks/action_plugins/hg_deprecation.py @@ -0,0 +1,15 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.action import ActionBase + + +class ActionModule(ActionBase): + + def run(self, tmp=None, task_vars=None): + self._supports_check_mode = False + result = super(ActionModule, self).run(tmp, task_vars) + result['changed'] = result['failed'] = False + result['msg'] = '' + self._display.deprecated("Mercurial support is deprecated") + return result diff --git a/awx/playbooks/action_plugins/project_archive.py b/awx/playbooks/action_plugins/project_archive.py new file mode 100644 index 0000000000..753650d12b --- /dev/null +++ b/awx/playbooks/action_plugins/project_archive.py @@ -0,0 +1,82 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import zipfile +import tarfile +import os + +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display + +display = Display() + + +class ActionModule(ActionBase): + def run(self, tmp=None, task_vars=None): + self._supports_check_mode = False + + result = super(ActionModule, self).run(tmp, task_vars) + + src = self._task.args.get("src") + proj_path = self._task.args.get("project_path") + force = self._task.args.get("force", False) + + try: + archive = zipfile.ZipFile(src) + get_filenames = archive.namelist + get_members = archive.infolist + except zipfile.BadZipFile: + archive = tarfile.open(src) + get_filenames = archive.getnames + get_members = archive.getmembers + except tarfile.ReadError: + result["failed"] = True + result["msg"] = "{0} is not a valid archive".format(src) + return result + + # Most well formed archives contain a single root directory, typically named + # project-name-1.0.0. The project contents should be inside that directory. + start_index = 0 + root_contents = set( + [filename.split(os.path.sep)[0] for filename in get_filenames()] + ) + if len(root_contents) == 1: + start_index = len(list(root_contents)[0]) + 1 + + for member in get_members(): + try: + filename = member.filename + except AttributeError: + filename = member.name + + # Skip the archive base directory + if not filename[start_index:]: + continue + + dest = os.path.join(proj_path, filename[start_index:]) + + if not force and os.path.exists(dest): + continue + + try: + is_dir = member.is_dir() + except AttributeError: + is_dir = member.isdir() + + if is_dir: + os.makedirs(dest, exist_ok=True) + else: + try: + member_f = archive.open(member) + except TypeError: + member_f = tarfile.ExFileObject(archive, member) + + with open(dest, "wb") as f: + f.write(member_f.read()) + member_f.close() + + archive.close() + + result["changed"] = True + return result diff --git a/awx/playbooks/library/project_archive.py b/awx/playbooks/library/project_archive.py new file mode 100644 index 0000000000..4a046e354d --- /dev/null +++ b/awx/playbooks/library/project_archive.py @@ -0,0 +1,40 @@ +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "status": ["stableinterface"], + "supported_by": "community", +} + + +DOCUMENTATION = """ +--- +module: project_archive +short_description: unpack a project archive +description: + - Unpacks an archive that contains a project, in order to support handling versioned + artifacts from (for example) GitHub Releases or Artifactory builds. + - Handles projects in the archive root, or in a single base directory of the archive. +version_added: "2.9" +options: + src: + description: + - The source archive of the project artifact + required: true + project_path: + description: + - Directory to write the project archive contents + required: true + force: + description: + - Files in the project_path will be overwritten by matching files in the archive + default: False + +author: + - "Philip Douglass" @philipsd6 +""" + +EXAMPLES = """ +- project_archive: + src: "{{ project_path }}/.archive/project.tar.gz" + project_path: "{{ project_path }}" + force: "{{ scm_clean }}" +""" diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index e572496497..169273d628 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -101,6 +101,50 @@ tags: - update_insights + - block: + - name: Ensure the project archive directory is present + file: + dest: "{{ project_path|quote }}/.archive" + state: directory + + - name: Get archive from url + get_url: + url: "{{ scm_url|quote }}" + dest: "{{ project_path|quote }}/.archive/" + url_username: "{{ scm_username|default(omit) }}" + url_password: "{{ scm_password|default(omit) }}" + force_basic_auth: true + register: get_archive + + - name: Unpack archive + project_archive: + src: "{{ get_archive.dest }}" + project_path: "{{ project_path|quote }}" + force: "{{ scm_clean }}" + when: get_archive.changed or scm_clean + register: unarchived + + - name: Find previous archives + find: + paths: "{{ project_path|quote }}/.archive/" + excludes: + - "{{ get_archive.dest|basename }}" + when: unarchived.changed + register: previous_archive + + - name: Remove previous archives + file: + path: "{{ item.path }}" + state: absent + loop: "{{ previous_archive.files }}" + when: previous_archive.files|default([]) + + - name: Set scm_version to archive sha1 checksum + set_fact: + scm_version: "{{ get_archive.checksum_src }}" + tags: + - update_archive + - name: Repository Version debug: msg: "Repository Version {{ scm_version }}" @@ -109,6 +153,7 @@ - update_hg - update_svn - update_insights + - update_archive - hosts: localhost gather_facts: false diff --git a/awx/playbooks/project_update_hg_tasks.yml b/awx/playbooks/project_update_hg_tasks.yml index 251013698f..3553d60984 100644 --- a/awx/playbooks/project_update_hg_tasks.yml +++ b/awx/playbooks/project_update_hg_tasks.yml @@ -1,4 +1,7 @@ --- +- name: Mercurial support is deprecated. + hg_deprecation: + - name: update project using hg hg: dest: "{{project_path|quote}}" diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index fe7c8c0ba3..716aea3aa7 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -310,7 +310,7 @@ REST_FRAMEWORK = { 'awx.api.parsers.JSONParser', ), 'DEFAULT_RENDERER_CLASSES': ( - 'rest_framework.renderers.JSONRenderer', + 'awx.api.renderers.DefaultJSONRenderer', 'awx.api.renderers.BrowsableAPIRenderer', ), 'DEFAULT_METADATA_CLASS': 'awx.api.metadata.Metadata', @@ -916,7 +916,7 @@ ASGI_APPLICATION = "awx.main.routing.application" CHANNEL_LAYERS = { "default": { - "BACKEND": "channels_redis.core.RedisChannelLayer", + "BACKEND": "awx.main.consumers.ExpiringRedisChannelLayer", "CONFIG": { "hosts": [BROKER_URL], "capacity": 10000, diff --git a/awx/ui/client/src/projects/add/projects-add.controller.js b/awx/ui/client/src/projects/add/projects-add.controller.js index c34a6ecdf2..952cb07974 100644 --- a/awx/ui/client/src/projects/add/projects-add.controller.js +++ b/awx/ui/client/src/projects/add/projects-add.controller.js @@ -23,7 +23,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', $scope.canEditOrg = true; const virtualEnvs = ConfigData.custom_virtualenvs || []; $scope.custom_virtualenvs_options = virtualEnvs; - + const [ProjectModel] = resolvedModels; $scope.canAdd = ProjectModel.options('actions.POST'); @@ -170,6 +170,14 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', $scope.lookupType = 'scm_credential'; $scope.scmBranchLabel = i18n._('SCM Branch/Tag/Revision'); break; + case 'archive': + $scope.credentialLabel = "SCM " + i18n._("Credential"); + $scope.urlPopover = '

' + i18n._('Example URLs for Remote Archive SCM include:') + '

' + + ''; + $scope.credRequired = false; + $scope.lookupType = 'scm_credential'; + break; case 'insights': $scope.pathRequired = false; $scope.scmRequired = false; diff --git a/awx/ui/client/src/projects/edit/projects-edit.controller.js b/awx/ui/client/src/projects/edit/projects-edit.controller.js index 27f6d3b376..8215bbab47 100644 --- a/awx/ui/client/src/projects/edit/projects-edit.controller.js +++ b/awx/ui/client/src/projects/edit/projects-edit.controller.js @@ -291,6 +291,14 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest', $scope.lookupType = 'scm_credential'; $scope.scmBranchLabel = i18n._('SCM Branch/Tag/Revision'); break; + case 'archive': + $scope.credentialLabel = "SCM " + i18n._("Credential"); + $scope.urlPopover = '

' + i18n._('Example URLs for Remote Archive SCM include:') + '

' + + ''; + $scope.credRequired = false; + $scope.lookupType = 'scm_credential'; + break; case 'insights': $scope.pathRequired = false; $scope.scmRequired = false; diff --git a/awx/ui/client/src/projects/projects.form.js b/awx/ui/client/src/projects/projects.form.js index 13ef01d2f0..dac0118c03 100644 --- a/awx/ui/client/src/projects/projects.form.js +++ b/awx/ui/client/src/projects/projects.form.js @@ -124,7 +124,7 @@ export default ['i18n', 'NotificationsList', 'TemplateList', scm_branch: { labelBind: "scmBranchLabel", type: 'text', - ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights'", + ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights' && scm_type.value !== 'archive'", ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)', awPopOver: '

' + i18n._("Branch to checkout. In addition to branches, you can input tags, commit hashes, and arbitrary refs. Some commit hashes and refs may not be availble unless you also provide a custom refspec.") + '

', dataTitle: i18n._('SCM Branch'), diff --git a/awx/ui/package-lock.json b/awx/ui/package-lock.json index 24b7837312..9f6c2322e1 100644 --- a/awx/ui/package-lock.json +++ b/awx/ui/package-lock.json @@ -4,6 +4,28 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "14.0.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.26.tgz", + "integrity": "sha512-W+fpe5s91FBGE0pEa0lnqGLL4USgpLgs4nokw16SrBBco/gQxuua7KnArSEOd5iaMqbbSHV10vUDkJYJJqpXKA==", + "dev": true + }, "@uirouter/angularjs": { "version": "1.0.18", "resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.18.tgz", @@ -740,7 +762,8 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", - "dev": true + "dev": true, + "optional": true }, "axios": { "version": "0.16.2", @@ -2299,16 +2322,215 @@ "dev": true }, "chromedriver": { - "version": "2.40.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-2.40.0.tgz", - "integrity": "sha512-ewvRQ1HMk0vpFSWYCk5hKDoEz5QMPplx5w3C6/Me+03y1imr67l3Hxl9U0jn3mu2N7+c7BoC7JtNW6HzbRAwDQ==", + "version": "77.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-77.0.0.tgz", + "integrity": "sha512-mZa1IVx4HD8rDaItWbnS470mmypgiWsDiu98r0NkiT4uLm3qrANl4vOU6no6vtWtLQiW5kt1POcIbjeNpsLbXA==", "dev": true, "requires": { - "del": "^3.0.0", + "del": "^4.1.1", "extract-zip": "^1.6.7", - "kew": "^0.7.0", "mkdirp": "^0.5.1", - "request": "^2.87.0" + "request": "^2.88.0", + "tcp-port-used": "^1.0.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "aws4": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "dev": true + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "requires": { + "mime-db": "1.44.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "cipher-base": { @@ -6458,16 +6680,6 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", "dev": true }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "dev": true, - "requires": { - "ajv": "^5.1.0", - "har-schema": "^2.0.0" - } - }, "hard-source-webpack-plugin": { "version": "0.5.18", "resolved": "https://registry.npmjs.org/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.5.18.tgz", @@ -7349,6 +7561,12 @@ "integrity": "sha1-x+NWzeoiWucbNtcPLnGpK6TkJZA=", "dev": true }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, "ipaddr.js": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", @@ -7658,6 +7876,12 @@ "unc-path-regex": "^0.1.0" } }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -7676,6 +7900,17 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "dev": true }, + "is2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", + "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "ip-regex": "^2.1.0", + "is-url": "^1.2.2" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -8173,12 +8408,6 @@ } } }, - "kew": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", - "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", - "dev": true - }, "killable": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", @@ -9826,7 +10055,8 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -11196,6 +11426,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, "public-encrypt": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", @@ -11816,34 +12052,6 @@ "is-finite": "^1.0.0" } }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", - "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", - "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -13171,6 +13379,33 @@ "xtend": "^4.0.0" } }, + "tcp-port-used": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.1.tgz", + "integrity": "sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q==", + "dev": true, + "requires": { + "debug": "4.1.0", + "is2": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", + "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "test-exclude": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", @@ -13355,6 +13590,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", "dev": true, + "optional": true, "requires": { "punycode": "^1.4.1" } diff --git a/awx/ui/package.json b/awx/ui/package.json index 59cb964b5c..e546bfc891 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -43,7 +43,7 @@ "babel-loader": "^7.1.2", "babel-plugin-istanbul": "^4.1.5", "babel-preset-env": "^1.6.0", - "chromedriver": "^2.35.0", + "chromedriver": "^77.0.0", "clean-webpack-plugin": "^0.1.16", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.5", diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json index 6e5590c6db..04fc14aa73 100644 --- a/awx/ui_next/package-lock.json +++ b/awx/ui_next/package-lock.json @@ -13,9 +13,9 @@ } }, "@babel/compat-data": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.10.3.tgz", - "integrity": "sha512-BDIfJ9uNZuI0LajPfoYV28lX8kyCPMHY6uY4WH1lJdcicmAfxCK5ASzaeV0D/wsUaRH/cLk+amuxtC37sZ8TUg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz", + "integrity": "sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==", "dev": true, "requires": { "browserslist": "^4.12.0", @@ -105,127 +105,127 @@ } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.3.tgz", - "integrity": "sha512-lo4XXRnBlU6eRM92FkiZxpo1xFLmv3VsPFk61zJKMm7XYJfwqXHsYJTY6agoc4a3L8QPw1HqWehO18coZgbT6A==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", + "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", "dev": true, "requires": { - "@babel/helper-explode-assignable-expression": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-explode-assignable-expression": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-builder-react-jsx": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.3.tgz", - "integrity": "sha512-vkxmuFvmovtqTZknyMGj9+uQAZzz5Z9mrbnkJnPkaYGfKTaSsYcjQdXP0lgrWLVh8wU6bCjOmXOpx+kqUi+S5Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz", + "integrity": "sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/types": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-builder-react-jsx-experimental": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.1.tgz", - "integrity": "sha512-irQJ8kpQUV3JasXPSFQ+LCCtJSc5ceZrPFVj6TElR6XCHssi3jV8ch3odIrNtjJFRZZVbrOEfJMI79TPU/h1pQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz", + "integrity": "sha512-Buewnx6M4ttG+NLkKyt7baQn7ScC/Td+e99G914fRU8fGIUivDDgVIQeDHFa5e4CRSJQt58WpNHhsAZgtzVhsg==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-module-imports": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-module-imports": "^7.10.4", + "@babel/types": "^7.10.5" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-compilation-targets": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.2.tgz", - "integrity": "sha512-hYgOhF4To2UTB4LTaZepN/4Pl9LD4gfbJx8A34mqoluT8TLbof1mhUlYuNWTEebONa8+UlCC4X0TEXu7AOUyGA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz", + "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.10.1", + "@babel/compat-data": "^7.10.4", "browserslist": "^4.12.0", "invariant": "^2.2.4", "levenary": "^1.1.1", @@ -241,366 +241,255 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.3.tgz", - "integrity": "sha512-iRT9VwqtdFmv7UheJWthGc/h2s7MqoweBF9RUj77NFZsg9VfISvBTum3k6coAhJ8RWv2tj3yUjA03HxPd0vfpQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz", + "integrity": "sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-member-expression-to-functions": "^7.10.3", - "@babel/helper-optimise-call-expression": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/helper-replace-supers": "^7.10.1", - "@babel/helper-split-export-declaration": "^7.10.1" + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.10.5", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.1.tgz", - "integrity": "sha512-Rx4rHS0pVuJn5pJOqaqcZR4XSgeF9G/pO/79t+4r7380tXFJdzImFnxMU19f83wjSrmKHq6myrM10pFHTGzkUA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz", + "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-regex": "^7.10.1", + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-regex": "^7.10.4", "regexpu-core": "^4.7.0" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-define-map": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.3.tgz", - "integrity": "sha512-bxRzDi4Sin/k0drWCczppOhov1sBSdBvXJObM1NLHQzjhXhwRtn7aRWGvLJWCYbuu2qUk3EKs6Ci9C9ps8XokQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.3", - "@babel/types": "^7.10.3", - "lodash": "^4.17.13" + "@babel/helper-function-name": "^7.10.4", + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-explode-assignable-expression": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.3.tgz", - "integrity": "sha512-0nKcR64XrOC3lsl+uhD15cwxPvaB6QKUDlD84OT9C3myRbhJqTMYir69/RWItUvHpharv0eJ/wk7fl34ONSwZw==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz", + "integrity": "sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==", "dev": true, "requires": { - "@babel/traverse": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" }, "dependencies": { - "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.3" - } - }, - "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", - "dev": true, - "requires": { - "@babel/types": "^7.10.1" - } - }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, - "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true } } }, @@ -623,56 +512,56 @@ } }, "@babel/helper-hoist-variables": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.3.tgz", - "integrity": "sha512-9JyafKoBt5h20Yv1+BXQMdcXXavozI1vt401KBiRc2qzUepbVnd7ogVNymY1xkQN9fekGwfxtotH2Yf5xsGzgg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", + "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-member-expression-to-functions": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.3.tgz", - "integrity": "sha512-q7+37c4EPLSjNb2NmWOjNwj0+BOyYlssuQ58kHEWk1Z78K5i8vTUsteq78HMieRPQSl/NtpQyJfdjt3qZ5V2vw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", + "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.11.0" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } @@ -687,409 +576,334 @@ } }, "@babel/helper-module-transforms": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz", - "integrity": "sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", + "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.10.1", - "@babel/helper-replace-supers": "^7.10.1", - "@babel/helper-simple-access": "^7.10.1", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/template": "^7.10.1", - "@babel/types": "^7.10.1", - "lodash": "^4.17.13" + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/template": "^7.10.4", + "@babel/types": "^7.11.0", + "lodash": "^4.17.19" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-optimise-call-expression": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.3.tgz", - "integrity": "sha512-kT2R3VBH/cnSz+yChKpaKRJQJWxdGoc6SjioRId2wkeV3bK0wLLioFpJROrX0U4xr/NmxSSAWT/9Ih5snwIIzg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-plugin-utils": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.3.tgz", - "integrity": "sha512-j/+j8NAWUTxOtx4LKHybpSClxHoq6I91DQ/mKgAXn5oNUPIUiGppjPIX3TDtJWPrdfP9Kfl7e4fgVMiQR9VE/g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true }, "@babel/helper-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.1.tgz", - "integrity": "sha512-7isHr19RsIJWWLLFn21ubFt223PjQyg1HY7CZEMRr820HttHPpVvrsIN3bUOo44DEfFV4kBXO7Abbn9KTUZV7g==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", + "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", "dev": true, "requires": { - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/helper-remap-async-to-generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.3.tgz", - "integrity": "sha512-sLB7666ARbJUGDO60ZormmhQOyqMX/shKBXZ7fy937s+3ID8gSrneMvKSSb+8xIM5V7Vn6uNVtOY1vIm26XLtA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz", + "integrity": "sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-wrap-function": "^7.10.1", - "@babel/template": "^7.10.3", - "@babel/traverse": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-wrap-function": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" - } - }, - "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" - } - }, - "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", - "dev": true, - "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true } } }, "@babel/helper-replace-supers": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz", - "integrity": "sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.1", - "@babel/helper-optimise-call-expression": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", + "integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "dev": true, "requires": { - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", + "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.0", + "@babel/types": "^7.11.0", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -1111,66 +925,94 @@ } }, "@babel/helper-simple-access": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz", - "integrity": "sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", "dev": true, "requires": { - "@babel/template": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz", + "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } @@ -1190,126 +1032,125 @@ "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" }, "@babel/helper-wrap-function": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.1.tgz", - "integrity": "sha512-C0MzRGteVDn+H32/ZgbAv5r56f2o1fZSA/rj/TYo8JEJNHg+9BdSmKBUND0shxWRztWhjlT2cvHYuynpPsVJwQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", + "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.1", - "@babel/template": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/helper-function-name": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", + "integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "dev": true, "requires": { - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", + "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.0", + "@babel/types": "^7.11.0", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -1331,125 +1172,124 @@ } }, "@babel/helpers": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.1.tgz", - "integrity": "sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", "dev": true, "requires": { - "@babel/template": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", + "integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "dev": true, "requires": { - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", + "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.0", + "@babel/types": "^7.11.0", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -1486,24 +1326,24 @@ "integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==" }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.3.tgz", - "integrity": "sha512-WUUWM7YTOudF4jZBAJIW9D7aViYC/Fn0Pln4RIHlQALyno3sXSjqmTA4Zy1TKC2D49RCR8Y/Pn4OIUtEypK3CA==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", + "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/helper-remap-async-to-generator": "^7.10.3", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.10.4", "@babel/plugin-syntax-async-generators": "^7.8.0" } }, "@babel/plugin-proposal-class-properties": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz", - "integrity": "sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", + "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-proposal-decorators": { @@ -1518,94 +1358,115 @@ } }, "@babel/plugin-proposal-dynamic-import": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz", - "integrity": "sha512-Cpc2yUVHTEGPlmiQzXj026kqwjEQAD9I4ZC16uzdbgWgitg/UHKHLffKNCQZ5+y8jpIZPJcKcwsr2HwPh+w3XA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", + "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0" } }, - "@babel/plugin-proposal-json-strings": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.1.tgz", - "integrity": "sha512-m8r5BmV+ZLpWPtMY2mOKN7wre6HIO4gfIiV+eOmsnZABNenrt/kzYBwrh+KOfgumSWpnlGs5F70J8afYMSJMBg==", + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz", + "integrity": "sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz", + "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.0" } }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.1.tgz", - "integrity": "sha512-56cI/uHYgL2C8HVuHOuvVowihhX0sxb3nnfVRzUeVHTWmRHTZrKuAh/OBIMggGU/S1g/1D2CRCXqP+3u7vX7iA==", + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz", + "integrity": "sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", + "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" } }, "@babel/plugin-proposal-numeric-separator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.1.tgz", - "integrity": "sha512-jjfym4N9HtCiNfyyLAVD8WqPYeHUrw4ihxuAynWj6zzp2gf9Ey2f7ImhFm6ikB3CLf5Z/zmcJDri6B4+9j9RsA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz", + "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-numeric-separator": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.3.tgz", - "integrity": "sha512-ZZh5leCIlH9lni5bU/wB/UcjtcVLgR8gc+FAgW2OOY+m9h1II3ItTO1/cewNUcsIDZSYcSaz/rYVls+Fb0ExVQ==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz", + "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-transform-parameters": "^7.10.1" + "@babel/plugin-transform-parameters": "^7.10.4" } }, "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.1.tgz", - "integrity": "sha512-VqExgeE62YBqI3ogkGoOJp1R6u12DFZjqwJhqtKc2o5m1YTUuUWnos7bZQFBhwkxIFpWYJ7uB75U7VAPPiKETA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz", + "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.3.tgz", - "integrity": "sha512-yyG3n9dJ1vZ6v5sfmIlMMZ8azQoqx/5/nZTSWX1td6L1H1bsjzA8TInDChpafCZiJkeOFzp/PtrfigAQXxI1Ng==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz", + "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0" } }, "@babel/plugin-proposal-private-methods": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.1.tgz", - "integrity": "sha512-RZecFFJjDiQ2z6maFprLgrdnm0OzoC23Mx89xf1CcEsxmHuzuXOdniEuI+S3v7vjQG4F5sa6YtUp+19sZuSxHg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz", + "integrity": "sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.1.tgz", - "integrity": "sha512-JjfngYRvwmPwmnbRZyNiPFI8zxCZb8euzbCG/LxyKdeTb59tVciKo9GK9bi6JYKInk1H11Dq9j/zRqIH4KigfQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz", + "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-async-generators": { @@ -1618,21 +1479,21 @@ } }, "@babel/plugin-syntax-class-properties": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.1.tgz", - "integrity": "sha512-Gf2Yx/iRs1JREDtVZ56OrjjgFHCaldpTnuy9BHla10qyVT3YkIIGEtoDWhyop0ksu1GvNjHIoYRBqm3zoR1jyQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", + "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-decorators": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.1.tgz", - "integrity": "sha512-a9OAbQhKOwSle1Vr0NJu/ISg1sPfdEkfRKWpgPuzhnWWzForou2gIeUIIwjAMHRekhhpJ7eulZlYs0H14Cbi+g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.4.tgz", + "integrity": "sha512-2NaoC6fAk2VMdhY1eerkfHV+lVYC1u8b+jmRJISqANCJlTxYy19HGdIkkQtix2UtkcPuPu+IlDgrVseZnU03bw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-dynamic-import": { @@ -1644,13 +1505,22 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, - "@babel/plugin-syntax-flow": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.10.1.tgz", - "integrity": "sha512-b3pWVncLBYoPP60UOTc7NMlbtsHQ6ITim78KQejNHK6WJ2mzV5kCcg4mIWpasAfJEgwVTibwo2e+FU7UEIKQUg==", + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-flow": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.10.4.tgz", + "integrity": "sha512-yxQsX1dJixF4qEEdzVbst3SZQ58Nrooz8NV9Z9GL4byTE25BvJgl5lf0RECUf0fh28rZBb/RYTWn/eeKwCMrZQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-json-strings": { @@ -1663,12 +1533,21 @@ } }, "@babel/plugin-syntax-jsx": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.1.tgz", - "integrity": "sha512-+OxyOArpVFXQeXKLO9o+r2I4dIoVoy6+Uu0vKELrlweDM3QJADZj+Z+5ERansZqIZBcLj42vHnDI8Rz9BnRIuQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz", + "integrity": "sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-nullish-coalescing-operator": { @@ -1681,12 +1560,12 @@ } }, "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.1.tgz", - "integrity": "sha512-uTd0OsHrpe3tH5gRPTxG8Voh99/WCU78vIm5NMRYPAqC8lR4vajt6KkCAknCHrx24vkPdd/05yfdGSB4EIY2mg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-object-rest-spread": { @@ -1717,245 +1596,244 @@ } }, "@babel/plugin-syntax-top-level-await": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.1.tgz", - "integrity": "sha512-hgA5RYkmZm8FTFT3yu2N9Bx7yVVOKYT6yEdXXo6j2JTm0wNxgqaGeQVaSHRjhfnQbX91DtjFB6McRFSlcJH3xQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz", + "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-typescript": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.1.tgz", - "integrity": "sha512-X/d8glkrAtra7CaQGMiGs/OGa6XgUzqPcBXCIGFCpCqnfGlT0Wfbzo/B89xHhnInTaItPK8LALblVXcUOEh95Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz", + "integrity": "sha512-oSAEz1YkBCAKr5Yiq8/BNtvSAPwkp/IyUnwZogd8p+F0RuYQQrLeRUzIQhueQTTBy/F+a40uS7OFKxnkRvmvFQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.1.tgz", - "integrity": "sha512-6AZHgFJKP3DJX0eCNJj01RpytUa3SOGawIxweHkNX2L6PYikOZmoh5B0d7hIHaIgveMjX990IAa/xK7jRTN8OA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz", + "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.1.tgz", - "integrity": "sha512-XCgYjJ8TY2slj6SReBUyamJn3k2JLUIiiR5b6t1mNCMSvv7yx+jJpaewakikp0uWFQSF7ChPPoe3dHmXLpISkg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz", + "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-remap-async-to-generator": "^7.10.1" + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.10.4" }, "dependencies": { "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.1.tgz", - "integrity": "sha512-B7K15Xp8lv0sOJrdVAoukKlxP9N59HS48V1J3U/JGj+Ad+MHq+am6xJVs85AgXrQn4LV8vaYFOB+pr/yIuzW8Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz", + "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.1.tgz", - "integrity": "sha512-8bpWG6TtF5akdhIm/uWTyjHqENpy13Fx8chg7pFH875aNLwX8JxIxqm08gmAT+Whe6AOmaTeLPe7dpLbXt+xUw==", + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz", + "integrity": "sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "lodash": "^4.17.13" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-classes": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.3.tgz", - "integrity": "sha512-irEX0ChJLaZVC7FvvRoSIxJlmk0IczFLcwaRXUArBKYHCHbOhe57aG8q3uw/fJsoSXvZhjRX960hyeAGlVBXZw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz", + "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-define-map": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-optimise-call-expression": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/helper-replace-supers": "^7.10.1", - "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-define-map": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", "globals": "^11.1.0" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-computed-properties": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.3.tgz", - "integrity": "sha512-GWzhaBOsdbjVFav96drOz7FzrcEW6AP5nax0gLIpstiFaI3LOb2tAg06TimaWU6YKOfUACK3FVrxPJ4GSc5TgA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz", + "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-destructuring": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.1.tgz", - "integrity": "sha512-V/nUc4yGWG71OhaTH705pU8ZSdM6c1KmmLP8ys59oOYbT7RpMYAR3MsVOt6OHL0WzG7BlTU076va9fjJyYzJMA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz", + "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.1.tgz", - "integrity": "sha512-19VIMsD1dp02RvduFUmfzj8uknaO3uiHHF0s3E1OHnVsNj8oge8EQ5RzHRbJjGSetRnkEuBYO7TG1M5kKjGLOA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz", + "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.1.tgz", - "integrity": "sha512-wIEpkX4QvX8Mo9W6XF3EdGttrIPZWozHfEaDTU0WJD/TDnXMvdDh30mzUl/9qWhnf7naicYartcEfUghTCSNpA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz", + "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.1.tgz", - "integrity": "sha512-lr/przdAbpEA2BUzRvjXdEDLrArGRRPwbaF9rvayuHRvdQ7lUTTkZnhZrJ4LE2jvgMRFF4f0YuPQ20vhiPYxtA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz", + "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==", "dev": true, "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-flow-strip-types": { @@ -1969,353 +1847,353 @@ } }, "@babel/plugin-transform-for-of": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.1.tgz", - "integrity": "sha512-US8KCuxfQcn0LwSCMWMma8M2R5mAjJGsmoCBVwlMygvmDUMkTCykc84IqN1M7t+agSfOmLYTInLCHJM+RUoz+w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz", + "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-function-name": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.1.tgz", - "integrity": "sha512-//bsKsKFBJfGd65qSNNh1exBy5Y9gD9ZN+DvrJ8f7HXr4avE5POW6zB7Rj6VnqHV33+0vXWUwJT0wSHubiAQkw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz", + "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-literals": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.1.tgz", - "integrity": "sha512-qi0+5qgevz1NHLZroObRm5A+8JJtibb7vdcPQF1KQE12+Y/xxl8coJ+TpPW9iRq+Mhw/NKLjm+5SHtAHCC7lAw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", + "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.1.tgz", - "integrity": "sha512-UmaWhDokOFT2GcgU6MkHC11i0NQcL63iqeufXWfRy6pUOGYeCGEKhvfFO6Vz70UfYJYHwveg62GS83Rvpxn+NA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", + "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.1.tgz", - "integrity": "sha512-31+hnWSFRI4/ACFr1qkboBbrTxoBIzj7qA69qlq8HY8p7+YCzkCT6/TvQ1a4B0z27VeWtAeJd6pr5G04dc1iHw==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz", + "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.1.tgz", - "integrity": "sha512-AQG4fc3KOah0vdITwt7Gi6hD9BtQP/8bhem7OjbaMoRNCH5Djx42O2vYMfau7QnAzQCa+RJnhJBmFFMGpQEzrg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", + "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-simple-access": "^7.10.1", + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.3.tgz", - "integrity": "sha512-GWXWQMmE1GH4ALc7YXW56BTh/AlzvDWhUNn9ArFF0+Cz5G8esYlVbXfdyHa1xaD1j+GnBoCeoQNlwtZTVdiG/A==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz", + "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==", "dev": true, "requires": { - "@babel/helper-hoist-variables": "^7.10.3", - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3", + "@babel/helper-hoist-variables": "^7.10.4", + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-umd": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.1.tgz", - "integrity": "sha512-EIuiRNMd6GB6ulcYlETnYYfgv4AxqrswghmBRQbWLHZxN4s7mupxzglnHqk9ZiUpDI4eRWewedJJNj67PWOXKA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", + "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.3.tgz", - "integrity": "sha512-I3EH+RMFyVi8Iy/LekQm948Z4Lz4yKT7rK+vuCAeRm0kTa6Z5W7xuhRxDNJv0FPya/her6AUgrDITb70YHtTvA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz", + "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.10.4" } }, "@babel/plugin-transform-new-target": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.1.tgz", - "integrity": "sha512-MBlzPc1nJvbmO9rPr1fQwXOM2iGut+JC92ku6PbiJMMK7SnQc1rytgpopveE3Evn47gzvGYeCdgfCDbZo0ecUw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz", + "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-object-super": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.1.tgz", - "integrity": "sha512-WnnStUDN5GL+wGQrJylrnnVlFhFmeArINIR9gjhSeYyvroGhBrSAXYg/RHsnfzmsa+onJrTJrEClPzgNmmQ4Gw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz", + "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-replace-supers": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4" } }, "@babel/plugin-transform-parameters": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.1.tgz", - "integrity": "sha512-tJ1T0n6g4dXMsL45YsSzzSDZCxiHXAQp/qHrucOq5gEHncTA3xDxnd5+sZcoQp+N1ZbieAaB8r/VUCG0gqseOg==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz", + "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-property-literals": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.1.tgz", - "integrity": "sha512-Kr6+mgag8auNrgEpbfIWzdXYOvqDHZOF0+Bx2xh4H2EDNwcbRb9lY6nkZg8oSjsX+DH9Ebxm9hOqtKW+gRDeNA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz", + "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-react-constant-elements": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.1.tgz", - "integrity": "sha512-V4os6bkWt/jbrzfyVcZn2ZpuHZkvj3vyBU0U/dtS8SZuMS7Rfx5oknTrtfyXJ2/QZk8gX7Yls5Z921ItNpE30Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.4.tgz", + "integrity": "sha512-cYmQBW1pXrqBte1raMkAulXmi7rjg3VI6ZLg9QIic8Hq7BtYXaWuZSxsr2siOMI6SWwpxjWfnwhTUrd7JlAV7g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-react-display-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.3.tgz", - "integrity": "sha512-dOV44bnSW5KZ6kYF6xSHBth7TFiHHZReYXH/JH3XnFNV+soEL1F5d8JT7AJ3ZBncd19Qul7SN4YpBnyWOnQ8KA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.4.tgz", + "integrity": "sha512-Zd4X54Mu9SBfPGnEcaGcOrVAYOtjT2on8QZkLKEq1S/tHexG39d9XXGZv19VfRrDjPJzFmPfTAqOQS1pfFOujw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-react-jsx": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.3.tgz", - "integrity": "sha512-Y21E3rZmWICRJnvbGVmDLDZ8HfNDIwjGF3DXYHx1le0v0mIHCs0Gv5SavyW5Z/jgAHLaAoJPiwt+Dr7/zZKcOQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz", + "integrity": "sha512-L+MfRhWjX0eI7Js093MM6MacKU4M6dnCRa/QPDwYMxjljzSCzzlzKzj9Pk4P3OtrPcxr2N3znR419nr3Xw+65A==", "dev": true, "requires": { - "@babel/helper-builder-react-jsx": "^7.10.3", - "@babel/helper-builder-react-jsx-experimental": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-builder-react-jsx": "^7.10.4", + "@babel/helper-builder-react-jsx-experimental": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-jsx-development": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.1.tgz", - "integrity": "sha512-XwDy/FFoCfw9wGFtdn5Z+dHh6HXKHkC6DwKNWpN74VWinUagZfDcEJc3Y8Dn5B3WMVnAllX8Kviaw7MtC5Epwg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.4.tgz", + "integrity": "sha512-RM3ZAd1sU1iQ7rI2dhrZRZGv0aqzNQMbkIUCS1txYpi9wHQ2ZHNjo5TwX+UD6pvFW4AbWqLVYvKy5qJSAyRGjQ==", "dev": true, "requires": { - "@babel/helper-builder-react-jsx-experimental": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-builder-react-jsx-experimental": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-jsx-self": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.1.tgz", - "integrity": "sha512-4p+RBw9d1qV4S749J42ZooeQaBomFPrSxa9JONLHJ1TxCBo3TzJ79vtmG2S2erUT8PDDrPdw4ZbXGr2/1+dILA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz", + "integrity": "sha512-yOvxY2pDiVJi0axdTWHSMi5T0DILN+H+SaeJeACHKjQLezEzhLx9nEF9xgpBLPtkZsks9cnb5P9iBEi21En3gg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-jsx-source": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.1.tgz", - "integrity": "sha512-neAbaKkoiL+LXYbGDvh6PjPG+YeA67OsZlE78u50xbWh2L1/C81uHiNP5d1fw+uqUIoiNdCC8ZB+G4Zh3hShJA==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz", + "integrity": "sha512-wTeqHVkN1lfPLubRiZH3o73f4rfon42HpgxUSs86Nc+8QIcm/B9s8NNVXu/gwGcOyd7yDib9ikxoDLxJP0UiDA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-pure-annotations": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.3.tgz", - "integrity": "sha512-n/fWYGqvTl7OLZs/QcWaKMFdADPvC3V6jYuEOpPyvz97onsW9TXn196fHnHW1ZgkO20/rxLOgKnEtN1q9jkgqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz", + "integrity": "sha512-+njZkqcOuS8RaPakrnR9KvxjoG1ASJWpoIv/doyWngId88JoFlPlISenGXjrVacZUIALGUr6eodRs1vmPnF23A==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-regenerator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.3.tgz", - "integrity": "sha512-H5kNeW0u8mbk0qa1jVIVTeJJL6/TJ81ltD4oyPx0P499DhMJrTmmIFCmJ3QloGpQG8K9symccB7S7SJpCKLwtw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz", + "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==", "dev": true, "requires": { "regenerator-transform": "^0.14.2" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.1.tgz", - "integrity": "sha512-qN1OMoE2nuqSPmpTqEM7OvJ1FkMEV+BjVeZZm9V9mq/x1JLKQ4pcv8riZJMNN3u2AUGl0ouOMjRr2siecvHqUQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz", + "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-runtime": { @@ -2339,108 +2217,109 @@ } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz", - "integrity": "sha512-AR0E/lZMfLstScFwztApGeyTHJ5u3JUKMjneqRItWeEqDdHWZwAOKycvQNCasCK/3r5YXsuNG25funcJDu7Y2g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz", + "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-spread": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.1.tgz", - "integrity": "sha512-8wTPym6edIrClW8FI2IoaePB91ETOtg36dOkj3bYcNe7aDMN2FXEoUa+WrmPc4xa1u2PQK46fUX2aCb+zo9rfw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz", + "integrity": "sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.1.tgz", - "integrity": "sha512-j17ojftKjrL7ufX8ajKvwRilwqTok4q+BjkknmQw9VNHnItTyMP5anPFzxFJdCQs7clLcWpCV3ma+6qZWLnGMA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz", + "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-regex": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-regex": "^7.10.4" } }, "@babel/plugin-transform-template-literals": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.3.tgz", - "integrity": "sha512-yaBn9OpxQra/bk0/CaA4wr41O0/Whkg6nqjqApcinxM7pro51ojhX6fv1pimAnVjVfDy14K0ULoRL70CA9jWWA==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz", + "integrity": "sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.1.tgz", - "integrity": "sha512-qX8KZcmbvA23zDi+lk9s6hC1FM7jgLHYIjuLgULgc8QtYnmB3tAVIYkNoKRQ75qWBeyzcoMoK8ZQmogGtC/w0g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz", + "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-typescript": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.3.tgz", - "integrity": "sha512-qU9Lu7oQyh3PGMQncNjQm8RWkzw6LqsWZQlZPQMgrGt6s3YiBIaQ+3CQV/FA/icGS5XlSWZGwo/l8ErTyelS0Q==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.11.0.tgz", + "integrity": "sha512-edJsNzTtvb3MaXQwj8403B7mZoGu9ElDJQZOKjGUnvilquxBA3IQoEIOvkX/1O8xfAsnHS/oQhe2w/IXrr+w0w==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/plugin-syntax-typescript": "^7.10.1" + "@babel/helper-create-class-features-plugin": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-typescript": "^7.10.4" } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.1.tgz", - "integrity": "sha512-zZ0Poh/yy1d4jeDWpx/mNwbKJVwUYJX73q+gyh4bwtG0/iUlzdEu0sLMda8yuDFS6LBQlT/ST1SJAR6zYwXWgw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz", + "integrity": "sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.1.tgz", - "integrity": "sha512-Y/2a2W299k0VIUdbqYm9X2qS6fE0CUBhhiPpimK6byy7OJ/kORLlIX+J6UrjgNu5awvs62k+6RSslxhcvVw2Tw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz", + "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/polyfill": { @@ -2462,70 +2341,74 @@ } }, "@babel/preset-env": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.10.3.tgz", - "integrity": "sha512-jHaSUgiewTmly88bJtMHbOd1bJf2ocYxb5BWKSDQIP5tmgFuS/n0gl+nhSrYDhT33m0vPxp+rP8oYYgPgMNQlg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.0.tgz", + "integrity": "sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg==", "dev": true, "requires": { - "@babel/compat-data": "^7.10.3", - "@babel/helper-compilation-targets": "^7.10.2", - "@babel/helper-module-imports": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/plugin-proposal-async-generator-functions": "^7.10.3", - "@babel/plugin-proposal-class-properties": "^7.10.1", - "@babel/plugin-proposal-dynamic-import": "^7.10.1", - "@babel/plugin-proposal-json-strings": "^7.10.1", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1", - "@babel/plugin-proposal-numeric-separator": "^7.10.1", - "@babel/plugin-proposal-object-rest-spread": "^7.10.3", - "@babel/plugin-proposal-optional-catch-binding": "^7.10.1", - "@babel/plugin-proposal-optional-chaining": "^7.10.3", - "@babel/plugin-proposal-private-methods": "^7.10.1", - "@babel/plugin-proposal-unicode-property-regex": "^7.10.1", + "@babel/compat-data": "^7.11.0", + "@babel/helper-compilation-targets": "^7.10.4", + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-proposal-async-generator-functions": "^7.10.4", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-dynamic-import": "^7.10.4", + "@babel/plugin-proposal-export-namespace-from": "^7.10.4", + "@babel/plugin-proposal-json-strings": "^7.10.4", + "@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", + "@babel/plugin-proposal-numeric-separator": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.11.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.10.4", + "@babel/plugin-proposal-optional-chaining": "^7.11.0", + "@babel/plugin-proposal-private-methods": "^7.10.4", + "@babel/plugin-proposal-unicode-property-regex": "^7.10.4", "@babel/plugin-syntax-async-generators": "^7.8.0", - "@babel/plugin-syntax-class-properties": "^7.10.1", + "@babel/plugin-syntax-class-properties": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", - "@babel/plugin-syntax-numeric-separator": "^7.10.1", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0", - "@babel/plugin-syntax-top-level-await": "^7.10.1", - "@babel/plugin-transform-arrow-functions": "^7.10.1", - "@babel/plugin-transform-async-to-generator": "^7.10.1", - "@babel/plugin-transform-block-scoped-functions": "^7.10.1", - "@babel/plugin-transform-block-scoping": "^7.10.1", - "@babel/plugin-transform-classes": "^7.10.3", - "@babel/plugin-transform-computed-properties": "^7.10.3", - "@babel/plugin-transform-destructuring": "^7.10.1", - "@babel/plugin-transform-dotall-regex": "^7.10.1", - "@babel/plugin-transform-duplicate-keys": "^7.10.1", - "@babel/plugin-transform-exponentiation-operator": "^7.10.1", - "@babel/plugin-transform-for-of": "^7.10.1", - "@babel/plugin-transform-function-name": "^7.10.1", - "@babel/plugin-transform-literals": "^7.10.1", - "@babel/plugin-transform-member-expression-literals": "^7.10.1", - "@babel/plugin-transform-modules-amd": "^7.10.1", - "@babel/plugin-transform-modules-commonjs": "^7.10.1", - "@babel/plugin-transform-modules-systemjs": "^7.10.3", - "@babel/plugin-transform-modules-umd": "^7.10.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.3", - "@babel/plugin-transform-new-target": "^7.10.1", - "@babel/plugin-transform-object-super": "^7.10.1", - "@babel/plugin-transform-parameters": "^7.10.1", - "@babel/plugin-transform-property-literals": "^7.10.1", - "@babel/plugin-transform-regenerator": "^7.10.3", - "@babel/plugin-transform-reserved-words": "^7.10.1", - "@babel/plugin-transform-shorthand-properties": "^7.10.1", - "@babel/plugin-transform-spread": "^7.10.1", - "@babel/plugin-transform-sticky-regex": "^7.10.1", - "@babel/plugin-transform-template-literals": "^7.10.3", - "@babel/plugin-transform-typeof-symbol": "^7.10.1", - "@babel/plugin-transform-unicode-escapes": "^7.10.1", - "@babel/plugin-transform-unicode-regex": "^7.10.1", + "@babel/plugin-syntax-top-level-await": "^7.10.4", + "@babel/plugin-transform-arrow-functions": "^7.10.4", + "@babel/plugin-transform-async-to-generator": "^7.10.4", + "@babel/plugin-transform-block-scoped-functions": "^7.10.4", + "@babel/plugin-transform-block-scoping": "^7.10.4", + "@babel/plugin-transform-classes": "^7.10.4", + "@babel/plugin-transform-computed-properties": "^7.10.4", + "@babel/plugin-transform-destructuring": "^7.10.4", + "@babel/plugin-transform-dotall-regex": "^7.10.4", + "@babel/plugin-transform-duplicate-keys": "^7.10.4", + "@babel/plugin-transform-exponentiation-operator": "^7.10.4", + "@babel/plugin-transform-for-of": "^7.10.4", + "@babel/plugin-transform-function-name": "^7.10.4", + "@babel/plugin-transform-literals": "^7.10.4", + "@babel/plugin-transform-member-expression-literals": "^7.10.4", + "@babel/plugin-transform-modules-amd": "^7.10.4", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/plugin-transform-modules-systemjs": "^7.10.4", + "@babel/plugin-transform-modules-umd": "^7.10.4", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.4", + "@babel/plugin-transform-new-target": "^7.10.4", + "@babel/plugin-transform-object-super": "^7.10.4", + "@babel/plugin-transform-parameters": "^7.10.4", + "@babel/plugin-transform-property-literals": "^7.10.4", + "@babel/plugin-transform-regenerator": "^7.10.4", + "@babel/plugin-transform-reserved-words": "^7.10.4", + "@babel/plugin-transform-shorthand-properties": "^7.10.4", + "@babel/plugin-transform-spread": "^7.11.0", + "@babel/plugin-transform-sticky-regex": "^7.10.4", + "@babel/plugin-transform-template-literals": "^7.10.4", + "@babel/plugin-transform-typeof-symbol": "^7.10.4", + "@babel/plugin-transform-unicode-escapes": "^7.10.4", + "@babel/plugin-transform-unicode-regex": "^7.10.4", "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "browserslist": "^4.12.0", "core-js-compat": "^3.6.2", "invariant": "^2.2.2", @@ -2534,28 +2417,28 @@ }, "dependencies": { "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -2581,18 +2464,18 @@ } }, "@babel/preset-react": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.10.1.tgz", - "integrity": "sha512-Rw0SxQ7VKhObmFjD/cUcKhPTtzpeviEFX1E6PgP+cYOhQ98icNqtINNFANlsdbQHrmeWnqdxA4Tmnl1jy5tp3Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.10.4.tgz", + "integrity": "sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-transform-react-display-name": "^7.10.1", - "@babel/plugin-transform-react-jsx": "^7.10.1", - "@babel/plugin-transform-react-jsx-development": "^7.10.1", - "@babel/plugin-transform-react-jsx-self": "^7.10.1", - "@babel/plugin-transform-react-jsx-source": "^7.10.1", - "@babel/plugin-transform-react-pure-annotations": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-transform-react-display-name": "^7.10.4", + "@babel/plugin-transform-react-jsx": "^7.10.4", + "@babel/plugin-transform-react-jsx-development": "^7.10.4", + "@babel/plugin-transform-react-jsx-self": "^7.10.4", + "@babel/plugin-transform-react-jsx-source": "^7.10.4", + "@babel/plugin-transform-react-pure-annotations": "^7.10.4" } }, "@babel/preset-typescript": { @@ -3427,9 +3310,9 @@ } }, "@types/babel__traverse": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.12.tgz", - "integrity": "sha512-t4CoEokHTfcyfb4hUaF9oOHu9RmmNWnm1CP0YmMqOOfClKascOmvlEM736vlqeScuGvBDsHkf8R2INd4DWreQA==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.13.tgz", + "integrity": "sha512-i+zS7t6/s9cdQvbqKDARrcbrPvtJGlbYsMkazo03nTAK3RX9FNrLllXys22uiTGJapPOTZTQ35nHh4ISph4SLQ==", "dev": true, "requires": { "@babel/types": "^7.3.0" @@ -3448,9 +3331,9 @@ "dev": true }, "@types/glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "requires": { "@types/minimatch": "*", @@ -3805,9 +3688,9 @@ "dev": true }, "abab": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", - "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", + "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==", "dev": true }, "accepts": { @@ -3956,9 +3839,9 @@ "dev": true }, "ajv-keywords": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.0.tgz", - "integrity": "sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true }, "alphanum-sort": { @@ -4257,14 +4140,14 @@ } }, "autoprefixer": { - "version": "9.8.4", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.4.tgz", - "integrity": "sha512-84aYfXlpUe45lvmS+HoAWKCkirI/sw4JK0/bTeeqgHYco3dcsOn0NqdejISjptsYwNji/21dnkDri9PsYKk89A==", + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", "dev": true, "requires": { "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001087", - "colorette": "^1.2.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", "postcss": "^7.0.32", @@ -4278,9 +4161,9 @@ "dev": true }, "aws4": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", - "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", + "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", "dev": true }, "axios": { @@ -4570,9 +4453,9 @@ } }, "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", + "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -4826,9 +4709,9 @@ } }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "dev": true }, "semver": { @@ -4985,6 +4868,16 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -5100,8 +4993,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browser-process-hrtime": { "version": "1.0.0", @@ -5235,15 +5127,15 @@ } }, "browserslist": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.2.tgz", - "integrity": "sha512-MfZaeYqR8StRZdstAK9hCKDd2StvePCYp5rHzQCPicUjfFliDgmuaBNPHYUTpAywBN8+Wc/d7NYVFkO0aqaBUw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.0.tgz", + "integrity": "sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001088", - "electron-to-chromium": "^1.3.483", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" + "caniuse-lite": "^1.0.30001111", + "electron-to-chromium": "^1.3.523", + "escalade": "^3.0.2", + "node-releases": "^1.1.60" } }, "bser": { @@ -5430,9 +5322,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001090", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001090.tgz", - "integrity": "sha512-QzPRKDCyp7RhjczTPZaqK3CjPA5Ht2UnXhZhCI4f7QiB5JK6KEuZBxIzyWnB3wO4hgAj4GMRxAhuiacfw0Psjg==", + "version": "1.0.30001117", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001117.tgz", + "integrity": "sha512-4tY0Fatzdx59kYjQs+bNxUwZB03ZEBgVmJ1UkFPz/Q8OLiUUbjct2EdpnXj0fvFTPej2EkbPIG0w8BWsjAyk1Q==", "dev": true }, "capture-exit": { @@ -5536,9 +5428,9 @@ } }, "chokidar": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", - "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", + "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", "dev": true, "requires": { "anymatch": "~3.1.1", @@ -5786,12 +5678,6 @@ "q": "^1.1.2" } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "codemirror": { "version": "5.53.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.53.2.tgz", @@ -5841,9 +5727,9 @@ } }, "colorette": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.0.tgz", - "integrity": "sha512-soRSroY+OF/8OdA3PTQXwaDJeMc7TfknKKrxeSCencL2a4+Tx5zhxmmv7hdpCjhKBjehzp8+bwe/T68K0hpIjw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "dev": true }, "colors": { @@ -7270,9 +7156,9 @@ "dev": true }, "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, "duplexify": { @@ -7304,16 +7190,15 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.483", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.483.tgz", - "integrity": "sha512-+05RF8S9rk8S0G8eBCqBRBaRq7+UN3lDs2DAvnG8SBSgQO3hjy0+qt4CmRk5eiuGbTcaicgXfPmBi31a+BD3lg==", + "version": "1.3.540", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.540.tgz", + "integrity": "sha512-IoGiZb8SMqTtkDYJtP8EtCdvv3VMtd1QoTlypO2RUBxRq/Wk0rU5IzhzhMckPaC9XxDqUvWsL0XKOBhTiYVN3w==", "dev": true }, "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", - "dev": true, + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -7325,10 +7210,9 @@ }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" } } }, @@ -7369,9 +7253,9 @@ } }, "enhanced-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz", - "integrity": "sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -7582,9 +7466,9 @@ } }, "escalade": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.1.tgz", - "integrity": "sha512-DR6NO3h9niOT+MZs7bjxlj2a1k+POu5RN8CLTPX2+i78bRi9eLe7+0zXgUHMnGXWybYcL61E9hGhPKqedy8tQA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", + "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==", "dev": true }, "escape-html": { @@ -8568,6 +8452,13 @@ "tslib": "^1.9.0" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "filesize": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.0.1.tgz", @@ -9076,13 +8967,27 @@ "dev": true }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "dev": true, "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, "harmony-reflect": { @@ -9212,7 +9117,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -9247,7 +9151,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -9445,12 +9348,6 @@ } } }, - "http-parser-js": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", - "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==", - "dev": true - }, "http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -9670,8 +9567,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", @@ -9803,12 +9699,6 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -9966,9 +9856,9 @@ "dev": true }, "is-docker": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", - "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", "dev": true }, "is-extendable": { @@ -10510,7 +10400,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } } } }, @@ -11034,15 +10928,6 @@ "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", "dev": true }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -11354,15 +11239,6 @@ "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", "dev": true }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -11401,17 +11277,6 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memoize-one": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", @@ -11593,14 +11458,12 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", @@ -11644,9 +11507,9 @@ } }, "minipass-pipeline": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz", - "integrity": "sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, "requires": { "minipass": "^3.0.0" @@ -11774,6 +11637,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "dev": true, + "optional": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -11835,9 +11705,9 @@ "dev": true }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, "next-tick": { @@ -12017,9 +11887,9 @@ } }, "node-releases": { - "version": "1.1.58", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.58.tgz", - "integrity": "sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==", + "version": "1.1.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz", + "integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==", "dev": true }, "normalize-package-data": { @@ -12093,12 +11963,6 @@ "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", "dev": true }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -12296,9 +12160,9 @@ } }, "open": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz", - "integrity": "sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.1.0.tgz", + "integrity": "sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA==", "dev": true, "requires": { "is-docker": "^2.0.0", @@ -12621,29 +12485,12 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, "p-each-series": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", @@ -12659,12 +12506,6 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -13140,14 +12981,14 @@ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" }, "portfinder": { - "version": "1.0.26", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz", - "integrity": "sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", "dev": true, "requires": { "async": "^2.6.2", "debug": "^3.1.1", - "mkdirp": "^0.5.1" + "mkdirp": "^0.5.5" }, "dependencies": { "debug": { @@ -13221,9 +13062,9 @@ } }, "postcss-calc": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.2.tgz", - "integrity": "sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.3.tgz", + "integrity": "sha512-IB/EAEmZhIMEIhG7Ov4x+l47UaXOS1n2f4FBUk/aKllQhtSCxWhTzn0nJgkqN7fo/jcWySvWTSB6Syk9L+31bA==", "dev": true, "requires": { "postcss": "^7.0.27", @@ -13726,15 +13567,15 @@ } }, "postcss-modules-local-by-default": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", - "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", "dev": true, "requires": { "icss-utils": "^4.1.1", - "postcss": "^7.0.16", + "postcss": "^7.0.32", "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.0" + "postcss-value-parser": "^4.1.0" } }, "postcss-modules-scope": { @@ -14244,12 +14085,6 @@ "react-is": "^16.8.4" } }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -14562,9 +14397,9 @@ "dev": true }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "dev": true } } @@ -14877,9 +14712,9 @@ } }, "react-scripts": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.1.tgz", - "integrity": "sha512-JpTdi/0Sfd31mZA6Ukx+lq5j1JoKItX7qqEK4OiACjVQletM1P38g49d9/D0yTxp9FrSF+xpJFStkGgKEIRjlQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.3.tgz", + "integrity": "sha512-oSnoWmii/iKdeQiwaO6map1lUaZLmG0xIUyb/HwCVFLT7gNbj8JZ9RmpvMCZ4fB98ZUMRfNmp/ft8uy/xD1RLA==", "dev": true, "requires": { "@babel/core": "7.9.0", @@ -14928,11 +14763,11 @@ "sass-loader": "8.0.2", "semver": "6.3.0", "style-loader": "0.23.1", - "terser-webpack-plugin": "2.3.5", + "terser-webpack-plugin": "2.3.8", "ts-pnp": "1.1.6", "url-loader": "2.3.0", "webpack": "4.42.0", - "webpack-dev-server": "3.10.3", + "webpack-dev-server": "3.11.0", "webpack-manifest-plugin": "2.2.0", "workbox-webpack-plugin": "4.3.1" }, @@ -15134,13 +14969,12 @@ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regenerator-transform": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.4.tgz", - "integrity": "sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", "dev": true, "requires": { - "@babel/runtime": "^7.8.4", - "private": "^0.1.8" + "@babel/runtime": "^7.8.4" } }, "regex-not": { @@ -15332,21 +15166,21 @@ } }, "request-promise-core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", - "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", "dev": true, "requires": { - "lodash": "^4.17.15" + "lodash": "^4.17.19" } }, "request-promise-native": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", - "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", "dev": true, "requires": { - "request-promise-core": "1.1.3", + "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" } @@ -15814,10 +15648,13 @@ } }, "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } }, "serve-index": { "version": "1.9.1", @@ -16170,13 +16007,14 @@ } }, "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", "dev": true, "requires": { "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" } }, "sockjs-client": { @@ -16962,19 +16800,19 @@ } }, "terser-webpack-plugin": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.5.tgz", - "integrity": "sha512-WlWksUoq+E4+JlJ+h+U+QUzXpcsMSSNXkDy9lBVkSqDn1w23Gg29L/ary9GeJVYCGiNJJX7LnVc4bwL1N3/g1w==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz", + "integrity": "sha512-/fKw3R+hWyHfYx7Bv6oPqmk4HGQcrWLtV3X6ggvPuwPNHSnzvVV51z6OaaCOus4YLjutYGOz3pEpbhe6Up2s1w==", "dev": true, "requires": { "cacache": "^13.0.1", - "find-cache-dir": "^3.2.0", - "jest-worker": "^25.1.0", - "p-limit": "^2.2.2", - "schema-utils": "^2.6.4", - "serialize-javascript": "^2.1.2", + "find-cache-dir": "^3.3.1", + "jest-worker": "^25.4.0", + "p-limit": "^2.3.0", + "schema-utils": "^2.6.6", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", - "terser": "^4.4.3", + "terser": "^4.6.12", "webpack-sources": "^1.4.3" }, "dependencies": { @@ -17675,12 +17513,12 @@ } }, "watchpack": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz", - "integrity": "sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", + "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", "dev": true, "requires": { - "chokidar": "^3.4.0", + "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", "neo-async": "^2.5.0", "watchpack-chokidar2": "^2.0.0" @@ -17729,7 +17567,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -17890,15 +17732,6 @@ "ajv-keywords": "^3.1.0" } }, - "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -17915,16 +17748,16 @@ } }, "terser-webpack-plugin": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz", - "integrity": "sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", "dev": true, "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^3.1.0", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", @@ -17947,9 +17780,9 @@ } }, "webpack-dev-server": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz", - "integrity": "sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -17960,31 +17793,31 @@ "debug": "^4.1.1", "del": "^4.1.1", "express": "^4.17.1", - "html-entities": "^1.2.1", + "html-entities": "^1.3.1", "http-proxy-middleware": "0.19.1", "import-local": "^2.0.0", "internal-ip": "^4.3.0", "ip": "^1.1.5", "is-absolute-url": "^3.0.3", "killable": "^1.0.1", - "loglevel": "^1.6.6", + "loglevel": "^1.6.8", "opn": "^5.5.0", "p-retry": "^3.0.1", - "portfinder": "^1.0.25", + "portfinder": "^1.0.26", "schema-utils": "^1.0.0", "selfsigned": "^1.10.7", "semver": "^6.3.0", "serve-index": "^1.9.1", - "sockjs": "0.3.19", + "sockjs": "0.3.20", "sockjs-client": "1.4.0", - "spdy": "^4.0.1", + "spdy": "^4.0.2", "strip-ansi": "^3.0.1", "supports-color": "^6.1.0", "url": "^0.11.0", "webpack-dev-middleware": "^3.7.2", "webpack-log": "^2.0.0", "ws": "^6.2.1", - "yargs": "12.0.5" + "yargs": "^13.3.2" }, "dependencies": { "ansi-regex": { @@ -18019,34 +17852,6 @@ "upath": "^1.1.1" } }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -18056,27 +17861,16 @@ "ms": "^2.1.1" } }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, "fsevents": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -18126,22 +17920,6 @@ "binary-extensions": "^1.0.0" } }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -18154,30 +17932,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, "readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", @@ -18189,12 +17943,6 @@ "readable-stream": "^2.0.2" } }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -18206,33 +17954,6 @@ "ajv-keywords": "^3.1.0" } }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -18251,38 +17972,6 @@ "has-flag": "^3.0.0" } }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, "ws": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", @@ -18291,36 +17980,6 @@ "requires": { "async-limiter": "~1.0.0" } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -18378,13 +18037,11 @@ } }, "websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", "dev": true, "requires": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, @@ -18404,9 +18061,9 @@ } }, "whatwg-fetch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.1.0.tgz", - "integrity": "sha512-pgmbsVWKpH9GxLXZmtdowDIqtb/rvPyjjQv3z9wLcmgWKFHilKnZD3ldgrOlwJoPGOUluQsRPWd52yVkPfmI1A==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz", + "integrity": "sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==", "dev": true }, "whatwg-mimetype": { diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json index 83818e3a86..2fc0e84108 100644 --- a/awx/ui_next/package.json +++ b/awx/ui_next/package.json @@ -49,7 +49,7 @@ "jest-websocket-mock": "^2.0.2", "mock-socket": "^9.0.3", "prettier": "^1.18.2", - "react-scripts": "^3.4.1" + "react-scripts": "^3.4.3" }, "scripts": { "start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start", diff --git a/awx/ui_next/src/api/mixins/Notifications.mixin.js b/awx/ui_next/src/api/mixins/Notifications.mixin.js index 0198f0054f..87a7002ec5 100644 --- a/awx/ui_next/src/api/mixins/Notifications.mixin.js +++ b/awx/ui_next/src/api/mixins/Notifications.mixin.js @@ -87,6 +87,13 @@ const NotificationsMixin = parent => notificationId, notificationType ) { + if (notificationType === 'approvals') { + return this.associateNotificationTemplatesApprovals( + resourceId, + notificationId + ); + } + if (notificationType === 'started') { return this.associateNotificationTemplatesStarted( resourceId, @@ -126,6 +133,13 @@ const NotificationsMixin = parent => notificationId, notificationType ) { + if (notificationType === 'approvals') { + return this.disassociateNotificationTemplatesApprovals( + resourceId, + notificationId + ); + } + if (notificationType === 'started') { return this.disassociateNotificationTemplatesStarted( resourceId, diff --git a/awx/ui_next/src/api/models/Credentials.js b/awx/ui_next/src/api/models/Credentials.js index ec7f97812d..95e954fc0c 100644 --- a/awx/ui_next/src/api/models/Credentials.js +++ b/awx/ui_next/src/api/models/Credentials.js @@ -6,6 +6,7 @@ class Credentials extends Base { this.baseUrl = '/api/v2/credentials/'; this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readInputSources = this.readInputSources.bind(this); } @@ -15,6 +16,10 @@ class Credentials extends Base { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readInputSources(id, params) { return this.http.get(`${this.baseUrl}${id}/input_sources/`, { params, diff --git a/awx/ui_next/src/api/models/Inventories.js b/awx/ui_next/src/api/models/Inventories.js index ab828e32d6..077a534d27 100644 --- a/awx/ui_next/src/api/models/Inventories.js +++ b/awx/ui_next/src/api/models/Inventories.js @@ -7,6 +7,7 @@ class Inventories extends InstanceGroupsMixin(Base) { this.baseUrl = '/api/v2/inventories/'; this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readHosts = this.readHosts.bind(this); this.readHostDetail = this.readHostDetail.bind(this); this.readGroups = this.readGroups.bind(this); @@ -20,6 +21,10 @@ class Inventories extends InstanceGroupsMixin(Base) { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + createHost(id, data) { return this.http.post(`${this.baseUrl}${id}/hosts/`, data); } diff --git a/awx/ui_next/src/api/models/JobTemplates.js b/awx/ui_next/src/api/models/JobTemplates.js index 0e2eba8079..4f631cec2a 100644 --- a/awx/ui_next/src/api/models/JobTemplates.js +++ b/awx/ui_next/src/api/models/JobTemplates.js @@ -16,6 +16,7 @@ class JobTemplates extends SchedulesMixin( this.disassociateLabel = this.disassociateLabel.bind(this); this.readCredentials = this.readCredentials.bind(this); this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readWebhookKey = this.readWebhookKey.bind(this); } @@ -66,6 +67,10 @@ class JobTemplates extends SchedulesMixin( }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readScheduleList(id, params) { return this.http.get(`${this.baseUrl}${id}/schedules/`, { params, diff --git a/awx/ui_next/src/api/models/NotificationTemplates.js b/awx/ui_next/src/api/models/NotificationTemplates.js index 7736921ad2..69cd5f4022 100644 --- a/awx/ui_next/src/api/models/NotificationTemplates.js +++ b/awx/ui_next/src/api/models/NotificationTemplates.js @@ -5,6 +5,10 @@ class NotificationTemplates extends Base { super(http); this.baseUrl = '/api/v2/notification_templates/'; } + + test(id) { + return this.http.post(`${this.baseUrl}${id}/test/`); + } } export default NotificationTemplates; diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index 3cbe64c284..76c15504dc 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -12,9 +12,42 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { return this.http.get(`${this.baseUrl}${id}/access_list/`, { params }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readTeams(id, params) { return this.http.get(`${this.baseUrl}${id}/teams/`, { params }); } + + readTeamsOptions(id) { + return this.http.options(`${this.baseUrl}${id}/teams/`); + } + + createUser(id, data) { + return this.http.post(`${this.baseUrl}${id}/users/`, data); + } + + readNotificationTemplatesApprovals(id, params) { + return this.http.get( + `${this.baseUrl}${id}/notification_templates_approvals/`, + { params } + ); + } + + associateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId } + ); + } + + disassociateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId, disassociate: true } + ); + } } export default Organizations; diff --git a/awx/ui_next/src/api/models/Projects.js b/awx/ui_next/src/api/models/Projects.js index 269ef18f8a..38879a2bc2 100644 --- a/awx/ui_next/src/api/models/Projects.js +++ b/awx/ui_next/src/api/models/Projects.js @@ -11,6 +11,7 @@ class Projects extends SchedulesMixin( this.baseUrl = '/api/v2/projects/'; this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readInventories = this.readInventories.bind(this); this.readPlaybooks = this.readPlaybooks.bind(this); this.readSync = this.readSync.bind(this); @@ -21,6 +22,10 @@ class Projects extends SchedulesMixin( return this.http.get(`${this.baseUrl}${id}/access_list/`, { params }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readInventories(id) { return this.http.get(`${this.baseUrl}${id}/inventories/`); } diff --git a/awx/ui_next/src/api/models/Teams.js b/awx/ui_next/src/api/models/Teams.js index 1a205993d4..180c59032c 100644 --- a/awx/ui_next/src/api/models/Teams.js +++ b/awx/ui_next/src/api/models/Teams.js @@ -35,6 +35,10 @@ class Teams extends Base { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readUsersAccessOptions(teamId) { return this.http.options(`${this.baseUrl}${teamId}/users/`); } diff --git a/awx/ui_next/src/api/models/WorkflowJobTemplates.js b/awx/ui_next/src/api/models/WorkflowJobTemplates.js index 3074608796..7c582cd57f 100644 --- a/awx/ui_next/src/api/models/WorkflowJobTemplates.js +++ b/awx/ui_next/src/api/models/WorkflowJobTemplates.js @@ -54,6 +54,10 @@ class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readSurvey(id) { return this.http.get(`${this.baseUrl}${id}/survey_spec/`); } @@ -65,6 +69,27 @@ class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) { destroySurvey(id) { return this.http.delete(`${this.baseUrl}${id}/survey_spec/`); } + + readNotificationTemplatesApprovals(id, params) { + return this.http.get( + `${this.baseUrl}${id}/notification_templates_approvals/`, + { params } + ); + } + + associateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId } + ); + } + + disassociateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId, disassociate: true } + ); + } } export default WorkflowJobTemplates; diff --git a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx index 78655e44d9..ca4c4a40b6 100644 --- a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx +++ b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx @@ -1,25 +1,46 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, Fragment } from 'react'; import { Link } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; import PropTypes from 'prop-types'; -import { Dropdown, DropdownPosition } from '@patternfly/react-core'; +import { + Dropdown, + DropdownPosition, + DropdownItem, +} from '@patternfly/react-core'; import { ToolbarAddButton } from '../PaginatedDataList'; +import { toTitleCase } from '../../util/strings'; +import { useKebabifiedMenu } from '../../contexts/Kebabified'; -function AddDropDownButton({ dropdownItems }) { +function AddDropDownButton({ dropdownItems, i18n }) { + const { isKebabified } = useKebabifiedMenu(); const [isOpen, setIsOpen] = useState(false); const element = useRef(null); - const toggle = e => { - if (!element || !element.current.contains(e.target)) { - setIsOpen(false); - } - }; - useEffect(() => { + const toggle = e => { + if (!isKebabified && (!element || !element.current.contains(e.target))) { + setIsOpen(false); + } + }; + document.addEventListener('click', toggle, false); return () => { document.removeEventListener('click', toggle); }; - }, []); + }, [isKebabified]); + + if (isKebabified) { + return ( + + {dropdownItems.map(item => ( + + {toTitleCase(`${i18n._(t`Add`)} ${item.label}`)} + + ))} + + ); + } return (
@@ -52,4 +73,4 @@ AddDropDownButton.propTypes = { }; export { AddDropDownButton as _AddDropDownButton }; -export default AddDropDownButton; +export default withI18n()(AddDropDownButton); diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index 2da3b02e9d..95cb910295 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx @@ -11,8 +11,12 @@ import { TeamsAPI, UsersAPI } from '../../api'; const readUsers = async queryParams => UsersAPI.read(Object.assign(queryParams, { is_superuser: false })); +const readUsersOptions = async () => UsersAPI.readOptions(); + const readTeams = async queryParams => TeamsAPI.read(queryParams); +const readTeamsOptions = async () => TeamsAPI.readOptions(); + class AddResourceRole extends React.Component { constructor(props) { super(props); @@ -259,6 +263,7 @@ class AddResourceRole extends React.Component { displayKey="username" onRowClick={this.handleResourceCheckboxClick} fetchItems={readUsers} + fetchOptions={readUsersOptions} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={selectedResourceRows} sortedColumnKey="username" @@ -270,6 +275,7 @@ class AddResourceRole extends React.Component { sortColumns={teamSortColumns} onRowClick={this.handleResourceCheckboxClick} fetchItems={readTeams} + fetchOptions={readTeamsOptions} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={selectedResourceRows} /> diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx index 461327587e..f9a73d24ce 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx @@ -29,6 +29,7 @@ function SelectResourceStep({ selectedLabel, selectedResourceRows, fetchItems, + fetchOptions, i18n, }) { const location = useLocation(); @@ -37,7 +38,7 @@ function SelectResourceStep({ isLoading, error, request: readResourceList, - result: { resources, itemCount }, + result: { resources, itemCount, relatedSearchableKeys, searchableKeys }, } = useRequest( useCallback(async () => { const queryParams = parseQueryString( @@ -45,14 +46,28 @@ function SelectResourceStep({ location.search ); - const { - data: { count, results }, - } = await fetchItems(queryParams); - return { resources: results, itemCount: count }; - }, [location, fetchItems, sortColumns]), + const [ + { + data: { count, results }, + }, + actionsResponse, + ] = await Promise.all([fetchItems(queryParams), fetchOptions()]); + return { + resources: results, + itemCount: count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), + }; + }, [location, fetchItems, fetchOptions, sortColumns]), { resources: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -84,6 +99,8 @@ function SelectResourceStep({ onRowClick={onRowClick} toolbarSearchColumns={searchColumns} toolbarSortColumns={sortColumns} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={item => ( i.id === item.id)} diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx index c4c83f9d3b..4e307b7595 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx @@ -35,6 +35,7 @@ describe('', () => { displayKey="username" onRowClick={() => {}} fetchItems={() => {}} + fetchOptions={() => {}} /> ); }); @@ -49,6 +50,15 @@ describe('', () => { ], }, }); + const options = jest.fn().mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); let wrapper; await act(async () => { wrapper = mountWithContexts( @@ -58,6 +68,7 @@ describe('', () => { displayKey="username" onRowClick={() => {}} fetchItems={handleSearch} + fetchOptions={options} /> ); }); @@ -78,6 +89,15 @@ describe('', () => { { id: 2, username: 'bar', url: 'item/2' }, ], }; + const options = jest.fn().mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); let wrapper; await act(async () => { wrapper = mountWithContexts( @@ -87,6 +107,7 @@ describe('', () => { displayKey="username" onRowClick={handleRowClick} fetchItems={() => ({ data })} + fetchOptions={options} selectedResourceRows={[]} /> ); diff --git a/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx b/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx index 339b5ed744..f7a1b2da67 100644 --- a/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx +++ b/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx @@ -21,6 +21,7 @@ function AssociateModal({ onClose, onAssociate, fetchRequest, + optionsRequest, isModalOpen = false, }) { const history = useHistory(); @@ -28,24 +29,35 @@ function AssociateModal({ const { request: fetchItems, - result: { items, itemCount }, + result: { items, itemCount, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, history.location.search); - const { - data: { count, results }, - } = await fetchRequest(params); + const [ + { + data: { count, results }, + }, + actionsResponse, + ] = await Promise.all([fetchRequest(params), optionsRequest()]); return { items: results, itemCount: count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; - }, [fetchRequest, history.location.search]), + }, [fetchRequest, optionsRequest, history.location.search]), { items: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -132,6 +144,8 @@ function AssociateModal({ key: 'name', }, ]} + searchableKeys={searchableKeys} + relatedSearchableKeys={relatedSearchableKeys} /> diff --git a/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx b/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx index 2b2280b38b..4b58e900e3 100644 --- a/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx +++ b/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx @@ -15,6 +15,15 @@ describe('', () => { const onClose = jest.fn(); const onAssociate = jest.fn().mockResolvedValue(); const fetchRequest = jest.fn().mockReturnValue({ data: { ...mockHosts } }); + const optionsRequest = jest.fn().mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); beforeEach(async () => { await act(async () => { @@ -23,6 +32,7 @@ describe('', () => { onClose={onClose} onAssociate={onAssociate} fetchRequest={fetchRequest} + optionsRequest={optionsRequest} isModalOpen /> ); diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx index 7fbcd63cfa..3d02eb43bd 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx @@ -1,10 +1,15 @@ import 'styled-components/macro'; import React, { useState, useEffect } from 'react'; -import { string, node, number } from 'prop-types'; +import { node, number, oneOfType, shape, string } from 'prop-types'; import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core'; import { DetailName, DetailValue } from '../DetailList'; import MultiButtonToggle from '../MultiButtonToggle'; -import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml'; +import { + yamlToJson, + jsonToYaml, + isJsonObject, + isJsonString, +} from '../../util/yaml'; import CodeMirrorInput from './CodeMirrorInput'; import { JSON_MODE, YAML_MODE } from './constants'; @@ -15,7 +20,7 @@ function getValueAsMode(value, mode) { } return '---'; } - const modeMatches = isJson(value) === (mode === JSON_MODE); + const modeMatches = isJsonString(value) === (mode === JSON_MODE); if (modeMatches) { return value; } @@ -23,12 +28,21 @@ function getValueAsMode(value, mode) { } function VariablesDetail({ value, label, rows, fullHeight }) { - const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE); - const [currentValue, setCurrentValue] = useState(value || '---'); + const [mode, setMode] = useState( + isJsonObject(value) || isJsonString(value) ? JSON_MODE : YAML_MODE + ); + const [currentValue, setCurrentValue] = useState( + isJsonObject(value) ? JSON.stringify(value, null, 2) : value || '---' + ); const [error, setError] = useState(null); useEffect(() => { - setCurrentValue(getValueAsMode(value, mode)); + setCurrentValue( + getValueAsMode( + isJsonObject(value) ? JSON.stringify(value, null, 2) : value, + mode + ) + ); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [value]); @@ -95,7 +109,7 @@ function VariablesDetail({ value, label, rows, fullHeight }) { ); } VariablesDetail.propTypes = { - value: string.isRequired, + value: oneOfType([shape({}), string]).isRequired, label: node.isRequired, rows: number, }; diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx index f39d413fac..4d63fcc663 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx @@ -7,7 +7,7 @@ import styled from 'styled-components'; import { Split, SplitItem } from '@patternfly/react-core'; import { CheckboxField, FieldTooltip } from '../FormField'; import MultiButtonToggle from '../MultiButtonToggle'; -import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml'; +import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml'; import CodeMirrorInput from './CodeMirrorInput'; import { JSON_MODE, YAML_MODE } from './constants'; @@ -30,7 +30,9 @@ function VariablesField({ tooltip, }) { const [field, meta, helpers] = useField(name); - const [mode, setMode] = useState(isJson(field.value) ? JSON_MODE : YAML_MODE); + const [mode, setMode] = useState( + isJsonString(field.value) ? JSON_MODE : YAML_MODE + ); return (
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx index 5f3886e20b..a43962bd76 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { string, func, bool, number } from 'prop-types'; import { Split, SplitItem } from '@patternfly/react-core'; import styled from 'styled-components'; -import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml'; +import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml'; import MultiButtonToggle from '../MultiButtonToggle'; import CodeMirrorInput from './CodeMirrorInput'; import { JSON_MODE, YAML_MODE } from './constants'; @@ -18,11 +18,11 @@ const SplitItemRight = styled(SplitItem)` function VariablesInput(props) { const { id, label, readOnly, rows, error, onError, className } = props; /* eslint-disable react/destructuring-assignment */ - const defaultValue = isJson(props.value) + const defaultValue = isJsonString(props.value) ? formatJson(props.value) : props.value; const [value, setValue] = useState(defaultValue); - const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE); + const [mode, setMode] = useState(isJsonString(value) ? JSON_MODE : YAML_MODE); const isControlled = !!props.onChange; /* eslint-enable react/destructuring-assignment */ diff --git a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx index 9e02017fdf..1ddfb57df6 100644 --- a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx +++ b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -9,103 +9,128 @@ import { ToolbarGroup, ToolbarItem, ToolbarToggleGroup, + Dropdown, + KebabToggle, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import ExpandCollapse from '../ExpandCollapse'; import Search from '../Search'; import Sort from '../Sort'; - import { SearchColumns, SortColumns, QSConfig } from '../../types'; +import { KebabifiedProvider } from '../../contexts/Kebabified'; -class DataListToolbar extends React.Component { - render() { - const { - itemCount, - clearAllFilters, - searchColumns, - searchableKeys, - relatedSearchableKeys, - sortColumns, - showSelectAll, - isAllSelected, - isCompact, - onSort, - onSearch, - onReplaceSearch, - onRemove, - onCompact, - onExpand, - onSelectAll, - additionalControls, - i18n, - qsConfig, - pagination, - } = this.props; +function DataListToolbar({ + itemCount, + clearAllFilters, + searchColumns, + searchableKeys, + relatedSearchableKeys, + sortColumns, + showSelectAll, + isAllSelected, + isCompact, + onSort, + onSearch, + onReplaceSearch, + onRemove, + onCompact, + onExpand, + onSelectAll, + additionalControls, + i18n, + qsConfig, + pagination, +}) { + const showExpandCollapse = onCompact && onExpand; + const [kebabIsOpen, setKebabIsOpen] = useState(false); + const [advancedSearchShown, setAdvancedSearchShown] = useState(false); - const showExpandCollapse = onCompact && onExpand; - return ( - - - {showSelectAll && ( - - - - - - )} - } breakpoint="lg"> + const onShowAdvancedSearch = shown => { + setAdvancedSearchShown(shown); + setKebabIsOpen(false); + }; + + return ( + + + {showSelectAll && ( + - - - - - - {showExpandCollapse && ( - - - - - - - - )} + + )} + } breakpoint="lg"> + + + + + + + + {showExpandCollapse && ( + + + + + + + + )} + {advancedSearchShown && ( + + } + isOpen={kebabIsOpen} + isPlain + dropdownItems={additionalControls.map(control => { + return ( + + {control} + + ); + })} + /> + + )} + {!advancedSearchShown && ( {additionalControls.map(control => ( {control} ))} - {pagination && itemCount > 0 && ( - {pagination} - )} - - - ); - } + )} + {!advancedSearchShown && pagination && itemCount > 0 && ( + {pagination} + )} + + + ); } DataListToolbar.propTypes = { diff --git a/awx/ui_next/src/components/DetailList/ObjectDetail.jsx b/awx/ui_next/src/components/DetailList/ObjectDetail.jsx new file mode 100644 index 0000000000..bf008866a8 --- /dev/null +++ b/awx/ui_next/src/components/DetailList/ObjectDetail.jsx @@ -0,0 +1,51 @@ +import 'styled-components/macro'; +import React from 'react'; +import { shape, node, number } from 'prop-types'; +import { TextListItemVariants } from '@patternfly/react-core'; +import { DetailName, DetailValue } from './Detail'; +import CodeMirrorInput from '../CodeMirrorInput'; + +function ObjectDetail({ value, label, rows, fullHeight }) { + return ( + <> + +
+ + {label} + +
+
+ + + + + ); +} +ObjectDetail.propTypes = { + value: shape.isRequired, + label: node.isRequired, + rows: number, +}; +ObjectDetail.defaultProps = { + rows: null, +}; + +export default ObjectDetail; diff --git a/awx/ui_next/src/components/DetailList/index.js b/awx/ui_next/src/components/DetailList/index.js index 6a12824bad..8bebb27ce4 100644 --- a/awx/ui_next/src/components/DetailList/index.js +++ b/awx/ui_next/src/components/DetailList/index.js @@ -3,3 +3,8 @@ export { default as Detail, DetailName, DetailValue } from './Detail'; export { default as DeletedDetail } from './DeletedDetail'; export { default as UserDateDetail } from './UserDateDetail'; export { default as DetailBadge } from './DetailBadge'; +/* + NOTE: ObjectDetail cannot be imported here, as it causes circular + dependencies in testing environment. Import it directly from + DetailList/ObjectDetail +*/ diff --git a/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx b/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx index cafde49f73..fab8186f67 100644 --- a/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx +++ b/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx @@ -61,7 +61,7 @@ function DisassociateButton({
-
-
- {isModalOpen && ( - + {({ isKebabified }) => ( + + {isKebabified ? ( + {i18n._(t`Delete`)} - , - +
+ + )} + {isModalOpen && ( + + {i18n._(t`Delete`)} + , + , + ]} > - {i18n._(t`Cancel`)} - , - ]} - > -
{i18n._(t`This action will delete the following:`)}
- {itemsToDelete.map(item => ( - - {item.name || item.username} -
-
- ))} -
+
{i18n._(t`This action will delete the following:`)}
+ {itemsToDelete.map(item => ( + + {item.name || item.username} +
+
+ ))} + + )} + )} - + ); } } diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx index c74d6b5865..2afeefde7e 100644 --- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx +++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx @@ -26,22 +26,33 @@ function ResourceAccessList({ i18n, apiModel, resource }) { const location = useLocation(); const { - result: { accessRecords, itemCount }, + result: { accessRecords, itemCount, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, request: fetchAccessRecords, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const response = await apiModel.readAccessList(resource.id, params); + const [response, actionsResponse] = await Promise.all([ + apiModel.readAccessList(resource.id, params), + apiModel.readAccessOptions(resource.id), + ]); return { accessRecords: response.data.results, itemCount: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [apiModel, location, resource.id]), { accessRecords: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -106,6 +117,8 @@ function ResourceAccessList({ i18n, apiModel, resource }) { key: 'last_name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( ', () => { beforeEach(async () => { OrganizationsAPI.readAccessList.mockResolvedValue({ data }); + OrganizationsAPI.readAccessOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); TeamsAPI.disassociateRole.mockResolvedValue({}); UsersAPI.disassociateRole.mockResolvedValue({}); await act(async () => { diff --git a/awx/ui_next/src/components/Schedule/Schedule.test.jsx b/awx/ui_next/src/components/Schedule/Schedule.test.jsx index f0f58c0710..e3c394cc95 100644 --- a/awx/ui_next/src/components/Schedule/Schedule.test.jsx +++ b/awx/ui_next/src/components/Schedule/Schedule.test.jsx @@ -6,10 +6,12 @@ import { mountWithContexts, waitForElement, } from '../../../testUtils/enzymeHelpers'; -import { SchedulesAPI } from '../../api'; +import { JobTemplatesAPI, SchedulesAPI } from '../../api'; import Schedule from './Schedule'; +jest.mock('../../api/models/JobTemplates'); jest.mock('../../api/models/Schedules'); +jest.mock('../../api/models/WorkflowJobTemplates'); SchedulesAPI.readDetail.mockResolvedValue({ data: { @@ -62,6 +64,22 @@ SchedulesAPI.readCredentials.mockResolvedValue({ }, }); +JobTemplatesAPI.readLaunch.mockResolvedValue({ + data: { + ask_credential_on_launch: false, + ask_diff_mode_on_launch: false, + ask_inventory_on_launch: false, + ask_job_type_on_launch: false, + ask_limit_on_launch: false, + ask_scm_branch_on_launch: false, + ask_skip_tags_on_launch: false, + ask_tags_on_launch: false, + ask_variables_on_launch: false, + ask_verbosity_on_launch: false, + survey_enabled: false, + }, +}); + describe('', () => { let wrapper; let history; diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx index 64b8863fe6..4c63669590 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx @@ -17,10 +17,15 @@ import ScheduleOccurrences from '../ScheduleOccurrences'; import ScheduleToggle from '../ScheduleToggle'; import { formatDateString } from '../../../util/dates'; import useRequest, { useDismissableError } from '../../../util/useRequest'; -import { SchedulesAPI } from '../../../api'; +import { + JobTemplatesAPI, + SchedulesAPI, + WorkflowJobTemplatesAPI, +} from '../../../api'; import DeleteButton from '../../DeleteButton'; import ErrorDetail from '../../ErrorDetail'; import ChipGroup from '../../ChipGroup'; +import { VariablesDetail } from '../../CodeMirrorInput'; const PromptTitle = styled(Title)` --pf-c-title--m-md--FontWeight: 700; @@ -35,9 +40,9 @@ function ScheduleDetail({ schedule, i18n }) { diff_mode, dtend, dtstart, + extra_data, job_tags, job_type, - inventory, limit, modified, name, @@ -67,20 +72,47 @@ function ScheduleDetail({ schedule, i18n }) { const { error, dismissError } = useDismissableError(deleteError); const { - result: [credentials, preview], + result: [credentials, preview, launchData], isLoading, error: readContentError, request: fetchCredentialsAndPreview, } = useRequest( useCallback(async () => { - const [{ data }, { data: schedulePreview }] = await Promise.all([ + const promises = [ SchedulesAPI.readCredentials(id), SchedulesAPI.createPreview({ rrule, }), - ]); - return [data.results, schedulePreview]; - }, [id, rrule]), + ]; + + if ( + schedule?.summary_fields?.unified_job_template?.unified_job_type === + 'job' + ) { + promises.push( + JobTemplatesAPI.readLaunch( + schedule.summary_fields.unified_job_template.id + ) + ); + } else if ( + schedule?.summary_fields?.unified_job_template?.unified_job_type === + 'workflow_job' + ) { + promises.push( + WorkflowJobTemplatesAPI.readLaunch( + schedule.summary_fields.unified_job_template.id + ) + ); + } else { + promises.push(Promise.resolve()); + } + + const [{ data }, { data: schedulePreview }, launch] = await Promise.all( + promises + ); + + return [data.results, schedulePreview, launch?.data]; + }, [id, schedule, rrule]), [] ); @@ -93,15 +125,33 @@ function ScheduleDetail({ schedule, i18n }) { rule.options.freq === RRule.MINUTELY && dtstart === dtend ? i18n._(t`None (Run Once)`) : rule.toText().replace(/^\w/, c => c.toUpperCase()); + + const { + ask_credential_on_launch, + ask_diff_mode_on_launch, + ask_inventory_on_launch, + ask_job_type_on_launch, + ask_limit_on_launch, + ask_scm_branch_on_launch, + ask_skip_tags_on_launch, + ask_tags_on_launch, + ask_variables_on_launch, + ask_verbosity_on_launch, + survey_enabled, + } = launchData || {}; + const showPromptedFields = - (credentials && credentials.length > 0) || - job_type || - (inventory && summary_fields.inventory) || - scm_branch || - limit || - typeof diff_mode === 'boolean' || - (job_tags && job_tags.length > 0) || - (skip_tags && skip_tags.length > 0); + ask_credential_on_launch || + ask_diff_mode_on_launch || + ask_inventory_on_launch || + ask_job_type_on_launch || + ask_limit_on_launch || + ask_scm_branch_on_launch || + ask_skip_tags_on_launch || + ask_tags_on_launch || + ask_variables_on_launch || + ask_verbosity_on_launch || + survey_enabled; if (isLoading) { return ; @@ -144,8 +194,10 @@ function ScheduleDetail({ schedule, i18n }) { {i18n._(t`Prompted Fields`)} - - {inventory && summary_fields.inventory && ( + {ask_job_type_on_launch && ( + + )} + {ask_inventory_on_launch && ( )} - - - {typeof diff_mode === 'boolean' && ( + {ask_scm_branch_on_launch && ( + + )} + {ask_limit_on_launch && ( + + )} + {ask_diff_mode_on_launch && typeof diff_mode === 'boolean' && ( )} - {credentials && credentials.length > 0 && ( + {ask_credential_on_launch && ( )} - {job_tags && job_tags.length > 0 && ( + {ask_tags_on_launch && job_tags && job_tags.length > 0 && ( )} - {skip_tags && skip_tags.length > 0 && ( + {ask_skip_tags_on_launch && skip_tags && skip_tags.length > 0 && ( )} + {(ask_variables_on_launch || survey_enabled) && ( + + )} )} diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx index fe9175c6de..da325174d5 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx @@ -2,14 +2,48 @@ import React from 'react'; import { Route } from 'react-router-dom'; import { createMemoryHistory } from 'history'; import { act } from 'react-dom/test-utils'; -import { SchedulesAPI } from '../../../api'; +import { SchedulesAPI, JobTemplatesAPI } from '../../../api'; import { mountWithContexts, waitForElement, } from '../../../../testUtils/enzymeHelpers'; import ScheduleDetail from './ScheduleDetail'; +jest.mock('../../../api/models/JobTemplates'); jest.mock('../../../api/models/Schedules'); +jest.mock('../../../api/models/WorkflowJobTemplates'); + +const allPrompts = { + data: { + ask_credential_on_launch: true, + ask_diff_mode_on_launch: true, + ask_inventory_on_launch: true, + ask_job_type_on_launch: true, + ask_limit_on_launch: true, + ask_scm_branch_on_launch: true, + ask_skip_tags_on_launch: true, + ask_tags_on_launch: true, + ask_variables_on_launch: true, + ask_verbosity_on_launch: true, + survey_enabled: true, + }, +}; + +const noPrompts = { + data: { + ask_credential_on_launch: false, + ask_diff_mode_on_launch: false, + ask_inventory_on_launch: false, + ask_job_type_on_launch: false, + ask_limit_on_launch: false, + ask_scm_branch_on_launch: false, + ask_skip_tags_on_launch: false, + ask_tags_on_launch: false, + ask_variables_on_launch: false, + ask_verbosity_on_launch: false, + survey_enabled: false, + }, +}; const schedule = { url: '/api/v2/schedules/1', @@ -53,6 +87,7 @@ const schedule = { dtstart: '2020-03-16T04:00:00Z', dtend: '2020-07-06T04:00:00Z', next_run: '2020-03-16T04:00:00Z', + extra_data: {}, }; SchedulesAPI.createPreview.mockResolvedValue({ @@ -79,6 +114,7 @@ describe('', () => { results: [], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( ', () => { expect(wrapper.find('Detail[label="Credentials"]').length).toBe(0); expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(0); expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(0); + expect(wrapper.find('VariablesDetail').length).toBe(0); }); test('details should render with the proper values with prompts', async () => { SchedulesAPI.readCredentials.mockResolvedValue({ @@ -151,6 +188,7 @@ describe('', () => { ], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(allPrompts); const scheduleWithPrompts = { ...schedule, job_type: 'run', @@ -161,6 +199,7 @@ describe('', () => { limit: 'localhost', diff_mode: true, verbosity: 1, + extra_data: { foo: 'fii' }, }; await act(async () => { wrapper = mountWithContexts( @@ -182,7 +221,6 @@ describe('', () => { ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - // await waitForElement(wrapper, 'Title', el => el.length > 0); expect( wrapper .find('Detail[label="Name"]') @@ -231,6 +269,7 @@ describe('', () => { expect(wrapper.find('Detail[label="Credentials"]').length).toBe(1); expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(1); expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(1); + expect(wrapper.find('VariablesDetail').length).toBe(1); }); test('error shown when error encountered fetching credentials', async () => { SchedulesAPI.readCredentials.mockRejectedValueOnce( @@ -245,6 +284,7 @@ describe('', () => { }, }) ); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( ', () => { results: [], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( ', () => { results: [], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( val.slice(0, -8)), + searchableKeys: Object.keys( + scheduleActions.data.actions?.GET || {} + ).filter(key => scheduleActions.data.actions?.GET[key].filterable), }; }, [location, loadSchedules, loadScheduleOptions]), { schedules: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -106,10 +116,6 @@ function ScheduleList({ actions && Object.prototype.hasOwnProperty.call(actions, 'POST') && !hideAddButton; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx b/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx index 8e6ea219c0..a23a146059 100644 --- a/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx +++ b/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx @@ -181,42 +181,32 @@ const FrequencyDetailSubform = ({ i18n }) => { }; const getRunEveryLabel = () => { + const intervalValue = interval.value; + switch (frequency.value) { case 'minute': - return i18n.plural({ - value: interval.value, - one: 'minute', - other: 'minutes', + return i18n._('{intervalValue, plural, one {minute} other {minutes}}', { + intervalValue, }); case 'hour': - return i18n.plural({ - value: interval.value, - one: 'hour', - other: 'hours', + return i18n._('{intervalValue, plural, one {hour} other {hours}}', { + intervalValue, }); case 'day': - return i18n.plural({ - value: interval.value, - one: 'day', - other: 'days', + return i18n._('{intervalValue, plural, one {day} other {days}}', { + intervalValue, }); case 'week': - return i18n.plural({ - value: interval.value, - one: 'week', - other: 'weeks', + return i18n._('{intervalValue, plural, one {week} other {weeks}}', { + intervalValue, }); case 'month': - return i18n.plural({ - value: interval.value, - one: 'month', - other: 'months', + return i18n._('{intervalValue, plural, one {month} other {months}}', { + intervalValue, }); case 'year': - return i18n.plural({ - value: interval.value, - one: 'year', - other: 'years', + return i18n._('{intervalValue, plural, one {year} other {years}}', { + intervalValue, }); default: throw new Error(i18n._(t`Frequency did not match an expected value`)); diff --git a/awx/ui_next/src/components/Search/Search.jsx b/awx/ui_next/src/components/Search/Search.jsx index 8049a326e7..e92f5c2d16 100644 --- a/awx/ui_next/src/components/Search/Search.jsx +++ b/awx/ui_next/src/components/Search/Search.jsx @@ -40,6 +40,7 @@ function Search({ location, searchableKeys, relatedSearchableKeys, + onShowAdvancedSearch, }) { const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false); const [searchKey, setSearchKey] = useState( @@ -62,7 +63,7 @@ function Search({ const { key: actualSearchKey } = columns.find( ({ name }) => name === target.innerText ); - + onShowAdvancedSearch(actualSearchKey === 'advanced'); setIsFilterDropdownOpen(false); setSearchKey(actualSearchKey); }; @@ -301,6 +302,7 @@ Search.propTypes = { columns: SearchColumns.isRequired, onSearch: PropTypes.func, onRemove: PropTypes.func, + onShowAdvancedSearch: PropTypes.func.isRequired, }; Search.defaultProps = { diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx index a34b6bcc09..6c1badfa56 100644 --- a/awx/ui_next/src/components/Search/Search.test.jsx +++ b/awx/ui_next/src/components/Search/Search.test.jsx @@ -36,7 +36,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -64,7 +69,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -83,6 +93,50 @@ describe('', () => { expect(onSearch).toBeCalledWith('description__icontains', 'test-321'); }); + test('changing key select to and from advanced causes onShowAdvancedSearch callback to be invoked', () => { + const columns = [ + { name: 'Name', key: 'name__icontains', isDefault: true }, + { name: 'Description', key: 'description__icontains' }, + { name: 'Advanced', key: 'advanced' }, + ]; + const onSearch = jest.fn(); + const onShowAdvancedSearch = jest.fn(); + const wrapper = mountWithContexts( + {}} + collapseListedFiltersBreakpoint="lg" + > + + + + + ); + + act(() => { + wrapper + .find('Select[aria-label="Simple key select"]') + .invoke('onSelect')({ target: { innerText: 'Advanced' } }); + }); + wrapper.update(); + expect(onShowAdvancedSearch).toHaveBeenCalledTimes(1); + expect(onShowAdvancedSearch).toBeCalledWith(true); + jest.clearAllMocks(); + act(() => { + wrapper + .find('Select[aria-label="Simple key select"]') + .invoke('onSelect')({ target: { innerText: 'Description' } }); + }); + wrapper.update(); + expect(onShowAdvancedSearch).toHaveBeenCalledTimes(1); + expect(onShowAdvancedSearch).toBeCalledWith(false); + }); + test('attempt to search with empty string', () => { const searchButton = 'button[aria-label="Search submit button"]'; const searchTextInput = 'input[aria-label="Search text input"]'; @@ -95,7 +149,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -119,7 +178,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -150,7 +214,11 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + , { context: { router: { history } } } @@ -197,6 +265,7 @@ describe('', () => { qsConfig={qsConfigNew} columns={columns} onRemove={onRemove} + onShowAdvancedSearch={jest.fn} /> , @@ -243,6 +312,7 @@ describe('', () => { qsConfig={qsConfigNew} columns={columns} onRemove={onRemove} + onShowAdvancedSearch={jest.fn} /> , @@ -277,7 +347,11 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + , { context: { router: { history } } } diff --git a/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx new file mode 100644 index 0000000000..0f2be56fdc --- /dev/null +++ b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx @@ -0,0 +1,68 @@ +import 'styled-components/macro'; +import React from 'react'; +import { oneOf } from 'prop-types'; +import { Label } from '@patternfly/react-core'; +import { + CheckCircleIcon, + ExclamationCircleIcon, + SyncAltIcon, + ExclamationTriangleIcon, + ClockIcon, +} from '@patternfly/react-icons'; +import styled, { keyframes } from 'styled-components'; + +const Spin = keyframes` + from { + transform: rotate(0); + } + to { + transform: rotate(1turn); + } +`; + +const RunningIcon = styled(SyncAltIcon)` + animation: ${Spin} 1.75s linear infinite; +`; + +const colors = { + success: 'green', + failed: 'red', + error: 'red', + running: 'blue', + pending: 'blue', + waiting: 'grey', + canceled: 'orange', +}; +const icons = { + success: CheckCircleIcon, + failed: ExclamationCircleIcon, + error: ExclamationCircleIcon, + running: RunningIcon, + pending: ClockIcon, + waiting: ClockIcon, + canceled: ExclamationTriangleIcon, +}; + +export default function StatusLabel({ status }) { + const label = status.charAt(0).toUpperCase() + status.slice(1); + const color = colors[status] || 'grey'; + const Icon = icons[status]; + + return ( + + ); +} + +StatusLabel.propTypes = { + status: oneOf([ + 'success', + 'failed', + 'error', + 'running', + 'pending', + 'waiting', + 'canceled', + ]).isRequired, +}; diff --git a/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx b/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx new file mode 100644 index 0000000000..58fb6c1a28 --- /dev/null +++ b/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import StatusLabel from './StatusLabel'; + +describe('StatusLabel', () => { + test('should render success', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('CheckCircleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('green'); + expect(wrapper.text()).toEqual('Success'); + }); + + test('should render failed', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('red'); + expect(wrapper.text()).toEqual('Failed'); + }); + + test('should render error', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('red'); + expect(wrapper.text()).toEqual('Error'); + }); + + test('should render running', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('SyncAltIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('blue'); + expect(wrapper.text()).toEqual('Running'); + }); + + test('should render pending', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ClockIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('blue'); + expect(wrapper.text()).toEqual('Pending'); + }); + + test('should render waiting', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ClockIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('grey'); + expect(wrapper.text()).toEqual('Waiting'); + }); + + test('should render canceled', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ExclamationTriangleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('orange'); + expect(wrapper.text()).toEqual('Canceled'); + }); +}); diff --git a/awx/ui_next/src/components/StatusLabel/index.js b/awx/ui_next/src/components/StatusLabel/index.js new file mode 100644 index 0000000000..b9dfc8cd99 --- /dev/null +++ b/awx/ui_next/src/components/StatusLabel/index.js @@ -0,0 +1 @@ +export { default } from './StatusLabel'; diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx index ceac1a32f5..ad83ce61e9 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx @@ -96,6 +96,7 @@ function UserAndTeamAccessAdd({ displayKey="name" onRowClick={handleResourceSelect} fetchItems={selectedResourceType.fetchItems} + fetchOptions={selectedResourceType.fetchOptions} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={resourcesSelected} sortedColumnKey="username" diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx index a46d5d87a3..7ad19c9057 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx @@ -43,6 +43,15 @@ describe('', () => { count: 1, }, }; + const options = { + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }; let wrapper; beforeEach(async () => { await act(async () => { @@ -111,11 +120,13 @@ describe('', () => { test('should call api to associate role', async () => { JobTemplatesAPI.read.mockResolvedValue(resources); + JobTemplatesAPI.readOptions.mockResolvedValue(options); UsersAPI.associateRole.mockResolvedValue({}); await act(async () => wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({ fetchItems: JobTemplatesAPI.read, + fetchOptions: JobTemplatesAPI.readOptions, label: 'Job template', selectedResource: 'jobTemplate', searchColumns: [ @@ -169,6 +180,7 @@ describe('', () => { test('should throw error', async () => { JobTemplatesAPI.read.mockResolvedValue(resources); + JobTemplatesAPI.readOptions.mockResolvedValue(options); UsersAPI.associateRole.mockRejectedValue( new Error({ response: { @@ -192,6 +204,7 @@ describe('', () => { await act(async () => wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({ fetchItems: JobTemplatesAPI.read, + fetchOptions: JobTemplatesAPI.readOptions, label: 'Job template', selectedResource: 'jobTemplate', searchColumns: [ diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js index cd922c23aa..843670e1a1 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js @@ -39,6 +39,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => JobTemplatesAPI.read(queryParams), + fetchOptions: () => JobTemplatesAPI.readOptions(), }, { selectedResource: 'workflowJobTemplate', @@ -69,6 +70,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => WorkflowJobTemplatesAPI.read(queryParams), + fetchOptions: () => WorkflowJobTemplatesAPI.readOptions(), }, { selectedResource: 'credential', @@ -87,6 +89,7 @@ export default function getResourceAccessConfig(i18n) { [`git`, i18n._(t`Git`)], [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], + [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], ], }, @@ -110,6 +113,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => CredentialsAPI.read(queryParams), + fetchOptions: () => CredentialsAPI.readOptions(), }, { selectedResource: 'inventory', @@ -136,6 +140,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => InventoriesAPI.read(queryParams), + fetchOptions: () => InventoriesAPI.readOptions(), }, { selectedResource: 'project', @@ -154,6 +159,7 @@ export default function getResourceAccessConfig(i18n) { [`git`, i18n._(t`Git`)], [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], + [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], ], }, @@ -177,6 +183,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => ProjectsAPI.read(queryParams), + fetchOptions: () => ProjectsAPI.readOptions(), }, { selectedResource: 'organization', @@ -203,6 +210,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => OrganizationsAPI.read(queryParams), + fetchOptions: () => OrganizationsAPI.readOptions(), }, ]; } diff --git a/awx/ui_next/src/contexts/Kebabified.jsx b/awx/ui_next/src/contexts/Kebabified.jsx new file mode 100644 index 0000000000..c50431c73f --- /dev/null +++ b/awx/ui_next/src/contexts/Kebabified.jsx @@ -0,0 +1,8 @@ +import React, { useContext } from 'react'; + +// eslint-disable-next-line import/prefer-default-export +export const KebabifiedContext = React.createContext({}); + +export const KebabifiedProvider = KebabifiedContext.Provider; +export const Kebabified = KebabifiedContext.Consumer; +export const useKebabifiedMenu = () => useContext(KebabifiedContext); diff --git a/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx b/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx index 7d123385ef..2ad56eaa7f 100644 --- a/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx @@ -26,7 +26,7 @@ function ApplicationTokenList({ i18n }) { const { error, isLoading, - result: { tokens, itemCount, actions, relatedSearchFields }, + result: { tokens, itemCount, relatedSearchableKeys, searchableKeys }, request: fetchTokens, } = useRequest( useCallback(async () => { @@ -52,13 +52,15 @@ function ApplicationTokenList({ i18n }) { return { tokens: modifiedResults, itemCount: count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [id, location.search]), - { tokens: [], itemCount: 0, actions: {}, relatedSearchFields: [] } + { tokens: [], itemCount: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { @@ -91,11 +93,6 @@ function ApplicationTokenList({ i18n }) { setSelected([]); }; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( <> { const params = parseQueryString(QS_CONFIG, location.search); @@ -46,16 +52,20 @@ function ApplicationsList({ i18n }) { applications: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { applications: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -89,10 +99,6 @@ function ApplicationsList({ i18n }) { }; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx index 0eab894a85..ac85aa7939 100644 --- a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx @@ -268,7 +268,10 @@ describe('', () => { test('handleCancel returns the user to credential detail', async () => { await waitForElement(wrapper, 'isLoading', el => el.length === 0); - wrapper.find('Button[aria-label="Cancel"]').simulate('click'); + await act(async () => { + wrapper.find('Button[aria-label="Cancel"]').simulate('click'); + }); + wrapper.update(); expect(history.location.pathname).toEqual('/credentials/3/details'); }); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx index 40a305e72f..bc91a98894 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx @@ -25,7 +25,7 @@ function CredentialsStep({ i18n }) { const history = useHistory(); const { - result: { credentials, count, actions, relatedSearchFields }, + result: { credentials, count, relatedSearchableKeys, searchableKeys }, error: credentialsError, isLoading: isCredentialsLoading, request: fetchCredentials, @@ -39,24 +39,21 @@ function CredentialsStep({ i18n }) { return { credentials: data.results, count: data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location.search]), - { credentials: [], count: 0, actions: {}, relatedSearchFields: [] } + { credentials: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { fetchCredentials(); }, [fetchCredentials]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - if (credentialsError) { return ; } diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx index bfcde651a4..f902b5e0a6 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx @@ -33,7 +33,13 @@ function HostGroupsList({ i18n, host }) { const invId = host.summary_fields.inventory.id; const { - result: { groups, itemCount, actions, relatedSearchFields }, + result: { + groups, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchGroups, @@ -55,16 +61,20 @@ function HostGroupsList({ i18n, host }) { groups: results, itemCount: count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [hostId, search]), { groups: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -108,6 +118,11 @@ function HostGroupsList({ i18n, host }) { [invId, hostId] ); + const fetchGroupsOptions = useCallback( + () => InventoriesAPI.readGroupsOptions(invId), + [invId] + ); + const { request: handleAssociate, error: associateError } = useRequest( useCallback( async groupsToAssociate => { @@ -128,10 +143,6 @@ function HostGroupsList({ i18n, host }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> @@ -218,6 +229,7 @@ function HostGroupsList({ i18n, host }) { setIsModalOpen(false)} diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx index 1b2984ed81..4720de35ed 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx @@ -207,6 +207,15 @@ describe('', () => { results: [{ id: 123, name: 'foo', url: '/api/v2/groups/123/' }], }, }); + InventoriesAPI.readGroupsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper.find('ToolbarAddButton').simulate('click'); }); diff --git a/awx/ui_next/src/screens/Host/HostList/HostList.jsx b/awx/ui_next/src/screens/Host/HostList/HostList.jsx index 3e6a2589c6..a11ec3f4d1 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostList.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostList.jsx @@ -29,7 +29,7 @@ function HostList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { hosts, count, actions, relatedSearchFields }, + result: { hosts, count, actions, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, request: fetchHosts, @@ -44,16 +44,20 @@ function HostList({ i18n }) { hosts: results[0].data.results, count: results[0].data.count, actions: results[1].data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( results[1]?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys(results[1].data.actions?.GET || {}).filter( + key => results[1].data.actions?.GET[key].filterable + ), }; }, [location]), { hosts: [], count: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -97,10 +101,6 @@ function HostList({ i18n }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( diff --git a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx index e2751f6f06..377fb453ab 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx @@ -54,11 +54,7 @@ function HostListItem({ i18n, host, isSelected, onSelect, detailUrl }) { {i18n._(t`Inventory`)} {host.summary_fields.inventory.name} diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx index a42048f503..4597bbd61c 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx @@ -17,10 +17,10 @@ import { InstanceGroupsAPI } from '../../api'; import RoutedTabs from '../../components/RoutedTabs'; import ContentError from '../../components/ContentError'; import ContentLoading from '../../components/ContentLoading'; +import JobList from '../../components/JobList'; import InstanceGroupDetails from './InstanceGroupDetails'; import InstanceGroupEdit from './InstanceGroupEdit'; -import Jobs from './Jobs'; import Instances from './Instances'; function InstanceGroup({ i18n, setBreadcrumb }) { @@ -126,7 +126,10 @@ function InstanceGroup({ i18n, setBreadcrumb }) { - + )} diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx index 8df5e5b863..0d6964559a 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx @@ -2,8 +2,8 @@ import React, { useCallback } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link, useHistory } from 'react-router-dom'; -import { Button } from '@patternfly/react-core'; -import 'styled-components/macro'; +import styled from 'styled-components'; +import { Button, Label } from '@patternfly/react-core'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; @@ -17,6 +17,10 @@ import { import useRequest, { useDismissableError } from '../../../util/useRequest'; import { InstanceGroupsAPI } from '../../../api'; +const Unavailable = styled.span` + color: var(--pf-global--danger-color--200); +`; + function InstanceGroupDetails({ instanceGroup, i18n }) { const { id, name } = instanceGroup; @@ -42,12 +46,28 @@ function InstanceGroupDetails({ instanceGroup, i18n }) { ); }; + const verifyIsIsolated = item => { + if (item.is_isolated) { + return ( + <> + {item.name} + + + + + ); + } + return <>{item.name}; + }; + return ( {i18n._(t`Unavailable`)}} + value={{i18n._(t`Unavailable`)}} dataCy="instance-group-used-capacity" /> )} diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.test.jsx index 7df34cf91d..15b091f9bf 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.test.jsx @@ -21,6 +21,7 @@ const instanceGroups = [ policy_instance_percentage: 50, percent_capacity_remaining: 60, is_containerized: false, + is_isolated: false, created: '2020-07-21T18:41:02.818081Z', modified: '2020-07-24T20:32:03.121079Z', summary_fields: { @@ -40,6 +41,7 @@ const instanceGroups = [ policy_instance_percentage: 0, percent_capacity_remaining: 0, is_containerized: true, + is_isolated: false, created: '2020-07-21T18:41:02.818081Z', modified: '2020-07-24T20:32:03.121079Z', summary_fields: { @@ -67,7 +69,11 @@ describe('', () => { }); wrapper.update(); - expectDetailToMatch(wrapper, 'Name', instanceGroups[0].name); + + expect(wrapper.find('Detail[label="Name"]').text()).toEqual( + expect.stringContaining(instanceGroups[0].name) + ); + expect(wrapper.find('Detail[label="Name"]')).toHaveLength(1); expectDetailToMatch(wrapper, 'Type', `Instance group`); const dates = wrapper.find('UserDateDetail'); expect(dates).toHaveLength(2); @@ -144,4 +150,18 @@ describe('', () => { expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(0); }); + + test('should display isolated label', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + expect( + wrapper.find('Label[aria-label="isolated instance"]').prop('children') + ).toEqual('Isolated'); + }); }); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx index 2f724479ee..b2f9bbaa9a 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx @@ -1,13 +1,37 @@ -import React from 'react'; -import { Card, PageSection } from '@patternfly/react-core'; +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { CardBody } from '../../../components/Card'; +import { InstanceGroupsAPI } from '../../../api'; +import InstanceGroupForm from '../shared/InstanceGroupForm'; + +function InstanceGroupEdit({ instanceGroup }) { + const history = useHistory(); + const [submitError, setSubmitError] = useState(null); + const detailsUrl = `/instance_groups/${instanceGroup.id}/details`; + + const handleSubmit = async values => { + try { + await InstanceGroupsAPI.update(instanceGroup.id, values); + history.push(detailsUrl); + } catch (error) { + setSubmitError(error); + } + }; + + const handleCancel = () => { + history.push(detailsUrl); + }; -function InstanceGroupEdit() { return ( - - -
Edit instance group
-
-
+ + + ); } diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx new file mode 100644 index 0000000000..45b94bd17d --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { InstanceGroupsAPI } from '../../../api'; + +import InstanceGroupEdit from './InstanceGroupEdit'; + +jest.mock('../../../api'); + +const instanceGroupData = { + id: 42, + type: 'instance_group', + url: '/api/v2/instance_groups/42/', + related: { + jobs: '/api/v2/instance_groups/42/jobs/', + instances: '/api/v2/instance_groups/7/instances/', + }, + name: 'Foo', + created: '2020-07-21T18:41:02.818081Z', + modified: '2020-07-24T20:32:03.121079Z', + capacity: 24, + committed_capacity: 0, + consumed_capacity: 0, + percent_capacity_remaining: 100.0, + jobs_running: 0, + jobs_total: 0, + instances: 1, + controller: null, + is_controller: false, + is_isolated: false, + is_containerized: false, + credential: null, + policy_instance_percentage: 46, + policy_instance_minimum: 12, + policy_instance_list: [], + pod_spec_override: '', + summary_fields: { + user_capabilities: { + edit: true, + delete: true, + }, + }, +}; + +const updatedInstanceGroup = { + name: 'Bar', + policy_instance_percentage: 42, +}; + +describe('', () => { + let wrapper; + let history; + + beforeAll(async () => { + history = createMemoryHistory(); + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('tower instance group name can not be updated', async () => { + let towerWrapper; + await act(async () => { + towerWrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + expect( + towerWrapper.find('input#instance-group-name').prop('disabled') + ).toBeTruthy(); + expect( + towerWrapper.find('input#instance-group-name').prop('value') + ).toEqual('tower'); + }); + + test('handleSubmit should call the api and redirect to details page', async () => { + await act(async () => { + wrapper.find('InstanceGroupForm').invoke('onSubmit')( + updatedInstanceGroup + ); + }); + expect(InstanceGroupsAPI.update).toHaveBeenCalledWith( + 42, + updatedInstanceGroup + ); + }); + + test('should navigate to instance group details when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); + }); + expect(history.location.pathname).toEqual('/instance_groups/42/details'); + }); + + test('should navigate to instance group details after successful submission', async () => { + await act(async () => { + wrapper.find('InstanceGroupForm').invoke('onSubmit')( + updatedInstanceGroup + ); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(history.location.pathname).toEqual('/instance_groups/42/details'); + }); + + test('failed form submission should show an error message', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + InstanceGroupsAPI.update.mockImplementationOnce(() => + Promise.reject(error) + ); + await act(async () => { + wrapper.find('InstanceGroupForm').invoke('onSubmit')( + updatedInstanceGroup + ); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx index 93e334d367..f531f8b182 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx @@ -6,15 +6,16 @@ import { Link } from 'react-router-dom'; import 'styled-components/macro'; import { Badge as PFBadge, - Progress, - ProgressMeasureLocation, - ProgressSize, Button, DataListAction as _DataListAction, DataListCheck, DataListItem, - DataListItemRow, DataListItemCells, + DataListItemRow, + Label, + Progress, + ProgressMeasureLocation, + ProgressSize, Tooltip, } from '@patternfly/react-core'; import { PencilAltIcon } from '@patternfly/react-icons'; @@ -46,6 +47,10 @@ const DataListAction = styled(_DataListAction)` grid-template-columns: 40px; `; +const Unavailable = styled.span` + color: var(--pf-global--danger-color--200); +`; + function InstanceGroupListItem({ instanceGroup, detailUrl, @@ -78,7 +83,7 @@ function InstanceGroupListItem({ /> ); } - return {i18n._(t`Unavailable`)}; + return {i18n._(t`Unavailable`)}; } return null; } @@ -108,6 +113,13 @@ function InstanceGroupListItem({ {instanceGroup.name} + {instanceGroup.is_isolated ? ( + + + + ) : null} ,
- - {({ me }) => ( - - )} - + diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx index 7b5355d201..070e64d0ae 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -85,7 +85,7 @@ function InventoryGroupDetail({ i18n, inventoryGroup }) { {i18n._(t`Edit`)} , + ] + : [] + } + /> + )} + renderItem={host => ( + row.id === host.id)} + onSelect={() => handleSelect(host)} + /> + )} + /> + ); +} + +SmartInventoryHostList.propTypes = { + inventory: Inventory.isRequired, +}; + +export default withI18n()(SmartInventoryHostList); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx new file mode 100644 index 0000000000..ae3f00d66f --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { InventoriesAPI } from '../../../api'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; +import SmartInventoryHostList from './SmartInventoryHostList'; +import mockInventory from '../shared/data.inventory.json'; +import mockHosts from '../shared/data.hosts.json'; + +jest.mock('../../../api'); + +describe('', () => { + describe('User has adhoc permissions', () => { + let wrapper; + const clonedInventory = { + ...mockInventory, + summary_fields: { + ...mockInventory.summary_fields, + user_capabilities: { + ...mockInventory.summary_fields.user_capabilities, + }, + }, + }; + + beforeAll(async () => { + InventoriesAPI.readHosts.mockResolvedValue({ + data: mockHosts, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + afterAll(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('initially renders successfully', () => { + expect(wrapper.find('SmartInventoryHostList').length).toBe(1); + }); + + test('should fetch hosts from api and render them in the list', () => { + expect(InventoriesAPI.readHosts).toHaveBeenCalled(); + expect(wrapper.find('SmartInventoryHostListItem').length).toBe(3); + }); + + test('should disable run commands button when no hosts are selected', () => { + wrapper.find('DataListCheck').forEach(el => { + expect(el.props().checked).toBe(false); + }); + const runCommandsButton = wrapper.find( + 'button[aria-label="Run commands"]' + ); + expect(runCommandsButton.length).toBe(1); + expect(runCommandsButton.prop('disabled')).toEqual(true); + }); + + test('should enable run commands button when at least one host is selected', () => { + act(() => { + wrapper.find('DataListCheck[id="select-host-2"]').invoke('onChange')( + true + ); + }); + wrapper.update(); + const runCommandsButton = wrapper.find( + 'button[aria-label="Run commands"]' + ); + expect(runCommandsButton.prop('disabled')).toEqual(false); + }); + + test('should select and deselect all items', async () => { + act(() => { + wrapper.find('DataListToolbar').invoke('onSelectAll')(true); + }); + wrapper.update(); + wrapper.find('DataListCheck').forEach(el => { + expect(el.props().checked).toEqual(true); + }); + act(() => { + wrapper.find('DataListToolbar').invoke('onSelectAll')(false); + }); + wrapper.update(); + wrapper.find('DataListCheck').forEach(el => { + expect(el.props().checked).toEqual(false); + }); + }); + + test('should show content error when api throws an error', async () => { + InventoriesAPI.readHosts.mockImplementation(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentError', el => el.length === 1); + }); + }); + + describe('User does not have adhoc permissions', () => { + let wrapper; + const clonedInventory = { + ...mockInventory, + summary_fields: { + user_capabilities: { + adhoc: false, + }, + }, + }; + + test('should hide run commands button', async () => { + InventoriesAPI.readHosts.mockResolvedValue({ + data: { results: [], count: 0 }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + const runCommandsButton = wrapper.find( + 'button[aria-label="Run commands"]' + ); + expect(runCommandsButton.length).toBe(0); + jest.clearAllMocks(); + wrapper.unmount(); + }); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx new file mode 100644 index 0000000000..30a807c459 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { string, bool, func } from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import 'styled-components/macro'; + +import { + DataListCheck, + DataListItem, + DataListItemCells, + DataListItemRow, +} from '@patternfly/react-core'; +import DataListCell from '../../../components/DataListCell'; +import Sparkline from '../../../components/Sparkline'; +import { Host } from '../../../types'; + +function SmartInventoryHostListItem({ + i18n, + detailUrl, + host, + isSelected, + onSelect, +}) { + const recentPlaybookJobs = host.summary_fields.recent_jobs.map(job => ({ + ...job, + type: 'job', + })); + + const labelId = `check-action-${host.id}`; + + return ( + + + + + + {host.name} + + , + + + , + + <> + {i18n._(t`Inventory`)} + + {host.summary_fields.inventory.name} + + + , + ]} + /> + + + ); +} + +SmartInventoryHostListItem.propTypes = { + detailUrl: string.isRequired, + host: Host.isRequired, + isSelected: bool.isRequired, + onSelect: func.isRequired, +}; + +export default withI18n()(SmartInventoryHostListItem); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx new file mode 100644 index 0000000000..9a33460fcb --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import SmartInventoryHostListItem from './SmartInventoryHostListItem'; + +const mockHost = { + id: 2, + name: 'Host Two', + url: '/api/v2/hosts/2', + inventory: 1, + summary_fields: { + inventory: { + id: 1, + name: 'Inv 1', + }, + user_capabilities: { + edit: true, + }, + recent_jobs: [], + }, +}; + +describe('', () => { + let wrapper; + + beforeEach(() => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + test('should render expected row cells', () => { + const cells = wrapper.find('DataListCell'); + expect(cells).toHaveLength(3); + expect(cells.at(0).text()).toEqual('Host Two'); + expect(cells.at(1).find('Sparkline').length).toEqual(1); + expect(cells.at(2).text()).toContain('Inv 1'); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx index 608e664e95..41a05952c2 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx @@ -1,10 +1,27 @@ -import React, { Component } from 'react'; -import { CardBody } from '../../../components/Card'; +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import SmartInventoryHostList from './SmartInventoryHostList'; +import SmartInventoryHost from '../SmartInventoryHost'; +import { Inventory } from '../../../types'; -class SmartInventoryHosts extends Component { - render() { - return Coming soon :); - } +function SmartInventoryHosts({ inventory, setBreadcrumb }) { + return ( + + + + + + + + + ); } +SmartInventoryHosts.propTypes = { + inventory: Inventory.isRequired, +}; + export default SmartInventoryHosts; diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx new file mode 100644 index 0000000000..1db767dfe4 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; +import SmartInventoryHosts from './SmartInventoryHosts'; + +jest.mock('../../../api'); + +describe('', () => { + test('should render smart inventory host list', () => { + const history = createMemoryHistory({ + initialEntries: ['/inventories/smart_inventory/1/hosts'], + }); + const match = { + path: '/inventories/smart_inventory/:id/hosts', + url: '/inventories/smart_inventory/1/hosts', + isExact: true, + }; + const wrapper = mountWithContexts( + , + { + context: { router: { history, route: { match } } }, + } + ); + expect(wrapper.find('SmartInventoryHostList').length).toBe(1); + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('should render smart inventory host details', async () => { + let wrapper; + const history = createMemoryHistory({ + initialEntries: ['/inventories/smart_inventory/1/hosts/2'], + }); + const match = { + path: '/inventories/smart_inventory/:id/hosts/:hostId', + url: '/inventories/smart_inventory/1/hosts/2', + isExact: true, + }; + await act(async () => { + wrapper = mountWithContexts( + {}} />, + { + context: { router: { history, route: { match } } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('SmartInventoryHost').length).toBe(1); + jest.clearAllMocks(); + wrapper.unmount(); + }); +}); diff --git a/awx/ui_next/src/screens/Job/Job.jsx b/awx/ui_next/src/screens/Job/Job.jsx index bc95cfaf39..9530c4eeb3 100644 --- a/awx/ui_next/src/screens/Job/Job.jsx +++ b/awx/ui_next/src/screens/Job/Job.jsx @@ -7,10 +7,8 @@ import { Card, PageSection } from '@patternfly/react-core'; import { JobsAPI } from '../../api'; import ContentError from '../../components/ContentError'; import RoutedTabs from '../../components/RoutedTabs'; - import JobDetail from './JobDetail'; import JobOutput from './JobOutput'; -import WorkflowDetail from './WorkflowDetail'; import { WorkflowOutput } from './WorkflowOutput'; import { JOB_TYPE_URL_SEGMENTS } from '../../constants'; @@ -129,7 +127,7 @@ class Job extends Component { {job && job.type === 'workflow_job' && [ - + , diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx index dc7a3e0e6b..96c0794d35 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { Link, useHistory } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Button, Chip } from '@patternfly/react-core'; +import { Button, Chip, Label } from '@patternfly/react-core'; import styled from 'styled-components'; import AlertModal from '../../../components/AlertModal'; @@ -84,6 +84,7 @@ function JobDetail({ job, i18n }) { instance_group: instanceGroup, inventory, job_template: jobTemplate, + workflow_job_template: workflowJobTemplate, labels, project, } = job.summary_fields; @@ -120,6 +121,22 @@ function JobDetail({ job, i18n }) { } }; + const isIsolatedInstanceGroup = item => { + if (item.is_isolated) { + return ( + <> + {item.name} + + + + + ); + } + return {item.name}; + }; + return ( @@ -143,7 +160,7 @@ function JobDetail({ job, i18n }) { /> {jobTemplate && ( {jobTemplate.name} @@ -151,6 +168,18 @@ function JobDetail({ job, i18n }) { } /> )} + {workflowJobTemplate && ( + + {workflowJobTemplate.name} + + } + /> + )} - {instanceGroup.name} - - } + value={isIsolatedInstanceGroup(instanceGroup)} /> )} {typeof job.job_slice_number === 'number' && diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx index e5bc71c53d..2d5ff5d5fe 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx @@ -31,7 +31,7 @@ describe('', () => { assertDetail('Status', 'Successful'); assertDetail('Started', '8/8/2019, 7:24:18 PM'); assertDetail('Finished', '8/8/2019, 7:24:50 PM'); - assertDetail('Template', mockJobData.summary_fields.job_template.name); + assertDetail('Job Template', mockJobData.summary_fields.job_template.name); assertDetail('Job Type', 'Run'); assertDetail('Launched By', mockJobData.summary_fields.created_by.username); assertDetail('Inventory', mockJobData.summary_fields.inventory.name); diff --git a/awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx b/awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx deleted file mode 100644 index 26d0384ab3..0000000000 --- a/awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -function WorkflowDetail() { - return
Workflow Detail!
; -} - -export default WorkflowDetail; diff --git a/awx/ui_next/src/screens/Job/WorkflowDetail/index.js b/awx/ui_next/src/screens/Job/WorkflowDetail/index.js deleted file mode 100644 index 3ced22dd95..0000000000 --- a/awx/ui_next/src/screens/Job/WorkflowDetail/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './WorkflowDetail'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx new file mode 100644 index 0000000000..7b8516f787 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx @@ -0,0 +1,113 @@ +import React, { useEffect, useCallback } from 'react'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; +import { Card, PageSection } from '@patternfly/react-core'; +import { CaretLeftIcon } from '@patternfly/react-icons'; +import { + Link, + Switch, + Route, + Redirect, + useParams, + useRouteMatch, + useLocation, +} from 'react-router-dom'; +import useRequest from '../../util/useRequest'; +import RoutedTabs from '../../components/RoutedTabs'; +import ContentError from '../../components/ContentError'; +import { NotificationTemplatesAPI } from '../../api'; +import NotificationTemplateDetail from './NotificationTemplateDetail'; +import NotificationTemplateEdit from './NotificationTemplateEdit'; + +function NotificationTemplate({ setBreadcrumb, i18n }) { + const { id: templateId } = useParams(); + const match = useRouteMatch(); + const location = useLocation(); + const { + result: template, + isLoading, + error, + request: fetchTemplate, + } = useRequest( + useCallback(async () => { + const { data } = await NotificationTemplatesAPI.readDetail(templateId); + setBreadcrumb(data); + return data; + }, [templateId, setBreadcrumb]), + null + ); + + useEffect(() => { + fetchTemplate(); + }, [fetchTemplate]); + + if (error) { + return ( + + + + {error.response.status === 404 && ( + + {i18n._(t`Notification Template not found.`)}{' '} + + {i18n._(t`View all Notification Templates.`)} + + + )} + + + + ); + } + + const showCardHeader = !isLoading && !location.pathname.endsWith('edit'); + const tabs = [ + { + name: ( + <> + + {i18n._(t`Back to Notifications`)} + + ), + link: `/notification_templates`, + id: 99, + }, + { + name: i18n._(t`Details`), + link: `${match.url}/details`, + id: 0, + }, + ]; + return ( + + + {showCardHeader && } + + + {template && ( + <> + + + + + + + + )} + + + + ); +} + +export default withI18n()(NotificationTemplate); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx new file mode 100644 index 0000000000..bbf39b61a9 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function NotificationTemplateAdd() { + return
; +} diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx new file mode 100644 index 0000000000..951ba5bd8b --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -0,0 +1,361 @@ +import React, { useCallback } from 'react'; +import { Link, useHistory } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { Button } from '@patternfly/react-core'; +import { t } from '@lingui/macro'; +import AlertModal from '../../../components/AlertModal'; +import { CardBody, CardActionsRow } from '../../../components/Card'; +import { + Detail, + DetailList, + DeletedDetail, +} from '../../../components/DetailList'; +import ObjectDetail from '../../../components/DetailList/ObjectDetail'; +import DeleteButton from '../../../components/DeleteButton'; +import ErrorDetail from '../../../components/ErrorDetail'; +import { NotificationTemplatesAPI } from '../../../api'; +import useRequest, { useDismissableError } from '../../../util/useRequest'; +import { NOTIFICATION_TYPES } from '../constants'; + +function NotificationTemplateDetail({ i18n, template }) { + const history = useHistory(); + + const { + notification_configuration: configuration, + summary_fields, + } = template; + + const { request: deleteTemplate, isLoading, error: deleteError } = useRequest( + useCallback(async () => { + await NotificationTemplatesAPI.destroy(template.id); + history.push(`/notification_templates`); + }, [template.id, history]) + ); + + const { error, dismissError } = useDismissableError(deleteError); + + return ( + + + + + {summary_fields.organization ? ( + + {summary_fields.organization.name} + + } + /> + ) : ( + + )} + + {template.notification_type === 'email' && ( + <> + + + + + + + + + )} + {template.notification_type === 'grafana' && ( + <> + + + + + + + )} + {template.notification_type === 'irc' && ( + <> + + + + + + + )} + {template.notification_type === 'mattermost' && ( + <> + + + + + + + )} + {template.notification_type === 'pagerduty' && ( + <> + + + + + )} + {template.notification_type === 'rocketchat' && ( + <> + + + + + + )} + {template.notification_type === 'slack' && ( + <> + + + + )} + {template.notification_type === 'twilio' && ( + <> + + + + + )} + {template.notification_type === 'webhook' && ( + <> + + + + + + + )} + + + {summary_fields.user_capabilities && + summary_fields.user_capabilities.edit && ( + + )} + {summary_fields.user_capabilities && + summary_fields.user_capabilities.delete && ( + + {i18n._(t`Delete`)} + + )} + + {error && ( + + {i18n._(t`Failed to delete notification.`)} + + + )} + + ); +} + +export default withI18n()(NotificationTemplateDetail); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js new file mode 100644 index 0000000000..118818bb64 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js @@ -0,0 +1,3 @@ +import NotificationTemplateDetail from './NotificationTemplateDetail'; + +export default NotificationTemplateDetail; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx new file mode 100644 index 0000000000..b089b6b89f --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; +import { CardBody } from '../../../components/Card'; +import { OrganizationsAPI } from '../../../api'; +import { Config } from '../../../contexts/Config'; + +import NotificationTemplateForm from '../shared/NotificationTemplateForm'; + +function NotificationTemplateEdit({ template }) { + const detailsUrl = `/notification_templates/${template.id}/details`; + const history = useHistory(); + const [formError, setFormError] = useState(null); + + const handleSubmit = async ( + values, + groupsToAssociate, + groupsToDisassociate + ) => { + try { + await OrganizationsAPI.update(template.id, values); + await Promise.all( + groupsToAssociate.map(id => + OrganizationsAPI.associateInstanceGroup(template.id, id) + ) + ); + await Promise.all( + groupsToDisassociate.map(id => + OrganizationsAPI.disassociateInstanceGroup(template.id, id) + ) + ); + history.push(detailsUrl); + } catch (error) { + setFormError(error); + } + }; + + const handleCancel = () => { + history.push(detailsUrl); + }; + + return ( + + + {({ me }) => ( + + )} + + + ); +} + +NotificationTemplateEdit.propTypes = { + template: PropTypes.shape().isRequired, +}; + +NotificationTemplateEdit.contextTypes = { + custom_virtualenvs: PropTypes.arrayOf(PropTypes.string), +}; + +export { NotificationTemplateEdit as _NotificationTemplateEdit }; +export default NotificationTemplateEdit; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/index.js new file mode 100644 index 0000000000..be9b40a69c --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/index.js @@ -0,0 +1,3 @@ +import NotificationTemplateEdit from './NotificationTemplateEdit'; + +export default NotificationTemplateEdit; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx new file mode 100644 index 0000000000..3dac16f6a2 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx @@ -0,0 +1,170 @@ +import React, { useCallback, useEffect } from 'react'; +import { useLocation, useRouteMatch } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Card, PageSection } from '@patternfly/react-core'; +import { NotificationTemplatesAPI } from '../../../api'; +import PaginatedDataList, { + ToolbarAddButton, + ToolbarDeleteButton, +} from '../../../components/PaginatedDataList'; +import AlertModal from '../../../components/AlertModal'; +import ErrorDetail from '../../../components/ErrorDetail'; +import DataListToolbar from '../../../components/DataListToolbar'; +import NotificationTemplateListItem from './NotificationTemplateListItem'; +import useRequest, { useDeleteItems } from '../../../util/useRequest'; +import useSelected from '../../../util/useSelected'; +import { getQSConfig, parseQueryString } from '../../../util/qs'; + +const QS_CONFIG = getQSConfig('notification-templates', { + page: 1, + page_size: 20, + order_by: 'name', +}); + +function NotificationTemplatesList({ i18n }) { + const location = useLocation(); + const match = useRouteMatch(); + + const addUrl = `${match.url}/add`; + + const { + result: { templates, count, actions }, + error: contentError, + isLoading: isTemplatesLoading, + request: fetchTemplates, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const responses = await Promise.all([ + NotificationTemplatesAPI.read(params), + NotificationTemplatesAPI.readOptions(), + ]); + return { + templates: responses[0].data.results, + count: responses[0].data.count, + actions: responses[1].data.actions, + }; + }, [location]), + { + templates: [], + count: 0, + actions: {}, + } + ); + + useEffect(() => { + fetchTemplates(); + }, [fetchTemplates]); + + const { selected, isAllSelected, handleSelect, setSelected } = useSelected( + templates + ); + + const { + isLoading: isDeleteLoading, + deleteItems: deleteTemplates, + deletionError, + clearDeletionError, + } = useDeleteItems( + useCallback(async () => { + return Promise.all( + selected.map(({ id }) => NotificationTemplatesAPI.destroy(id)) + ); + }, [selected]), + { + qsConfig: QS_CONFIG, + allItemsSelected: isAllSelected, + fetchItems: fetchTemplates, + } + ); + + const handleDelete = async () => { + await deleteTemplates(); + setSelected([]); + }; + + const canAdd = actions && actions.POST; + + return ( + <> + + + ( + setSelected([...templates])} + qsConfig={QS_CONFIG} + additionalControls={[ + ...(canAdd + ? [] + : []), + , + ]} + /> + )} + renderItem={template => ( + row.id === template.id)} + onSelect={() => handleSelect(template)} + /> + )} + emptyStateControls={ + canAdd ? : null + } + /> + + + + {i18n._(t`Failed to delete one or more organizations.`)} + + + + ); +} + +export default withI18n()(NotificationTemplatesList); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx new file mode 100644 index 0000000000..d39bffe087 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx @@ -0,0 +1,202 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { OrganizationsAPI } from '../../../api'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import NotificationTemplateList from './NotificationTemplateList'; + +jest.mock('../../../api'); + +const mockTemplates = { + data: { + count: 3, + results: [ + { + name: 'Boston', + id: 1, + url: '/notification_templates/1', + type: 'slack', + summary_fields: { + recent_notifications: [ + { + status: 'success', + }, + ], + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + name: 'Minneapolis', + id: 2, + url: '/notification_templates/2', + summary_fields: { + recent_notifications: [], + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + name: 'Philidelphia', + id: 3, + url: '/notification_templates/3', + summary_fields: { + recent_notifications: [ + { + status: 'failed', + }, + { + status: 'success', + }, + ], + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + ], + }, +}; + +describe('', () => { + let wrapper; + beforeEach(() => { + OrganizationsAPI.read.mockResolvedValue(mockTemplates); + OrganizationsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + }, + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should load notifications', async () => { + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1); + expect(wrapper.find('NotificationTemplateListItem').length).toBe(3); + }); + + test('should select item', async () => { + const itemCheckboxInput = 'input#select-template-1'; + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(false); + await act(async () => { + wrapper + .find(itemCheckboxInput) + .closest('DataListCheck') + .props() + .onChange(); + }); + wrapper.update(); + expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(true); + }); + + test('should delete notifications', async () => { + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1); + await act(async () => { + wrapper + .find('Checkbox#select-all') + .props() + .onChange(true); + }); + wrapper.update(); + await act(async () => { + wrapper.find('button[aria-label="Delete"]').simulate('click'); + wrapper.update(); + }); + const deleteButton = global.document.querySelector( + 'body div[role="dialog"] button[aria-label="confirm delete"]' + ); + expect(deleteButton).not.toEqual(null); + await act(async () => { + deleteButton.click(); + }); + expect(OrganizationsAPI.destroy).toHaveBeenCalledTimes(3); + expect(OrganizationsAPI.read).toHaveBeenCalledTimes(2); + }); + + test('should show error dialog shown for failed deletion', async () => { + const itemCheckboxInput = 'input#select-template-1'; + OrganizationsAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/organizations/1', + }, + data: 'An error occurred', + }, + }) + ); + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + await act(async () => { + wrapper + .find(itemCheckboxInput) + .closest('DataListCheck') + .props() + .onChange(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('button[aria-label="Delete"]').simulate('click'); + wrapper.update(); + }); + const deleteButton = global.document.querySelector( + 'body div[role="dialog"] button[aria-label="confirm delete"]' + ); + expect(deleteButton).not.toEqual(null); + await act(async () => { + deleteButton.click(); + }); + wrapper.update(); + + const modal = wrapper.find('Modal'); + expect(modal.prop('isOpen')).toEqual(true); + expect(modal.prop('title')).toEqual('Error!'); + }); + + test('should show add button', async () => { + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(wrapper.find('ToolbarAddButton').length).toBe(1); + }); + + test('should hide add button (rbac)', async () => { + OrganizationsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + }, + }, + }); + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(wrapper.find('ToolbarAddButton').length).toBe(0); + }); +}); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx new file mode 100644 index 0000000000..0087e7f9a8 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -0,0 +1,122 @@ +import 'styled-components/macro'; +import React, { useState, useEffect, useCallback } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; +import { + Button, + DataListAction as _DataListAction, + DataListCheck, + DataListItem, + DataListItemCells, + DataListItemRow, + Tooltip, +} from '@patternfly/react-core'; +import { PencilAltIcon, BellIcon } from '@patternfly/react-icons'; +import { NotificationTemplatesAPI } from '../../../api'; +import DataListCell from '../../../components/DataListCell'; +import StatusLabel from '../../../components/StatusLabel'; +import useRequest from '../../../util/useRequest'; +import { NOTIFICATION_TYPES } from '../constants'; + +const DataListAction = styled(_DataListAction)` + align-items: center; + display: grid; + grid-gap: 16px; + grid-template-columns: 40px 40px; +`; + +function NotificationTemplateListItem({ + template, + detailUrl, + isSelected, + onSelect, + i18n, +}) { + const recentNotifications = template.summary_fields?.recent_notifications; + const latestStatus = recentNotifications + ? recentNotifications[0]?.status + : null; + const [status, setStatus] = useState(latestStatus); + + useEffect(() => { + setStatus(latestStatus); + }, [latestStatus]); + + const { request: sendTestNotification, isLoading, error } = useRequest( + useCallback(() => { + NotificationTemplatesAPI.test(template.id); + setStatus('running'); + }, [template.id]) + ); + + useEffect(() => { + if (error) { + setStatus('error'); + } + }, [error]); + + const labelId = `template-name-${template.id}`; + + return ( + + + + + + {template.name} + + , + + {status && } + , + + {i18n._(t`Type:`)}{' '} + {NOTIFICATION_TYPES[template.notification_type] || + template.notification_type} + , + ]} + /> + + + + + {template.summary_fields.user_capabilities.edit ? ( + + + + ) : ( +
+ )} + + + + ); +} + +export default withI18n()(NotificationTemplateListItem); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx new file mode 100644 index 0000000000..5a4566779e --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { NotificationTemplatesAPI } from '../../../api'; +import NotificationTemplateListItem from './NotificationTemplateListItem'; + +jest.mock('../../../api/models/NotificationTemplates'); + +const template = { + id: 3, + notification_type: 'slack', + name: 'Test Notification', + summary_fields: { + user_capabilities: { + edit: true, + }, + recent_notifications: [ + { + status: 'success', + }, + ], + }, +}; + +describe('', () => { + test('should render template row', () => { + const wrapper = mountWithContexts( + + ); + + const cells = wrapper.find('DataListCell'); + expect(cells).toHaveLength(3); + expect(cells.at(0).text()).toEqual('Test Notification'); + expect(cells.at(1).text()).toEqual('Success'); + expect(cells.at(2).text()).toEqual('Type: Slack'); + }); + + test('should send test notification', async () => { + NotificationTemplatesAPI.test.mockResolvedValue({}); + + const wrapper = mountWithContexts( + + ); + await act(async () => { + wrapper + .find('Button') + .at(0) + .invoke('onClick')(); + }); + expect(NotificationTemplatesAPI.test).toHaveBeenCalledTimes(1); + expect( + wrapper + .find('DataListCell') + .at(1) + .text() + ).toEqual('Running'); + }); +}); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js new file mode 100644 index 0000000000..335e76dd6c --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js @@ -0,0 +1,4 @@ +import NotificationTemplateList from './NotificationTemplateList'; + +export default NotificationTemplateList; +export { default as NotificationTemplateListItem } from './NotificationTemplateListItem'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx index 857201bc6b..2ae913202f 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx @@ -1,28 +1,51 @@ -import React, { Component, Fragment } from 'react'; +import React, { useState, useCallback } from 'react'; +import { Route, Switch, useRouteMatch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - PageSection, - PageSectionVariants, - Title, -} from '@patternfly/react-core'; +import NotificationTemplateList from './NotificationTemplateList'; +import NotificationTemplateAdd from './NotificationTemplateAdd'; +import NotificationTemplate from './NotificationTemplate'; +import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; -class NotificationTemplates extends Component { - render() { - const { i18n } = this.props; - const { light } = PageSectionVariants; +function NotificationTemplates({ i18n }) { + const match = useRouteMatch(); + const [breadcrumbConfig, setBreadcrumbConfig] = useState({ + '/notification_templates': i18n._(t`Notification Templates`), + '/notification_templates/add': i18n._(t`Create New Notification Template`), + }); - return ( - - - - {i18n._(t`Notification Templates`)} - - - - - ); - } + const updateBreadcrumbConfig = useCallback( + notification => { + const { id } = notification; + setBreadcrumbConfig({ + '/notification_templates': i18n._(t`Notification Templates`), + '/notification_templates/add': i18n._( + t`Create New Notification Template` + ), + [`/notification_templates/${id}`]: notification.name, + [`/notification_templates/${id}/edit`]: i18n._(t`Edit Details`), + [`/notification_templates/${id}/details`]: i18n._(t`Details`), + }); + }, + [i18n] + ); + + return ( + <> + + + + + + + + + + + + + + ); } export default withI18n()(NotificationTemplates); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx index 93babc8e06..9333850cf9 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx @@ -1,18 +1,14 @@ import React from 'react'; - import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; - import NotificationTemplates from './NotificationTemplates'; describe('', () => { let pageWrapper; let pageSections; - let title; beforeEach(() => { pageWrapper = mountWithContexts(); pageSections = pageWrapper.find('PageSection'); - title = pageWrapper.find('Title'); }); afterEach(() => { @@ -22,8 +18,6 @@ describe('', () => { test('initially renders without crashing', () => { expect(pageWrapper.length).toBe(1); expect(pageSections.length).toBe(2); - expect(title.length).toBe(1); - expect(title.props().size).toBe('2xl'); expect(pageSections.first().props().variant).toBe('light'); }); }); diff --git a/awx/ui_next/src/screens/NotificationTemplate/constants.js b/awx/ui_next/src/screens/NotificationTemplate/constants.js new file mode 100644 index 0000000000..5937e48743 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/constants.js @@ -0,0 +1,12 @@ +/* eslint-disable-next-line import/prefer-default-export */ +export const NOTIFICATION_TYPES = { + email: 'Email', + grafana: 'Grafana', + irc: 'IRC', + mattermost: 'Mattermost', + pagerduty: 'Pagerduty', + rocketchat: 'Rocket.Chat', + slack: 'Slack', + twilio: 'Twilio', + webhook: 'Webhook', +}; diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx new file mode 100644 index 0000000000..c08caaa3e5 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx @@ -0,0 +1,3 @@ +export default function NotificationTemplateForm() { + // +} diff --git a/awx/ui_next/src/screens/Organization/Organization.jsx b/awx/ui_next/src/screens/Organization/Organization.jsx index 6e8cf87f1d..d16113d8dc 100644 --- a/awx/ui_next/src/screens/Organization/Organization.jsx +++ b/awx/ui_next/src/screens/Organization/Organization.jsx @@ -200,6 +200,7 @@ class Organization extends Component { id={Number(match.params.id)} canToggleNotifications={canToggleNotifications} apiModel={OrganizationsAPI} + showApprovalsToggle /> )} diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx index 152004ae5c..7b3c0eeda2 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx @@ -31,7 +31,13 @@ function OrganizationsList({ i18n }) { const addUrl = `${match.url}/add`; const { - result: { organizations, organizationCount, actions, relatedSearchFields }, + result: { + organizations, + organizationCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading: isOrgsLoading, request: fetchOrganizations, @@ -46,16 +52,20 @@ function OrganizationsList({ i18n }) { organizations: orgs.data.results, organizationCount: orgs.data.count, actions: orgActions.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( orgActions?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys(orgActions.data.actions?.GET || {}).filter( + key => orgActions.data.actions?.GET[key].filterable + ), }; }, [location]), { organizations: [], organizationCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -90,10 +100,6 @@ function OrganizationsList({ i18n }) { const hasContentLoading = isDeleteLoading || isOrgsLoading; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...organizations] : []); diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx index 51f78c173c..37d01a9e0a 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx @@ -62,12 +62,10 @@ function OrganizationListItem({ /> - - - {organization.name} - - + + + {organization.name} + , @@ -85,11 +83,7 @@ function OrganizationListItem({ , ]} /> - + {organization.summary_fields.user_capabilities.edit ? ( - - )} - - - - ); + let user_type; + if (user.is_superuser) { + user_type = i18n._(t`System Administrator`); + } else if (user.is_system_auditor) { + user_type = i18n._(t`System Auditor`); + } else { + user_type = i18n._(t`Normal User`); } + + return ( + + + + + + {user.username} + + , + + {user.first_name && ( + + {i18n._(t`First Name`)} + {user.first_name} + + )} + , + + {user.last_name && ( + + {i18n._(t`Last Name`)} + {user.last_name} + + )} + , + + {user_type} + , + ]} + /> + + {user.summary_fields.user_capabilities.edit && ( + + + + )} + + + + ); } + +UserListItem.prototype = { + user: User.isRequired, + detailUrl: string.isRequired, + isSelected: bool.isRequired, + onSelect: func.isRequired, +}; + export default withI18n()(UserListItem); diff --git a/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx b/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx index acdc7eecc0..e11b6da00c 100644 --- a/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx +++ b/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx @@ -28,12 +28,18 @@ describe('UserListItem with full permissions', () => { ); }); - test('initially renders succesfully', () => { + test('initially renders successfully', () => { expect(wrapper.length).toBe(1); }); test('edit button shown to users with edit capabilities', () => { expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); }); + + test('should display user type', () => { + expect( + wrapper.find('DataListCell[aria-label="user type"]').prop('children') + ).toEqual('System Administrator'); + }); }); describe('UserListItem without full permissions', () => { diff --git a/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx b/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx index bc01af942d..d31c1c7670 100644 --- a/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx +++ b/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx @@ -19,7 +19,7 @@ export default function UserOrganizationListItem({ organization }) { to={`/organizations/${organization.id}/details`} id={labelId} > - {organization.name} + {organization.name} , diff --git a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx index 25d245cffd..868cbb9e55 100644 --- a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx +++ b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx @@ -20,7 +20,7 @@ function UserTeamList({ i18n }) { const { id: userId } = useParams(); const { - result: { teams, count, actions, relatedSearchFields }, + result: { teams, count, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, request: fetchOrgs, @@ -39,17 +39,19 @@ function UserTeamList({ i18n }) { return { teams: results, count: teamCount, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [userId, location.search]), { teams: [], count: 0, - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -57,11 +59,6 @@ function UserTeamList({ i18n }) { fetchOrgs(); }, [fetchOrgs]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( { + const [response, actionsResponse] = await Promise.all([ + TokensAPI.readDetail(tokenId), + TokensAPI.readOptions(), + ]); + setBreadcrumb(user, response.data); + return { + token: response.data, + actions: actionsResponse.data.actions.POST, + }; + }, [setBreadcrumb, user, tokenId]), + { token: null, actions: null } + ); + useEffect(() => { + fetchToken(); + }, [fetchToken]); + + const tabsArray = [ + { + name: ( + <> + + {i18n._(t`Back to Tokens`)} + + ), + link: `/users/${id}/tokens`, + id: 99, + }, + { + name: i18n._(t`Details`), + link: `/users/${id}/tokens/${tokenId}/details`, + id: 0, + }, + ]; + + let showCardHeader = true; + + if (location.pathname.endsWith('edit')) { + showCardHeader = false; + } + + if (!isLoading && error) { + return ( + + + + {error.response.status === 404 && ( + + {i18n._(t`Token not found.`)}{' '} + + {i18n._(t`View all tokens.`)} + + + )} + + + + ); + } + + return ( + <> + {showCardHeader && } + + + {token && ( + + + + )} + + {!isLoading && ( + + {id && ( + {i18n._(t`View Tokens`)} + )} + + )} + + + + ); +} + +export default withI18n()(UserToken); diff --git a/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx b/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx new file mode 100644 index 0000000000..8e71f1b085 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { TokensAPI } from '../../../api'; +import UserToken from './UserToken'; + +jest.mock('../../../api/models/Tokens'); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + tokenId: 2, + }), +})); +describe('', () => { + let wrapper; + const user = { + id: 1, + type: 'user', + url: '/api/v2/users/1/', + summary_fields: { + user_capabilities: { + edit: true, + delete: false, + }, + }, + created: '2020-06-19T12:55:13.138692Z', + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + email: 'a@g.com', + }; + test('should call api for token details and actions', async () => { + TokensAPI.readDetail.mockResolvedValue({ + id: 2, + type: 'o_auth2_access_token', + url: '/api/v2/tokens/2/', + + summary_fields: { + user: { + id: 1, + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + }, + application: { + id: 3, + name: 'hg', + }, + }, + created: '2020-06-23T19:56:38.422053Z', + modified: '2020-06-23T19:56:38.441353Z', + description: 'cdfsg', + scope: 'read', + }); + TokensAPI.readOptions.mockResolvedValue({ + data: { actions: { POST: true } }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('UserToken').length).toBe(1); + }); + test('should call api for token details and actions', async () => { + TokensAPI.readDetail.mockResolvedValue({ + id: 2, + type: 'o_auth2_access_token', + url: '/api/v2/tokens/2/', + + summary_fields: { + user: { + id: 1, + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + }, + application: { + id: 3, + name: 'hg', + }, + }, + created: '2020-06-23T19:56:38.422053Z', + modified: '2020-06-23T19:56:38.441353Z', + description: 'cdfsg', + scope: 'read', + }); + TokensAPI.readOptions.mockResolvedValue({ + data: { actions: { POST: true } }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(TokensAPI.readDetail).toBeCalledWith(2); + expect(TokensAPI.readOptions).toBeCalled(); + }); +}); diff --git a/awx/ui_next/src/screens/User/UserToken/index.js b/awx/ui_next/src/screens/User/UserToken/index.js new file mode 100644 index 0000000000..f899410e7d --- /dev/null +++ b/awx/ui_next/src/screens/User/UserToken/index.js @@ -0,0 +1 @@ +export { default } from './UserToken'; diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx new file mode 100644 index 0000000000..4e6891767d --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx @@ -0,0 +1,89 @@ +import React, { useCallback } from 'react'; +import { Link, useHistory, useParams } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Button } from '@patternfly/react-core'; + +import AlertModal from '../../../components/AlertModal'; +import { CardBody, CardActionsRow } from '../../../components/Card'; +import DeleteButton from '../../../components/DeleteButton'; +import { + DetailList, + Detail, + UserDateDetail, +} from '../../../components/DetailList'; +import ErrorDetail from '../../../components/ErrorDetail'; +import { TokensAPI } from '../../../api'; +import useRequest, { useDismissableError } from '../../../util/useRequest'; +import { toTitleCase } from '../../../util/strings'; + +function UserTokenDetail({ token, canEditOrDelete, i18n }) { + const { scope, description, created, modified, summary_fields } = token; + const history = useHistory(); + const { id, tokenId } = useParams(); + const { request: deleteToken, isLoading, error: deleteError } = useRequest( + useCallback(async () => { + await TokensAPI.destroy(tokenId); + history.push(`/users/${id}/tokens`); + }, [tokenId, id, history]) + ); + const { error, dismissError } = useDismissableError(deleteError); + + return ( + + + + + + + + + + {canEditOrDelete && ( + <> + + + {i18n._(t`Delete`)} + + + )} + + {error && ( + + {i18n._(t`Failed to user token.`)} + + + )} + + ); +} + +export default withI18n()(UserTokenDetail); diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx new file mode 100644 index 0000000000..a3462f758f --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { TokensAPI } from '../../../api'; +import UserTokenDetail from './UserTokenDetail'; + +jest.mock('../../../api/models/Tokens'); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + tokenId: 2, + }), +})); +describe('', () => { + let wrapper; + const token = { + id: 2, + type: 'o_auth2_access_token', + url: '/api/v2/tokens/2/', + + summary_fields: { + user: { + id: 1, + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + }, + application: { + id: 3, + name: 'hg', + }, + }, + created: '2020-06-23T19:56:38.422053Z', + modified: '2020-06-23T19:56:38.441353Z', + description: 'cdfsg', + scope: 'read', + }; + test('should call api for token details and actions', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('UserTokenDetail').length).toBe(1); + }); + test('should call api for token details and actions', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + + expect(wrapper.find('Detail[label="Application"]').prop('value')).toBe( + 'hg' + ); + expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe( + 'cdfsg' + ); + expect(wrapper.find('Detail[label="Scope"]').prop('value')).toBe('Read'); + expect(wrapper.find('UserDateDetail[label="Created"]').prop('date')).toBe( + '2020-06-23T19:56:38.422053Z' + ); + expect( + wrapper.find('UserDateDetail[label="Last Modified"]').prop('date') + ).toBe('2020-06-23T19:56:38.441353Z'); + expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(1); + expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(1); + }); + test('should not render edit or delete buttons', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(0); + expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(0); + }); + test('should delete token properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await act(async () => + wrapper.find('Button[aria-label="Delete"]').prop('onClick')() + ); + wrapper.update(); + await act(async () => wrapper.find('DeleteButton').prop('onConfirm')()); + expect(TokensAPI.destroy).toBeCalledWith(2); + }); + test('should throw deletion error', async () => { + TokensAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/tokens', + }, + data: 'An error occurred', + status: 400, + }, + }) + ); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await act(async () => + wrapper.find('Button[aria-label="Delete"]').prop('onClick')() + ); + wrapper.update(); + await act(async () => wrapper.find('DeleteButton').prop('onConfirm')()); + expect(TokensAPI.destroy).toBeCalledWith(2); + wrapper.update(); + expect(wrapper.find('ErrorDetail').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/index.js b/awx/ui_next/src/screens/User/UserTokenDetail/index.js new file mode 100644 index 0000000000..a6a9011996 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTokenDetail/index.js @@ -0,0 +1 @@ +export { default } from './UserTokenDetail'; diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx index e30f2c6fa2..4427b4c886 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx @@ -28,7 +28,7 @@ function UserTokenList({ i18n }) { error, isLoading, request: fetchTokens, - result: { tokens, itemCount, actions, relatedSearchFields }, + result: { tokens, itemCount, relatedSearchableKeys, searchableKeys }, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); @@ -53,13 +53,15 @@ function UserTokenList({ i18n }) { return { tokens: modifiedResults, itemCount: count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [id, location.search]), - { tokens: [], itemCount: 0, actions: {}, relatedSearchFields: [] } + { tokens: [], itemCount: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { @@ -93,10 +95,7 @@ function UserTokenList({ i18n }) { }; const canAdd = true; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); + return ( <> @@ -41,10 +43,14 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) { {token.summary_fields?.application?.name ? ( {i18n._(t`Application`)} - {token.summary_fields.application.name} + + {token.summary_fields.application.name} + ) : ( - i18n._(t`Personal access token`) + + {i18n._(t`Personal access token`)} + )} , @@ -53,7 +59,7 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) { , - {formatDateStringUTC(token.expires)} + {formatDateString(token.expires)} , ]} /> diff --git a/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx b/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx index dc072a6546..c73519d7f9 100644 --- a/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx +++ b/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx @@ -3,17 +3,20 @@ import { withI18n } from '@lingui/react'; import { Switch, Route, useParams } from 'react-router-dom'; import UserTokenAdd from '../UserTokenAdd'; import UserTokenList from '../UserTokenList'; +import UserToken from '../UserToken'; -function UserTokens() { - const { id: userId } = useParams(); - +function UserTokens({ setBreadcrumb, user }) { + const { id } = useParams(); return ( - + + + + - + ); diff --git a/awx/ui_next/src/screens/User/Users.jsx b/awx/ui_next/src/screens/User/Users.jsx index 6f21f8be10..e9fe2d4ef2 100644 --- a/awx/ui_next/src/screens/User/Users.jsx +++ b/awx/ui_next/src/screens/User/Users.jsx @@ -18,7 +18,7 @@ function Users({ i18n }) { const match = useRouteMatch(); const addUserBreadcrumb = useCallback( - user => { + (user, token) => { if (!user) { return; } @@ -34,6 +34,10 @@ function Users({ i18n }) { [`/users/${user.id}/organizations`]: i18n._(t`Organizations`), [`/users/${user.id}/tokens`]: i18n._(t`Tokens`), [`/users/${user.id}/tokens/add`]: i18n._(t`Create user token`), + [`/users/${user.id}/tokens/${token && token.id}`]: `Application Name`, + [`/users/${user.id}/tokens/${token && token.id}/details`]: i18n._( + t`Details` + ), }); }, [i18n] diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index 7a66ae3c68..5ee55ef5d9 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -148,7 +148,7 @@ export const Project = shape({ created: string, name: string.isRequired, description: string, - scm_type: oneOf(['', 'git', 'hg', 'svn', 'insights']), + scm_type: oneOf(['', 'git', 'hg', 'svn', 'archive', 'insights']), scm_url: string, scm_branch: string, scm_refspec: string, diff --git a/awx/ui_next/src/util/yaml.js b/awx/ui_next/src/util/yaml.js index ec11b09613..177cb754bf 100644 --- a/awx/ui_next/src/util/yaml.js +++ b/awx/ui_next/src/util/yaml.js @@ -22,7 +22,11 @@ export function jsonToYaml(jsonString) { return yaml.safeDump(value); } -export function isJson(jsonString) { +export function isJsonObject(value) { + return typeof value === 'object' && value !== null; +} + +export function isJsonString(jsonString) { if (typeof jsonString !== 'string') { return false; } @@ -40,7 +44,7 @@ export function parseVariableField(variableField) { if (variableField === '---' || variableField === '{}') { return {}; } - if (!isJson(variableField)) { + if (!isJsonString(variableField)) { variableField = yamlToJson(variableField); } variableField = JSON.parse(variableField); diff --git a/awx_collection/meta/runtime.yml b/awx_collection/meta/runtime.yml index 3980ccc14c..d8c2535871 100644 --- a/awx_collection/meta/runtime.yml +++ b/awx_collection/meta/runtime.yml @@ -13,3 +13,5 @@ plugin_routing: deprecation: removal_date: TBD warning_text: see plugin documentation for details + tower_notifitcation: + redirect: tower_notification_template diff --git a/awx_collection/plugins/inventory/tower.py b/awx_collection/plugins/inventory/tower.py index 872e2a3328..7dc4aaa1a3 100644 --- a/awx_collection/plugins/inventory/tower.py +++ b/awx_collection/plugins/inventory/tower.py @@ -72,7 +72,7 @@ from ansible.errors import AnsibleParserError, AnsibleOptionsError from ansible.plugins.inventory import BaseInventoryPlugin from ansible.config.manager import ensure_type -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def handle_error(**kwargs): @@ -104,12 +104,12 @@ class InventoryModule(BaseInventoryPlugin): # Defer processing of params to logic shared with the modules module_params = {} - for plugin_param, module_param in TowerModule.short_params.items(): + for plugin_param, module_param in TowerAPIModule.short_params.items(): opt_val = self.get_option(plugin_param) if opt_val is not None: module_params[module_param] = opt_val - module = TowerModule( + module = TowerAPIModule( argument_spec={}, direct_params=module_params, error_callback=handle_error, warn_callback=self.warn_callback ) diff --git a/awx_collection/plugins/lookup/tower_api.py b/awx_collection/plugins/lookup/tower_api.py index 9829507125..76b32be60a 100644 --- a/awx_collection/plugins/lookup/tower_api.py +++ b/awx_collection/plugins/lookup/tower_api.py @@ -115,7 +115,7 @@ from ansible.plugins.lookup import LookupBase from ansible.errors import AnsibleError from ansible.module_utils._text import to_native from ansible.utils.display import Display -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule class LookupModule(LookupBase): @@ -133,13 +133,13 @@ class LookupModule(LookupBase): # Defer processing of params to logic shared with the modules module_params = {} - for plugin_param, module_param in TowerModule.short_params.items(): + for plugin_param, module_param in TowerAPIModule.short_params.items(): opt_val = self.get_option(plugin_param) if opt_val is not None: module_params[module_param] = opt_val # Create our module - module = TowerModule( + module = TowerAPIModule( argument_spec={}, direct_params=module_params, error_callback=self.handle_error, warn_callback=self.warn_callback ) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index d0120ec003..36e3d045f0 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -1,37 +1,18 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils.basic import AnsibleModule, env_fallback +from . tower_module import TowerModule from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError -from ansible.module_utils.six import PY2, string_types -from ansible.module_utils.six.moves import StringIO -from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode +from ansible.module_utils.six import PY2 +from ansible.module_utils.six.moves.urllib.parse import urlencode from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.http_cookiejar import CookieJar -from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError -from socket import gethostbyname import re from json import loads, dumps -from os.path import isfile, expanduser, split, join, exists, isdir -from os import access, R_OK, getcwd -from distutils.util import strtobool - -try: - import yaml - HAS_YAML = True -except ImportError: - HAS_YAML = False -class ConfigFileException(Exception): - pass - - -class ItemNotDefined(Exception): - pass - - -class TowerModule(AnsibleModule): +class TowerAPIModule(TowerModule): + # TODO: Move the collection version check into tower_module.py # This gets set by the make process so whatever is in here is irrelevant _COLLECTION_VERSION = "0.0.1-devel" _COLLECTION_TYPE = "awx" @@ -41,197 +22,16 @@ class TowerModule(AnsibleModule): 'awx': 'AWX', 'tower': 'Red Hat Ansible Tower', } - url = None - AUTH_ARGSPEC = dict( - tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), - tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), - tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), - validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), - tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), - tower_config_file=dict(type='path', required=False, default=None), - ) - short_params = { - 'host': 'tower_host', - 'username': 'tower_username', - 'password': 'tower_password', - 'verify_ssl': 'validate_certs', - 'oauth_token': 'tower_oauthtoken', - } - host = '127.0.0.1' - username = None - password = None - verify_ssl = True - oauth_token = None - oauth_token_id = None session = None cookie_jar = CookieJar() - authenticated = False - config_name = 'tower_cli.cfg' - ENCRYPTED_STRING = "$encrypted$" - version_checked = False - error_callback = None - warn_callback = None def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): - full_argspec = {} - full_argspec.update(TowerModule.AUTH_ARGSPEC) - full_argspec.update(argument_spec) kwargs['supports_check_mode'] = True - self.error_callback = error_callback - self.warn_callback = warn_callback - - self.json_output = {'changed': False} - - if direct_params is not None: - self.params = direct_params - else: - super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs) - - self.load_config_files() - - # Parameters specified on command line will override settings in any config - for short_param, long_param in self.short_params.items(): - direct_value = self.params.get(long_param) - if direct_value is not None: - setattr(self, short_param, direct_value) - - # Perform magic depending on whether tower_oauthtoken is a string or a dict - if self.params.get('tower_oauthtoken'): - token_param = self.params.get('tower_oauthtoken') - if type(token_param) is dict: - if 'token' in token_param: - self.oauth_token = self.params.get('tower_oauthtoken')['token'] - else: - self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") - elif isinstance(token_param, string_types): - self.oauth_token = self.params.get('tower_oauthtoken') - else: - error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) - self.fail_json(msg=error_msg) - - # Perform some basic validation - if not re.match('^https{0,1}://', self.host): - self.host = "https://{0}".format(self.host) - - # Try to parse the hostname as a url - try: - self.url = urlparse(self.host) - except Exception as e: - self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e)) - - # Try to resolve the hostname - hostname = self.url.netloc.split(':')[0] - try: - gethostbyname(hostname) - except Exception as e: - self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e)) - + super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, + error_callback=error_callback, warn_callback=warn_callback, **kwargs) self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl) - def load_config_files(self): - # Load configs like TowerCLI would have from least import to most - config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))] - local_dir = getcwd() - config_files.append(join(local_dir, self.config_name)) - while split(local_dir)[1]: - local_dir = split(local_dir)[0] - config_files.insert(2, join(local_dir, ".{0}".format(self.config_name))) - - # If we have a specified tower config, load it - if self.params.get('tower_config_file'): - duplicated_params = [ - fn for fn in self.AUTH_ARGSPEC - if fn != 'tower_config_file' and self.params.get(fn) is not None - ] - if duplicated_params: - self.warn(( - 'The parameter(s) {0} were provided at the same time as tower_config_file. ' - 'Precedence may be unstable, we suggest either using config file or params.' - ).format(', '.join(duplicated_params))) - try: - # TODO: warn if there are conflicts with other params - self.load_config(self.params.get('tower_config_file')) - except ConfigFileException as cfe: - # Since we were told specifically to load this we want it to fail if we have an error - self.fail_json(msg=cfe) - else: - for config_file in config_files: - if exists(config_file) and not isdir(config_file): - # Only throw a formatting error if the file exists and is not a directory - try: - self.load_config(config_file) - except ConfigFileException: - self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file)) - - def load_config(self, config_path): - # Validate the config file is an actual file - if not isfile(config_path): - raise ConfigFileException('The specified config file does not exist') - - if not access(config_path, R_OK): - raise ConfigFileException("The specified config file cannot be read") - - # Read in the file contents: - with open(config_path, 'r') as f: - config_string = f.read() - - # First try to yaml load the content (which will also load json) - try: - try_config_parsing = True - if HAS_YAML: - try: - config_data = yaml.load(config_string, Loader=yaml.SafeLoader) - # If this is an actual ini file, yaml will return the whole thing as a string instead of a dict - if type(config_data) is not dict: - raise AssertionError("The yaml config file is not properly formatted as a dict.") - try_config_parsing = False - - except(AttributeError, yaml.YAMLError, AssertionError): - try_config_parsing = True - - if try_config_parsing: - # TowerCLI used to support a config file with a missing [general] section by prepending it if missing - if '[general]' not in config_string: - config_string = '[general]\n{0}'.format(config_string) - - config = ConfigParser() - - try: - placeholder_file = StringIO(config_string) - # py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3 - # This "if" removes the deprecation warning - if hasattr(config, 'read_file'): - config.read_file(placeholder_file) - else: - config.readfp(placeholder_file) - - # If we made it here then we have values from reading the ini file, so let's pull them out into a dict - config_data = {} - for honorred_setting in self.short_params: - try: - config_data[honorred_setting] = config.get('general', honorred_setting) - except NoOptionError: - pass - - except Exception as e: - raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e)) - - except Exception as e: - raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) - - # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here - for honorred_setting in self.short_params: - if honorred_setting in config_data: - # Veriffy SSL must be a boolean - if honorred_setting == 'verify_ssl': - if type(config_data[honorred_setting]) is str: - setattr(self, honorred_setting, strtobool(config_data[honorred_setting])) - else: - setattr(self, honorred_setting, bool(config_data[honorred_setting])) - else: - setattr(self, honorred_setting, config_data[honorred_setting]) - @staticmethod def param_to_endpoint(name): exceptions = { @@ -305,32 +105,39 @@ class TowerModule(AnsibleModule): return response['json']['results'][0] - def resolve_name_to_id(self, endpoint, name_or_id): - # Try to resolve the object by name + def get_one_by_name_or_id(self, endpoint, name_or_id): name_field = 'name' if endpoint == 'users': name_field = 'username' - response = self.get_endpoint(endpoint, **{'data': {name_field: name_or_id}}) - if response['status_code'] == 400: - self.fail_json(msg="Unable to try and resolve {0} for {1} : {2}".format(endpoint, name_or_id, response['json']['detail'])) + query_params = {'or__{0}'.format(name_field): name_or_id} + try: + query_params['or__id'] = int(name_or_id) + except ValueError: + # If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail + pass + + response = self.get_endpoint(endpoint, **{'data': query_params}) + if response['status_code'] != 200: + self.fail_json( + msg="Failed to query endpoint {0} for {1} {2} ({3}), see results".format(endpoint, name_field, name_or_id, response['status_code']), + resuls=response + ) if response['json']['count'] == 1: - return response['json']['results'][0]['id'] + return response['json']['results'][0] + elif response['json']['count'] > 1: + for tower_object in response['json']['results']: + # ID takes priority, so we match on that first + if str(tower_object['id']) == name_or_id: + return tower_object + # We didn't match on an ID but we found more than 1 object, therefore the results are ambiguous + self.fail_json(msg="The requested name or id was ambiguous and resulted in too many items") elif response['json']['count'] == 0: - try: - int(name_or_id) - # If we got 0 items by name, maybe they gave us an ID, let's try looking it up by ID - response = self.head_endpoint("{0}/{1}".format(endpoint, name_or_id), **{'return_none_on_404': True}) - if response is not None: - return name_or_id - except ValueError: - # If we got a value error than we didn't have an integer so we can just pass and fall down to the fail - pass - self.fail_json(msg="The {0} {1} was not found on the Tower server".format(endpoint, name_or_id)) - else: - self.fail_json(msg="Found too many names {0} at endpoint {1} try using an ID instead of a name".format(name_or_id, endpoint)) + + def resolve_name_to_id(self, endpoint, name_or_id): + return self.get_one_by_name_or_id(endpoint, name_or_id)['id'] def make_request(self, method, endpoint, *args, **kwargs): # In case someone is calling us directly; make sure we were given a method, let's not just assume a GET @@ -650,13 +457,13 @@ class TowerModule(AnsibleModule): """ if isinstance(obj, dict): for val in obj.values(): - if TowerModule.has_encrypted_values(val): + if TowerAPIModule.has_encrypted_values(val): return True elif isinstance(obj, list): for val in obj: - if TowerModule.has_encrypted_values(val): + if TowerAPIModule.has_encrypted_values(val): return True - elif obj == TowerModule.ENCRYPTED_STRING: + elif obj == TowerAPIModule.ENCRYPTED_STRING: return True return False @@ -678,10 +485,9 @@ class TowerModule(AnsibleModule): # This will exit from the module on its own # If the method successfully updates an item and on_update param is defined, # the on_update parameter will be called as a method pasing in this object and the json from the response - # This will return one of three things: + # This will return one of two things: # 1. None if the existing_item does not need to be updated # 2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module. - # 3. An ItemNotDefined exception, if the existing_item does not exist # Note: common error codes from the Tower API can cause the module to fail response = None if existing_item: @@ -777,25 +583,6 @@ class TowerModule(AnsibleModule): # Sanity check: Did the server send back some kind of internal error? self.warn('Failed to release tower token {0}: {1}'.format(self.oauth_token_id, e)) - def fail_json(self, **kwargs): - # Try to log out if we are authenticated - self.logout() - if self.error_callback: - self.error_callback(**kwargs) - else: - super(TowerModule, self).fail_json(**kwargs) - - def exit_json(self, **kwargs): - # Try to log out if we are authenticated - self.logout() - super(TowerModule, self).exit_json(**kwargs) - - def warn(self, warning): - if self.warn_callback is not None: - self.warn_callback(warning) - else: - super(TowerModule, self).warn(warning) - def is_job_done(self, job_status): if job_status in ['new', 'pending', 'waiting', 'running']: return False diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py new file mode 100644 index 0000000000..fc4e232f1b --- /dev/null +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -0,0 +1,53 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from . tower_module import TowerModule +from ansible.module_utils.basic import missing_required_lib + +try: + from awxkit.api.client import Connection + from awxkit.api.pages.api import ApiV2 + from awxkit.api import get_registered_page + HAS_AWX_KIT = True +except ImportError: + HAS_AWX_KIT = False + + +class TowerAWXKitModule(TowerModule): + connection = None + apiV2Ref = None + + def __init__(self, argument_spec, **kwargs): + kwargs['supports_check_mode'] = False + + super(TowerAWXKitModule, self).__init__(argument_spec=argument_spec, **kwargs) + + # Die if we don't have AWX_KIT installed + if not HAS_AWX_KIT: + self.exit_json(msg=missing_required_lib('awxkit')) + + # Establish our conneciton object + self.connection = Connection(self.host, verify=self.verify_ssl) + + def authenticate(self): + try: + if self.oauth_token: + self.connection.login(None, None, token=self.oauth_token) + self.authenticated = True + elif self.username: + self.connection.login(username=self.username, password=self.password) + self.authenticated = True + except Exception: + self.exit_json("Failed to authenticate") + + def get_api_v2_object(self): + if not self.apiV2Ref: + if not self.authenticated: + self.authenticate() + v2_index = get_registered_page('/api/v2/')(self.connection).get() + self.api_ref = ApiV2(connection=self.connection, **{'json': v2_index}) + return self.api_ref + + def logout(self): + if self.authenticated: + self.connection.logout() diff --git a/awx_collection/plugins/module_utils/ansible_tower.py b/awx_collection/plugins/module_utils/tower_legacy.py similarity index 97% rename from awx_collection/plugins/module_utils/ansible_tower.py rename to awx_collection/plugins/module_utils/tower_legacy.py index 17d6a38680..3c8408610d 100644 --- a/awx_collection/plugins/module_utils/ansible_tower.py +++ b/awx_collection/plugins/module_utils/tower_legacy.py @@ -91,7 +91,7 @@ def tower_check_mode(module): module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo)) -class TowerModule(AnsibleModule): +class TowerLegacyModule(AnsibleModule): def __init__(self, argument_spec, **kwargs): args = dict( tower_host=dict(), @@ -110,7 +110,7 @@ class TowerModule(AnsibleModule): ('tower_config_file', 'validate_certs'), )) - super(TowerModule, self).__init__(argument_spec=args, **kwargs) + super(TowerLegacyModule, self).__init__(argument_spec=args, **kwargs) if not HAS_TOWER_CLI: self.fail_json(msg=missing_required_lib('ansible-tower-cli'), diff --git a/awx_collection/plugins/module_utils/tower_module.py b/awx_collection/plugins/module_utils/tower_module.py new file mode 100644 index 0000000000..553a35248c --- /dev/null +++ b/awx_collection/plugins/module_utils/tower_module.py @@ -0,0 +1,239 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.six import PY2, string_types +from ansible.module_utils.six.moves import StringIO +from ansible.module_utils.six.moves.urllib.parse import urlparse +from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError +from socket import gethostbyname +import re +from os.path import isfile, expanduser, split, join, exists, isdir +from os import access, R_OK, getcwd +from distutils.util import strtobool + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +class ConfigFileException(Exception): + pass + + +class ItemNotDefined(Exception): + pass + + +class TowerModule(AnsibleModule): + url = None + AUTH_ARGSPEC = dict( + tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), + tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), + tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), + validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), + tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), + tower_config_file=dict(type='path', required=False, default=None), + ) + short_params = { + 'host': 'tower_host', + 'username': 'tower_username', + 'password': 'tower_password', + 'verify_ssl': 'validate_certs', + 'oauth_token': 'tower_oauthtoken', + } + host = '127.0.0.1' + username = None + password = None + verify_ssl = True + oauth_token = None + oauth_token_id = None + authenticated = False + config_name = 'tower_cli.cfg' + ENCRYPTED_STRING = "$encrypted$" + version_checked = False + error_callback = None + warn_callback = None + + def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs): + full_argspec = {} + full_argspec.update(TowerModule.AUTH_ARGSPEC) + full_argspec.update(argument_spec) + kwargs['supports_check_mode'] = True + + self.error_callback = error_callback + self.warn_callback = warn_callback + + self.json_output = {'changed': False} + + if direct_params is not None: + self.params = direct_params + else: + super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs) + + self.load_config_files() + + # Parameters specified on command line will override settings in any config + for short_param, long_param in self.short_params.items(): + direct_value = self.params.get(long_param) + if direct_value is not None: + setattr(self, short_param, direct_value) + + # Perform magic depending on whether tower_oauthtoken is a string or a dict + if self.params.get('tower_oauthtoken'): + token_param = self.params.get('tower_oauthtoken') + if type(token_param) is dict: + if 'token' in token_param: + self.oauth_token = self.params.get('tower_oauthtoken')['token'] + else: + self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") + elif isinstance(token_param, string_types): + self.oauth_token = self.params.get('tower_oauthtoken') + else: + error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) + self.fail_json(msg=error_msg) + + # Perform some basic validation + if not re.match('^https{0,1}://', self.host): + self.host = "https://{0}".format(self.host) + + # Try to parse the hostname as a url + try: + self.url = urlparse(self.host) + except Exception as e: + self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e)) + + # Try to resolve the hostname + hostname = self.url.netloc.split(':')[0] + try: + gethostbyname(hostname) + except Exception as e: + self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e)) + + def load_config_files(self): + # Load configs like TowerCLI would have from least import to most + config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))] + local_dir = getcwd() + config_files.append(join(local_dir, self.config_name)) + while split(local_dir)[1]: + local_dir = split(local_dir)[0] + config_files.insert(2, join(local_dir, ".{0}".format(self.config_name))) + + # If we have a specified tower config, load it + if self.params.get('tower_config_file'): + duplicated_params = [ + fn for fn in self.AUTH_ARGSPEC + if fn != 'tower_config_file' and self.params.get(fn) is not None + ] + if duplicated_params: + self.warn(( + 'The parameter(s) {0} were provided at the same time as tower_config_file. ' + 'Precedence may be unstable, we suggest either using config file or params.' + ).format(', '.join(duplicated_params))) + try: + # TODO: warn if there are conflicts with other params + self.load_config(self.params.get('tower_config_file')) + except ConfigFileException as cfe: + # Since we were told specifically to load this we want it to fail if we have an error + self.fail_json(msg=cfe) + else: + for config_file in config_files: + if exists(config_file) and not isdir(config_file): + # Only throw a formatting error if the file exists and is not a directory + try: + self.load_config(config_file) + except ConfigFileException: + self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file)) + + def load_config(self, config_path): + # Validate the config file is an actual file + if not isfile(config_path): + raise ConfigFileException('The specified config file does not exist') + + if not access(config_path, R_OK): + raise ConfigFileException("The specified config file cannot be read") + + # Read in the file contents: + with open(config_path, 'r') as f: + config_string = f.read() + + # First try to yaml load the content (which will also load json) + try: + try_config_parsing = True + if HAS_YAML: + try: + config_data = yaml.load(config_string, Loader=yaml.SafeLoader) + # If this is an actual ini file, yaml will return the whole thing as a string instead of a dict + if type(config_data) is not dict: + raise AssertionError("The yaml config file is not properly formatted as a dict.") + try_config_parsing = False + + except(AttributeError, yaml.YAMLError, AssertionError): + try_config_parsing = True + + if try_config_parsing: + # TowerCLI used to support a config file with a missing [general] section by prepending it if missing + if '[general]' not in config_string: + config_string = '[general]\n{0}'.format(config_string) + + config = ConfigParser() + + try: + placeholder_file = StringIO(config_string) + # py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3 + # This "if" removes the deprecation warning + if hasattr(config, 'read_file'): + config.read_file(placeholder_file) + else: + config.readfp(placeholder_file) + + # If we made it here then we have values from reading the ini file, so let's pull them out into a dict + config_data = {} + for honorred_setting in self.short_params: + try: + config_data[honorred_setting] = config.get('general', honorred_setting) + except NoOptionError: + pass + + except Exception as e: + raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e)) + + except Exception as e: + raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) + + # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here + for honorred_setting in self.short_params: + if honorred_setting in config_data: + # Veriffy SSL must be a boolean + if honorred_setting == 'verify_ssl': + if type(config_data[honorred_setting]) is str: + setattr(self, honorred_setting, strtobool(config_data[honorred_setting])) + else: + setattr(self, honorred_setting, bool(config_data[honorred_setting])) + else: + setattr(self, honorred_setting, config_data[honorred_setting]) + + def logout(self): + # This method is intended to be overridden + pass + + def fail_json(self, **kwargs): + # Try to log out if we are authenticated + self.logout() + if self.error_callback: + self.error_callback(**kwargs) + else: + super(TowerModule, self).fail_json(**kwargs) + + def exit_json(self, **kwargs): + # Try to log out if we are authenticated + self.logout() + super(TowerModule, self).exit_json(**kwargs) + + def warn(self, warning): + if self.warn_callback is not None: + self.warn_callback(warning) + else: + super(TowerModule, self).warn(warning) diff --git a/awx_collection/plugins/modules/tower_credential.py b/awx_collection/plugins/modules/tower_credential.py index 97a801aa8f..2d4cbd3331 100644 --- a/awx_collection/plugins/modules/tower_credential.py +++ b/awx_collection/plugins/modules/tower_credential.py @@ -269,7 +269,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule KIND_CHOICES = { 'ssh': 'Machine', @@ -336,7 +336,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']]) + module = TowerAPIModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']]) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_credential_input_source.py b/awx_collection/plugins/modules/tower_credential_input_source.py index bc2cb85579..cdc55cb1f0 100644 --- a/awx_collection/plugins/modules/tower_credential_input_source.py +++ b/awx_collection/plugins/modules/tower_credential_input_source.py @@ -70,7 +70,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -85,7 +85,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters description = module.params.get('description') diff --git a/awx_collection/plugins/modules/tower_credential_type.py b/awx_collection/plugins/modules/tower_credential_type.py index 561ae78f5a..53f0cc45c9 100644 --- a/awx_collection/plugins/modules/tower_credential_type.py +++ b/awx_collection/plugins/modules/tower_credential_type.py @@ -81,7 +81,7 @@ EXAMPLES = ''' RETURN = ''' # ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule KIND_CHOICES = { 'ssh': 'Machine', @@ -105,7 +105,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py new file mode 100644 index 0000000000..bd951d1744 --- /dev/null +++ b/awx_collection/plugins/modules/tower_export.py @@ -0,0 +1,166 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_export +author: "John Westcott IV (@john-westcott-iv)" +version_added: "3.7" +short_description: export resources from Ansible Tower. +description: + - Export assets from Ansible Tower. +options: + all: + description: + - Export all assets + type: bool + default: 'False' + organizations: + description: + - organization name to export + type: str + users: + description: + - user name to export + type: str + teams: + description: + - team name to export + type: str + credential_types: + description: + - credential type name to export + type: str + credentials: + description: + - credential name to export + type: str + notification_templates: + description: + - notification template name to export + type: str + inventory_sources: + description: + - inventory soruce to export + type: str + inventory: + description: + - inventory name to export + type: str + projects: + description: + - project name to export + type: str + job_templates: + description: + - job template name to export + type: str + workflow_job_templates: + description: + - workflow name to export + type: str +requirements: + - "awxkit >= 9.3.0" +notes: + - Specifying a name of "all" for any asset type will export all items of that asset type. +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Export all tower assets + tower_export: + all: True +- name: Export all inventories + tower_export: + inventory: 'all' +- name: Export a job template named "My Template" and all Credentials + tower_export: + job_template: "My Template" + credential: 'all' +''' + +from os import environ +import logging +from ansible.module_utils.six.moves import StringIO +from ..module_utils.tower_awxkit import TowerAWXKitModule + +try: + from awxkit.api.pages.api import EXPORTABLE_RESOURCES + HAS_EXPORTABLE_RESOURCES = True +except ImportError: + HAS_EXPORTABLE_RESOURCES = False + + +def main(): + argument_spec = dict( + all=dict(type='bool', default=False), + ) + + # We are not going to raise an error here because the __init__ method of TowerAWXKitModule will do that for us + if HAS_EXPORTABLE_RESOURCES: + for resource in EXPORTABLE_RESOURCES: + argument_spec[resource] = dict(type='str') + + module = TowerAWXKitModule(argument_spec=argument_spec) + + if not HAS_EXPORTABLE_RESOURCES: + module.fail_json(msg="Your version of awxkit does not have import/export") + + # The export process will never change a Tower system + module.json_output['changed'] = False + + # The exporter code currently works like the following: + # Empty string == all assets of that type + # Non-Empty string = just one asset of that type (by name or ID) + # Asset type not present or None = skip asset type (unless everything is None, then export all) + # Here we are going to setup a dict of values to export + export_args = {} + for resource in EXPORTABLE_RESOURCES: + if module.params.get('all') or module.params.get(resource) == 'all': + # If we are exporting everything or we got the keyword "all" we pass in an empty string for this asset type + export_args[resource] = '' + else: + # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items + export_args[resource] = module.params.get(resource) + + # Currently the import process does not return anything on error + # It simply just logs to pythons logger + # Setup a log gobbler to get error messages from import_assets + log_capture_string = StringIO() + ch = logging.StreamHandler(log_capture_string) + for logger_name in ['awxkit.api.pages.api', 'awxkit.api.pages.page']: + logger = logging.getLogger(logger_name) + logger.setLevel(logging.WARNING) + ch.setLevel(logging.WARNING) + + logger.addHandler(ch) + log_contents = '' + + # Run the import process + try: + module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) + module.exit_json(**module.json_output) + except Exception as e: + module.fail_json(msg="Failed to export assets {0}".format(e)) + finally: + # Finally consume the logs incase there were any errors and die if there were + log_contents = log_capture_string.getvalue() + log_capture_string.close() + if log_contents != '': + module.fail_json(msg=log_contents) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_group.py b/awx_collection/plugins/modules/tower_group.py index a64826eb88..9e2eaf4e7e 100644 --- a/awx_collection/plugins/modules/tower_group.py +++ b/awx_collection/plugins/modules/tower_group.py @@ -76,7 +76,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -94,7 +94,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_host.py b/awx_collection/plugins/modules/tower_host.py index cb4712a27c..f6bfe55404 100644 --- a/awx_collection/plugins/modules/tower_host.py +++ b/awx_collection/plugins/modules/tower_host.py @@ -72,7 +72,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -89,7 +89,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py new file mode 100644 index 0000000000..a39a98a5e3 --- /dev/null +++ b/awx_collection/plugins/modules/tower_import.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_import +author: "John Westcott (@john-westcott-iv)" +version_added: "3.7" +short_description: import resources into Ansible Tower. +description: + - Import assets into Ansible Tower. See + U(https://www.ansible.com/tower) for an overview. +options: + assets: + description: + - The assets to import. + - This can be the output of tower_export or loaded from a file + required: True + type: dict +requirements: + - "awxkit >= 9.3.0" +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Export all assets + tower_export: + all: True + registeR: export_output + +- name: Import all tower assets from our export + tower_import: + assets: "{{ export_output.assets }}" + +- name: Load data from a json file created by a command like awx export --organization Default + tower_import: + assets: "{{ lookup('file', 'org.json') | from_json() }}" +''' + +from ..module_utils.tower_awxkit import TowerAWXKitModule + +# These two lines are not needed if awxkit changes to do progamatic notifications on issues +from ansible.module_utils.six.moves import StringIO +import logging + +# In this module we don't use EXPORTABLE_RESOURCES, we just want to validate that our installed awxkit has import/export +try: + from awxkit.api.pages.api import EXPORTABLE_RESOURCES + HAS_EXPORTABLE_RESOURCES = True +except ImportError: + HAS_EXPORTABLE_RESOURCES = False + + +def main(): + argument_spec = dict( + assets=dict(type='dict', required=True) + ) + + module = TowerAWXKitModule(argument_spec=argument_spec, supports_check_mode=False) + + assets = module.params.get('assets') + + if not HAS_EXPORTABLE_RESOURCES: + module.fail_json(msg="Your version of awxkit does not appear to have import/export") + + # Currently the import process does not return anything on error + # It simply just logs to pythons logger + # Setup a log gobbler to get error messages from import_assets + logger = logging.getLogger('awxkit.api.pages.api') + logger.setLevel(logging.WARNING) + log_capture_string = StringIO() + ch = logging.StreamHandler(log_capture_string) + ch.setLevel(logging.WARNING) + logger.addHandler(ch) + log_contents = '' + + # Run the import process + try: + module.json_output['changed'] = module.get_api_v2_object().import_assets(assets) + except Exception as e: + module.fail_json(msg="Failed to import assets {0}".format(e)) + finally: + # Finally consume the logs incase there were any errors and die if there were + log_contents = log_capture_string.getvalue() + log_capture_string.close() + if log_contents != '': + module.fail_json(msg=log_contents) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_inventory.py b/awx_collection/plugins/modules/tower_inventory.py index d0ced63048..7f03645e01 100644 --- a/awx_collection/plugins/modules/tower_inventory.py +++ b/awx_collection/plugins/modules/tower_inventory.py @@ -71,7 +71,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -88,7 +88,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index afa6c229e2..5b0e2961df 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -144,7 +144,7 @@ EXAMPLES = ''' private: false ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule from json import dumps @@ -184,7 +184,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_job_cancel.py b/awx_collection/plugins/modules/tower_job_cancel.py index 5e82834f6c..7404d452a4 100644 --- a/awx_collection/plugins/modules/tower_job_cancel.py +++ b/awx_collection/plugins/modules/tower_job_cancel.py @@ -50,7 +50,7 @@ id: ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -61,7 +61,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters job_id = module.params.get('job_id') diff --git a/awx_collection/plugins/modules/tower_job_launch.py b/awx_collection/plugins/modules/tower_job_launch.py index f3447bf24c..25a1c52fdf 100644 --- a/awx_collection/plugins/modules/tower_job_launch.py +++ b/awx_collection/plugins/modules/tower_job_launch.py @@ -124,7 +124,7 @@ status: sample: pending ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -146,7 +146,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) optional_args = {} # Extract our parameters diff --git a/awx_collection/plugins/modules/tower_job_list.py b/awx_collection/plugins/modules/tower_job_list.py index 2ecfd9d98a..642a48b03b 100644 --- a/awx_collection/plugins/modules/tower_job_list.py +++ b/awx_collection/plugins/modules/tower_job_list.py @@ -80,7 +80,7 @@ results: ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -93,7 +93,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, mutually_exclusive=[ ('page', 'all_pages'), diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index 1f1d776f28..1f12d5aadd 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -317,7 +317,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -388,7 +388,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py index 77a6977c5c..5e5801b25f 100644 --- a/awx_collection/plugins/modules/tower_job_wait.py +++ b/awx_collection/plugins/modules/tower_job_wait.py @@ -92,7 +92,7 @@ status: ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import time @@ -120,7 +120,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters job_id = module.params.get('job_id') diff --git a/awx_collection/plugins/modules/tower_label.py b/awx_collection/plugins/modules/tower_label.py index d0820d93a8..6a3a8288a2 100644 --- a/awx_collection/plugins/modules/tower_label.py +++ b/awx_collection/plugins/modules/tower_label.py @@ -54,7 +54,7 @@ EXAMPLES = ''' organization: My Organization ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -67,7 +67,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_license.py b/awx_collection/plugins/modules/tower_license.py index a1d9840d50..25d337cc24 100644 --- a/awx_collection/plugins/modules/tower_license.py +++ b/awx_collection/plugins/modules/tower_license.py @@ -43,12 +43,12 @@ EXAMPLES = ''' eula_accepted: True ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): - module = TowerModule( + module = TowerAPIModule( argument_spec=dict( data=dict(type='dict', required=True), eula_accepted=dict(type='bool', required=True), diff --git a/awx_collection/plugins/modules/tower_meta.py b/awx_collection/plugins/modules/tower_meta.py index 6d5c801ade..9455bdf0f4 100644 --- a/awx_collection/plugins/modules/tower_meta.py +++ b/awx_collection/plugins/modules/tower_meta.py @@ -62,11 +62,11 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): - module = TowerModule(argument_spec={}) + module = TowerAPIModule(argument_spec={}) namespace = { 'awx': 'awx', 'tower': 'ansible' diff --git a/awx_collection/plugins/modules/tower_notification.py b/awx_collection/plugins/modules/tower_notification_template.py similarity index 97% rename from awx_collection/plugins/modules/tower_notification.py rename to awx_collection/plugins/modules/tower_notification_template.py index bfe672a50e..ec25038e34 100644 --- a/awx_collection/plugins/modules/tower_notification.py +++ b/awx_collection/plugins/modules/tower_notification_template.py @@ -15,7 +15,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- -module: tower_notification +module: tower_notification_template author: "Samuel Carpentier (@samcarpentier)" short_description: create, update, or destroy Ansible Tower notification. description: @@ -203,7 +203,7 @@ extends_documentation_fragment: awx.awx.auth EXAMPLES = ''' - name: Add Slack notification with custom messages - tower_notification: + tower_notification_template: name: slack notification organization: Default notification_type: slack @@ -222,7 +222,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" - name: Add webhook notification - tower_notification: + tower_notification_template: name: webhook notification notification_type: webhook notification_configuration: @@ -233,7 +233,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" - name: Add email notification - tower_notification: + tower_notification_template: name: email notification notification_type: email notification_configuration: @@ -250,7 +250,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" - name: Add twilio notification - tower_notification: + tower_notification_template: name: twilio notification notification_type: twilio notification_configuration: @@ -263,7 +263,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" - name: Add PagerDuty notification - tower_notification: + tower_notification_template: name: pagerduty notification notification_type: pagerduty notification_configuration: @@ -275,7 +275,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" - name: Add IRC notification - tower_notification: + tower_notification_template: name: irc notification notification_type: irc notification_configuration: @@ -290,7 +290,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" - name: Delete notification - tower_notification: + tower_notification_template: name: old notification state: absent tower_config_file: "~/tower_cli.cfg" @@ -300,7 +300,7 @@ EXAMPLES = ''' RETURN = ''' # ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule OLD_INPUT_NAMES = ( 'username', 'sender', 'recipients', 'use_tls', @@ -355,7 +355,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index fbbbf2885c..1637828149 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -88,7 +88,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -106,7 +106,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 12f9e2809c..8662029f66 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -55,10 +55,12 @@ options: - The refspec to use for the SCM resource. type: str default: '' - scm_credential: + credential: description: - Name of the credential to use with this SCM resource. type: str + aliases: + - scm_credential scm_clean: description: - Remove local modifications before updating. @@ -86,11 +88,13 @@ options: type: bool aliases: - scm_allow_override - job_timeout: + timeout: description: - The amount of time (in seconds) to run before the SCM Update is canceled. A value of 0 means no timeout. default: 0 type: int + aliases: + - job_timeout custom_virtualenv: description: - Local absolute file path containing a custom Python virtualenv to use @@ -157,7 +161,7 @@ EXAMPLES = ''' import time -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def wait_for_project_update(module, last_request): @@ -188,13 +192,13 @@ def main(): local_path=dict(), scm_branch=dict(default=''), scm_refspec=dict(default=''), - scm_credential=dict(), + credential=dict(aliases=['scm_credential']), scm_clean=dict(type='bool', default=False), scm_delete_on_update=dict(type='bool', default=False), scm_update_on_launch=dict(type='bool', default=False), scm_update_cache_timeout=dict(type='int', default=0), allow_override=dict(type='bool', aliases=['scm_allow_override']), - job_timeout=dict(type='int', default=0), + timeout=dict(type='int', default=0, aliases=['job_timeout']), custom_virtualenv=dict(), organization=dict(required=True), notification_templates_started=dict(type="list", elements='str'), @@ -205,7 +209,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') @@ -217,13 +221,13 @@ def main(): local_path = module.params.get('local_path') scm_branch = module.params.get('scm_branch') scm_refspec = module.params.get('scm_refspec') - scm_credential = module.params.get('scm_credential') + credential = module.params.get('credential') scm_clean = module.params.get('scm_clean') scm_delete_on_update = module.params.get('scm_delete_on_update') scm_update_on_launch = module.params.get('scm_update_on_launch') scm_update_cache_timeout = module.params.get('scm_update_cache_timeout') allow_override = module.params.get('allow_override') - job_timeout = module.params.get('job_timeout') + timeout = module.params.get('timeout') custom_virtualenv = module.params.get('custom_virtualenv') organization = module.params.get('organization') state = module.params.get('state') @@ -231,8 +235,8 @@ def main(): # Attempt to look up the related items the user specified (these will fail the module if not found) org_id = module.resolve_name_to_id('organizations', organization) - if scm_credential is not None: - scm_credential_id = module.resolve_name_to_id('credentials', scm_credential) + if credential is not None: + credential = module.resolve_name_to_id('credentials', credential) # Attempt to look up project based on the provided name and org ID project = module.get_one('projects', **{ @@ -276,7 +280,7 @@ def main(): 'scm_refspec': scm_refspec, 'scm_clean': scm_clean, 'scm_delete_on_update': scm_delete_on_update, - 'timeout': job_timeout, + 'timeout': timeout, 'organization': org_id, 'scm_update_on_launch': scm_update_on_launch, 'scm_update_cache_timeout': scm_update_cache_timeout, @@ -284,8 +288,8 @@ def main(): } if description is not None: project_fields['description'] = description - if scm_credential is not None: - project_fields['credential'] = scm_credential_id + if credential is not None: + project_fields['credential'] = credential if allow_override is not None: project_fields['allow_override'] = allow_override if scm_type == '': diff --git a/awx_collection/plugins/modules/tower_receive.py b/awx_collection/plugins/modules/tower_receive.py index b673e9b81d..bd08682503 100644 --- a/awx_collection/plugins/modules/tower_receive.py +++ b/awx_collection/plugins/modules/tower_receive.py @@ -134,7 +134,7 @@ assets: sample: [ {}, {} ] ''' -from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI +from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI try: from tower_cli.cli.transfer.receive import Receiver @@ -163,7 +163,7 @@ def main(): workflow=dict(type='list', default=[], elements='str'), ) - module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) + module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False) module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="awx.awx:14.0.0") diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index 4cba215b0a..f06cc16e4d 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -89,7 +89,7 @@ EXAMPLES = ''' state: present ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -109,7 +109,7 @@ def main(): state=dict(choices=['present', 'absent'], default='present'), ) - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) role_type = module.params.pop('role') role_field = role_type + '_role' @@ -126,11 +126,10 @@ def main(): resource_data = {} for param in resource_param_keys: endpoint = module.param_to_endpoint(param) - name_field = 'username' if param == 'user' else 'name' resource_name = params.get(param) if resource_name: - resource = module.get_one(endpoint, **{'data': {name_field: resource_name}}) + resource = module.get_one_by_name_or_id(module.param_to_endpoint(param), resource_name) if not resource: module.fail_json( msg='Failed to update role, {0} not found in {1}'.format(param, endpoint), @@ -170,14 +169,14 @@ def main(): if response['status_code'] == 204: module.json_output['changed'] = True else: - module.fail_json(msg="Failed to grant role {0}".format(response['json']['detail'])) + module.fail_json(msg="Failed to grant role. {0}".format(response['json'].get('detail', response['json'].get('msg', 'unknown')))) else: for an_id in list(set(existing_associated_ids) & set(new_association_list)): response = module.post_endpoint(association_endpoint, **{'data': {'id': int(an_id), 'disassociate': True}}) if response['status_code'] == 204: module.json_output['changed'] = True else: - module.fail_json(msg="Failed to revoke role {0}".format(response['json']['detail'])) + module.fail_json(msg="Failed to revoke role. {0}".format(response['json'].get('detail', response['json'].get('msg', 'unknown')))) module.exit_json(**module.json_output) diff --git a/awx_collection/plugins/modules/tower_schedule.py b/awx_collection/plugins/modules/tower_schedule.py index 24f8468e4a..4922aaa688 100644 --- a/awx_collection/plugins/modules/tower_schedule.py +++ b/awx_collection/plugins/modules/tower_schedule.py @@ -136,7 +136,7 @@ EXAMPLES = ''' register: result ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -161,7 +161,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters rrule = module.params.get('rrule') diff --git a/awx_collection/plugins/modules/tower_send.py b/awx_collection/plugins/modules/tower_send.py index 7ac60ece59..772b2b67ec 100644 --- a/awx_collection/plugins/modules/tower_send.py +++ b/awx_collection/plugins/modules/tower_send.py @@ -81,7 +81,7 @@ import os import sys from ansible.module_utils.six.moves import StringIO -from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI +from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI from tempfile import mkstemp @@ -103,7 +103,7 @@ def main(): password_management=dict(default='default', choices=['default', 'random']), ) - module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) + module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False) module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="awx.awx:14.0.0") diff --git a/awx_collection/plugins/modules/tower_settings.py b/awx_collection/plugins/modules/tower_settings.py index 9db41d9975..c2e8ed1ae5 100644 --- a/awx_collection/plugins/modules/tower_settings.py +++ b/awx_collection/plugins/modules/tower_settings.py @@ -70,7 +70,7 @@ EXAMPLES = ''' last_name: "surname" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule try: import yaml @@ -111,7 +111,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, required_one_of=[['name', 'settings']], mutually_exclusive=[['name', 'settings']], diff --git a/awx_collection/plugins/modules/tower_team.py b/awx_collection/plugins/modules/tower_team.py index e1506b2425..8ed56e48dc 100644 --- a/awx_collection/plugins/modules/tower_team.py +++ b/awx_collection/plugins/modules/tower_team.py @@ -60,7 +60,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -74,7 +74,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 165590520d..ee6fd5c200 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -117,7 +117,7 @@ tower_token: returned: on successful create ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def return_token(module, last_response): @@ -143,7 +143,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, mutually_exclusive=[ ('existing_token', 'existing_token_id'), diff --git a/awx_collection/plugins/modules/tower_user.py b/awx_collection/plugins/modules/tower_user.py index 7d049de016..15c41cb081 100644 --- a/awx_collection/plugins/modules/tower_user.py +++ b/awx_collection/plugins/modules/tower_user.py @@ -102,7 +102,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -119,7 +119,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters username = module.params.get('username') diff --git a/awx_collection/plugins/modules/tower_workflow_job_template.py b/awx_collection/plugins/modules/tower_workflow_job_template.py index c3ad692af4..8fb350b919 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template.py @@ -137,7 +137,7 @@ EXAMPLES = ''' organization: Default ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -176,7 +176,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 16902e9421..7ef9e14619 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -157,7 +157,7 @@ EXAMPLES = ''' - my-first-node ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -185,7 +185,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters identifier = module.params.get('identifier') diff --git a/awx_collection/plugins/modules/tower_workflow_launch.py b/awx_collection/plugins/modules/tower_workflow_launch.py index 8ef73d82fc..249feeed35 100644 --- a/awx_collection/plugins/modules/tower_workflow_launch.py +++ b/awx_collection/plugins/modules/tower_workflow_launch.py @@ -91,7 +91,7 @@ EXAMPLES = ''' wait: False ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json import time @@ -111,7 +111,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) optional_args = {} # Extract our parameters diff --git a/awx_collection/plugins/modules/tower_workflow_template.py b/awx_collection/plugins/modules/tower_workflow_template.py index 9a652a4373..a8557b2ad2 100644 --- a/awx_collection/plugins/modules/tower_workflow_template.py +++ b/awx_collection/plugins/modules/tower_workflow_template.py @@ -108,8 +108,8 @@ EXAMPLES = ''' RETURN = ''' # ''' -from ..module_utils.ansible_tower import ( - TowerModule, +from ..module_utils.tower_legacy import ( + TowerLegacyModule, tower_auth_config, tower_check_mode ) @@ -140,7 +140,7 @@ def main(): state=dict(choices=['present', 'absent'], default='present'), ) - module = TowerModule( + module = TowerLegacyModule( argument_spec=argument_spec, supports_check_mode=False ) diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py index 025b8d033d..5db00d6325 100644 --- a/awx_collection/test/awx/conftest.py +++ b/awx_collection/test/awx/conftest.py @@ -10,7 +10,7 @@ from contextlib import redirect_stdout, suppress from unittest import mock import logging -from requests.models import Response +from requests.models import Response, PreparedRequest import pytest @@ -23,6 +23,14 @@ try: except ImportError: HAS_TOWER_CLI = False +try: + # Because awxkit will be a directory at the root of this makefile and we are using python3, import awxkit will work even if its not installed. + # However, awxkit will not contain api whih causes a stack failure down on line 170 when we try to mock it. + # So here we are importing awxkit.api to prevent that. Then you only get an error on tests for awxkit functionality. + import awxkit.api + HAS_AWX_KIT = True +except ImportError: + HAS_AWX_KIT = False logger = logging.getLogger('awx.main.tests') @@ -90,7 +98,8 @@ def run_module(request, collection_import): if 'params' in kwargs and method == 'GET': # query params for GET are handled a bit differently by # tower-cli and python requests as opposed to REST framework APIRequestFactory - kwargs_copy.setdefault('data', {}) + if not kwargs_copy.get('data'): + kwargs_copy['data'] = {} if isinstance(kwargs['params'], dict): kwargs_copy['data'].update(kwargs['params']) elif isinstance(kwargs['params'], list): @@ -117,6 +126,8 @@ def run_module(request, collection_import): request_user.username, resp.status_code ) + resp.request = PreparedRequest() + resp.request.prepare(method=method, url=url) return resp def new_open(self, method, url, **kwargs): @@ -142,11 +153,22 @@ def run_module(request, collection_import): def mock_load_params(self): self.params = module_params - with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params): + if getattr(resource_module, 'TowerAWXKitModule', None): + resource_class = resource_module.TowerAWXKitModule + elif getattr(resource_module, 'TowerAPIModule', None): + resource_class = resource_module.TowerAPIModule + elif getattr(resource_module, 'TowerLegacyModule', None): + resource_class = resource_module.TowerLegacyModule + else: + raise("The module has neither a TowerLegacyModule, TowerAWXKitModule or a TowerAPIModule") + + with mock.patch.object(resource_class, '_load_params', new=mock_load_params): # Call the test utility (like a mock server) instead of issuing HTTP requests with mock.patch('ansible.module_utils.urls.Request.open', new=new_open): if HAS_TOWER_CLI: tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request) + elif HAS_AWX_KIT: + tower_cli_mgr = mock.patch('awxkit.api.client.requests.Session.request', new=new_request) else: tower_cli_mgr = suppress() with tower_cli_mgr: diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py new file mode 100644 index 0000000000..a4ffad3c48 --- /dev/null +++ b/awx_collection/test/awx/test_completeness.py @@ -0,0 +1,268 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +from awx.main.tests.functional.conftest import _request +from ansible.module_utils.six import PY2, string_types +import yaml +import os +import re + +# Analysis variables +# ----------------------------------------------------------------------------------------------------------- + +# Read-only endpoints are dynamically created by an options page with no POST section. +# Normally a read-only endpoint should not have a module (i.e. /api/v2/me) but sometimes we reuse a name +# For example, we have a tower_role module but /api/v2/roles is a read only endpoint. +# This list indicates which read-only endpoints have associated modules with them. +read_only_endpoints_with_modules = ['tower_settings', 'tower_role'] + +# If a module should not be created for an endpoint and the endpoint is not read-only add it here +# THINK HARD ABOUT DOING THIS +no_module_for_endpoint = [] + +# Some modules work on the related fields of an endpoint. These modules will not have an auto-associated endpoint +no_endpoint_for_module = [ + 'tower_import', 'tower_meta', 'tower_export', 'tower_job_launch', 'tower_job_wait', 'tower_job_list', + 'tower_license', 'tower_ping', 'tower_receive', 'tower_send', 'tower_workflow_launch', 'tower_job_cancel', + 'tower_workflow_template', +] + +# Global module parameters we can ignore +ignore_parameters = [ + 'state', 'new_name', +] + +# Some modules take additional parameters that do not appear in the API +# Add the module name as the key with the value being the list of params to ignore +no_api_parameter_ok = { + # The wait is for whether or not to wait for a project update on change + 'tower_project': ['wait'], + # Existing_token and id are for working with an existing tokens + 'tower_token': ['existing_token', 'existing_token_id'], + # /survey spec is now how we handle associations + # We take an organization here to help with the lookups only + 'tower_job_template': ['survey_spec', 'organization'], + # Organization is how we looking job templates + 'tower_workflow_job_template_node': ['organization'], + # Survey is how we handle associations + 'tower_workflow_job_template': ['survey'], +} + +# When this tool was created we were not feature complete. Adding something in here indicates a module +# that needs to be developed. If the module is found on the file system it will auto-detect that the +# work is being done and will bypass this check. At some point this module should be removed from this list. +needs_development = [ + 'tower_ad_hoc_command', 'tower_application', 'tower_instance_group', 'tower_inventory_script', + 'tower_workflow_approval' +] +needs_param_development = { + 'tower_host': ['instance_id'], + 'tower_inventory': ['insights_credential'], +} +# ----------------------------------------------------------------------------------------------------------- + +return_value = 0 +read_only_endpoint = [] + + +def cause_error(msg): + global return_value + return_value = 255 + return msg + + +def determine_state(module_id, endpoint, module, parameter, api_option, module_option): + # This is a hierarchical list of things that are ok/failures based on conditions + + # If we know this module needs development this is a non-blocking failure + if module_id in needs_development and module == 'N/A': + return "Failed (non-blocking), module needs development" + + # If the module is a read only endpoint: + # If it has no module on disk that is ok. + # If it has a module on disk but its listed in read_only_endpoints_with_modules that is ok + # Else we have a module for a read only endpoint that should not exit + if module_id in read_only_endpoint: + if module == 'N/A': + # There may be some cases where a read only endpoint has a module + return "OK, this endpoint is read-only and should not have a module" + elif module_id in read_only_endpoints_with_modules: + return "OK, module params can not be checked to read-only" + else: + return cause_error("Failed, read-only endpoint should not have an associated module") + + # If the endpoint is listed as not needing a module and we don't have one we are ok + if module_id in no_module_for_endpoint and module == 'N/A': + return "OK, this endpoint should not have a module" + + # If module is listed as not needing an endpoint and we don't have one we are ok + if module_id in no_endpoint_for_module and endpoint == 'N/A': + return "OK, this module does not require an endpoint" + + # All of the end/point module conditionals are done so if we don't have a module or endpoint we have a problem + if module == 'N/A': + return cause_error('Failed, missing module') + if endpoint == 'N/A': + return cause_error('Failed, why does this module have no endpoint') + + # Now perform parameter checks + + # First, if the parameter is in the ignore_parameters list we are ok + if parameter in ignore_parameters: + return "OK, globally ignored parameter" + + # If both the api option and the module option are both either objects or none + if (api_option is None) ^ (module_option is None): + # If the API option is node and the parameter is in the no_api_parameter list we are ok + if api_option is None and parameter in no_api_parameter_ok.get(module, {}): + return 'OK, no api parameter is ok' + # If we know this parameter needs development and we don't have a module option we are non-blocking + if module_option is None and parameter in needs_param_development.get(module_id, {}): + return "Failed (non-blocking), parameter needs development" + # Check for deprecated in the node, if its deprecated and has no api option we are ok, otherwise we have a problem + if module_option and module_option.get('description'): + description = '' + if isinstance(module_option.get('description'), string_types): + description = module_option.get('description') + else: + description = " ".join(module_option.get('description')) + + if 'deprecated' in description.lower(): + if api_option is None: + return 'OK, deprecated module option' + else: + return cause_error('Failed, module marks option as deprecated but option still exists in API') + # If we don't have a corresponding API option but we are a list then we are likely a relation + if not api_option and module_option and module_option.get('type', 'str') == 'list': + return "OK, Field appears to be relation" + # TODO, at some point try and check the object model to confirm its actually a relation + return cause_error('Failed, option mismatch') + + # We made it through all of the checks so we are ok + return 'OK' + + +def test_completeness(collection_import, request, admin_user, job_template): + option_comparison = {} + # Load a list of existing module files from disk + base_folder = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) + ) + module_directory = os.path.join(base_folder, 'plugins', 'modules') + for root, dirs, files in os.walk(module_directory): + if root == module_directory: + for filename in files: + if re.match('^tower_.*.py$', filename): + module_name = filename[:-3] + option_comparison[module_name] = { + 'endpoint': 'N/A', + 'api_options': {}, + 'module_options': {}, + 'module_name': module_name, + } + resource_module = collection_import('plugins.modules.{0}'.format(module_name)) + option_comparison[module_name]['module_options'] = yaml.load( + resource_module.DOCUMENTATION, + Loader=yaml.SafeLoader + )['options'] + + endpoint_response = _request('get')( + url='/api/v2/', + user=admin_user, + expect=None, + ) + for endpoint in endpoint_response.data.keys(): + # Module names are singular and endpoints are plural so we need to convert to singular + singular_endpoint = '{0}'.format(endpoint) + if singular_endpoint.endswith('ies'): + singular_endpoint = singular_endpoint[:-3] + if singular_endpoint != 'settings' and singular_endpoint.endswith('s'): + singular_endpoint = singular_endpoint[:-1] + module_name = 'tower_{0}'.format(singular_endpoint) + + endpoint_url = endpoint_response.data.get(endpoint) + + # If we don't have a module for this endpoint then we can create an empty one + if module_name not in option_comparison: + option_comparison[module_name] = {} + option_comparison[module_name]['module_name'] = 'N/A' + option_comparison[module_name]['module_options'] = {} + + # Add in our endpoint and an empty api_options + option_comparison[module_name]['endpoint'] = endpoint_url + option_comparison[module_name]['api_options'] = {} + + # Get out the endpoint, load and parse its options page + options_response = _request('options')( + url=endpoint_url, + user=admin_user, + expect=None, + ) + if 'POST' in options_response.data.get('actions', {}): + option_comparison[module_name]['api_options'] = options_response.data.get('actions').get('POST') + else: + read_only_endpoint.append(module_name) + + # Parse through our data to get string lengths to make a pretty report + longest_module_name = 0 + longest_option_name = 0 + longest_endpoint = 0 + for module in option_comparison: + if len(option_comparison[module]['module_name']) > longest_module_name: + longest_module_name = len(option_comparison[module]['module_name']) + if len(option_comparison[module]['endpoint']) > longest_endpoint: + longest_endpoint = len(option_comparison[module]['endpoint']) + for option in option_comparison[module]['api_options'], option_comparison[module]['module_options']: + if len(option) > longest_option_name: + longest_option_name = len(option) + + # Print out some headers + print("".join([ + "End Point", " " * (longest_endpoint - len("End Point")), + " | Module Name", " " * (longest_module_name - len("Module Name")), + " | Option", " " * (longest_option_name - len("Option")), + " | API | Module | State", + ])) + print("-|-".join([ + "-" * longest_endpoint, + "-" * longest_module_name, + "-" * longest_option_name, + "---", + "------", + "---------------------------------------------", + ])) + + # Print out all of our data + for module in sorted(option_comparison): + module_data = option_comparison[module] + all_param_names = list(set(module_data['api_options']) | set(module_data['module_options'])) + for parameter in sorted(all_param_names): + print("".join([ + module_data['endpoint'], " " * (longest_endpoint - len(module_data['endpoint'])), " | ", + module_data['module_name'], " " * (longest_module_name - len(module_data['module_name'])), " | ", + parameter, " " * (longest_option_name - len(parameter)), " | ", + " X " if (parameter in module_data['api_options']) else ' ', " | ", + ' X ' if (parameter in module_data['module_options']) else ' ', " | ", + determine_state( + module, + module_data['endpoint'], + module_data['module_name'], + parameter, + module_data['api_options'][parameter] if (parameter in module_data['api_options']) else None, + module_data['module_options'][parameter] if (parameter in module_data['module_options']) else None, + ), + ])) + # This handles cases were we got no params from the options page nor from the modules + if len(all_param_names) == 0: + print("".join([ + module_data['endpoint'], " " * (longest_endpoint - len(module_data['endpoint'])), " | ", + module_data['module_name'], " " * (longest_module_name - len(module_data['module_name'])), " | ", + "N/A", " " * (longest_option_name - len("N/A")), " | ", + ' ', " | ", + ' ', " | ", + determine_state(module, module_data['endpoint'], module_data['module_name'], 'N/A', None, None), + ])) + + if return_value != 0: + raise Exception("One or more failures caused issues") diff --git a/awx_collection/test/awx/test_module_utils.py b/awx_collection/test/awx/test_module_utils.py index 0f443890e8..2903621768 100644 --- a/awx_collection/test/awx/test_module_utils.py +++ b/awx_collection/test/awx/test_module_utils.py @@ -4,6 +4,7 @@ __metaclass__ = type import json import sys +from awx.main.models import Organization, Team from requests.models import Response from unittest import mock @@ -30,12 +31,12 @@ def mock_ping_response(self, method, url, **kwargs): def test_version_warning(collection_import, silence_warning): - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule cli_data = {'ANSIBLE_MODULE_ARGS': {}} testargs = ['module_file2.py', json.dumps(cli_data)] with mock.patch.object(sys, 'argv', testargs): with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): - my_module = TowerModule(argument_spec=dict()) + my_module = TowerAPIModule(argument_spec=dict()) my_module._COLLECTION_VERSION = "1.0.0" my_module._COLLECTION_TYPE = "not-junk" my_module.collection_to_version['not-junk'] = 'not-junk' @@ -46,12 +47,12 @@ def test_version_warning(collection_import, silence_warning): def test_type_warning(collection_import, silence_warning): - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule cli_data = {'ANSIBLE_MODULE_ARGS': {}} testargs = ['module_file2.py', json.dumps(cli_data)] with mock.patch.object(sys, 'argv', testargs): with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): - my_module = TowerModule(argument_spec={}) + my_module = TowerAPIModule(argument_spec={}) my_module._COLLECTION_VERSION = "1.2.3" my_module._COLLECTION_TYPE = "junk" my_module.collection_to_version['junk'] = 'junk' @@ -63,7 +64,7 @@ def test_type_warning(collection_import, silence_warning): def test_duplicate_config(collection_import, silence_warning): # imports done here because of PATH issues unique to this test suite - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule data = { 'name': 'zigzoom', 'zig': 'zoom', @@ -71,12 +72,12 @@ def test_duplicate_config(collection_import, silence_warning): 'tower_config_file': 'my_config' } - with mock.patch.object(TowerModule, 'load_config') as mock_load: + with mock.patch.object(TowerAPIModule, 'load_config') as mock_load: argument_spec = dict( name=dict(required=True), zig=dict(type='str'), ) - TowerModule(argument_spec=argument_spec, direct_params=data) + TowerAPIModule(argument_spec=argument_spec, direct_params=data) assert mock_load.mock_calls[-1] == mock.call('my_config') silence_warning.assert_called_once_with( @@ -92,8 +93,8 @@ def test_no_templated_values(collection_import): Those replacements should happen at build time, so they should not be checked into source. """ - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule - assert TowerModule._COLLECTION_VERSION == "0.0.1-devel", ( + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule + assert TowerAPIModule._COLLECTION_VERSION == "0.0.1-devel", ( 'The collection version is templated when the collection is built ' 'and the code should retain the placeholder of "0.0.1-devel".' ) @@ -102,3 +103,25 @@ def test_no_templated_values(collection_import): 'The inventory plugin FQCN is templated when the collection is built ' 'and the code should retain the default of awx.awx.' ) + + +def test_conflicting_name_and_id(run_module, admin_user): + """In the event that 2 related items match our search criteria in this way: + one item has an id that matches input + one item has a name that matches input + We should preference the id over the name. + Otherwise, the universality of the tower_api lookup plugin is compromised. + """ + org_by_id = Organization.objects.create(name='foo') + slug = str(org_by_id.id) + org_by_name = Organization.objects.create(name=slug) + result = run_module('tower_team', { + 'name': 'foo_team', 'description': 'fooin around', + 'organization': slug + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + team = Team.objects.filter(name='foo_team').first() + assert str(team.organization_id) == slug, ( + 'Lookup by id should be preferenced over name in cases of conflict.' + ) + assert team.organization.name == 'foo' diff --git a/awx_collection/test/awx/test_notification.py b/awx_collection/test/awx/test_notification_template.py similarity index 91% rename from awx_collection/test/awx/test_notification.py rename to awx_collection/test/awx/test_notification_template.py index 9d916d1dc9..28f7c4ecee 100644 --- a/awx_collection/test/awx/test_notification.py +++ b/awx_collection/test/awx/test_notification_template.py @@ -34,7 +34,7 @@ def test_create_modify_notification_template(run_module, admin_user, organizatio 'use_tls': False, 'use_ssl': False, 'timeout': 4 } - result = run_module('tower_notification', dict( + result = run_module('tower_notification_template', dict( name='foo-notification-template', organization=organization.name, notification_type='email', @@ -49,7 +49,7 @@ def test_create_modify_notification_template(run_module, admin_user, organizatio # Test no-op, this is impossible if the notification_configuration is given # because we cannot determine if password fields changed - result = run_module('tower_notification', dict( + result = run_module('tower_notification_template', dict( name='foo-notification-template', organization=organization.name, notification_type='email', @@ -59,7 +59,7 @@ def test_create_modify_notification_template(run_module, admin_user, organizatio # Test a change in the configuration nt_config['timeout'] = 12 - result = run_module('tower_notification', dict( + result = run_module('tower_notification_template', dict( name='foo-notification-template', organization=organization.name, notification_type='email', @@ -74,7 +74,7 @@ def test_create_modify_notification_template(run_module, admin_user, organizatio @pytest.mark.django_db def test_invalid_notification_configuration(run_module, admin_user, organization): - result = run_module('tower_notification', dict( + result = run_module('tower_notification_template', dict( name='foo-notification-template', organization=organization.name, notification_type='email', @@ -92,7 +92,7 @@ def test_deprecated_to_modern_no_op(run_module, admin_user, organization): 'X-Custom-Header': 'value123' } } - result = run_module('tower_notification', dict( + result = run_module('tower_notification_template', dict( name='foo-notification-template', organization=organization.name, notification_type='webhook', @@ -101,7 +101,7 @@ def test_deprecated_to_modern_no_op(run_module, admin_user, organization): assert not result.get('failed', False), result.get('msg', result) assert result.pop('changed', None), result - result = run_module('tower_notification', dict( + result = run_module('tower_notification_template', dict( name='foo-notification-template', organization=organization.name, notification_type='webhook', diff --git a/awx_collection/tests/integration/targets/tower_export/aliases b/awx_collection/tests/integration/targets/tower_export/aliases new file mode 100644 index 0000000000..527d07c3cb --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_export/aliases @@ -0,0 +1 @@ +skip/python2 diff --git a/awx_collection/tests/integration/targets/tower_export/tasks/main.yml b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml new file mode 100644 index 0000000000..7ffbc15820 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + org_name1: "AWX-Collection-tests-tower_export-organization-{{ test_id }}" + org_name2: "AWX-Collection-tests-tower_export-organization2-{{ test_id }}" + inventory_name1: "AWX-Collection-tests-tower_export-inv1-{{ test_id }}" + +- block: + - name: Create some organizations + tower_organization: + name: "{{ item }}" + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" + + - name: Create an inventory + tower_inventory: + name: "{{ inventory_name1 }}" + organization: "{{ org_name1 }}" + + - name: Export all tower assets + tower_export: + all: true + register: all_assets + + - assert: + that: + - all_assets is not changed + - all_assets is successful + - all_assets['assets']['organizations'] | length() >= 2 + + - name: Export all inventories + tower_export: + inventory: 'all' + register: inventory_export + + - assert: + that: + - inventory_export is successful + - inventory_export is not changed + - inventory_export['assets']['inventory'] | length() >= 1 + - "'organizations' not in inventory_export['assets']" + + # This mimics the example in the module + - name: Export an all and a specific + tower_export: + inventory: 'all' + organizations: "{{ org_name1 }}" + register: mixed_export + + - assert: + that: + - mixed_export is successful + - mixed_export is not changed + - mixed_export['assets']['inventory'] | length() >= 1 + - mixed_export['assets']['organizations'] | length() == 1 + - "'workflow_job_templates' not in mixed_export['assets']" + + always: + - name: Remove our inventory + tower_inventory: + name: "{{ inventory_name1 }}" + organization: "{{ org_name1 }}" + state: absent + + - name: Remove test organizations + tower_organization: + name: "{{ item }}" + state: absent + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" diff --git a/awx_collection/tests/integration/targets/tower_import/aliases b/awx_collection/tests/integration/targets/tower_import/aliases new file mode 100644 index 0000000000..527d07c3cb --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_import/aliases @@ -0,0 +1 @@ +skip/python2 diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml new file mode 100644 index 0000000000..9835ff89a5 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -0,0 +1,108 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + org_name1: "AWX-Collection-tests-tower_import-organization-{{ test_id }}" + org_name2: "AWX-Collection-tests-tower_import-organization2-{{ test_id }}" + +- block: + - name: "Import something" + tower_import: + assets: + organizations: + - name: "{{ org_name1 }}" + description: "" + max_hosts: 0 + custom_virtualenv: null + related: + notification_templates: [] + notification_templates_started: [] + notification_templates_success: [] + notification_templates_error: [] + notification_templates_approvals: [] + natural_key: + name: "Default" + type: "organization" + register: import_output + + - assert: + that: + - import_output is changed + + - name: "Import something again (awxkit is not idempotent, this tests a failure)" + tower_import: + assets: + organizations: + - name: "{{ org_name1 }}" + description: "" + max_hosts: 0 + custom_virtualenv: null + related: + notification_templates: [] + notification_templates_started: [] + notification_templates_success: [] + notification_templates_error: [] + notification_templates_approvals: [] + natural_key: + name: "Default" + type: "organization" + register: import_output + ignore_errors: true + + - assert: + that: + - import_output is failed + - "'Organization with this Name already exists' in import_output.msg" + + - name: "Write out a json file" + copy: + content: | + { + "organizations": [ + { + "name": "{{ org_name2 }}", + "description": "", + "max_hosts": 0, + "custom_virtualenv": null, + "related": { + "notification_templates": [], + "notification_templates_started": [], + "notification_templates_success": [], + "notification_templates_error": [], + "notification_templates_approvals": [] + }, + "natural_key": { + "name": "Default", + "type": "organization" + } + } + ] + } + dest: ./org.json + + - name: "Load assets from a file" + tower_import: + assets: "{{ lookup('file', 'org.json') | from_json() }}" + register: import_output + + - assert: + that: + - import_output is changed + + always: + - name: Remove organizations + tower_organization: + name: "{{ item }}" + state: absent + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" + + - name: Delete org.json + file: + path: ./org.json + state: absent diff --git a/awx_collection/tests/integration/targets/tower_notification/tasks/main.yml b/awx_collection/tests/integration/targets/tower_notification_template/tasks/main.yml similarity index 77% rename from awx_collection/tests/integration/targets/tower_notification/tasks/main.yml rename to awx_collection/tests/integration/targets/tower_notification_template/tasks/main.yml index ea3e96b90a..a4d41571cf 100644 --- a/awx_collection/tests/integration/targets/tower_notification/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_notification_template/tasks/main.yml @@ -1,14 +1,14 @@ --- - name: Generate names set_fact: - slack_not: "AWX-Collection-tests-tower_notification-slack-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - webhook_not: "AWX-Collection-tests-tower_notification-wehbook-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - email_not: "AWX-Collection-tests-tower_notification-email-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - twillo_not: "AWX-Collection-tests-tower_notification-twillo-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - pd_not: "AWX-Collection-tests-tower_notification-pd-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - irc_not: "AWX-Collection-tests-tower_notification-irc-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + slack_not: "AWX-Collection-tests-tower_notification_template-slack-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + webhook_not: "AWX-Collection-tests-tower_notification_template-wehbook-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + email_not: "AWX-Collection-tests-tower_notification_template-email-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + twillo_not: "AWX-Collection-tests-tower_notification_template-twillo-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + pd_not: "AWX-Collection-tests-tower_notification_template-pd-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + irc_not: "AWX-Collection-tests-tower_notification_template-irc-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" -- name: Test deprecation warnings +- name: Test deprecation warnings with legacy name tower_notification: name: "{{ slack_not }}" organization: Default @@ -54,7 +54,7 @@ - result['deprecations'] | length() == 25 - name: Create Slack notification with custom messages - tower_notification: + tower_notification_template: name: "{{ slack_not }}" organization: Default notification_type: slack @@ -76,7 +76,7 @@ - result is changed - name: Delete Slack notification - tower_notification: + tower_notification_template: name: "{{ slack_not }}" organization: Default state: absent @@ -87,7 +87,7 @@ - result is changed - name: Add webhook notification - tower_notification: + tower_notification_template: name: "{{ webhook_not }}" organization: Default notification_type: webhook @@ -102,7 +102,7 @@ - result is changed - name: Delete webhook notification - tower_notification: + tower_notification_template: name: "{{ webhook_not }}" organization: Default state: absent @@ -113,7 +113,7 @@ - result is changed - name: Add email notification - tower_notification: + tower_notification_template: name: "{{ email_not }}" organization: Default notification_type: email @@ -134,7 +134,7 @@ - result is changed - name: Delete email notification - tower_notification: + tower_notification_template: name: "{{ email_not }}" organization: Default state: absent @@ -145,7 +145,7 @@ - result is changed - name: Add twilio notification - tower_notification: + tower_notification_template: name: "{{ twillo_not }}" organization: Default notification_type: twilio @@ -162,7 +162,7 @@ - result is changed - name: Delete twilio notification - tower_notification: + tower_notification_template: name: "{{ twillo_not }}" organization: Default state: absent @@ -173,7 +173,7 @@ - result is changed - name: Add PagerDuty notification - tower_notification: + tower_notification_template: name: "{{ pd_not }}" organization: Default notification_type: pagerduty @@ -189,7 +189,7 @@ - result is changed - name: Delete PagerDuty notification - tower_notification: + tower_notification_template: name: "{{ pd_not }}" organization: Default state: absent @@ -200,7 +200,7 @@ - result is changed - name: Add IRC notification - tower_notification: + tower_notification_template: name: "{{ irc_not }}" organization: Default notification_type: irc @@ -219,7 +219,7 @@ - result is changed - name: Delete IRC notification - tower_notification: + tower_notification_template: name: "{{ irc_not }}" organization: Default state: absent diff --git a/awx_collection/tests/integration/targets/tower_role/tasks/main.yml b/awx_collection/tests/integration/targets/tower_role/tasks/main.yml index f0c26a7e04..4102a11402 100644 --- a/awx_collection/tests/integration/targets/tower_role/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_role/tasks/main.yml @@ -1,74 +1,112 @@ --- +- name: Generate a test id + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + - name: Generate names set_fact: - username: "AWX-Collection-tests-tower_role-user-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + username: "AWX-Collection-tests-tower_role-user-{{ test_id }}" + project_name: "AWX-Collection-tests-tower_role-project-{{ test_id }}" -- name: Create a User - tower_user: - first_name: Joe - last_name: User - username: "{{ username }}" - password: "{{ 65535 | random | to_uuid }}" - email: joe@example.org - state: present - register: result +- block: + - name: Create a User + tower_user: + first_name: Joe + last_name: User + username: "{{ username }}" + password: "{{ 65535 | random | to_uuid }}" + email: joe@example.org + state: present + register: result -- assert: - that: - - "result is changed" + - assert: + that: + - "result is changed" -- name: Add Joe to the update role of the default Project - tower_role: - user: "{{ username }}" - role: update - project: Demo Project - state: "{{ item }}" - register: result - with_items: - - "present" - - "absent" + - name: Create a project + tower_project: + name: "{{ project_name }}" + organization: Default + scm_type: git + scm_url: https://github.com/ansible/test-playbooks + wait: false + register: project_info -- assert: - that: - - "result is changed" + - assert: + that: + - project_info is changed -- name: Create a workflow - tower_workflow_job_template: - name: test-role-workflow - organization: Default - state: present + - name: Add Joe to the update role of the default Project + tower_role: + user: "{{ username }}" + role: update + project: "Demo Project" + state: "{{ item }}" + register: result + with_items: + - "present" + - "absent" -- name: Add Joe to workflow execute role - tower_role: - user: "{{ username }}" - role: execute - workflow: test-role-workflow - state: present - register: result + - assert: + that: + - "result is changed" -- assert: - that: - - "result is changed" + - name: Add Joe to the new project by ID + tower_role: + user: "{{ username }}" + role: update + project: "{{ project_info['id'] }}" + state: "{{ item }}" + register: result + with_items: + - "present" + - "absent" -- name: Add Joe to workflow execute role, no-op - tower_role: - user: "{{ username }}" - role: execute - workflow: test-role-workflow - state: present - register: result + - assert: + that: + - "result is changed" -- assert: - that: - - "result is not changed" + - name: Create a workflow + tower_workflow_job_template: + name: test-role-workflow + organization: Default + state: present -- name: Delete a User - tower_user: - username: "{{ username }}" - email: joe@example.org - state: absent - register: result + - name: Add Joe to workflow execute role + tower_role: + user: "{{ username }}" + role: execute + workflow: test-role-workflow + state: present + register: result -- assert: - that: - - "result is changed" + - assert: + that: + - "result is changed" + + - name: Add Joe to workflow execute role, no-op + tower_role: + user: "{{ username }}" + role: execute + workflow: test-role-workflow + state: present + register: result + + - assert: + that: + - "result is not changed" + + always: + - name: Delete a User + tower_user: + username: "{{ username }}" + email: joe@example.org + state: absent + register: result + + - name: Delete the project + tower_project: + name: "{{ project_name }}" + organization: Default + state: absent + register: result diff --git a/awx_collection/tests/sanity/ignore-2.10.txt b/awx_collection/tests/sanity/ignore-2.10.txt index 76f35a9a85..93f7dd6d7b 100644 --- a/awx_collection/tests/sanity/ignore-2.10.txt +++ b/awx_collection/tests/sanity/ignore-2.10.txt @@ -4,3 +4,12 @@ plugins/modules/tower_workflow_template.py validate-modules:deprecation-mismatch plugins/modules/tower_credential.py pylint:wrong-collection-deprecated-version-tag plugins/modules/tower_job_wait.py pylint:wrong-collection-deprecated-version-tag plugins/modules/tower_notification.py pylint:wrong-collection-deprecated-version-tag +plugins/inventory/tower.py pylint:raise-missing-from +plugins/inventory/tower.py pylint:super-with-arguments +plugins/lookup/tower_schedule_rrule.py pylint:raise-missing-from +plugins/module_utils/tower_api.py pylint:super-with-arguments +plugins/module_utils/tower_awxkit.py pylint:super-with-arguments +plugins/module_utils/tower_legacy.py pylint:super-with-arguments +plugins/module_utils/tower_module.py pylint:super-with-arguments +plugins/module_utils/tower_module.py pylint:raise-missing-from +test/awx/conftest.py pylint:raise-missing-from diff --git a/awx_collection/tests/sanity/ignore-2.9.txt b/awx_collection/tests/sanity/ignore-2.9.txt index 9242eefca9..f6ee755ea0 100644 --- a/awx_collection/tests/sanity/ignore-2.9.txt +++ b/awx_collection/tests/sanity/ignore-2.9.txt @@ -4,3 +4,12 @@ plugins/modules/tower_send.py validate-modules:deprecation-mismatch plugins/modules/tower_send.py validate-modules:invalid-documentation plugins/modules/tower_workflow_template.py validate-modules:deprecation-mismatch plugins/modules/tower_workflow_template.py validate-modules:invalid-documentation +plugins/inventory/tower.py pylint:raise-missing-from +plugins/inventory/tower.py pylint:super-with-arguments +plugins/lookup/tower_schedule_rrule.py pylint:raise-missing-from +plugins/module_utils/tower_api.py pylint:super-with-arguments +plugins/module_utils/tower_awxkit.py pylint:super-with-arguments +plugins/module_utils/tower_legacy.py pylint:super-with-arguments +plugins/module_utils/tower_module.py pylint:super-with-arguments +plugins/module_utils/tower_module.py pylint:raise-missing-from +test/awx/conftest.py pylint:raise-missing-from diff --git a/awx_collection/tools/roles/generate/templates/tower_module.j2 b/awx_collection/tools/roles/generate/templates/tower_module.j2 index a9834db28d..3606cff547 100644 --- a/awx_collection/tools/roles/generate/templates/tower_module.j2 +++ b/awx_collection/tools/roles/generate/templates/tower_module.j2 @@ -96,7 +96,7 @@ EXAMPLES = ''' {% endif %} ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -142,7 +142,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters {% for option in item['json']['actions']['POST'] %} diff --git a/awx_collection/tools/roles/template_galaxy/tasks/main.yml b/awx_collection/tools/roles/template_galaxy/tasks/main.yml index 414f55b7de..96eb26413c 100644 --- a/awx_collection/tools/roles/template_galaxy/tasks/main.yml +++ b/awx_collection/tools/roles/template_galaxy/tasks/main.yml @@ -2,7 +2,7 @@ - name: Set the collection version in the tower_api.py file replace: path: "{{ collection_path }}/plugins/module_utils/tower_api.py" - regexp: '^ _COLLECTION_VERSION = "devel"' + regexp: '^ _COLLECTION_VERSION = "0.0.1-devel"' replace: ' _COLLECTION_VERSION = "{{ collection_version }}"' when: - "awx_template_version | default(True)" diff --git a/awxkit/VERSION b/awxkit/VERSION index 4b964e9654..7b3b6e02bb 100644 --- a/awxkit/VERSION +++ b/awxkit/VERSION @@ -1 +1 @@ -14.0.0 +14.1.0 diff --git a/awxkit/awxkit/api/client.py b/awxkit/awxkit/api/client.py index 7844a7fa11..77c71a569b 100644 --- a/awxkit/awxkit/api/client.py +++ b/awxkit/awxkit/api/client.py @@ -15,12 +15,11 @@ class ConnectionException(exc.Common): class Token_Auth(requests.auth.AuthBase): - def __init__(self, token, auth_type='Token'): + def __init__(self, token): self.token = token - self.auth_type = auth_type def __call__(self, request): - request.headers['Authorization'] = '{0.auth_type} {0.token}'.format(self) + request.headers['Authorization'] = 'Bearer {0.token}'.format(self) return request @@ -57,7 +56,7 @@ class Connection(object): else: self.session.auth = (username, password) elif token: - self.session.auth = Token_Auth(token, auth_type=kwargs.get('auth_type', 'Token')) + self.session.auth = Token_Auth(token) else: self.session.auth = None diff --git a/awxkit/awxkit/awx/utils.py b/awxkit/awxkit/awx/utils.py index 2238297b67..d25e555ad6 100644 --- a/awxkit/awxkit/awx/utils.py +++ b/awxkit/awxkit/awx/utils.py @@ -90,7 +90,7 @@ def as_user(v, username, password=None): if session_id: del connection.session.cookies['sessionid'] if access_token: - kwargs = dict(token=access_token, auth_type='Bearer') + kwargs = dict(token=access_token) else: kwargs = connection.get_session_requirements() else: diff --git a/awxkit/awxkit/cli/client.py b/awxkit/awxkit/cli/client.py index 8013523921..f14d6df135 100755 --- a/awxkit/awxkit/cli/client.py +++ b/awxkit/awxkit/cli/client.py @@ -70,9 +70,10 @@ class CLI(object): subparsers = {} original_action = None - def __init__(self, stdout=sys.stdout, stderr=sys.stderr): + def __init__(self, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin): self.stdout = stdout self.stderr = stderr + self.stdin = stdin def get_config(self, key): """Helper method for looking up the value of a --conf.xyz flag""" @@ -87,7 +88,7 @@ class CLI(object): token = self.get_config('token') if token: self.root.connection.login( - None, None, token=token, auth_type='Bearer' + None, None, token=token, ) else: config.use_sessions = True diff --git a/awxkit/awxkit/cli/format.py b/awxkit/awxkit/cli/format.py index e8c5a6903e..d35c61efbb 100644 --- a/awxkit/awxkit/cli/format.py +++ b/awxkit/awxkit/cli/format.py @@ -40,7 +40,7 @@ def add_authentication_arguments(parser, env): def add_output_formatting_arguments(parser, env): - formatting = parser.add_argument_group('output formatting') + formatting = parser.add_argument_group('input/output formatting') formatting.add_argument( '-f', @@ -49,7 +49,7 @@ def add_output_formatting_arguments(parser, env): choices=FORMATTERS.keys(), default=env.get('TOWER_FORMAT', 'json'), help=( - 'specify an output format' + 'specify a format for the input and output' ), ) formatting.add_argument( diff --git a/awxkit/awxkit/cli/resource.py b/awxkit/awxkit/cli/resource.py index f22795fab2..8e30accad2 100644 --- a/awxkit/awxkit/cli/resource.py +++ b/awxkit/awxkit/cli/resource.py @@ -1,8 +1,9 @@ +import yaml import json import os -import sys from awxkit import api, config +from awxkit.exceptions import ImportExportError from awxkit.utils import to_str from awxkit.api.pages import Page from awxkit.api.pages.api import EXPORTABLE_RESOURCES @@ -135,7 +136,13 @@ class Import(CustomCommand): parser.print_help() raise SystemExit() - data = json.load(sys.stdin) + fmt = client.get_config('format') + if fmt == 'json': + data = json.load(client.stdin) + elif fmt == 'yaml': + data = yaml.safe_load(client.stdin) + else: + raise ImportExportError("Unsupported format for Import: " + fmt) client.authenticate() client.v2.import_assets(data) diff --git a/awxkit/test/cli/test_format.py b/awxkit/test/cli/test_format.py index 7166fb841c..5ab6e55d6c 100644 --- a/awxkit/test/cli/test_format.py +++ b/awxkit/test/cli/test_format.py @@ -1,10 +1,13 @@ +import io import json import yaml from awxkit.api.pages import Page from awxkit.api.pages.users import Users, User +from awxkit.cli import CLI from awxkit.cli.format import format_response +from awxkit.cli.resource import Import def test_json_empty_list(): @@ -44,3 +47,26 @@ def test_yaml_list(): page = Users.from_json(users) formatted = format_response(page, fmt='yaml') assert yaml.safe_load(formatted) == users + + +def test_yaml_import(): + class MockedV2: + def import_assets(self, data): + self._parsed_data = data + + def _dummy_authenticate(): + pass + + yaml_fd = io.StringIO( + """ + workflow_job_templates: + - name: Workflow1 + """ + ) + cli = CLI(stdin=yaml_fd) + cli.parse_args(['--conf.format', 'yaml']) + cli.v2 = MockedV2() + cli.authenticate = _dummy_authenticate + + Import().handle(cli, None) + assert cli.v2._parsed_data['workflow_job_templates'][0]['name'] diff --git a/docs/custom_virtualenvs.md b/docs/custom_virtualenvs.md index 9b47c5d5ca..78bde90758 100644 --- a/docs/custom_virtualenvs.md +++ b/docs/custom_virtualenvs.md @@ -83,15 +83,17 @@ index aa8b304..eb05f91 100644 + virtualenv /opt/my-envs/my-custom-env + /opt/my-envs/my-custom-env/bin/pip install psutil + -diff --git a/installer/image_build/templates/Dockerfile.j2 b/installer/image_build/templates/Dockerfile.j2 -index d69e2c9..a08bae5 100644 ---- a/installer/image_build/templates/Dockerfile.j2 -+++ b/installer/image_build/templates/Dockerfile.j2 -@@ -34,6 +34,7 @@ RUN yum -y install epel-release && \ - pip install virtualenv supervisor && \ - VENV_BASE=/var/lib/awx/venv make requirements_ansible && \ - VENV_BASE=/var/lib/awx/venv make requirements_awx && \ -+ VENV_BASE=/var/lib/awx/venv make requirements_custom && \ +diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 +index d3b582ffcb..220ac760a3 100644 +--- a/installer/roles/image_build/templates/Dockerfile.j2 ++++ b/installer/roles/image_build/templates/Dockerfile.j2 +@@ -165,6 +165,7 @@ RUN openssl req -nodes -newkey rsa:2048 -keyout /etc/nginx/nginx.key -out /etc/n + chmod 640 /etc/nginx/nginx.{csr,key,crt} + {% else %} + COPY --from=builder /var/lib/awx /var/lib/awx ++COPY --from=builder /opt/my-envs /opt/my-envs + RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage + {% endif %} ``` Once the AWX API is available, update the `CUSTOM_VENV_PATHS` setting as described in `Preparing a New Custom Virtualenv`. diff --git a/docs/websockets.md b/docs/websockets.md index c9daad83eb..12b81248b4 100644 --- a/docs/websockets.md +++ b/docs/websockets.md @@ -16,7 +16,9 @@ Previously, AWX leveraged RabbitMQ to deliver Ansible events that emanated from #### Broadcast Backplane Token -AWX node(s) connect to every other node via the Websocket backplane. Authentication is accomplished via a shared secret that is exchanged via the http header `secret`. The shared secret payload consists of a a `secret`, containing the shared secret, and a `nonce` which is used to mitigate replay attack windows. +AWX node(s) connect to every other node via the Websocket backplane. The backplane websockets initiate from the `wsbroadcast` process and connect to other nodes via the same nginx process that serves webpage websocket connections and marshalls incoming web/API requests. If you have configured AWX to run with an ssl terminated connection in front of nginx then you likely will have nginx configured to handle http traffic and thus the websocket connection will flow unencrypted over http. If you have nginx configured with ssl enabled, then the websocket traffic will flow encrypted. + +Authentication is accomplished via a shared secret that is generated and set at playbook install time. The shared secret is used to derive a payload that is exchanged via the http(s) header `secret`. The shared secret payload consists of a a `secret`, containing the shared secret, and a `nonce` which is used to mitigate replay attack windows. Note that the nonce timestamp is considered valid if it is within `300` second threshold. This is to allow for machine clock skews. ``` @@ -28,6 +30,8 @@ Note that the nonce timestamp is considered valid if it is within `300` second t The payload is encrypted using `HMAC-SHA256` with `settings.BROADCAST_WEBSOCKET_SECRET` as the key. The final payload that is sent, including the http header, is of the form: `secret: nonce_plaintext:HMAC_SHA256({"secret": settings.BROADCAST_WEBSOCKET_SECRET, "nonce": nonce_plaintext})`. +Upon receiving the payload, AWX decrypts the `secret` header using the known shared secret and ensures the `secret` value of the decrypted payload matches the known shared secret, `settings.BROADCAST_WEBSOCKET_SECRET`. If it does not match, the connection is closed. If it does match, the `nonce` is compared to the current time. If the nonce is off by more than `300` seconds, the connection is closed. If both tests pass, the connection is accepted. + ## Protocol You can connect to the AWX channels implementation using any standard websocket library by pointing it to `/websocket`. You must diff --git a/installer/build.yml b/installer/build.yml index 13b2b430b9..8ef6f2b1ce 100644 --- a/installer/build.yml +++ b/installer/build.yml @@ -1,7 +1,7 @@ --- - name: Build AWX Docker Images hosts: all - gather_facts: false + gather_facts: true roles: - {role: image_build} - {role: image_push, when: "docker_registry is defined"} diff --git a/installer/inventory b/installer/inventory index e8609a9b9c..daa6ba6b7d 100644 --- a/installer/inventory +++ b/installer/inventory @@ -119,6 +119,11 @@ create_preload_data=True # your credentials secret_key=awxsecret +# By default a broadcast websocket secret will be generated. +# If you would like to *rerun the playbook*, you need to set a unique password. +# Otherwise it would generate a new one every playbook run. +# broadcast_websocket_secret= + # Build AWX with official logos # Requires cloning awx-logos repo as a sibling of this project. # Review the trademark guidelines at https://github.com/ansible/awx-logos/blob/master/TRADEMARKS.md diff --git a/installer/roles/image_build/defaults/main.yml b/installer/roles/image_build/defaults/main.yml index 2618c9b40d..ab152975ce 100644 --- a/installer/roles/image_build/defaults/main.yml +++ b/installer/roles/image_build/defaults/main.yml @@ -1,3 +1,7 @@ --- create_preload_data: true build_dev: false + +# Helper vars to construct the proper download URL for the current architecture +tini_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm" }[ansible_facts.architecture] }}' +kubectl_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm" }[ansible_facts.architecture] }}' diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 index ecded902ea..c16b4b4a2c 100644 --- a/installer/roles/image_build/templates/Dockerfile.j2 +++ b/installer/roles/image_build/templates/Dockerfile.j2 @@ -35,7 +35,6 @@ RUN dnf -y update && \ glibc-langpack-en \ libcurl-devel \ libffi-devel \ - libstdc++.so.6 \ libtool-ltdl-devel \ make \ nodejs \ @@ -115,7 +114,6 @@ RUN dnf -y install \ # Install runtime requirements RUN dnf -y update && \ - dnf -y install https://github.com/krallin/tini/releases/download/v0.18.0/tini_0.18.0.rpm && \ dnf -y install epel-release 'dnf-command(config-manager)' && \ dnf module -y enable 'postgresql:10' && \ dnf config-manager --set-enabled PowerTools && \ @@ -140,12 +138,17 @@ RUN dnf -y update && \ vim-minimal \ which \ xmlsec1-openssl && \ - dnf -y --repofrompath gcloud,https://packages.cloud.google.com/yum/repos/cloud-sdk-el8-x86_64 \ - --setopt gcloud.gpgkey=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg \ - install kubectl && \ dnf -y install centos-release-stream && dnf -y install "rsyslog >= 8.1911.0" && dnf -y remove centos-release-stream && \ dnf -y clean all +# Install kubectl +RUN curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.17.8/bin/linux/{{ kubectl_architecture | default('amd64') }}/kubectl && \ + chmod a+x /usr/bin/kubectl + +# Install tini +RUN curl -L -o /usr/bin/tini https://github.com/krallin/tini/releases/download/v0.19.0/tini-{{ tini_architecture | default('amd64') }} && \ + chmod +x /usr/bin/tini + RUN python3 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if build_dev|bool %}flake8{% endif %} RUN rm -rf /root/.cache && rm -rf /tmp/* @@ -244,7 +247,7 @@ CMD ["/bin/bash"] USER 1000 EXPOSE 8052 -ENTRYPOINT ["tini", "--"] +ENTRYPOINT ["/usr/bin/tini", "--"] CMD /usr/bin/launch_awx.sh VOLUME /var/lib/nginx {% endif %} diff --git a/installer/roles/kubernetes/tasks/main.yml b/installer/roles/kubernetes/tasks/main.yml index bb4065f211..9e0d6f4f7d 100644 --- a/installer/roles/kubernetes/tasks/main.yml +++ b/installer/roles/kubernetes/tasks/main.yml @@ -1,9 +1,10 @@ --- - name: Generate broadcast websocket secret set_fact: - broadcast_websocket_secret: "{{ lookup('password', '/dev/null', length=128) }}" + broadcast_websocket_secret: "{{ lookup('password', '/dev/null length=128') }}" run_once: true no_log: true + when: broadcast_websocket_secret is not defined - fail: msg: "Only set one of kubernetes_context or openshift_host" diff --git a/installer/roles/kubernetes/templates/postgresql-values.yml.j2 b/installer/roles/kubernetes/templates/postgresql-values.yml.j2 index 658b898505..ea6ba29230 100644 --- a/installer/roles/kubernetes/templates/postgresql-values.yml.j2 +++ b/installer/roles/kubernetes/templates/postgresql-values.yml.j2 @@ -34,12 +34,22 @@ master: {{ affinity | to_nice_yaml(indent=2) | indent(width=4, indentfirst=True) }} {% endif %} {% endif %} -{% if pg_image_registry is defined %} image: +{% if pg_image_registry is defined %} registry: {{ pg_image_registry }} +{% endif %} + # The default bitnami image from the chart doesn't work on ARM + repository: postgres + tag: '11' volumePermissions: image: +{% if pg_image_registry is defined %} registry: {{ pg_image_registry }} +{% endif %} + # The default bitnami image from the chart doesn't work on ARM + repository: alpine + tag: '3' +{% if pg_image_registry is defined %} metrics: image: registry: {{ pg_image_registry }} diff --git a/installer/roles/local_docker/tasks/compose.yml b/installer/roles/local_docker/tasks/compose.yml index 120b81cc1a..9a95ddabc3 100644 --- a/installer/roles/local_docker/tasks/compose.yml +++ b/installer/roles/local_docker/tasks/compose.yml @@ -12,22 +12,22 @@ - name: Create Docker Compose Configuration template: - src: "{{ item }}.j2" - dest: "{{ docker_compose_dir }}/{{ item }}" - mode: 0600 - with_items: - - environment.sh - - credentials.py - - docker-compose.yml - - nginx.conf - - redis.conf + src: "{{ item.file }}.j2" + dest: "{{ docker_compose_dir }}/{{ item.file }}" + mode: "{{ item.mode }}" + loop: + - file: environment.sh + mode: "0600" + - file: credentials.py + mode: "0600" + - file: docker-compose.yml + mode: "0600" + - file: nginx.conf + mode: "0600" + - file: redis.conf + mode: "0664" register: awx_compose_config -- name: Set redis config to other group readable to satisfy redis-server - file: - path: "{{ docker_compose_dir }}/redis.conf" - mode: 0666 - - name: Render SECRET_KEY file copy: content: "{{ secret_key }}" diff --git a/installer/roles/local_docker/tasks/main.yml b/installer/roles/local_docker/tasks/main.yml index ad87f16fb4..e2b793e50e 100644 --- a/installer/roles/local_docker/tasks/main.yml +++ b/installer/roles/local_docker/tasks/main.yml @@ -1,9 +1,10 @@ --- - name: Generate broadcast websocket secret set_fact: - broadcast_websocket_secret: "{{ lookup('password', '/dev/null', length=128) }}" + broadcast_websocket_secret: "{{ lookup('password', '/dev/null length=128') }}" run_once: true no_log: true + when: broadcast_websocket_secret is not defined - import_tasks: upgrade_postgres.yml when: diff --git a/requirements/requirements.in b/requirements/requirements.in index cc194454a7..f8126fb081 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -36,6 +36,7 @@ pygerduty pyparsing python-radius python3-saml +python-ldap>=3.3.1 # https://github.com/python-ldap/python-ldap/issues/270 pyyaml>=5.3.1 # minimum version to pull in new pyyaml for CVE-2017-18342 schedule==0.6.0 social-auth-core==3.3.1 # see UPGRADE BLOCKERs diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8820e46b29..8408960f28 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -93,7 +93,7 @@ pyrad==2.3 # via django-radius pyrsistent==0.15.7 # via jsonschema python-daemon==2.2.4 # via ansible-runner python-dateutil==2.8.1 # via adal, kubernetes -python-ldap==3.2.0 # via django-auth-ldap +python-ldap==3.3.1 # via -r /awx_devel/requirements/requirements.in, django-auth-ldap python-radius==1.0 # via -r /awx_devel/requirements/requirements.in python-string-utils==1.0.0 # via openshift python3-openid==3.1.0 # via social-auth-core diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index 55db209e4e..5bdbaadd67 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -92,7 +92,7 @@ pycurl==7.43.0.1 # via -r /awx_devel/requirements/requirements_ansible. pygments==2.5.2 # via azure-cli-core, knack pyjwt==1.7.1 # via adal, azure-cli-core pykerberos==1.2.1 # via requests-kerberos -pynacl==1.3.0 # via paramiko +pynacl==1.4.0 # via paramiko pyopenssl==19.1.0 # via azure-cli-core, requests-credssp pyparsing==2.4.5 # via packaging python-dateutil==2.8.1 # via adal, azure-storage, botocore, kubernetes diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 6fd0b2ed2f..3dbcc2f97c 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -12,7 +12,7 @@ pytest-django pytest-pythonpath pytest-mock==1.11.1 pytest-timeout -pytest-xdist +pytest-xdist==1.34.0 # 2.0.0 broke zuul for some reason tox # for awxkit logutils jupyter