diff --git a/.dockerignore b/.dockerignore index f5faf1f0e3..46c83b0467 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1 @@ -.git awx/ui/node_modules diff --git a/.gitignore b/.gitignore index 9e7a41e6cc..b0d0e0bcfa 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ awx/ui_next/src/locales/ awx/ui_next/coverage/ awx/ui_next/build awx/ui_next/.env.local +awx/ui_next/instrumented rsyslog.pid tools/prometheus/data tools/docker-compose/Dockerfile @@ -146,3 +147,8 @@ use_dev_supervisor.txt *.unison.tmp *.# /tools/docker-compose/overrides/ +/awx/ui_next/.ui-built +/Dockerfile +/_build/ +/_build_kube_dev/ +/Dockerfile.kube-dev diff --git a/CHANGELOG.md b/CHANGELOG.md index c50243b1e8..6237a9f54a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,50 @@ 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/`. +# 17.0.1 (January 26, 2021) +- Fixed pgdocker directory permissions issue with Local Docker installer: https://github.com/ansible/awx/pull/9152 +- Fixed a bug in the UI which caused toggle settings to not be changed when clicked: https://github.com/ansible/awx/pull/9093 + +# 17.0.0 (January 22, 2021) +- AWX now requires PostgreSQL 12 by default: https://github.com/ansible/awx/pull/8943 + **Note:** users who encounter permissions errors at upgrade time should `chown -R ~/.awx/pgdocker` to ensure it's owned by the user running the install playbook +- Added support for region name for OpenStack inventory: https://github.com/ansible/awx/issues/5080 +- Added the ability to chain undefined attributes in custom notification templates: https://github.com/ansible/awx/issues/8677 +- Dramatically simplified the `image_build` role: https://github.com/ansible/awx/pull/8980 +- Fixed a bug which can cause schema migrations to fail at install time: https://github.com/ansible/awx/issues/9077 +- Fixed a bug which caused the `is_superuser` user property to be out of date in certain circumstances: https://github.com/ansible/awx/pull/8833 +- Fixed a bug which sometimes results in race conditions on setting access: https://github.com/ansible/awx/pull/8580 +- Fixed a bug which sometimes causes an unexpected delay in stdout for some playbooks: https://github.com/ansible/awx/issues/9085 +- (UI) Added support for credential password prompting on job launch: https://github.com/ansible/awx/pull/9028 +- (UI) Added the ability to configure LDAP settings in the UI: https://github.com/ansible/awx/issues/8291 +- (UI) Added a sync button to the Project detail view: https://github.com/ansible/awx/issues/8847 +- (UI) Added a form for configuring Google Outh 2.0 settings: https://github.com/ansible/awx/pull/8762 +- (UI) Added searchable keys and related keys to the Credentials list: https://github.com/ansible/awx/issues/8603 +- (UI) Added support for advanced search and copying to Notification Templates: https://github.com/ansible/awx/issues/7879 +- (UI) Added support for prompting on workflow nodes: https://github.com/ansible/awx/issues/5913 +- (UI) Added support for session timeouts: https://github.com/ansible/awx/pull/8250 +- (UI) Fixed a bug that broke websocket streaming for the insecure ws:// protocol: https://github.com/ansible/awx/pull/8877 +- (UI) Fixed a bug in the user interface when a translation for the browser's preferred locale isn't available: https://github.com/ansible/awx/issues/8884 +- (UI) Fixed bug where navigating from one survey question form directly to another wasn't reloading the form: https://github.com/ansible/awx/issues/7522 +- (UI) Fixed a bug which can cause an uncaught error while launching a Job Template: https://github.com/ansible/awx/issues/8936 +- Updated autobahn to address CVE-2020-35678 + +## 16.0.0 (December 10, 2020) +- AWX now ships with a reimagined user interface. **Please read this before upgrading:** https://groups.google.com/g/awx-project/c/KuT5Ao92HWo +- Removed support for syncing inventory from Red Hat CloudForms - https://github.com/ansible/awx/commit/0b701b3b2 +- Removed support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 +- Upgraded NodeJS to actively maintained LTS 14.15.1 - https://github.com/ansible/awx/pull/8766 +- Added Git-LFS to the default image build - https://github.com/ansible/awx/pull/8700 +- Added the ability to specify `metadata.labels` in the podspec for container groups - https://github.com/ansible/awx/issues/8486 +- Added support for Kubernetes pod annotations - https://github.com/ansible/awx/pull/8434 +- Added the ability to label the web container in local Docker installs - https://github.com/ansible/awx/pull/8449 +- Added additional metadata (as an extra var) to playbook runs to report the SCM branch name - https://github.com/ansible/awx/pull/8433 +- Fixed a bug that caused k8s installations to fail due to an incorrect Helm repo - https://github.com/ansible/awx/issues/8715 +- Fixed a bug that prevented certain Workflow Approval resources from being deleted - https://github.com/ansible/awx/pull/8612 +- Fixed a bug that prevented the deletion of inventories stuck in "pending deletion" state - https://github.com/ansible/awx/issues/8525 +- Fixed a display bug in webhook notifications with certain unicode characters - https://github.com/ansible/awx/issues/7400 +- Improved support for exporting dependent objects (Inventory Hosts and Groups) in the `awx export` CLI tool - https://github.com/ansible/awx/commit/607bc0788 + ## 15.0.1 (October 20, 2020) - Added several optimizations to improve performance for a variety of high-load simultaneous job launch use cases https://github.com/ansible/awx/pull/8403 - Added the ability to source roles and collections from requirements.yaml files (not just requirements.yml) - https://github.com/ansible/awx/issues/4540 @@ -88,7 +132,7 @@ This is a list of high-level changes for each release of AWX. A full list of com - Fixed a bug that caused rsyslogd's configuration file to have world-readable file permissions, potentially leaking secrets (CVE-2020-10782) ## 12.0.0 (Jun 9, 2020) -- Removed memcached as a dependency of AWX (https://github.com/ansible/awx/pull/7240) +- Removed memcached as a dependency of AWX (https://github.com/ansible/awx/pull/7240) - Moved to a single container image build instead of separate awx_web and awx_task images. The container image is just `awx` (https://github.com/ansible/awx/pull/7228) - Official AWX container image builds now use a two-stage container build process that notably reduces the size of our published images (https://github.com/ansible/awx/pull/7017) - Removed support for HipChat notifications ([EoL announcement](https://www.atlassian.com/partnerships/slack/faq#faq-98b17ca3-247f-423b-9a78-70a91681eff0)); all previously-created HipChat notification templates will be deleted due to this removal. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd3da38b51..e311ecfa1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ If you're not using Docker for Mac, or Docker for Windows, you may need, or choo #### Frontend Development -See [the ui development documentation](awx/ui/README.md). +See [the ui development documentation](awx/ui_next/CONTRIBUTING.md). ### Build the environment @@ -158,7 +158,7 @@ $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 44251b476f98 gcr.io/ansible-tower-engineering/awx_devel:devel "/entrypoint.sh /bin…" 27 seconds ago Up 23 seconds 0.0.0.0:6899->6899/tcp, 0.0.0.0:7899-7999->7899-7999/tcp, 0.0.0.0:8013->8013/tcp, 0.0.0.0:8043->8043/tcp, 0.0.0.0:8080->8080/tcp, 22/tcp, 0.0.0.0:8888->8888/tcp tools_awx_run_9e820694d57e 40de380e3c2e redis:latest "docker-entrypoint.s…" 28 seconds ago Up 26 seconds -b66a506d3007 postgres:10 "docker-entrypoint.s…" 28 seconds ago Up 26 seconds 0.0.0.0:5432->5432/tcp tools_postgres_1 +b66a506d3007 postgres:12 "docker-entrypoint.s…" 28 seconds ago Up 26 seconds 0.0.0.0:5432->5432/tcp tools_postgres_1 ``` **NOTE** diff --git a/INSTALL.md b/INSTALL.md index dfbd0cbe7e..f7ab93a6e3 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -60,7 +60,7 @@ Please note that deploying from `HEAD` (or the latest commit) is **not** stable, For more on how to clone the repo, view [git clone help](https://git-scm.com/docs/git-clone). -Once you have a local copy, run commands within the root of the project tree. +Once you have a local copy, run the commands in the following sections from the root of the project tree. ### AWX branding @@ -83,7 +83,7 @@ Before you can run a deployment, you'll need the following installed in your loc - [GNU Make](https://www.gnu.org/software/make/) - [Git](https://git-scm.com/) Requires Version 1.8.4+ - Python 3.6+ -- [Node 10.x LTS version](https://nodejs.org/en/download/) +- [Node 14.x LTS version](https://nodejs.org/en/download/) + This is only required if you're [building your own container images](#official-vs-building-images) with `use_container_for_build=false` - [NPM 6.x LTS](https://docs.npmjs.com/) + This is only required if you're [building your own container images](#official-vs-building-images) with `use_container_for_build=false` @@ -497,7 +497,7 @@ Before starting the install process, review the [inventory](./installer/inventor *docker_compose_dir* -> When using docker-compose, the `docker-compose.yml` file will be created there (default `/tmp/awxcompose`). +> When using docker-compose, the `docker-compose.yml` file will be created there (default `~/.awx/awxcompose`). *custom_venv_dir* diff --git a/Makefile b/Makefile index 6a7d4af5a5..9cf77e597e 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,8 @@ PYCURL_SSL_LIBRARY ?= openssl COMPOSE_TAG ?= $(GIT_BRANCH) COMPOSE_HOST ?= $(shell hostname) -VENV_BASE ?= /venv +VENV_BASE ?= /var/lib/awx/venv/ +COLLECTION_BASE ?= /var/lib/awx/vendor/awx_ansible_collections SCL_PREFIX ?= CELERY_SCHEDULE_FILE ?= /var/lib/awx/beat.db @@ -266,11 +267,27 @@ collectstatic: fi; \ mkdir -p awx/public/static && $(PYTHON) manage.py collectstatic --clear --noinput > /dev/null 2>&1 +UWSGI_DEV_RELOAD_COMMAND ?= supervisorctl restart tower-processes:awx-dispatcher tower-processes:awx-receiver + uwsgi: collectstatic @if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/awx/bin/activate; \ fi; \ - uwsgi -b 32768 --socket 127.0.0.1:8050 --module=awx.wsgi:application --home=/venv/awx --chdir=/awx_devel/ --vacuum --processes=5 --harakiri=120 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --lazy-apps --logformat "%(addr) %(method) %(uri) - %(proto) %(status)" --hook-accepting1="exec:supervisorctl restart tower-processes:awx-dispatcher tower-processes:awx-receiver" + uwsgi -b 32768 \ + --socket 127.0.0.1:8050 \ + --module=awx.wsgi:application \ + --home=/var/lib/awx/venv/awx \ + --chdir=/awx_devel/ \ + --vacuum \ + --processes=5 \ + --harakiri=120 --master \ + --no-orphans \ + --py-autoreload 1 \ + --max-requests=1000 \ + --stats /tmp/stats.socket \ + --lazy-apps \ + --logformat "%(addr) %(method) %(uri) - %(proto) %(status)" \ + --hook-accepting1="exec: $(UWSGI_DEV_RELOAD_COMMAND)" daphne: @if [ "$(VENV_BASE)" ]; then \ @@ -340,7 +357,7 @@ check: flake8 pep8 # pyflakes pylint awx-link: [ -d "/awx_devel/awx.egg-info" ] || python3 /awx_devel/setup.py egg_info_dev - cp -f /tmp/awx.egg-link /venv/awx/lib/python$(PYTHON_VERSION)/site-packages/awx.egg-link + cp -f /tmp/awx.egg-link /var/lib/awx/venv/awx/lib/python$(PYTHON_VERSION)/site-packages/awx.egg-link TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests @@ -462,19 +479,24 @@ endif # UI TASKS # -------------------------------------- -awx/ui_next/node_modules: - $(NPM_BIN) --prefix awx/ui_next install + +UI_BUILD_FLAG_FILE = awx/ui_next/.ui-built clean-ui: rm -rf node_modules rm -rf awx/ui_next/node_modules rm -rf awx/ui_next/build + rm -rf awx/ui_next/src/locales/_build + rm -rf $(UI_BUILD_FLAG_FILE) + git checkout awx/ui_next/src/locales -ui-release: ui-devel -ui-devel: awx/ui_next/node_modules - $(NPM_BIN) --prefix awx/ui_next run extract-strings - $(NPM_BIN) --prefix awx/ui_next run compile-strings - $(NPM_BIN) --prefix awx/ui_next run build +awx/ui_next/node_modules: + $(NPM_BIN) --prefix awx/ui_next --loglevel warn --ignore-scripts install + +$(UI_BUILD_FLAG_FILE): + $(NPM_BIN) --prefix awx/ui_next --loglevel warn run extract-strings + $(NPM_BIN) --prefix awx/ui_next --loglevel warn run compile-strings + $(NPM_BIN) --prefix awx/ui_next --loglevel warn run build git checkout awx/ui_next/src/locales mkdir -p awx/public/static/css mkdir -p awx/public/static/js @@ -482,6 +504,12 @@ ui-devel: awx/ui_next/node_modules cp -r awx/ui_next/build/static/css/* awx/public/static/css cp -r awx/ui_next/build/static/js/* awx/public/static/js cp -r awx/ui_next/build/static/media/* awx/public/static/media + touch $@ + +ui-release: awx/ui_next/node_modules $(UI_BUILD_FLAG_FILE) + +ui-devel: awx/ui_next/node_modules + @$(MAKE) -B $(UI_BUILD_FLAG_FILE) ui-zuul-lint-and-test: $(NPM_BIN) --prefix awx/ui_next install @@ -567,15 +595,18 @@ docker-compose-clean: awx/projects # Base development image build docker-compose-build: - ansible localhost -m template -a "src=installer/roles/image_build/templates/Dockerfile.j2 dest=tools/docker-compose/Dockerfile" -e build_dev=True - docker build -t ansible/awx_devel -f tools/docker-compose/Dockerfile \ - --cache-from=$(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) . + ansible-playbook installer/dockerfile.yml -e build_dev=True + docker build -t ansible/awx_devel \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + --cache-from=$(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) . docker tag ansible/awx_devel $(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) #docker push $(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) # For use when developing on "isolated" AWX deployments docker-compose-isolated-build: docker-compose-build - docker build -t ansible/awx_isolated -f tools/docker-isolated/Dockerfile . + docker build -t ansible/awx_isolated \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + -f tools/docker-isolated/Dockerfile . docker tag ansible/awx_isolated $(DEV_DOCKER_TAG_BASE)/awx_isolated:$(COMPOSE_TAG) #docker push $(DEV_DOCKER_TAG_BASE)/awx_isolated:$(COMPOSE_TAG) @@ -607,7 +638,21 @@ clean-elk: docker rm tools_kibana_1 psql-container: - docker run -it --net tools_default --rm postgres:10 sh -c 'exec psql -h "postgres" -p "5432" -U postgres' + docker run -it --net tools_default --rm postgres:12 sh -c 'exec psql -h "postgres" -p "5432" -U postgres' VERSION: @echo "awx: $(VERSION)" + +Dockerfile: installer/roles/dockerfile/templates/Dockerfile.j2 + ansible-playbook installer/dockerfile.yml + +Dockerfile.kube-dev: installer/roles/dockerfile/templates/Dockerfile.j2 + ansible-playbook installer/dockerfile.yml \ + -e dockerfile_name=Dockerfile.kube-dev \ + -e kube_dev=True \ + -e template_dest=_build_kube_dev + +awx-kube-dev-build: Dockerfile.kube-dev + docker build -f Dockerfile.kube-dev \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + -t $(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG) . diff --git a/README.md b/README.md index 23249c35e6..45bec0d81c 100644 --- a/README.md +++ b/README.md @@ -16,20 +16,20 @@ Contributing ------------ - Refer to the [Contributing guide](./CONTRIBUTING.md) to get started developing, testing, and building AWX. -- All code submissions are done through pull requests against the `devel` branch. -- All contributors must use git commit --signoff for any commit to be merged, and agree that usage of --signoff constitutes agreement with the terms of [DCO 1.1](./DCO_1_1.md) -- Take care to make sure no merge commits are in the submission, and use `git rebase` vs `git merge` for this reason. -- If submitting a large code change, it's a good idea to join the `#ansible-awx` channel on irc.freenode.net, and talk about what you would like to do or add first. This not only helps everyone know what's going on, it also helps save time and effort, if the community decides some changes are needed. +- All code submissions are made through pull requests against the `devel` branch. +- All contributors must use git commit --signoff for any commit to be merged and agree that usage of --signoff constitutes agreement with the terms of [DCO 1.1](./DCO_1_1.md) +- Take care to make sure no merge commits are in the submission, and use `git rebase` vs. `git merge` for this reason. +- If submitting a large code change, it's a good idea to join the `#ansible-awx` channel on irc.freenode.net and talk about what you would like to do or add first. This not only helps everyone know what's going on, but it also helps save time and effort if the community decides some changes are needed. Reporting Issues ---------------- -If you're experiencing a problem that you feel is a bug in AWX, or have ideas for how to improve AWX, we encourage you to open an issue, and share your feedback. But before opening a new issue, we ask that you please take a look at our [Issues guide](./ISSUES.md). +If you're experiencing a problem that you feel is a bug in AWX or have ideas for improving AWX, we encourage you to open an issue and share your feedback. But before opening a new issue, we ask that you please take a look at our [Issues guide](./ISSUES.md). Code of Conduct --------------- -We ask all of our community members and contributors to adhere to the [Ansible code of conduct](http://docs.ansible.com/ansible/latest/community/code_of_conduct.html). If you have questions, or need assistance, please reach out to our community team at [codeofconduct@ansible.com](mailto:codeofconduct@ansible.com) +We ask all of our community members and contributors to adhere to the [Ansible code of conduct](http://docs.ansible.com/ansible/latest/community/code_of_conduct.html). If you have questions or need assistance, please reach out to our community team at [codeofconduct@ansible.com](mailto:codeofconduct@ansible.com) Get Involved ------------ @@ -43,4 +43,3 @@ License ------- [Apache v2](./LICENSE.md) - diff --git a/VERSION b/VERSION index 2bbd2b4b42..3e17df0287 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -15.0.1 +17.0.1 diff --git a/awx/api/serializers.py b/awx/api/serializers.py index abd30a7fab..ecce831a19 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -640,7 +640,7 @@ class EmptySerializer(serializers.Serializer): class UnifiedJobTemplateSerializer(BaseSerializer): - # As a base serializer, the capabilities prefetch is not used directly, + # As a base serializer, the capabilities prefetch is not used directly, # instead they are derived from the Workflow Job Template Serializer and the Job Template Serializer, respectively. capabilities_prefetch = [] @@ -1748,7 +1748,7 @@ class HostSerializer(BaseSerializerWithVariables): attrs['variables'] = json.dumps(vars_dict) if Group.objects.filter(name=name, inventory=inventory).exists(): raise serializers.ValidationError(_('A Group with that name already exists.')) - + return super(HostSerializer, self).validate(attrs) def to_representation(self, obj): @@ -3945,12 +3945,12 @@ class ProjectUpdateEventSerializer(JobEventSerializer): return UriCleaner.remove_sensitive(obj.stdout) def get_event_data(self, obj): - # the project update playbook uses the git, hg, or svn modules + # the project update playbook uses the git or svn modules # to clone repositories, and those modules are prone to printing # raw SCM URLs in their stdout (which *could* contain passwords) # attempt to detect and filter HTTP basic auth passwords in the stdout # of these types of events - if obj.event_data.get('task_action') in ('git', 'hg', 'svn'): + if obj.event_data.get('task_action') in ('git', 'svn'): try: return json.loads( UriCleaner.remove_sensitive( diff --git a/awx/api/templates/api/_schedule_detail.md b/awx/api/templates/api/_schedule_detail.md index 04eecb4f23..d42b2ac352 100644 --- a/awx/api/templates/api/_schedule_detail.md +++ b/awx/api/templates/api/_schedule_detail.md @@ -4,7 +4,6 @@ The following lists the expected format and details of our rrules: * DTSTART is expected to be in UTC * INTERVAL is required * SECONDLY is not supported -* TZID is not supported * RRULE must precede the rule statements * BYDAY is supported but not BYDAY with a numerical prefix * BYYEARDAY and BYWEEKNO are not supported diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 87a12a7d51..43e845af0c 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -242,8 +242,6 @@ class DashboardView(APIView): git_failed_projects = git_projects.filter(last_job_failed=True) svn_projects = user_projects.filter(scm_type='svn') 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'] = {} @@ -257,11 +255,6 @@ class DashboardView(APIView): 'failures_url': reverse('api:project_list', request=request) + "?scm_type=svn&last_job_failed=True", 'total': svn_projects.count(), 'failed': svn_failed_projects.count()} - data['scm_types']['hg'] = {'url': reverse('api:project_list', request=request) + "?scm_type=hg", - 'label': 'Mercurial', - '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", diff --git a/awx/api/views/organization.py b/awx/api/views/organization.py index 06172af79f..d03dfcc86f 100644 --- a/awx/api/views/organization.py +++ b/awx/api/views/organization.py @@ -13,6 +13,7 @@ from django.utils.translation import ugettext_lazy as _ from awx.main.models import ( ActivityStream, Inventory, + Host, Project, JobTemplate, WorkflowJobTemplate, @@ -98,6 +99,7 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI organization__id=org_id).count() org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter( organization__id=org_id).count() + org_counts['hosts'] = Host.objects.org_active_count(org_id) full_context['related_field_counts'] = {} full_context['related_field_counts'][org_id] = org_counts diff --git a/awx/conf/settings.py b/awx/conf/settings.py index d2733ce879..4b18e3d9f6 100644 --- a/awx/conf/settings.py +++ b/awx/conf/settings.py @@ -4,6 +4,7 @@ import logging import sys import threading import time +import os # Django from django.conf import LazySettings @@ -247,6 +248,7 @@ class SettingsWrapper(UserSettingsHolder): # These values have to be stored via self.__dict__ in this way to get # around the magic __setattr__ method on this class (which is used to # store API-assigned settings in the database). + self.__dict__['__forks__'] = {} self.__dict__['default_settings'] = default_settings self.__dict__['_awx_conf_settings'] = self self.__dict__['_awx_conf_preload_expires'] = None @@ -255,6 +257,26 @@ class SettingsWrapper(UserSettingsHolder): self.__dict__['cache'] = EncryptedCacheProxy(cache, registry) self.__dict__['registry'] = registry + # record the current pid so we compare it post-fork for + # processes like the dispatcher and callback receiver + self.__dict__['pid'] = os.getpid() + + def __clean_on_fork__(self): + pid = os.getpid() + # if the current pid does *not* match the value on self, it means + # that value was copied on fork, and we're now in a *forked* process; + # the *first* time we enter this code path (on setting access), + # forcibly close DB/cache sockets and set a marker so we don't run + # this code again _in this process_ + # + if pid != self.__dict__['pid'] and pid not in self.__dict__['__forks__']: + self.__dict__['__forks__'][pid] = True + # It's important to close these post-fork, because we + # don't want the forked processes to inherit the open sockets + # for the DB and cache connections (that way lies race conditions) + connection.close() + django_cache.close() + @cached_property def all_supported_settings(self): return self.registry.get_registered_settings() @@ -330,6 +352,7 @@ class SettingsWrapper(UserSettingsHolder): self.cache.set_many(settings_to_cache, timeout=SETTING_CACHE_TIMEOUT) def _get_local(self, name, validate=True): + self.__clean_on_fork__() self._preload_cache() cache_key = Setting.get_cache_key(name) try: diff --git a/awx/locale/django.pot b/awx/locale/django.pot index 3d2cf41999..e5fbe05390 100644 --- a/awx/locale/django.pot +++ b/awx/locale/django.pot @@ -3354,6 +3354,15 @@ msgid "" "common scenarios." msgstr "" +#: awx/main/models/credential/__init__.py:824 +msgid "Region Name" +msgstr "" + +#: awx/main/models/credential/__init__.py:826 +msgid "" +"For some cloud providers, like OVH, region must be specified." +msgstr "" + #: awx/main/models/credential/__init__.py:824 #: awx/main/models/credential/__init__.py:1131 #: awx/main/models/credential/__init__.py:1166 diff --git a/awx/locale/en-us/LC_MESSAGES/django.po b/awx/locale/en-us/LC_MESSAGES/django.po index 3d2cf41999..e5fbe05390 100644 --- a/awx/locale/en-us/LC_MESSAGES/django.po +++ b/awx/locale/en-us/LC_MESSAGES/django.po @@ -3354,6 +3354,15 @@ msgid "" "common scenarios." msgstr "" +#: awx/main/models/credential/__init__.py:824 +msgid "Region Name" +msgstr "" + +#: awx/main/models/credential/__init__.py:826 +msgid "" +"For some cloud providers, like OVH, region must be specified." +msgstr "" + #: awx/main/models/credential/__init__.py:824 #: awx/main/models/credential/__init__.py:1131 #: awx/main/models/credential/__init__.py:1166 diff --git a/awx/locale/fr/LC_MESSAGES/django.po b/awx/locale/fr/LC_MESSAGES/django.po index 62c2ba7292..bcb54c548b 100644 --- a/awx/locale/fr/LC_MESSAGES/django.po +++ b/awx/locale/fr/LC_MESSAGES/django.po @@ -3294,6 +3294,16 @@ msgid "" "common scenarios." msgstr "Les domaines OpenStack définissent les limites administratives. Ils sont nécessaires uniquement pour les URL d’authentification Keystone v3. Voir la documentation Ansible Tower pour les scénarios courants." +#: awx/main/models/credential/__init__.py:824 +msgid "Region Name" +msgstr "Nom de la region" + +#: awx/main/models/credential/__init__.py:826 +msgid "" +"For some cloud providers, like OVH, region must be specified." +msgstr "" +"Chez certains fournisseurs, comme OVH, vous devez spécifier le nom de la région" + #: awx/main/models/credential/__init__.py:812 #: awx/main/models/credential/__init__.py:1110 #: awx/main/models/credential/__init__.py:1144 diff --git a/awx/main/access.py b/awx/main/access.py index 0ecb025d92..89a6c0607d 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -333,14 +333,14 @@ class BaseAccess(object): report_violation(_("License has expired.")) free_instances = validation_info.get('free_instances', 0) - available_instances = validation_info.get('available_instances', 0) + instance_count = validation_info.get('instance_count', 0) if add_host_name: host_exists = Host.objects.filter(name=add_host_name).exists() if not host_exists and free_instances == 0: - report_violation(_("License count of %s instances has been reached.") % available_instances) + report_violation(_("License count of %s instances has been reached.") % instance_count) elif not host_exists and free_instances < 0: - report_violation(_("License count of %s instances has been exceeded.") % available_instances) + report_violation(_("License count of %s instances has been exceeded.") % instance_count) elif not add_host_name and free_instances < 0: report_violation(_("Host count exceeds available instances.")) diff --git a/awx/main/analytics/collectors.py b/awx/main/analytics/collectors.py index 0f51ad179a..b0ac43cc65 100644 --- a/awx/main/analytics/collectors.py +++ b/awx/main/analytics/collectors.py @@ -280,14 +280,16 @@ def _copy_table(table, query, path): return file.file_list() -@register('events_table', '1.1', format='csv', description=_('Automation task records'), expensive=True) +@register('events_table', '1.2', format='csv', description=_('Automation task records'), expensive=True) def events_table(since, full_path, until, **kwargs): events_query = '''COPY (SELECT main_jobevent.id, main_jobevent.created, + main_jobevent.modified, main_jobevent.uuid, main_jobevent.parent_uuid, main_jobevent.event, main_jobevent.event_data::json->'task_action' AS task_action, + (CASE WHEN event = 'playbook_on_stats' THEN event_data END) as playbook_on_stats, main_jobevent.failed, main_jobevent.changed, main_jobevent.playbook, diff --git a/awx/main/analytics/core.py b/awx/main/analytics/core.py index 2c77444929..e9f7f99bc0 100644 --- a/awx/main/analytics/core.py +++ b/awx/main/analytics/core.py @@ -68,7 +68,7 @@ def register(key, version, description=None, format='json', expensive=False): @register('projects_by_scm_type', 1) def projects_by_scm_type(): - return {'git': 5, 'svn': 1, 'hg': 0} + return {'git': 5, 'svn': 1} """ def decorate(f): @@ -102,7 +102,7 @@ def gather(dest=None, module=None, subset = None, since = None, until = now(), c last_run = since or settings.AUTOMATION_ANALYTICS_LAST_GATHER or (now() - timedelta(weeks=4)) logger.debug("Last analytics run was: {}".format(settings.AUTOMATION_ANALYTICS_LAST_GATHER)) - + if _valid_license() is False: logger.exception("Invalid License provided, or No License Provided") return None diff --git a/awx/main/analytics/metrics.py b/awx/main/analytics/metrics.py index 9346de0a80..20bf8ae830 100644 --- a/awx/main/analytics/metrics.py +++ b/awx/main/analytics/metrics.py @@ -68,7 +68,7 @@ def metrics(): 'external_logger_type': getattr(settings, 'LOG_AGGREGATOR_TYPE', 'None') }) - LICENSE_INSTANCE_TOTAL.set(str(license_info.get('available_instances', 0))) + LICENSE_INSTANCE_TOTAL.set(str(license_info.get('instance_count', 0))) LICENSE_INSTANCE_FREE.set(str(license_info.get('free_instances', 0))) current_counts = counts(None) diff --git a/awx/main/consumers.py b/awx/main/consumers.py index b6d8872ebd..4fc1196dbe 100644 --- a/awx/main/consumers.py +++ b/awx/main/consumers.py @@ -75,7 +75,7 @@ class WebsocketSecretAuthHelper: nonce_diff = now - nonce_parsed if abs(nonce_diff) > nonce_tolerance: logger.warn(f"Potential replay attack or machine(s) time out of sync by {nonce_diff} seconds.") - raise ValueError("Potential replay attack or machine(s) time out of sync by {nonce_diff} seconds.") + raise ValueError(f"Potential replay attack or machine(s) time out of sync by {nonce_diff} seconds.") return True diff --git a/awx/main/dispatch/worker/callback.py b/awx/main/dispatch/worker/callback.py index fd96a4f04e..e61a516094 100644 --- a/awx/main/dispatch/worker/callback.py +++ b/awx/main/dispatch/worker/callback.py @@ -38,6 +38,7 @@ class CallbackBrokerWorker(BaseWorker): MAX_RETRIES = 2 last_stats = time.time() + last_flush = time.time() total = 0 last_event = '' prof = None @@ -52,7 +53,7 @@ class CallbackBrokerWorker(BaseWorker): def read(self, queue): try: - res = self.redis.blpop(settings.CALLBACK_QUEUE, timeout=settings.JOB_EVENT_BUFFER_SECONDS) + res = self.redis.blpop(settings.CALLBACK_QUEUE, timeout=1) if res is None: return {'event': 'FLUSH'} self.total += 1 @@ -102,6 +103,7 @@ class CallbackBrokerWorker(BaseWorker): now = tz_now() if ( force or + (time.time() - self.last_flush) > settings.JOB_EVENT_BUFFER_SECONDS or any([len(events) >= 1000 for events in self.buff.values()]) ): for cls, events in self.buff.items(): @@ -124,6 +126,7 @@ class CallbackBrokerWorker(BaseWorker): for e in events: emit_event_detail(e) self.buff = {} + self.last_flush = time.time() def perform_work(self, body): try: diff --git a/awx/main/exceptions.py b/awx/main/exceptions.py index 8aadfd80b0..64cbc94783 100644 --- a/awx/main/exceptions.py +++ b/awx/main/exceptions.py @@ -30,3 +30,10 @@ class _AwxTaskError(): AwxTaskError = _AwxTaskError() + + +class PostRunError(Exception): + def __init__(self, msg, status='failed', tb=''): + self.status = status + self.tb = tb + super(PostRunError, self).__init__(msg) diff --git a/awx/main/isolated/manager.py b/awx/main/isolated/manager.py index 623bb5700e..de4783e277 100644 --- a/awx/main/isolated/manager.py +++ b/awx/main/isolated/manager.py @@ -7,6 +7,7 @@ import tempfile import time import logging import yaml +import datetime from django.conf import settings import ansible_runner @@ -123,6 +124,7 @@ class IsolatedManager(object): dir=private_data_dir ) params = self.runner_params.copy() + params.get('envvars', dict())['ANSIBLE_CALLBACK_WHITELIST'] = 'profile_tasks' params['playbook'] = playbook params['private_data_dir'] = iso_dir if idle_timeout: @@ -149,7 +151,6 @@ class IsolatedManager(object): # don't rsync source control metadata (it can be huge!) '- /project/.git', '- /project/.svn', - '- /project/.hg', # don't rsync job events that are in the process of being written '- /artifacts/job_events/*-partial.json.tmp', # don't rsync the ssh_key FIFO @@ -169,7 +170,8 @@ class IsolatedManager(object): extravars = { 'src': self.private_data_dir, 'dest': settings.AWX_PROOT_BASE_PATH, - 'ident': self.ident + 'ident': self.ident, + 'job_id': self.instance.id, } if playbook: extravars['playbook'] = playbook @@ -205,7 +207,10 @@ class IsolatedManager(object): :param interval: an interval (in seconds) to wait between status polls """ interval = interval if interval is not None else settings.AWX_ISOLATED_CHECK_INTERVAL - extravars = {'src': self.private_data_dir} + extravars = { + 'src': self.private_data_dir, + 'job_id': self.instance.id + } status = 'failed' rc = None last_check = time.time() @@ -221,9 +226,13 @@ class IsolatedManager(object): logger.warning('Isolated job {} was manually canceled.'.format(self.instance.id)) logger.debug('Checking on isolated job {} with `check_isolated.yml`.'.format(self.instance.id)) + time_start = datetime.datetime.now() runner_obj = self.run_management_playbook('check_isolated.yml', self.private_data_dir, extravars=extravars) + time_end = datetime.datetime.now() + time_diff = time_end - time_start + logger.debug('Finished checking on isolated job {} with `check_isolated.yml` took {} seconds.'.format(self.instance.id, time_diff.total_seconds())) status, rc = runner_obj.status, runner_obj.rc if self.check_callback is not None and not self.captured_command_artifact: diff --git a/awx/main/management/commands/cleanup_jobs.py b/awx/main/management/commands/cleanup_jobs.py index dd6304e242..66953acde9 100644 --- a/awx/main/management/commands/cleanup_jobs.py +++ b/awx/main/management/commands/cleanup_jobs.py @@ -21,7 +21,7 @@ from awx.main.signals import ( disable_computed_fields ) -from awx.main.management.commands.deletion import AWXCollector, pre_delete +from awx.main.utils.deletion import AWXCollector, pre_delete class Command(BaseCommand): diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index c92215560e..a86cc3db48 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -19,6 +19,9 @@ from django.core.management.base import BaseCommand, CommandError from django.db import connection, transaction from django.utils.encoding import smart_text +# DRF error class to distinguish license exceptions +from rest_framework.exceptions import PermissionDenied + # AWX inventory imports from awx.main.models.inventory import ( Inventory, @@ -31,11 +34,12 @@ from awx.main.utils.safe_yaml import sanitize_jinja # other AWX imports from awx.main.models.rbac import batch_role_ancestor_rebuilding +# TODO: remove proot utils once we move to running inv. updates in containers from awx.main.utils import ( - ignore_inventory_computed_fields, check_proot_installed, wrap_args_with_proot, build_proot_temp_dir, + ignore_inventory_computed_fields, get_licenser ) from awx.main.signals import disable_activity_stream @@ -53,11 +57,11 @@ No license. See http://www.ansible.com/renew for license information.''' LICENSE_MESSAGE = '''\ -Number of licensed instances exceeded, would bring available instances to %(new_count)d, system is licensed for %(available_instances)d. +Number of licensed instances exceeded, would bring available instances to %(new_count)d, system is licensed for %(instance_count)d. See http://www.ansible.com/renew for license extension information.''' DEMO_LICENSE_MESSAGE = '''\ -Demo mode free license count exceeded, would bring available instances to %(new_count)d, demo mode allows %(available_instances)d. +Demo mode free license count exceeded, would bring available instances to %(new_count)d, demo mode allows %(instance_count)d. See http://www.ansible.com/renew for licensing information.''' @@ -75,13 +79,11 @@ class AnsibleInventoryLoader(object): /usr/bin/ansible/ansible-inventory -i hosts --list ''' - def __init__(self, source, is_custom=False, venv_path=None, verbosity=0): + def __init__(self, source, venv_path=None, verbosity=0): self.source = source - self.source_dir = functioning_dir(self.source) - self.is_custom = is_custom - self.tmp_private_dir = None - self.method = 'ansible-inventory' self.verbosity = verbosity + # TODO: remove once proot has been removed + self.tmp_private_dir = None if venv_path: self.venv_path = venv_path else: @@ -131,38 +133,34 @@ class AnsibleInventoryLoader(object): # NOTE: why do we add "python" to the start of these args? # the script that runs ansible-inventory specifies a python interpreter # that makes no sense in light of the fact that we put all the dependencies - # inside of /venv/ansible, so we override the specified interpreter + # inside of /var/lib/awx/venv/ansible, so we override the specified interpreter # https://github.com/ansible/ansible/issues/50714 bargs = ['python', ansible_inventory_path, '-i', self.source] - bargs.extend(['--playbook-dir', self.source_dir]) + bargs.extend(['--playbook-dir', functioning_dir(self.source)]) if self.verbosity: # INFO: -vvv, DEBUG: -vvvvv, for inventory, any more than 3 makes little difference bargs.append('-{}'.format('v' * min(5, self.verbosity * 2 + 1))) logger.debug('Using base command: {}'.format(' '.join(bargs))) return bargs + # TODO: Remove this once we move to running ansible-inventory in containers + # and don't need proot for process isolation anymore def get_proot_args(self, cmd, env): cwd = os.getcwd() if not check_proot_installed(): raise RuntimeError("proot is not installed but is configured for use") kwargs = {} - if self.is_custom: - # use source's tmp dir for proot, task manager will delete folder - logger.debug("Using provided directory '{}' for isolation.".format(self.source_dir)) - kwargs['proot_temp_dir'] = self.source_dir - cwd = self.source_dir - else: - # we cannot safely store tmp data in source dir or trust script contents - if env['AWX_PRIVATE_DATA_DIR']: - # If this is non-blank, file credentials are being used and we need access - private_data_dir = functioning_dir(env['AWX_PRIVATE_DATA_DIR']) - logger.debug("Using private credential data in '{}'.".format(private_data_dir)) - kwargs['private_data_dir'] = private_data_dir - self.tmp_private_dir = build_proot_temp_dir() - logger.debug("Using fresh temporary directory '{}' for isolation.".format(self.tmp_private_dir)) - kwargs['proot_temp_dir'] = self.tmp_private_dir - kwargs['proot_show_paths'] = [functioning_dir(self.source), settings.AWX_ANSIBLE_COLLECTIONS_PATHS] + # we cannot safely store tmp data in source dir or trust script contents + if env['AWX_PRIVATE_DATA_DIR']: + # If this is non-blank, file credentials are being used and we need access + private_data_dir = functioning_dir(env['AWX_PRIVATE_DATA_DIR']) + logger.debug("Using private credential data in '{}'.".format(private_data_dir)) + kwargs['private_data_dir'] = private_data_dir + self.tmp_private_dir = build_proot_temp_dir() + logger.debug("Using fresh temporary directory '{}' for isolation.".format(self.tmp_private_dir)) + kwargs['proot_temp_dir'] = self.tmp_private_dir + kwargs['proot_show_paths'] = [functioning_dir(self.source), settings.AWX_ANSIBLE_COLLECTIONS_PATHS] logger.debug("Running from `{}` working directory.".format(cwd)) if self.venv_path != settings.ANSIBLE_VENV_PATH: @@ -170,12 +168,14 @@ class AnsibleInventoryLoader(object): return wrap_args_with_proot(cmd, cwd, **kwargs) + def command_to_json(self, cmd): data = {} stdout, stderr = '', '' env = self.build_env() - if ((self.is_custom or 'AWX_PRIVATE_DATA_DIR' in env) and + # TODO: remove proot args once inv. updates run in containers + if (('AWX_PRIVATE_DATA_DIR' in env) and getattr(settings, 'AWX_PROOT_ENABLED', False)): cmd = self.get_proot_args(cmd, env) @@ -184,11 +184,13 @@ class AnsibleInventoryLoader(object): stdout = smart_text(stdout) stderr = smart_text(stderr) + # TODO: can be removed when proot is removed if self.tmp_private_dir: shutil.rmtree(self.tmp_private_dir, True) + if proc.returncode != 0: raise RuntimeError('%s failed (rc=%d) with stdout:\n%s\nstderr:\n%s' % ( - self.method, proc.returncode, stdout, stderr)) + 'ansible-inventory', proc.returncode, stdout, stderr)) for line in stderr.splitlines(): logger.error(line) @@ -231,9 +233,9 @@ class Command(BaseCommand): action='store_true', default=False, help='overwrite (rather than merge) variables') parser.add_argument('--keep-vars', dest='keep_vars', action='store_true', default=False, - help='use database variables if set') + help='DEPRECATED legacy option, has no effect') parser.add_argument('--custom', dest='custom', action='store_true', default=False, - help='this is a custom inventory script') + help='DEPRECATED indicates a custom inventory script, no longer used') parser.add_argument('--source', dest='source', type=str, default=None, metavar='s', help='inventory directory, file, or script to load') parser.add_argument('--enabled-var', dest='enabled_var', type=str, @@ -259,10 +261,10 @@ class Command(BaseCommand): 'specifies the unique, immutable instance ID, may be ' 'specified as "foo.bar" to traverse nested dicts.') - def set_logging_level(self): + def set_logging_level(self, verbosity): log_levels = dict(enumerate([logging.WARNING, logging.INFO, logging.DEBUG, 0])) - logger.setLevel(log_levels.get(self.verbosity, 0)) + logger.setLevel(log_levels.get(verbosity, 0)) def _get_instance_id(self, variables, default=''): ''' @@ -322,7 +324,8 @@ class Command(BaseCommand): else: raise NotImplementedError('Value of enabled {} not understood.'.format(enabled)) - def get_source_absolute_path(self, source): + @staticmethod + def get_source_absolute_path(source): if not os.path.exists(source): raise IOError('Source does not exist: %s' % source) source = os.path.join(os.getcwd(), os.path.dirname(source), @@ -330,61 +333,6 @@ class Command(BaseCommand): source = os.path.normpath(os.path.abspath(source)) return source - def load_inventory_from_database(self): - ''' - Load inventory and related objects from the database. - ''' - # Load inventory object based on name or ID. - if self.inventory_id: - q = dict(id=self.inventory_id) - else: - q = dict(name=self.inventory_name) - try: - self.inventory = Inventory.objects.get(**q) - except Inventory.DoesNotExist: - raise CommandError('Inventory with %s = %s cannot be found' % list(q.items())[0]) - except Inventory.MultipleObjectsReturned: - raise CommandError('Inventory with %s = %s returned multiple results' % list(q.items())[0]) - logger.info('Updating inventory %d: %s' % (self.inventory.pk, - self.inventory.name)) - - # Load inventory source if specified via environment variable (when - # inventory_import is called from an InventoryUpdate task). - inventory_source_id = os.getenv('INVENTORY_SOURCE_ID', None) - inventory_update_id = os.getenv('INVENTORY_UPDATE_ID', None) - if inventory_source_id: - try: - self.inventory_source = InventorySource.objects.get(pk=inventory_source_id, - inventory=self.inventory) - except InventorySource.DoesNotExist: - raise CommandError('Inventory source with id=%s not found' % - inventory_source_id) - try: - self.inventory_update = InventoryUpdate.objects.get(pk=inventory_update_id) - except InventoryUpdate.DoesNotExist: - raise CommandError('Inventory update with id=%s not found' % - inventory_update_id) - # Otherwise, create a new inventory source to capture this invocation - # via command line. - else: - with ignore_inventory_computed_fields(): - self.inventory_source, created = InventorySource.objects.get_or_create( - inventory=self.inventory, - source='file', - source_path=os.path.abspath(self.source), - overwrite=self.overwrite, - overwrite_vars=self.overwrite_vars, - ) - self.inventory_update = self.inventory_source.create_inventory_update( - _eager_fields=dict( - job_args=json.dumps(sys.argv), - job_env=dict(os.environ.items()), - job_cwd=os.getcwd()) - ) - - # FIXME: Wait or raise error if inventory is being updated by another - # source. - def _batch_add_m2m(self, related_manager, *objs, **kwargs): key = (related_manager.instance.pk, related_manager.through._meta.db_table) flush = bool(kwargs.get('flush', False)) @@ -894,9 +842,9 @@ class Command(BaseCommand): source_vars = self.all_group.variables remote_license_type = source_vars.get('tower_metadata', {}).get('license_type', None) if remote_license_type is None: - raise CommandError('Unexpected Error: Tower inventory plugin missing needed metadata!') + raise PermissionDenied('Unexpected Error: Tower inventory plugin missing needed metadata!') if local_license_type != remote_license_type: - raise CommandError('Tower server licenses must match: source: {} local: {}'.format( + raise PermissionDenied('Tower server licenses must match: source: {} local: {}'.format( remote_license_type, local_license_type )) @@ -905,10 +853,10 @@ class Command(BaseCommand): local_license_type = license_info.get('license_type', 'UNLICENSED') if local_license_type == 'UNLICENSED': logger.error(LICENSE_NON_EXISTANT_MESSAGE) - raise CommandError('No license found!') + raise PermissionDenied('No license found!') elif local_license_type == 'open': return - available_instances = license_info.get('available_instances', 0) + instance_count = license_info.get('instance_count', 0) free_instances = license_info.get('free_instances', 0) time_remaining = license_info.get('time_remaining', 0) hard_error = license_info.get('trial', False) is True or license_info['instance_count'] == 10 @@ -916,24 +864,24 @@ class Command(BaseCommand): if time_remaining <= 0: if hard_error: logger.error(LICENSE_EXPIRED_MESSAGE) - raise CommandError("License has expired!") + raise PermissionDenied("License has expired!") else: logger.warning(LICENSE_EXPIRED_MESSAGE) # special check for tower-type inventory sources # but only if running the plugin TOWER_SOURCE_FILES = ['tower.yml', 'tower.yaml'] - if self.inventory_source.source == 'tower' and any(f in self.source for f in TOWER_SOURCE_FILES): + if self.inventory_source.source == 'tower' and any(f in self.inventory_source.source_path for f in TOWER_SOURCE_FILES): # only if this is the 2nd call to license check, we cannot compare before running plugin if hasattr(self, 'all_group'): self.remote_tower_license_compare(local_license_type) if free_instances < 0: d = { 'new_count': new_count, - 'available_instances': available_instances, + 'instance_count': instance_count, } if hard_error: logger.error(LICENSE_MESSAGE % d) - raise CommandError('License count exceeded!') + raise PermissionDenied('License count exceeded!') else: logger.warning(LICENSE_MESSAGE % d) @@ -948,7 +896,7 @@ class Command(BaseCommand): active_count = Host.objects.org_active_count(org.id) if active_count > org.max_hosts: - raise CommandError('Host limit for organization exceeded!') + raise PermissionDenied('Host limit for organization exceeded!') def mark_license_failure(self, save=True): self.inventory_update.license_error = True @@ -959,16 +907,103 @@ class Command(BaseCommand): self.inventory_update.save(update_fields=['org_host_limit_error']) def handle(self, *args, **options): - self.verbosity = int(options.get('verbosity', 1)) - self.set_logging_level() - self.inventory_name = options.get('inventory_name', None) - self.inventory_id = options.get('inventory_id', None) - venv_path = options.get('venv', None) + # Load inventory and related objects from database. + inventory_name = options.get('inventory_name', None) + inventory_id = options.get('inventory_id', None) + if inventory_name and inventory_id: + raise CommandError('--inventory-name and --inventory-id are mutually exclusive') + elif not inventory_name and not inventory_id: + raise CommandError('--inventory-name or --inventory-id is required') + + with advisory_lock('inventory_{}_import'.format(inventory_id)): + # Obtain rest of the options needed to run update + raw_source = options.get('source', None) + if not raw_source: + raise CommandError('--source is required') + verbosity = int(options.get('verbosity', 1)) + self.set_logging_level(verbosity) + venv_path = options.get('venv', None) + + # Load inventory object based on name or ID. + if inventory_id: + q = dict(id=inventory_id) + else: + q = dict(name=inventory_name) + try: + inventory = Inventory.objects.get(**q) + except Inventory.DoesNotExist: + raise CommandError('Inventory with %s = %s cannot be found' % list(q.items())[0]) + except Inventory.MultipleObjectsReturned: + raise CommandError('Inventory with %s = %s returned multiple results' % list(q.items())[0]) + logger.info('Updating inventory %d: %s' % (inventory.pk, inventory.name)) + + + # Create ad-hoc inventory source and inventory update objects + with ignore_inventory_computed_fields(): + source = Command.get_source_absolute_path(raw_source) + + inventory_source, created = InventorySource.objects.get_or_create( + inventory=inventory, + source='file', + source_path=os.path.abspath(source), + overwrite=bool(options.get('overwrite', False)), + overwrite_vars=bool(options.get('overwrite_vars', False)), + ) + inventory_update = inventory_source.create_inventory_update( + _eager_fields=dict( + job_args=json.dumps(sys.argv), + job_env=dict(os.environ.items()), + job_cwd=os.getcwd()) + ) + + data = AnsibleInventoryLoader( + source=source, venv_path=venv_path, verbosity=verbosity + ).load() + + logger.debug('Finished loading from source: %s', source) + + status, tb, exc = 'error', '', None + try: + self.perform_update(options, data, inventory_update) + status = 'successful' + except Exception as e: + exc = e + if isinstance(e, KeyboardInterrupt): + status = 'canceled' + else: + tb = traceback.format_exc() + + with ignore_inventory_computed_fields(): + inventory_update = InventoryUpdate.objects.get(pk=inventory_update.pk) + inventory_update.result_traceback = tb + inventory_update.status = status + inventory_update.save(update_fields=['status', 'result_traceback']) + inventory_source.status = status + inventory_source.save(update_fields=['status']) + + if exc: + logger.error(str(exc)) + + if exc: + if isinstance(exc, CommandError): + sys.exit(1) + raise exc + + def perform_update(self, options, data, inventory_update): + """Shared method for both awx-manage CLI updates and inventory updates + from the tasks system. + + This saves the inventory data to the database, calling load_into_database + but also wraps that method in a host of options processing + """ + # outside of normal options, these are needed as part of programatic interface + self.inventory = inventory_update.inventory + self.inventory_source = inventory_update.inventory_source + self.inventory_update = inventory_update + + # the update options, could be parser object or dict self.overwrite = bool(options.get('overwrite', False)) self.overwrite_vars = bool(options.get('overwrite_vars', False)) - self.keep_vars = bool(options.get('keep_vars', False)) - self.is_custom = bool(options.get('custom', False)) - self.source = options.get('source', None) self.enabled_var = options.get('enabled_var', None) self.enabled_value = options.get('enabled_value', None) self.group_filter = options.get('group_filter', None) or r'^.+$' @@ -976,17 +1011,6 @@ class Command(BaseCommand): self.exclude_empty_groups = bool(options.get('exclude_empty_groups', False)) self.instance_id_var = options.get('instance_id_var', None) - self.invoked_from_dispatcher = False if os.getenv('INVENTORY_SOURCE_ID', None) is None else True - - # Load inventory and related objects from database. - if self.inventory_name and self.inventory_id: - raise CommandError('--inventory-name and --inventory-id are mutually exclusive') - elif not self.inventory_name and not self.inventory_id: - raise CommandError('--inventory-name or --inventory-id is required') - if (self.overwrite or self.overwrite_vars) and self.keep_vars: - raise CommandError('--overwrite/--overwrite-vars and --keep-vars are mutually exclusive') - if not self.source: - raise CommandError('--source is required') try: self.group_filter_re = re.compile(self.group_filter) except re.error: @@ -997,146 +1021,115 @@ class Command(BaseCommand): raise CommandError('invalid regular expression for --host-filter') begin = time.time() - with advisory_lock('inventory_{}_update'.format(self.inventory_id)): - self.load_inventory_from_database() + + # Since perform_update can be invoked either through the awx-manage CLI + # or from the task system, we need to create a new lock at this level + # (even though inventory_import.Command.handle -- which calls + # perform_update -- has its own lock, inventory_ID_import) + with advisory_lock('inventory_{}_perform_update'.format(self.inventory.id)): try: self.check_license() - except CommandError as e: + except PermissionDenied as e: self.mark_license_failure(save=True) raise e try: # Check the per-org host limits self.check_org_host_limit() - except CommandError as e: + except PermissionDenied as e: self.mark_org_limits_failure(save=True) raise e - status, tb, exc = 'error', '', None - try: - if settings.SQL_DEBUG: - queries_before = len(connection.queries) + if settings.SQL_DEBUG: + queries_before = len(connection.queries) - # Update inventory update for this command line invocation. - with ignore_inventory_computed_fields(): - iu = self.inventory_update - if iu.status != 'running': - with transaction.atomic(): - self.inventory_update.status = 'running' - self.inventory_update.save() + # Update inventory update for this command line invocation. + with ignore_inventory_computed_fields(): + # TODO: move this to before perform_update + iu = self.inventory_update + if iu.status != 'running': + with transaction.atomic(): + self.inventory_update.status = 'running' + self.inventory_update.save() - source = self.get_source_absolute_path(self.source) + logger.info('Processing JSON output...') + inventory = MemInventory( + group_filter_re=self.group_filter_re, host_filter_re=self.host_filter_re) + inventory = dict_to_mem_data(data, inventory=inventory) - data = AnsibleInventoryLoader(source=source, is_custom=self.is_custom, - venv_path=venv_path, verbosity=self.verbosity).load() + logger.info('Loaded %d groups, %d hosts', len(inventory.all_group.all_groups), + len(inventory.all_group.all_hosts)) - logger.debug('Finished loading from source: %s', source) - logger.info('Processing JSON output...') - inventory = MemInventory( - group_filter_re=self.group_filter_re, host_filter_re=self.host_filter_re) - inventory = dict_to_mem_data(data, inventory=inventory) + if self.exclude_empty_groups: + inventory.delete_empty_groups() - del data # forget dict from import, could be large + self.all_group = inventory.all_group - logger.info('Loaded %d groups, %d hosts', len(inventory.all_group.all_groups), - len(inventory.all_group.all_hosts)) + if settings.DEBUG: + # depending on inventory source, this output can be + # *exceedingly* verbose - crawling a deeply nested + # inventory/group data structure and printing metadata about + # each host and its memberships + # + # it's easy for this scale of data to overwhelm pexpect, + # (and it's likely only useful for purposes of debugging the + # actual inventory import code), so only print it if we have to: + # https://github.com/ansible/ansible-tower/issues/7414#issuecomment-321615104 + self.all_group.debug_tree() - if self.exclude_empty_groups: - inventory.delete_empty_groups() - - self.all_group = inventory.all_group - - if settings.DEBUG: - # depending on inventory source, this output can be - # *exceedingly* verbose - crawling a deeply nested - # inventory/group data structure and printing metadata about - # each host and its memberships - # - # it's easy for this scale of data to overwhelm pexpect, - # (and it's likely only useful for purposes of debugging the - # actual inventory import code), so only print it if we have to: - # https://github.com/ansible/ansible-tower/issues/7414#issuecomment-321615104 - self.all_group.debug_tree() - - with batch_role_ancestor_rebuilding(): - # If using with transaction.atomic() with try ... catch, - # with transaction.atomic() must be inside the try section of the code as per Django docs - try: - # Ensure that this is managed as an atomic SQL transaction, - # and thus properly rolled back if there is an issue. - with transaction.atomic(): - # Merge/overwrite inventory into database. - if settings.SQL_DEBUG: - logger.warning('loading into database...') - with ignore_inventory_computed_fields(): - if getattr(settings, 'ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC', True): + with batch_role_ancestor_rebuilding(): + # If using with transaction.atomic() with try ... catch, + # with transaction.atomic() must be inside the try section of the code as per Django docs + try: + # Ensure that this is managed as an atomic SQL transaction, + # and thus properly rolled back if there is an issue. + with transaction.atomic(): + # Merge/overwrite inventory into database. + if settings.SQL_DEBUG: + logger.warning('loading into database...') + with ignore_inventory_computed_fields(): + if getattr(settings, 'ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC', True): + self.load_into_database() + else: + with disable_activity_stream(): self.load_into_database() - else: - with disable_activity_stream(): - self.load_into_database() - if settings.SQL_DEBUG: - queries_before2 = len(connection.queries) - self.inventory.update_computed_fields() - if settings.SQL_DEBUG: - logger.warning('update computed fields took %d queries', - len(connection.queries) - queries_before2) - # Check if the license is valid. - # If the license is not valid, a CommandError will be thrown, - # and inventory update will be marked as invalid. - # with transaction.atomic() will roll back the changes. - license_fail = True - self.check_license() + if settings.SQL_DEBUG: + queries_before2 = len(connection.queries) + self.inventory.update_computed_fields() + if settings.SQL_DEBUG: + logger.warning('update computed fields took %d queries', + len(connection.queries) - queries_before2) - # Check the per-org host limits - license_fail = False - self.check_org_host_limit() - except CommandError as e: - if license_fail: - self.mark_license_failure() - else: - self.mark_org_limits_failure() - raise e + # Check if the license is valid. + # If the license is not valid, a CommandError will be thrown, + # and inventory update will be marked as invalid. + # with transaction.atomic() will roll back the changes. + license_fail = True + self.check_license() - if settings.SQL_DEBUG: - logger.warning('Inventory import completed for %s in %0.1fs', - self.inventory_source.name, time.time() - begin) + # Check the per-org host limits + license_fail = False + self.check_org_host_limit() + except PermissionDenied as e: + if license_fail: + self.mark_license_failure(save=True) else: - logger.info('Inventory import completed for %s in %0.1fs', - self.inventory_source.name, time.time() - begin) - status = 'successful' + self.mark_org_limits_failure(save=True) + raise e - # If we're in debug mode, then log the queries and time - # used to do the operation. if settings.SQL_DEBUG: - queries_this_import = connection.queries[queries_before:] - sqltime = sum(float(x['time']) for x in queries_this_import) - logger.warning('Inventory import required %d queries ' - 'taking %0.3fs', len(queries_this_import), - sqltime) - except Exception as e: - if isinstance(e, KeyboardInterrupt): - status = 'canceled' - exc = e - elif isinstance(e, CommandError): - exc = e + logger.warning('Inventory import completed for %s in %0.1fs', + self.inventory_source.name, time.time() - begin) else: - tb = traceback.format_exc() - exc = e + logger.info('Inventory import completed for %s in %0.1fs', + self.inventory_source.name, time.time() - begin) - if not self.invoked_from_dispatcher: - with ignore_inventory_computed_fields(): - self.inventory_update = InventoryUpdate.objects.get(pk=self.inventory_update.pk) - self.inventory_update.result_traceback = tb - self.inventory_update.status = status - self.inventory_update.save(update_fields=['status', 'result_traceback']) - self.inventory_source.status = status - self.inventory_source.save(update_fields=['status']) - - if exc: - logger.error(str(exc)) - - if exc: - if isinstance(exc, CommandError): - sys.exit(1) - raise exc + # If we're in debug mode, then log the queries and time + # used to do the operation. + if settings.SQL_DEBUG: + queries_this_import = connection.queries[queries_before:] + sqltime = sum(float(x['time']) for x in queries_this_import) + logger.warning('Inventory import required %d queries ' + 'taking %0.3fs', len(queries_this_import), + sqltime) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 759c2daa98..8bfd273811 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -181,4 +181,4 @@ class MigrationRanCheckMiddleware(MiddlewareMixin): plan = executor.migration_plan(executor.loader.graph.leaf_nodes()) if bool(plan) and \ getattr(resolve(request.path), 'url_name', '') != 'migrations_notran': - return redirect(reverse("ui:migrations_notran")) + return redirect(reverse("ui_next:migrations_notran")) diff --git a/awx/main/migrations/0123_drop_hg_support.py b/awx/main/migrations/0123_drop_hg_support.py new file mode 100644 index 0000000000..089c6bba6f --- /dev/null +++ b/awx/main/migrations/0123_drop_hg_support.py @@ -0,0 +1,23 @@ +from django.db import migrations, models +from awx.main.migrations._hg_removal import delete_hg_scm + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0122_really_remove_cloudforms_inventory'), + ] + + operations = [ + migrations.RunPython(delete_hg_scm), + migrations.AlterField( + model_name='project', + name='scm_type', + field=models.CharField(blank=True, choices=[('', 'Manual'), ('git', 'Git'), ('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'), ('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/migrations/_hg_removal.py b/awx/main/migrations/_hg_removal.py new file mode 100644 index 0000000000..70ca0b5a29 --- /dev/null +++ b/awx/main/migrations/_hg_removal.py @@ -0,0 +1,19 @@ +import logging + +from awx.main.utils.common import set_current_apps + +logger = logging.getLogger('awx.main.migrations') + + +def delete_hg_scm(apps, schema_editor): + set_current_apps(apps) + Project = apps.get_model('main', 'Project') + ProjectUpdate = apps.get_model('main', 'ProjectUpdate') + + ProjectUpdate.objects.filter(project__scm_type='hg').update(scm_type='') + update_ct = Project.objects.filter(scm_type='hg').update(scm_type='') + + if update_ct: + logger.warn('Changed {} mercurial projects to manual, deprecation period ended'.format( + update_ct + )) diff --git a/awx/main/migrations/_inventory_source_vars.py b/awx/main/migrations/_inventory_source_vars.py index edd2e6827a..263b5666a2 100644 --- a/awx/main/migrations/_inventory_source_vars.py +++ b/awx/main/migrations/_inventory_source_vars.py @@ -1,9 +1,13 @@ import json +import re +import logging from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import iri_to_uri FrozenInjectors = dict() +logger = logging.getLogger('awx.main.migrations') class PluginFileInjector(object): @@ -129,6 +133,7 @@ class azure_rm(PluginFileInjector): ret['exclude_host_filters'].append("location not in {}".format(repr(python_regions))) return ret + class ec2(PluginFileInjector): plugin_name = 'aws_ec2' namespace = 'amazon' @@ -586,6 +591,7 @@ class openstack(PluginFileInjector): ret['inventory_hostname'] = use_host_name_for_name(source_vars['use_hostnames']) return ret + class rhv(PluginFileInjector): """ovirt uses the custom credential templating, and that is all """ diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 509378fcf5..87fa5d791f 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -81,10 +81,17 @@ User.add_to_class('accessible_objects', user_accessible_objects) def enforce_bigint_pk_migration(): + # + # NOTE: this function is not actually in use anymore, + # but has been intentionally kept for historical purposes, + # and to serve as an illustration if we ever need to perform + # bulk modification/migration of event data in the future. + # # see: https://github.com/ansible/awx/issues/6010 # look at all the event tables and verify that they have been fully migrated # from the *old* int primary key table to the replacement bigint table # if not, attempt to migrate them in the background + # for tblname in ( 'main_jobevent', 'main_inventoryupdateevent', 'main_projectupdateevent', 'main_adhoccommandevent', diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 66db962430..e8a2884083 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -819,6 +819,11 @@ ManagedCredentialType( 'It is only needed for Keystone v3 authentication ' 'URLs. Refer to Ansible Tower documentation for ' 'common scenarios.') + }, { + 'id': 'region', + 'label': ugettext_noop('Region Name'), + 'type': 'string', + 'help_text': ugettext_noop('For some cloud providers, like OVH, region must be specified'), }, { 'id': 'verify_ssl', 'label': ugettext_noop('Verify SSL'), diff --git a/awx/main/models/credential/injectors.py b/awx/main/models/credential/injectors.py index 75d1f17bfe..ef30b91945 100644 --- a/awx/main/models/credential/injectors.py +++ b/awx/main/models/credential/injectors.py @@ -82,6 +82,7 @@ def _openstack_data(cred): if cred.has_input('domain'): openstack_auth['domain_name'] = cred.get_input('domain', default='') verify_state = cred.get_input('verify_ssl', default=True) + openstack_data = { 'clouds': { 'devstack': { @@ -90,6 +91,10 @@ def _openstack_data(cred): }, }, } + + if cred.has_input('project_region_name'): + openstack_data['clouds']['devstack']['region_name'] = cred.get_input('project_region_name', default='') + return openstack_data diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 11d97c7690..74a9896385 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -12,7 +12,7 @@ from django.core.mail.message import EmailMessage from django.db import connection from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_str, force_text -from jinja2 import sandbox +from jinja2 import sandbox, ChainableUndefined from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError # AWX @@ -357,7 +357,7 @@ class JobNotificationMixin(object): 'url': 'https://towerhost/#/jobs/playbook/1010', 'approval_status': 'approved', 'approval_node_name': 'Approve Me', - 'workflow_url': 'https://towerhost/#/workflows/1010', + 'workflow_url': 'https://towerhost/#/jobs/workflow/1010', 'job_metadata': """{'url': 'https://towerhost/$/jobs/playbook/13', 'traceback': '', 'status': 'running', @@ -429,7 +429,7 @@ class JobNotificationMixin(object): raise RuntimeError("Define me") def build_notification_message(self, nt, status): - env = sandbox.ImmutableSandboxedEnvironment() + env = sandbox.ImmutableSandboxedEnvironment(undefined=ChainableUndefined) from awx.api.serializers import UnifiedJobSerializer job_serialization = UnifiedJobSerializer(self).to_representation(self) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index bc52d4269c..65fb8304ce 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -52,7 +52,6 @@ class ProjectOptions(models.Model): SCM_TYPE_CHOICES = [ ('', _('Manual')), ('git', _('Git')), - ('hg', _('Mercurial')), ('svn', _('Subversion')), ('insights', _('Red Hat Insights')), ('archive', _('Remote Archive')), diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index dd8bc3e894..d9ac8afcf9 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -620,7 +620,7 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio return reverse('api:workflow_job_detail', kwargs={'pk': self.pk}, request=request) def get_ui_url(self): - return urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.pk)) + return urljoin(settings.TOWER_URL_BASE, '/#/jobs/workflow/{}'.format(self.pk)) def notification_data(self): result = super(WorkflowJob, self).notification_data() @@ -752,7 +752,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): return None def get_ui_url(self): - return urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id)) + return urljoin(settings.TOWER_URL_BASE, '/#/jobs/workflow/{}'.format(self.workflow_job.id)) def _get_parent_field_name(self): return 'workflow_approval_template' @@ -840,7 +840,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin): return (msg, body) def context(self, approval_status): - workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id)) + workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/jobs/workflow/{}'.format(self.workflow_job.id)) return {'approval_status': approval_status, 'approval_node_name': self.workflow_approval_template.name, 'workflow_url': workflow_url, diff --git a/awx/main/signals.py b/awx/main/signals.py index adfbc65d01..0a29fa9d6c 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -121,6 +121,27 @@ def sync_superuser_status_to_rbac(instance, **kwargs): Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).members.remove(instance) +def sync_rbac_to_superuser_status(instance, sender, **kwargs): + 'When the is_superuser flag is false but a user has the System Admin role, update the database to reflect that' + if kwargs['action'] in ['post_add', 'post_remove', 'post_clear']: + new_status_value = bool(kwargs['action'] == 'post_add') + if hasattr(instance, 'singleton_name'): # duck typing, role.members.add() vs user.roles.add() + role = instance + if role.singleton_name == ROLE_SINGLETON_SYSTEM_ADMINISTRATOR: + if kwargs['pk_set']: + kwargs['model'].objects.filter(pk__in=kwargs['pk_set']).update(is_superuser=new_status_value) + elif kwargs['action'] == 'post_clear': + kwargs['model'].objects.all().update(is_superuser=False) + else: + user = instance + if kwargs['action'] == 'post_clear': + user.is_superuser = False + user.save(update_fields=['is_superuser']) + elif kwargs['model'].objects.filter(pk__in=kwargs['pk_set'], singleton_name=ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).exists(): + user.is_superuser = new_status_value + user.save(update_fields=['is_superuser']) + + def rbac_activity_stream(instance, sender, **kwargs): # Only if we are associating/disassociating if kwargs['action'] in ['pre_add', 'pre_remove']: @@ -197,6 +218,7 @@ m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through) m2m_changed.connect(rbac_activity_stream, Role.members.through) m2m_changed.connect(rbac_activity_stream, Role.parents.through) post_save.connect(sync_superuser_status_to_rbac, sender=User) +m2m_changed.connect(sync_rbac_to_superuser_status, Role.members.through) pre_delete.connect(cleanup_detached_labels_on_deleted_parent, sender=UnifiedJob) pre_delete.connect(cleanup_detached_labels_on_deleted_parent, sender=UnifiedJobTemplate) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 2a1c85e23d..e6cc61b7dd 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -23,7 +23,6 @@ import fcntl from pathlib import Path from uuid import uuid4 import urllib.parse as urlparse -import shlex # Django from django.conf import settings @@ -61,10 +60,10 @@ from awx.main.models import ( Inventory, InventorySource, SmartInventoryMembership, Job, AdHocCommand, ProjectUpdate, InventoryUpdate, SystemJob, JobEvent, ProjectUpdateEvent, InventoryUpdateEvent, AdHocCommandEvent, SystemJobEvent, - build_safe_env, enforce_bigint_pk_migration + build_safe_env ) from awx.main.constants import ACTIVE_STATES -from awx.main.exceptions import AwxTaskError +from awx.main.exceptions import AwxTaskError, PostRunError from awx.main.queue import CallbackQueueDispatcher from awx.main.isolated import manager as isolated_manager from awx.main.dispatch.publish import task @@ -79,6 +78,7 @@ from awx.main.utils.external_logging import reconfigure_rsyslog from awx.main.utils.safe_yaml import safe_dump, sanitize_jinja from awx.main.utils.reload import stop_local_services from awx.main.utils.pglock import advisory_lock +from awx.main.utils.handlers import SpecialInventoryHandler from awx.main.consumers import emit_channel_notification from awx.main import analytics from awx.conf import settings_registry @@ -138,12 +138,6 @@ def dispatch_startup(): if Instance.objects.me().is_controller(): awx_isolated_heartbeat() - # at process startup, detect the need to migrate old event records from int - # to bigint; at *some point* in the future, once certain versions of AWX - # and Tower fall out of use/support, we can probably just _assume_ that - # everybody has moved to bigint, and remove this code entirely - enforce_bigint_pk_migration() - # Update Tower's rsyslog.conf file based on loggins settings in the db reconfigure_rsyslog() @@ -378,6 +372,7 @@ def gather_analytics(): from awx.conf.models import Setting from rest_framework.fields import DateTimeField + from awx.main.signals import disable_activity_stream if not settings.INSIGHTS_TRACKING_STATE: return if not (settings.AUTOMATION_ANALYTICS_URL and settings.REDHAT_USERNAME and settings.REDHAT_PASSWORD): @@ -414,7 +409,8 @@ def gather_analytics(): if not _gather_and_ship(incremental_collectors, since=start, until=until): break start = until - settings.AUTOMATION_ANALYTICS_LAST_GATHER = until + with disable_activity_stream(): + settings.AUTOMATION_ANALYTICS_LAST_GATHER = until if subset: _gather_and_ship(subset, since=since, until=gather_time) @@ -736,6 +732,12 @@ def update_host_smart_inventory_memberships(): @task(queue=get_local_queuename) def migrate_legacy_event_data(tblname): + # + # NOTE: this function is not actually in use anymore, + # but has been intentionally kept for historical purposes, + # and to serve as an illustration if we ever need to perform + # bulk modification/migration of event data in the future. + # if 'event' not in tblname: return with advisory_lock(f'bigint_migration_{tblname}', wait=False) as acquired: @@ -1225,6 +1227,13 @@ class BaseTask(object): Ansible runner puts a parent_uuid on each event, no matter what the type. AWX only saves the parent_uuid if the event is for a Job. ''' + # cache end_line locally for RunInventoryUpdate tasks + # which generate job events from two 'streams': + # ansible-inventory and the awx.main.commands.inventory_import + # logger + if isinstance(self, RunInventoryUpdate): + self.end_line = event_data['end_line'] + if event_data.get(self.event_data_key, None): if self.event_data_key != 'job_id': event_data.pop('parent_uuid', None) @@ -1253,7 +1262,7 @@ class BaseTask(object): # so it *should* have a negligible performance impact task = event_data.get('event_data', {}).get('task_action') try: - if task in ('git', 'hg', 'svn'): + if task in ('git', 'svn'): event_data_json = json.dumps(event_data) event_data_json = UriCleaner.remove_sensitive(event_data_json) event_data = json.loads(event_data_json) @@ -1521,6 +1530,12 @@ class BaseTask(object): try: self.post_run_hook(self.instance, status) + except PostRunError as exc: + if status == 'successful': + status = exc.status + extra_update_fields['job_explanation'] = exc.args[0] + if exc.tb: + extra_update_fields['result_traceback'] = exc.tb except Exception: logger.exception('{} Post run hook errored.'.format(self.instance.log_format)) @@ -2141,7 +2156,7 @@ class RunProjectUpdate(BaseTask): elif not scm_branch: raise RuntimeError('Could not determine a revision to run from project.') elif not scm_branch: - scm_branch = {'hg': 'tip'}.get(project_update.scm_type, 'HEAD') + scm_branch = 'HEAD' galaxy_creds_are_defined = ( project_update.project.organization and @@ -2150,7 +2165,7 @@ class RunProjectUpdate(BaseTask): if not galaxy_creds_are_defined and ( settings.AWX_ROLES_ENABLED or settings.AWX_COLLECTIONS_ENABLED ): - logger.debug( + logger.warning( 'Galaxy role/collection syncing is enabled, but no ' f'credentials are configured for {project_update.project.organization}.' ) @@ -2417,9 +2432,10 @@ class RunProjectUpdate(BaseTask): shutil.rmtree(stage_path) # cannot trust content update produced if self.job_private_data_dir: - # copy project folder before resetting to default branch - # because some git-tree-specific resources (like submodules) might matter - self.make_local_copy(instance, self.job_private_data_dir) + if status == 'successful': + # copy project folder before resetting to default branch + # because some git-tree-specific resources (like submodules) might matter + self.make_local_copy(instance, self.job_private_data_dir) if self.original_branch: # for git project syncs, non-default branches can be problems # restore to branch the repo was on before this run @@ -2461,6 +2477,14 @@ class RunInventoryUpdate(BaseTask): event_model = InventoryUpdateEvent event_data_key = 'inventory_update_id' + # TODO: remove once inv updates run in containers + def should_use_proot(self, inventory_update): + ''' + Return whether this task should use proot. + ''' + return getattr(settings, 'AWX_PROOT_ENABLED', False) + + # TODO: remove once inv updates run in containers @property def proot_show_paths(self): return [settings.AWX_ANSIBLE_COLLECTIONS_PATHS] @@ -2485,15 +2509,11 @@ class RunInventoryUpdate(BaseTask): return injector.build_private_data(inventory_update, private_data_dir) def build_env(self, inventory_update, private_data_dir, isolated, private_data_files=None): - """Build environment dictionary for inventory import. + """Build environment dictionary for ansible-inventory. - This used to be the mechanism by which any data that needs to be passed - to the inventory update script is set up. In particular, this is how - inventory update is aware of its proper credentials. - - Most environment injection is now accomplished by the credential - injectors. The primary purpose this still serves is to - still point to the inventory update INI or config file. + Most environment variables related to credentials or configuration + are accomplished by the inventory source injectors (in this method) + or custom credential type injectors (in main run method). """ env = super(RunInventoryUpdate, self).build_env(inventory_update, private_data_dir, @@ -2501,8 +2521,11 @@ class RunInventoryUpdate(BaseTask): private_data_files=private_data_files) if private_data_files is None: private_data_files = {} - self.add_awx_venv(env) - # Pass inventory source ID to inventory script. + # TODO: remove once containers replace custom venvs + self.add_ansible_venv(inventory_update.ansible_virtualenv_path, env, isolated=isolated) + + # Legacy environment variables, were used as signal to awx-manage command + # now they are provided in case some scripts may be relying on them env['INVENTORY_SOURCE_ID'] = str(inventory_update.inventory_source_id) env['INVENTORY_UPDATE_ID'] = str(inventory_update.pk) env.update(STANDARD_INVENTORY_UPDATE_ENV) @@ -2565,47 +2588,25 @@ class RunInventoryUpdate(BaseTask): if inventory is None: raise RuntimeError('Inventory Source is not associated with an Inventory.') - # Piece together the initial command to run via. the shell. - args = ['awx-manage', 'inventory_import'] - args.extend(['--inventory-id', str(inventory.pk)]) + args = ['ansible-inventory', '--list', '--export'] - # Add appropriate arguments for overwrite if the inventory_update - # object calls for it. - if inventory_update.overwrite: - args.append('--overwrite') - if inventory_update.overwrite_vars: - args.append('--overwrite-vars') + # Add arguments for the source inventory file/script/thing + source_location = self.pseudo_build_inventory(inventory_update, private_data_dir) + args.append('-i') + args.append(source_location) - # Declare the virtualenv the management command should activate - # as it calls ansible-inventory - args.extend(['--venv', inventory_update.ansible_virtualenv_path]) + args.append('--output') + args.append(os.path.join(private_data_dir, 'artifacts', 'output.json')) - src = inventory_update.source - if inventory_update.enabled_var: - args.extend(['--enabled-var', shlex.quote(inventory_update.enabled_var)]) - args.extend(['--enabled-value', shlex.quote(inventory_update.enabled_value)]) + if os.path.isdir(source_location): + playbook_dir = source_location else: - if getattr(settings, '%s_ENABLED_VAR' % src.upper(), False): - args.extend(['--enabled-var', - getattr(settings, '%s_ENABLED_VAR' % src.upper())]) - if getattr(settings, '%s_ENABLED_VALUE' % src.upper(), False): - args.extend(['--enabled-value', - getattr(settings, '%s_ENABLED_VALUE' % src.upper())]) - if inventory_update.host_filter: - args.extend(['--host-filter', shlex.quote(inventory_update.host_filter)]) - if getattr(settings, '%s_EXCLUDE_EMPTY_GROUPS' % src.upper()): - args.append('--exclude-empty-groups') - if getattr(settings, '%s_INSTANCE_ID_VAR' % src.upper(), False): - args.extend(['--instance-id-var', - "'{}'".format(getattr(settings, '%s_INSTANCE_ID_VAR' % src.upper())),]) - # Add arguments for the source inventory script - args.append('--source') - args.append(self.pseudo_build_inventory(inventory_update, private_data_dir)) - if src == 'custom': - args.append("--custom") - args.append('-v%d' % inventory_update.verbosity) - if settings.DEBUG: - args.append('--traceback') + playbook_dir = os.path.dirname(source_location) + args.extend(['--playbook-dir', playbook_dir]) + + if inventory_update.verbosity: + args.append('-' + 'v' * min(5, inventory_update.verbosity * 2 + 1)) + return args def build_inventory(self, inventory_update, private_data_dir): @@ -2645,11 +2646,9 @@ class RunInventoryUpdate(BaseTask): def build_cwd(self, inventory_update, private_data_dir): ''' - There are two cases where the inventory "source" is in a different + There is one case where the inventory "source" is in a different location from the private data: - - deprecated vendored inventory scripts in awx/plugins/inventory - SCM, where source needs to live in the project folder - in these cases, the inventory does not exist in the standard tempdir ''' src = inventory_update.source if src == 'scm' and inventory_update.source_project_update: @@ -2707,6 +2706,75 @@ class RunInventoryUpdate(BaseTask): # This follows update, not sync, so make copy here RunProjectUpdate.make_local_copy(source_project, private_data_dir) + def post_run_hook(self, inventory_update, status): + if status != 'successful': + return # nothing to save, step out of the way to allow error reporting + + private_data_dir = inventory_update.job_env['AWX_PRIVATE_DATA_DIR'] + expected_output = os.path.join(private_data_dir, 'artifacts', 'output.json') + with open(expected_output) as f: + data = json.load(f) + + # build inventory save options + options = dict( + overwrite=inventory_update.overwrite, + overwrite_vars=inventory_update.overwrite_vars, + ) + src = inventory_update.source + + if inventory_update.enabled_var: + options['enabled_var'] = inventory_update.enabled_var + options['enabled_value'] = inventory_update.enabled_value + else: + if getattr(settings, '%s_ENABLED_VAR' % src.upper(), False): + options['enabled_var'] = getattr(settings, '%s_ENABLED_VAR' % src.upper()) + if getattr(settings, '%s_ENABLED_VALUE' % src.upper(), False): + options['enabled_value'] = getattr(settings, '%s_ENABLED_VALUE' % src.upper()) + + if inventory_update.host_filter: + options['host_filter'] = inventory_update.host_filter + + if getattr(settings, '%s_EXCLUDE_EMPTY_GROUPS' % src.upper()): + options['exclude_empty_groups'] = True + if getattr(settings, '%s_INSTANCE_ID_VAR' % src.upper(), False): + options['instance_id_var'] = getattr(settings, '%s_INSTANCE_ID_VAR' % src.upper()) + + # Verbosity is applied to saving process, as well as ansible-inventory CLI option + if inventory_update.verbosity: + options['verbosity'] = inventory_update.verbosity + + handler = SpecialInventoryHandler( + self.event_handler, self.cancel_callback, + verbosity=inventory_update.verbosity, + job_timeout=self.get_instance_timeout(self.instance), + start_time=inventory_update.started, + counter=self.event_ct, initial_line=self.end_line + ) + inv_logger = logging.getLogger('awx.main.commands.inventory_import') + formatter = inv_logger.handlers[0].formatter + formatter.job_start = inventory_update.started + handler.formatter = formatter + inv_logger.handlers[0] = handler + + from awx.main.management.commands.inventory_import import Command as InventoryImportCommand + cmd = InventoryImportCommand() + try: + # save the inventory data to database. + # canceling exceptions will be handled in the global post_run_hook + cmd.perform_update(options, data, inventory_update) + except PermissionDenied as exc: + logger.exception('License error saving {} content'.format(inventory_update.log_format)) + raise PostRunError(str(exc), status='error') + except PostRunError: + logger.exception('Error saving {} content, rolling back changes'.format(inventory_update.log_format)) + raise + except Exception: + logger.exception('Exception saving {} content, rolling back changes.'.format( + inventory_update.log_format)) + raise PostRunError( + 'Error occured while saving inventory data, see traceback or server logs', + status='error', tb=traceback.format_exc()) + @task(queue=get_local_queuename) class RunAdHocCommand(BaseTask): diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py index f5221ef5f1..d45b1fa083 100644 --- a/awx/main/tests/functional/api/test_organization_counts.py +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -2,7 +2,7 @@ import pytest from awx.api.versioning import reverse -from awx.main.models import Project +from awx.main.models import Project, Host @pytest.fixture @@ -81,6 +81,8 @@ def test_org_counts_detail_admin(resourced_organization, user, get): assert response.status_code == 200 counts = response.data['summary_fields']['related_field_counts'] + assert counts['hosts'] == 0 + counts.pop('hosts') assert counts == COUNTS_PRIMES @@ -93,6 +95,8 @@ def test_org_counts_detail_member(resourced_organization, user, get): assert response.status_code == 200 counts = response.data['summary_fields']['related_field_counts'] + assert counts['hosts'] == 0 + counts.pop('hosts') assert counts == { 'users': COUNTS_PRIMES['users'], # Policy is that members can see other users and admins 'admins': COUNTS_PRIMES['admins'], @@ -111,6 +115,7 @@ def test_org_counts_list_admin(resourced_organization, user, get): assert response.status_code == 200 counts = response.data['results'][0]['summary_fields']['related_field_counts'] + assert 'hosts' not in counts # doesn't show in list view assert counts == COUNTS_PRIMES @@ -123,6 +128,7 @@ def test_org_counts_list_member(resourced_organization, user, get): assert response.status_code == 200 counts = response.data['results'][0]['summary_fields']['related_field_counts'] + assert 'hosts' not in counts # doesn't show in list view assert counts == { 'users': COUNTS_PRIMES['users'], # Policy is that members can see other users and admins @@ -145,6 +151,7 @@ def test_new_org_zero_counts(user, post): new_org_list = post_response.render().data counts_dict = new_org_list['summary_fields']['related_field_counts'] + assert 'hosts' not in counts_dict # doesn't show in list view assert counts_dict == COUNTS_ZEROS @@ -167,6 +174,19 @@ def test_two_organizations(resourced_organization, organizations, user, get): assert counts[org_id_zero] == COUNTS_ZEROS +@pytest.mark.django_db +def test_hosts_counted(resourced_organization, user, get): + admin_user = user('admin', True) + assert Host.objects.org_active_count(resourced_organization.id) == 0 + resourced_organization.inventories.first().hosts.create(name='Some Host') + assert Host.objects.org_active_count(resourced_organization.id) == 1 + response = get(reverse('api:organization_detail', kwargs={'pk': resourced_organization.pk}), admin_user) + assert response.status_code == 200 + + counts = response.data['summary_fields']['related_field_counts'] + assert counts['hosts'] == Host.objects.org_active_count(resourced_organization.id) == 1 + + @pytest.mark.django_db def test_scan_JT_counted(resourced_organization, user, get): admin_user = user('admin', True) @@ -180,7 +200,10 @@ def test_scan_JT_counted(resourced_organization, user, get): # Test detail view detail_response = get(reverse('api:organization_detail', kwargs={'pk': resourced_organization.pk}), admin_user) assert detail_response.status_code == 200 - assert detail_response.data['summary_fields']['related_field_counts'] == counts_dict + counts = detail_response.data['summary_fields']['related_field_counts'] + assert 'hosts' in counts + counts.pop('hosts') + assert counts == counts_dict @pytest.mark.django_db @@ -205,4 +228,7 @@ def test_JT_not_double_counted(resourced_organization, user, get): # Test detail view detail_response = get(reverse('api:organization_detail', kwargs={'pk': resourced_organization.pk}), admin_user) assert detail_response.status_code == 200 - assert detail_response.data['summary_fields']['related_field_counts'] == counts_dict + counts = detail_response.data['summary_fields']['related_field_counts'] + assert 'hosts' in counts + counts.pop('hosts') + assert counts == counts_dict diff --git a/awx/main/tests/functional/api/test_settings.py b/awx/main/tests/functional/api/test_settings.py index 8a1f50035f..c478b70817 100644 --- a/awx/main/tests/functional/api/test_settings.py +++ b/awx/main/tests/functional/api/test_settings.py @@ -393,3 +393,43 @@ def test_saml_x509cert_validation(patch, get, admin, headers): } }) assert resp.status_code == 200 + + +@pytest.mark.django_db +def test_github_settings(get, put, patch, delete, admin): + url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'github'}) + get(url, user=admin, expect=200) + delete(url, user=admin, expect=204) + response = get(url, user=admin, expect=200) + data = dict(response.data.items()) + put(url, user=admin, data=data, expect=200) + patch(url, user=admin, data={'SOCIAL_AUTH_GITHUB_KEY': '???'}, expect=200) + response = get(url, user=admin, expect=200) + assert response.data['SOCIAL_AUTH_GITHUB_KEY'] == '???' + data.pop('SOCIAL_AUTH_GITHUB_KEY') + put(url, user=admin, data=data, expect=200) + response = get(url, user=admin, expect=200) + assert response.data['SOCIAL_AUTH_GITHUB_KEY'] == '' + + +@pytest.mark.django_db +def test_github_enterprise_settings(get, put, patch, delete, admin): + url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'github-enterprise'}) + get(url, user=admin, expect=200) + delete(url, user=admin, expect=204) + response = get(url, user=admin, expect=200) + data = dict(response.data.items()) + put(url, user=admin, data=data, expect=200) + patch(url, user=admin, data={ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL': 'example.com', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL': 'example.com', + }, expect=200) + response = get(url, user=admin, expect=200) + assert response.data['SOCIAL_AUTH_GITHUB_ENTERPRISE_URL'] == 'example.com' + assert response.data['SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL'] == 'example.com' + data.pop('SOCIAL_AUTH_GITHUB_ENTERPRISE_URL') + data.pop('SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL') + put(url, user=admin, data=data, expect=200) + response = get(url, user=admin, expect=200) + assert response.data['SOCIAL_AUTH_GITHUB_ENTERPRISE_URL'] == '' + assert response.data['SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL'] == '' diff --git a/awx/main/tests/functional/commands/test_cleanup_jobs.py b/awx/main/tests/functional/commands/test_cleanup_jobs.py index ed50698338..98be403d1f 100644 --- a/awx/main/tests/functional/commands/test_cleanup_jobs.py +++ b/awx/main/tests/functional/commands/test_cleanup_jobs.py @@ -6,7 +6,7 @@ from collections import OrderedDict from django.db.models.deletion import Collector, SET_NULL, CASCADE from django.core.management import call_command -from awx.main.management.commands.deletion import AWXCollector +from awx.main.utils.deletion import AWXCollector from awx.main.models import ( JobTemplate, User, Job, JobEvent, Notification, WorkflowJobNode, JobHostSummary diff --git a/awx/main/tests/functional/commands/test_inventory_import.py b/awx/main/tests/functional/commands/test_inventory_import.py index a0b1095c98..0500ef197c 100644 --- a/awx/main/tests/functional/commands/test_inventory_import.py +++ b/awx/main/tests/functional/commands/test_inventory_import.py @@ -9,6 +9,9 @@ import os # Django from django.core.management.base import CommandError +# for license errors +from rest_framework.exceptions import PermissionDenied + # AWX from awx.main.management.commands import inventory_import from awx.main.models import Inventory, Host, Group, InventorySource @@ -83,7 +86,7 @@ class MockLoader: return self._data -def mock_logging(self): +def mock_logging(self, level): pass @@ -322,6 +325,6 @@ def test_tower_version_compare(): "version": "2.0.1-1068-g09684e2c41" } } - with pytest.raises(CommandError): + with pytest.raises(PermissionDenied): cmd.remote_tower_license_compare('very_supported') cmd.remote_tower_license_compare('open') diff --git a/awx/main/tests/functional/models/test_job.py b/awx/main/tests/functional/models/test_job.py index ac8912506f..c6c4d2d6e6 100644 --- a/awx/main/tests/functional/models/test_job.py +++ b/awx/main/tests/functional/models/test_job.py @@ -16,7 +16,7 @@ def test_awx_virtualenv_from_settings(inventory, project, machine_credential): ) jt.credentials.add(machine_credential) job = jt.create_unified_job() - assert job.ansible_virtualenv_path == '/venv/ansible' + assert job.ansible_virtualenv_path == '/var/lib/awx/venv/ansible' @pytest.mark.django_db @@ -43,28 +43,28 @@ def test_awx_custom_virtualenv(inventory, project, machine_credential, organizat jt.credentials.add(machine_credential) job = jt.create_unified_job() - job.organization.custom_virtualenv = '/venv/fancy-org' + job.organization.custom_virtualenv = '/var/lib/awx/venv/fancy-org' job.organization.save() - assert job.ansible_virtualenv_path == '/venv/fancy-org' + assert job.ansible_virtualenv_path == '/var/lib/awx/venv/fancy-org' - job.project.custom_virtualenv = '/venv/fancy-proj' + job.project.custom_virtualenv = '/var/lib/awx/venv/fancy-proj' job.project.save() - assert job.ansible_virtualenv_path == '/venv/fancy-proj' + assert job.ansible_virtualenv_path == '/var/lib/awx/venv/fancy-proj' - job.job_template.custom_virtualenv = '/venv/fancy-jt' + job.job_template.custom_virtualenv = '/var/lib/awx/venv/fancy-jt' job.job_template.save() - assert job.ansible_virtualenv_path == '/venv/fancy-jt' + assert job.ansible_virtualenv_path == '/var/lib/awx/venv/fancy-jt' @pytest.mark.django_db def test_awx_custom_virtualenv_without_jt(project): - project.custom_virtualenv = '/venv/fancy-proj' + project.custom_virtualenv = '/var/lib/awx/venv/fancy-proj' project.save() job = Job(project=project) job.save() job = Job.objects.get(pk=job.id) - assert job.ansible_virtualenv_path == '/venv/fancy-proj' + assert job.ansible_virtualenv_path == '/var/lib/awx/venv/fancy-proj' @pytest.mark.django_db diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index 4601668d25..fc28c92294 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -214,6 +214,9 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential f"'{inventory_filename}' file not found in inventory update runtime files {content.keys()}" env.pop('ANSIBLE_COLLECTIONS_PATHS', None) # collection paths not relevant to this test + env.pop('PYTHONPATH') + env.pop('VIRTUAL_ENV') + env.pop('PROOT_TMP_DIR') base_dir = os.path.join(DATA, 'plugins') if not os.path.exists(base_dir): os.mkdir(base_dir) diff --git a/awx/main/tests/functional/test_rbac_user.py b/awx/main/tests/functional/test_rbac_user.py index ca3d268b18..b62a0db25f 100644 --- a/awx/main/tests/functional/test_rbac_user.py +++ b/awx/main/tests/functional/test_rbac_user.py @@ -4,7 +4,7 @@ from unittest import mock from django.test import TransactionTestCase from awx.main.access import UserAccess, RoleAccess, TeamAccess -from awx.main.models import User, Organization, Inventory +from awx.main.models import User, Organization, Inventory, Role class TestSysAuditorTransactional(TransactionTestCase): @@ -170,4 +170,34 @@ def test_org_admin_cannot_delete_member_attached_to_other_group(org_admin, org_m access = UserAccess(org_admin) other_org.member_role.members.add(org_member) assert not access.can_delete(org_member) - \ No newline at end of file + + +@pytest.mark.parametrize('reverse', (True, False)) +@pytest.mark.django_db +def test_consistency_of_is_superuser_flag(reverse): + users = [User.objects.create(username='rando_{}'.format(i)) for i in range(2)] + for u in users: + assert u.is_superuser is False + + system_admin = Role.singleton('system_administrator') + if reverse: + for u in users: + u.roles.add(system_admin) + else: + system_admin.members.add(*[u.id for u in users]) # like .add(42, 54) + + for u in users: + u.refresh_from_db() + assert u.is_superuser is True + + users[0].roles.clear() + for u in users: + u.refresh_from_db() + assert users[0].is_superuser is False + assert users[1].is_superuser is True + + system_admin.members.clear() + + for u in users: + u.refresh_from_db() + assert u.is_superuser is False diff --git a/awx/main/tests/unit/api/test_logger.py b/awx/main/tests/unit/api/test_logger.py index 95ee8f9a98..dc4c32f229 100644 --- a/awx/main/tests/unit/api/test_logger.py +++ b/awx/main/tests/unit/api/test_logger.py @@ -35,16 +35,17 @@ data_loggly = { # Test reconfigure logging settings function # name this whatever you want @pytest.mark.parametrize( - 'enabled, log_type, host, port, protocol, expected_config', [ + 'enabled, log_type, host, port, protocol, errorfile, expected_config', [ ( True, 'loggly', 'http://logs-01.loggly.com/inputs/1fd38090-2af1-4e1e-8d80-492899da0f71/tag/http/', None, 'https', + '/var/log/tower/rsyslog.err', '\n'.join([ 'template(name="awx" type="string" string="%rawmsg-after-pri%")\nmodule(load="omhttp")', - 'action(type="omhttp" server="logs-01.loggly.com" serverport="80" usehttps="off" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" errorfile="/var/log/tower/rsyslog.err" action.resumeInterval="5" restpath="inputs/1fd38090-2af1-4e1e-8d80-492899da0f71/tag/http/")', # noqa + 'action(type="omhttp" server="logs-01.loggly.com" serverport="80" usehttps="off" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" action.resumeInterval="5" errorfile="/var/log/tower/rsyslog.err" restpath="inputs/1fd38090-2af1-4e1e-8d80-492899da0f71/tag/http/")', # noqa ]) ), ( @@ -53,6 +54,7 @@ data_loggly = { 'localhost', 9000, 'udp', + '', # empty errorfile '\n'.join([ 'template(name="awx" type="string" string="%rawmsg-after-pri%")', 'action(type="omfwd" target="localhost" port="9000" protocol="udp" action.resumeRetryCount="-1" action.resumeInterval="5" template="awx")', # noqa @@ -64,6 +66,7 @@ data_loggly = { 'localhost', 9000, 'tcp', + '/var/log/tower/rsyslog.err', '\n'.join([ 'template(name="awx" type="string" string="%rawmsg-after-pri%")', 'action(type="omfwd" target="localhost" port="9000" protocol="tcp" action.resumeRetryCount="-1" action.resumeInterval="5" template="awx")', # noqa @@ -75,9 +78,10 @@ data_loggly = { 'https://yoursplunk/services/collector/event', None, None, + '/var/log/tower/rsyslog.err', '\n'.join([ 'template(name="awx" type="string" string="%rawmsg-after-pri%")\nmodule(load="omhttp")', - 'action(type="omhttp" server="yoursplunk" serverport="443" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" errorfile="/var/log/tower/rsyslog.err" action.resumeInterval="5" restpath="services/collector/event")', # noqa + 'action(type="omhttp" server="yoursplunk" serverport="443" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" action.resumeInterval="5" errorfile="/var/log/tower/rsyslog.err" restpath="services/collector/event")', # noqa ]) ), ( @@ -86,9 +90,10 @@ data_loggly = { 'http://yoursplunk/services/collector/event', None, None, + '/var/log/tower/rsyslog.err', '\n'.join([ 'template(name="awx" type="string" string="%rawmsg-after-pri%")\nmodule(load="omhttp")', - 'action(type="omhttp" server="yoursplunk" serverport="80" usehttps="off" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" errorfile="/var/log/tower/rsyslog.err" action.resumeInterval="5" restpath="services/collector/event")', # noqa + 'action(type="omhttp" server="yoursplunk" serverport="80" usehttps="off" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" action.resumeInterval="5" errorfile="/var/log/tower/rsyslog.err" restpath="services/collector/event")', # noqa ]) ), ( @@ -97,9 +102,10 @@ data_loggly = { 'https://yoursplunk:8088/services/collector/event', None, None, + '/var/log/tower/rsyslog.err', '\n'.join([ 'template(name="awx" type="string" string="%rawmsg-after-pri%")\nmodule(load="omhttp")', - 'action(type="omhttp" server="yoursplunk" serverport="8088" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" errorfile="/var/log/tower/rsyslog.err" action.resumeInterval="5" restpath="services/collector/event")', # noqa + 'action(type="omhttp" server="yoursplunk" serverport="8088" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" action.resumeInterval="5" errorfile="/var/log/tower/rsyslog.err" restpath="services/collector/event")', # noqa ]) ), ( @@ -108,9 +114,10 @@ data_loggly = { 'https://yoursplunk/services/collector/event', 8088, None, + '/var/log/tower/rsyslog.err', '\n'.join([ 'template(name="awx" type="string" string="%rawmsg-after-pri%")\nmodule(load="omhttp")', - 'action(type="omhttp" server="yoursplunk" serverport="8088" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" errorfile="/var/log/tower/rsyslog.err" action.resumeInterval="5" restpath="services/collector/event")', # noqa + 'action(type="omhttp" server="yoursplunk" serverport="8088" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" action.resumeInterval="5" errorfile="/var/log/tower/rsyslog.err" restpath="services/collector/event")', # noqa ]) ), ( @@ -119,9 +126,10 @@ data_loggly = { 'yoursplunk.org/services/collector/event', 8088, 'https', + '/var/log/tower/rsyslog.err', '\n'.join([ 'template(name="awx" type="string" string="%rawmsg-after-pri%")\nmodule(load="omhttp")', - 'action(type="omhttp" server="yoursplunk.org" serverport="8088" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" errorfile="/var/log/tower/rsyslog.err" action.resumeInterval="5" restpath="services/collector/event")', # noqa + 'action(type="omhttp" server="yoursplunk.org" serverport="8088" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" action.resumeInterval="5" errorfile="/var/log/tower/rsyslog.err" restpath="services/collector/event")', # noqa ]) ), ( @@ -130,9 +138,10 @@ data_loggly = { 'http://yoursplunk.org/services/collector/event', 8088, None, + '/var/log/tower/rsyslog.err', '\n'.join([ 'template(name="awx" type="string" string="%rawmsg-after-pri%")\nmodule(load="omhttp")', - 'action(type="omhttp" server="yoursplunk.org" serverport="8088" usehttps="off" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" errorfile="/var/log/tower/rsyslog.err" action.resumeInterval="5" restpath="services/collector/event")', # noqa + 'action(type="omhttp" server="yoursplunk.org" serverport="8088" usehttps="off" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" action.resumeInterval="5" errorfile="/var/log/tower/rsyslog.err" restpath="services/collector/event")', # noqa ]) ), ( @@ -141,14 +150,15 @@ data_loggly = { 'https://endpoint5.collection.us2.sumologic.com/receiver/v1/http/ZaVnC4dhaV0qoiETY0MrM3wwLoDgO1jFgjOxE6-39qokkj3LGtOroZ8wNaN2M6DtgYrJZsmSi4-36_Up5TbbN_8hosYonLKHSSOSKY845LuLZBCBwStrHQ==', # noqa None, 'https', + '/var/log/tower/rsyslog.err', '\n'.join([ 'template(name="awx" type="string" string="%rawmsg-after-pri%")\nmodule(load="omhttp")', - 'action(type="omhttp" server="endpoint5.collection.us2.sumologic.com" serverport="443" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" errorfile="/var/log/tower/rsyslog.err" action.resumeInterval="5" restpath="receiver/v1/http/ZaVnC4dhaV0qoiETY0MrM3wwLoDgO1jFgjOxE6-39qokkj3LGtOroZ8wNaN2M6DtgYrJZsmSi4-36_Up5TbbN_8hosYonLKHSSOSKY845LuLZBCBwStrHQ==")', # noqa + 'action(type="omhttp" server="endpoint5.collection.us2.sumologic.com" serverport="443" usehttps="on" allowunsignedcerts="off" skipverifyhost="off" action.resumeRetryCount="-1" template="awx" action.resumeInterval="5" errorfile="/var/log/tower/rsyslog.err" restpath="receiver/v1/http/ZaVnC4dhaV0qoiETY0MrM3wwLoDgO1jFgjOxE6-39qokkj3LGtOroZ8wNaN2M6DtgYrJZsmSi4-36_Up5TbbN_8hosYonLKHSSOSKY845LuLZBCBwStrHQ==")', # noqa ]) ), ] ) -def test_rsyslog_conf_template(enabled, log_type, host, port, protocol, expected_config): +def test_rsyslog_conf_template(enabled, log_type, host, port, protocol, errorfile, expected_config): mock_settings, _ = _mock_logging_defaults() @@ -159,6 +169,7 @@ def test_rsyslog_conf_template(enabled, log_type, host, port, protocol, expected setattr(mock_settings, 'LOG_AGGREGATOR_ENABLED', enabled) setattr(mock_settings, 'LOG_AGGREGATOR_TYPE', log_type) setattr(mock_settings, 'LOG_AGGREGATOR_HOST', host) + setattr(mock_settings, 'LOG_AGGREGATOR_RSYSLOGD_ERROR_LOG_FILE', errorfile) if port: setattr(mock_settings, 'LOG_AGGREGATOR_PORT', port) if protocol: diff --git a/awx/main/tests/unit/commands/test_inventory_import.py b/awx/main/tests/unit/commands/test_inventory_import.py index 105086bcb8..db3e01408b 100644 --- a/awx/main/tests/unit/commands/test_inventory_import.py +++ b/awx/main/tests/unit/commands/test_inventory_import.py @@ -33,32 +33,6 @@ class TestInvalidOptions: assert 'inventory-id' in str(err.value) assert 'exclusive' in str(err.value) - def test_invalid_options_id_and_keep_vars(self): - # You can't overwrite and keep_vars at the same time, that wouldn't make sense - cmd = Command() - with pytest.raises(CommandError) as err: - cmd.handle( - inventory_id=42, overwrite=True, keep_vars=True - ) - assert 'overwrite-vars' in str(err.value) - assert 'exclusive' in str(err.value) - - def test_invalid_options_id_but_no_source(self): - # Need a source to import - cmd = Command() - with pytest.raises(CommandError) as err: - cmd.handle( - inventory_id=42, overwrite=True, keep_vars=True - ) - assert 'overwrite-vars' in str(err.value) - assert 'exclusive' in str(err.value) - with pytest.raises(CommandError) as err: - cmd.handle( - inventory_id=42, overwrite_vars=True, keep_vars=True - ) - assert 'overwrite-vars' in str(err.value) - assert 'exclusive' in str(err.value) - def test_invalid_options_missing_source(self): cmd = Command() with pytest.raises(CommandError) as err: diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 0de5dce996..166ea95f19 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -180,7 +180,7 @@ def test_openstack_client_config_generation(mocker, source, expected, private_da 'source_vars_dict': {}, 'get_cloud_credential': mocker.Mock(return_value=credential), 'get_extra_credentials': lambda x: [], - 'ansible_virtualenv_path': '/venv/foo' + 'ansible_virtualenv_path': '/var/lib/awx/venv/foo' }) cloud_config = update.build_private_data(inventory_update, private_data_dir) cloud_credential = yaml.safe_load( @@ -224,6 +224,52 @@ def test_openstack_client_config_generation_with_project_domain_name(mocker, sou 'source_vars_dict': {}, 'get_cloud_credential': mocker.Mock(return_value=credential), 'get_extra_credentials': lambda x: [], + 'ansible_virtualenv_path': '/var/lib/awx/venv/foo' + }) + cloud_config = update.build_private_data(inventory_update, private_data_dir) + cloud_credential = yaml.safe_load( + cloud_config.get('credentials')[credential] + ) + assert cloud_credential['clouds'] == { + 'devstack': { + 'auth': { + 'auth_url': 'https://keystone.openstack.example.org', + 'password': 'secrete', + 'project_name': 'demo-project', + 'username': 'demo', + 'domain_name': 'my-demo-domain', + 'project_domain_name': 'project-domain', + }, + 'verify': expected, + 'private': True, + } + } + + +@pytest.mark.parametrize("source,expected", [ + (None, True), (False, False), (True, True) +]) +def test_openstack_client_config_generation_with_project_region_name(mocker, source, expected, private_data_dir): + update = tasks.RunInventoryUpdate() + credential_type = CredentialType.defaults['openstack']() + inputs = { + 'host': 'https://keystone.openstack.example.org', + 'username': 'demo', + 'password': 'secrete', + 'project': 'demo-project', + 'domain': 'my-demo-domain', + 'project_domain_name': 'project-domain', + 'project_region_name': 'region-name', + } + if source is not None: + inputs['verify_ssl'] = source + credential = Credential(pk=1, credential_type=credential_type, inputs=inputs) + + inventory_update = mocker.Mock(**{ + 'source': 'openstack', + 'source_vars_dict': {}, + 'get_cloud_credential': mocker.Mock(return_value=credential), + 'get_extra_credentials': lambda x: [], 'ansible_virtualenv_path': '/venv/foo' }) cloud_config = update.build_private_data(inventory_update, private_data_dir) @@ -242,6 +288,7 @@ def test_openstack_client_config_generation_with_project_domain_name(mocker, sou }, 'verify': expected, 'private': True, + 'region_name': 'region-name', } } @@ -267,7 +314,7 @@ def test_openstack_client_config_generation_with_private_source_vars(mocker, sou 'source_vars_dict': {'private': source}, 'get_cloud_credential': mocker.Mock(return_value=credential), 'get_extra_credentials': lambda x: [], - 'ansible_virtualenv_path': '/venv/foo' + 'ansible_virtualenv_path': '/var/lib/awx/venv/foo' }) cloud_config = update.build_private_data(inventory_update, private_data_dir) cloud_credential = yaml.load( @@ -625,13 +672,13 @@ class TestGenericRun(): def test_invalid_custom_virtualenv(self, patch_Job, private_data_dir): job = Job(project=Project(), inventory=Inventory()) - job.project.custom_virtualenv = '/venv/missing' + job.project.custom_virtualenv = '/var/lib/awx/venv/missing' task = tasks.RunJob() with pytest.raises(tasks.InvalidVirtualenvError) as e: task.build_env(job, private_data_dir) - assert 'Invalid virtual environment selected: /venv/missing' == str(e.value) + assert 'Invalid virtual environment selected: /var/lib/awx/venv/missing' == str(e.value) class TestAdhocRun(TestJobExecution): @@ -1909,19 +1956,16 @@ class TestProjectUpdateCredentials(TestJobExecution): parametrize = { 'test_username_and_password_auth': [ 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'), ] @@ -2061,8 +2105,8 @@ class TestInventoryUpdateCredentials(TestJobExecution): credential, env, {}, [], private_data_dir ) - assert '--custom' in ' '.join(args) - script = args[args.index('--source') + 1] + assert '-i' in ' '.join(args) + script = args[args.index('-i') + 1] with open(script, 'r') as f: assert f.read() == inventory_update.source_script.script assert env['FOO'] == 'BAR' diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 00fc73c631..283a028f3f 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -222,9 +222,8 @@ def update_scm_url(scm_type, url, username=True, password=True, ''' # Handle all of the URL formats supported by the SCM systems: # 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', 'archive'): + if scm_type not in ('git', 'svn', 'insights', 'archive'): raise ValueError(_('Unsupported SCM type "%s"') % str(scm_type)) if not url.strip(): return '' @@ -256,8 +255,8 @@ def update_scm_url(scm_type, url, username=True, password=True, # SCP style before passed to git module. parts = urllib.parse.urlsplit('git+ssh://%s' % modified_url) # Handle local paths specified without file scheme (e.g. /path/to/foo). - # Only supported by git and hg. - elif scm_type in ('git', 'hg'): + # Only supported by git. + elif scm_type == 'git': if not url.startswith('/'): parts = urllib.parse.urlsplit('file:///%s' % url) else: @@ -268,7 +267,6 @@ def update_scm_url(scm_type, url, username=True, password=True, # Validate that scheme is valid for given scm_type. scm_type_schemes = { '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'), 'archive': ('http', 'https'), @@ -300,12 +298,6 @@ def update_scm_url(scm_type, url, username=True, password=True, if scm_type == 'git' and parts.scheme.endswith('ssh') and parts.hostname in special_git_hosts and netloc_password: #raise ValueError('Password not allowed for SSH access to %s.' % parts.hostname) netloc_password = '' - special_hg_hosts = ('bitbucket.org', 'altssh.bitbucket.org') - if scm_type == 'hg' and parts.scheme == 'ssh' and parts.hostname in special_hg_hosts and netloc_username != 'hg': - raise ValueError(_('Username must be "hg" for SSH access to %s.') % parts.hostname) - if scm_type == 'hg' and parts.scheme == 'ssh' and netloc_password: - #raise ValueError('Password not supported for SSH with Mercurial.') - netloc_password = '' 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]) diff --git a/awx/main/management/commands/deletion.py b/awx/main/utils/deletion.py similarity index 100% rename from awx/main/management/commands/deletion.py rename to awx/main/utils/deletion.py diff --git a/awx/main/utils/external_logging.py b/awx/main/utils/external_logging.py index f551dad2c2..403105edf4 100644 --- a/awx/main/utils/external_logging.py +++ b/awx/main/utils/external_logging.py @@ -18,6 +18,7 @@ def construct_rsyslog_conf_template(settings=settings): timeout = getattr(settings, 'LOG_AGGREGATOR_TCP_TIMEOUT', 5) max_disk_space = getattr(settings, 'LOG_AGGREGATOR_MAX_DISK_USAGE_GB', 1) spool_directory = getattr(settings, 'LOG_AGGREGATOR_MAX_DISK_USAGE_PATH', '/var/lib/awx').rstrip('/') + error_log_file = getattr(settings, 'LOG_AGGREGATOR_RSYSLOGD_ERROR_LOG_FILE', '') if not os.access(spool_directory, os.W_OK): spool_directory = '/var/lib/awx' @@ -74,9 +75,10 @@ def construct_rsyslog_conf_template(settings=settings): f'skipverifyhost="{skip_verify}"', 'action.resumeRetryCount="-1"', 'template="awx"', - 'errorfile="/var/log/tower/rsyslog.err"', f'action.resumeInterval="{timeout}"' ] + if error_log_file: + params.append(f'errorfile="{error_log_file}"') if parsed.path: path = urlparse.quote(parsed.path[1:], safe='/=') if parsed.query: diff --git a/awx/main/utils/formatters.py b/awx/main/utils/formatters.py index 171a994435..8afd121d5c 100644 --- a/awx/main/utils/formatters.py +++ b/awx/main/utils/formatters.py @@ -9,6 +9,7 @@ import socket from datetime import datetime from dateutil.tz import tzutc +from django.utils.timezone import now from django.core.serializers.json import DjangoJSONEncoder from django.conf import settings @@ -17,8 +18,15 @@ class TimeFormatter(logging.Formatter): ''' Custom log formatter used for inventory imports ''' + def __init__(self, start_time=None, **kwargs): + if start_time is None: + self.job_start = now() + else: + self.job_start = start_time + super(TimeFormatter, self).__init__(**kwargs) + def format(self, record): - record.relativeSeconds = record.relativeCreated / 1000.0 + record.relativeSeconds = (now() - self.job_start).total_seconds() return logging.Formatter.format(self, record) diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index c5e0014f8e..b6eefd9c59 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -7,6 +7,10 @@ import os.path # Django from django.conf import settings +from django.utils.timezone import now + +# AWX +from awx.main.exceptions import PostRunError class RSysLogHandler(logging.handlers.SysLogHandler): @@ -40,6 +44,58 @@ class RSysLogHandler(logging.handlers.SysLogHandler): pass +class SpecialInventoryHandler(logging.Handler): + """Logging handler used for the saving-to-database part of inventory updates + ran by the task system + this dispatches events directly to be processed by the callback receiver, + as opposed to ansible-runner + """ + + def __init__(self, event_handler, cancel_callback, job_timeout, verbosity, + start_time=None, counter=0, initial_line=0, **kwargs): + self.event_handler = event_handler + self.cancel_callback = cancel_callback + self.job_timeout = job_timeout + if start_time is None: + self.job_start = now() + else: + self.job_start = start_time + self.last_check = self.job_start + self.counter = counter + self.skip_level = [logging.WARNING, logging.INFO, logging.DEBUG, 0][verbosity] + self._current_line = initial_line + super(SpecialInventoryHandler, self).__init__(**kwargs) + + def emit(self, record): + # check cancel and timeout status regardless of log level + this_time = now() + if (this_time - self.last_check).total_seconds() > 0.5: # cancel callback is expensive + self.last_check = this_time + if self.cancel_callback(): + raise PostRunError('Inventory update has been canceled', status='canceled') + if self.job_timeout and ((this_time - self.job_start).total_seconds() > self.job_timeout): + raise PostRunError('Inventory update has timed out', status='canceled') + + # skip logging for low severity logs + if record.levelno < self.skip_level: + return + + self.counter += 1 + msg = self.format(record) + n_lines = len(msg.strip().split('\n')) # don't count line breaks at boundry of text + dispatch_data = dict( + created=now().isoformat(), + event='verbose', + counter=self.counter, + stdout=msg, + start_line=self._current_line, + end_line=self._current_line + n_lines + ) + self._current_line += n_lines + + self.event_handler(dispatch_data) + + ColorHandler = logging.StreamHandler if settings.COLOR_LOGS is True: diff --git a/awx/main/utils/licensing.py b/awx/main/utils/licensing.py index efefcf6944..9b248536b5 100644 --- a/awx/main/utils/licensing.py +++ b/awx/main/utils/licensing.py @@ -11,7 +11,7 @@ The Licenser class can do the following: import base64 import configparser -from datetime import datetime +from datetime import datetime, timezone import collections import copy import io @@ -73,9 +73,12 @@ def validate_entitlement_manifest(data): buff.write(export) z = zipfile.ZipFile(buff) + subs = [] for f in z.filelist: if f.filename.startswith('export/entitlements') and f.filename.endswith('.json'): - return json.loads(z.open(f).read()) + subs.append(json.loads(z.open(f).read())) + if subs: + return subs raise ValueError(_("Invalid manifest: manifest contains no subscriptions.")) @@ -131,21 +134,61 @@ class Licenser(object): def license_from_manifest(self, manifest): + def is_appropriate_manifest_sub(sub): + if sub['pool']['activeSubscription'] is False: + return False + now = datetime.now(timezone.utc) + if parse_date(sub['startDate']) > now: + return False + if parse_date(sub['endDate']) < now: + return False + products = sub['pool']['providedProducts'] + if any(product.get('productId') == '480' for product in products): + return True + return False + + def _can_aggregate(sub, license): + # We aggregate multiple subs into a larger meta-sub, if they match + # + # No current sub in aggregate + if not license: + return True + # Same SKU type (SER vs MCT vs others)? + if license['sku'][0:3] != sub['pool']['productId'][0:3]: + return False + return True + # Parse output for subscription metadata to build config license = dict() - license['sku'] = manifest['pool']['productId'] - try: - license['instance_count'] = manifest['pool']['exported'] - except KeyError: - license['instance_count'] = manifest['pool']['quantity'] - license['subscription_name'] = manifest['pool']['productName'] - license['pool_id'] = manifest['pool']['id'] - license['license_date'] = parse_date(manifest['endDate']).strftime('%s') - license['product_name'] = manifest['pool']['productName'] - license['valid_key'] = True - license['license_type'] = 'enterprise' - license['satellite'] = False + for sub in manifest: + if not is_appropriate_manifest_sub(sub): + logger.warning("Subscription %s (%s) in manifest is not active or for another product" % + (sub['pool']['productName'], sub['pool']['productId'])) + continue + if not _can_aggregate(sub, license): + logger.warning("Subscription %s (%s) in manifest does not match other manifest subscriptions" % + (sub['pool']['productName'], sub['pool']['productId'])) + continue + license.setdefault('sku', sub['pool']['productId']) + license.setdefault('subscription_name', sub['pool']['productName']) + license.setdefault('pool_id', sub['pool']['id']) + license.setdefault('product_name', sub['pool']['productName']) + license.setdefault('valid_key', True) + license.setdefault('license_type', 'enterprise') + license.setdefault('satellite', False) + # Use the nearest end date + endDate = parse_date(sub['endDate']) + currentEndDateStr = license.get('license_date', '4102462800') # 2100-01-01 + currentEndDate = datetime.fromtimestamp(int(currentEndDateStr), timezone.utc) + if endDate < currentEndDate: + license['license_date'] = endDate.strftime('%s') + instances = sub['quantity'] + license['instance_count'] = license.get('instance_count', 0) + instances + license['subscription_name'] = re.sub(r'[\d]* Managed Nodes', '%d Managed Nodes' % license['instance_count'], license['subscription_name']) + + if not license: + logger.error("No valid subscriptions found in manifest") self._attrs.update(license) settings.LICENSE = self._attrs return self._attrs @@ -214,11 +257,15 @@ class Licenser(object): def get_satellite_subs(self, host, user, pw): + port = None try: verify = str(self.config.get("rhsm", "repo_ca_cert")) + port = str(self.config.get("server", "port")) except Exception as e: logger.exception('Unable to read rhsm config to get ca_cert location. {}'.format(str(e))) verify = getattr(settings, 'REDHAT_CANDLEPIN_VERIFY', True) + if port: + host = ':'.join([host, port]) json = [] try: orgs = requests.get( @@ -272,7 +319,7 @@ class Licenser(object): return False # Products that contain Ansible Tower products = sub.get('providedProducts', []) - if any(map(lambda product: product.get('productId', None) == "480", products)): + if any(product.get('productId') == '480' for product in products): return True return False @@ -373,10 +420,9 @@ class Licenser(object): current_instances = Host.objects.active_count() else: current_instances = 0 - available_instances = int(attrs.get('instance_count', None) or 0) + instance_count = int(attrs.get('instance_count', 0)) attrs['current_instances'] = current_instances - attrs['available_instances'] = available_instances - free_instances = (available_instances - current_instances) + free_instances = (instance_count - current_instances) attrs['free_instances'] = max(0, free_instances) license_date = int(attrs.get('license_date', 0) or 0) diff --git a/awx/playbooks/action_plugins/hg_deprecation.py b/awx/playbooks/action_plugins/hg_deprecation.py deleted file mode 100644 index d4593b2360..0000000000 --- a/awx/playbooks/action_plugins/hg_deprecation.py +++ /dev/null @@ -1,15 +0,0 @@ -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/check_isolated.yml b/awx/playbooks/check_isolated.yml index 18b3305846..472b772fbb 100644 --- a/awx/playbooks/check_isolated.yml +++ b/awx/playbooks/check_isolated.yml @@ -9,6 +9,9 @@ - ansible.posix tasks: + - name: "Output job the playbook is running for" + debug: + msg: "Checking on job {{ job_id }}" - name: Determine if daemon process is alive. shell: "ansible-runner is-alive {{src}}" diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index 74e55e7ada..a7b7007d56 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -48,12 +48,6 @@ tags: - update_git - - block: - - name: include hg tasks - include_tasks: project_update_hg_tasks.yml - tags: - - update_hg - - block: - name: update project using svn subversion: @@ -150,7 +144,6 @@ msg: "Repository Version {{ scm_version }}" tags: - update_git - - update_hg - update_svn - update_insights - update_archive diff --git a/awx/playbooks/project_update_hg_tasks.yml b/awx/playbooks/project_update_hg_tasks.yml deleted file mode 100644 index 3553d60984..0000000000 --- a/awx/playbooks/project_update_hg_tasks.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -- name: Mercurial support is deprecated. - hg_deprecation: - -- name: update project using hg - hg: - dest: "{{project_path|quote}}" - repo: "{{scm_url|quote}}" - revision: "{{scm_branch|quote}}" - force: "{{scm_clean}}" - register: hg_result - -- name: Set the hg repository version - set_fact: - scm_version: "{{ hg_result['after'] }}" - when: "'after' in hg_result" - -- name: parse hg version string properly - set_fact: - scm_version: "{{scm_version|regex_replace('^([A-Za-z0-9]+).*$', '\\1')}}" diff --git a/awx/playbooks/run_isolated.yml b/awx/playbooks/run_isolated.yml index 4e3b7b54ee..76ea42d17c 100644 --- a/awx/playbooks/run_isolated.yml +++ b/awx/playbooks/run_isolated.yml @@ -13,6 +13,10 @@ - ansible.posix tasks: + - name: "Output job the playbook is running for" + debug: + msg: "Checking on job {{ job_id }}" + - name: synchronize job environment with isolated host synchronize: copy_links: true diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 6204486456..31422ebb33 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -116,7 +116,7 @@ LOGIN_URL = '/api/login/' # Absolute filesystem path to the directory to host projects (with playbooks). # This directory should not be web-accessible. -PROJECTS_ROOT = os.path.join(BASE_DIR, 'projects') +PROJECTS_ROOT = '/var/lib/awx/projects/' # Absolute filesystem path to the directory to host collections for # running inventory imports, isolated playbooks @@ -125,10 +125,10 @@ AWX_ANSIBLE_COLLECTIONS_PATHS = os.path.join(BASE_DIR, 'vendor', 'awx_ansible_co # Absolute filesystem path to the directory for job status stdout (default for # development and tests, default for production defined in production.py). This # directory should not be web-accessible -JOBOUTPUT_ROOT = os.path.join(BASE_DIR, 'job_output') +JOBOUTPUT_ROOT = '/var/lib/awx/job_status/' # Absolute filesystem path to the directory to store logs -LOG_ROOT = os.path.join(BASE_DIR) +LOG_ROOT = '/var/log/tower/' # The heartbeat file for the tower scheduler SCHEDULE_METADATA_LOCATION = os.path.join(BASE_DIR, '.tower_cycle') @@ -196,9 +196,9 @@ LOCAL_STDOUT_EXPIRE_TIME = 2592000 # events into the database JOB_EVENT_WORKERS = 4 -# The number of seconds (must be an integer) to buffer callback receiver bulk +# The number of seconds to buffer callback receiver bulk # writes in memory before flushing via JobEvent.objects.bulk_create() -JOB_EVENT_BUFFER_SECONDS = 1 +JOB_EVENT_BUFFER_SECONDS = .1 # The interval at which callback receiver statistics should be # recorded @@ -248,6 +248,7 @@ TEMPLATES = [ 'django.template.context_processors.static', 'django.template.context_processors.tz', 'django.contrib.messages.context_processors.messages', + 'awx.ui.context_processors.csp', 'social_django.context_processors.backends', 'social_django.context_processors.login_redirect', ], @@ -343,6 +344,9 @@ AUTHENTICATION_BACKENDS = ( 'social_core.backends.github.GithubOAuth2', 'social_core.backends.github.GithubOrganizationOAuth2', 'social_core.backends.github.GithubTeamOAuth2', + 'social_core.backends.github_enterprise.GithubEnterpriseOAuth2', + 'social_core.backends.github_enterprise.GithubEnterpriseOrganizationOAuth2', + 'social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2', 'social_core.backends.azuread.AzureADOAuth2', 'awx.sso.backends.SAMLAuth', 'django.contrib.auth.backends.ModelBackend', @@ -519,6 +523,20 @@ SOCIAL_AUTH_GITHUB_TEAM_SECRET = '' SOCIAL_AUTH_GITHUB_TEAM_ID = '' SOCIAL_AUTH_GITHUB_TEAM_SCOPE = ['user:email', 'read:org'] +SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY = '' +SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET = '' +SOCIAL_AUTH_GITHUB_ENTERPRISE_SCOPE = ['user:email', 'read:org'] + +SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY = '' +SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET = '' +SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME = '' +SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SCOPE = ['user:email', 'read:org'] + +SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY = '' +SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET = '' +SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID = '' +SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SCOPE = ['user:email', 'read:org'] + SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = '' SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET = '' @@ -661,7 +679,7 @@ INV_ENV_VARIABLE_BLOCKED = ("HOME", "USER", "_", "TERM") # ---------------- EC2_ENABLED_VAR = 'ec2_state' EC2_ENABLED_VALUE = 'running' -EC2_INSTANCE_ID_VAR = 'ec2_id' +EC2_INSTANCE_ID_VAR = 'instance_id' EC2_EXCLUDE_EMPTY_GROUPS = True # ------------ @@ -769,6 +787,7 @@ LOG_AGGREGATOR_LEVEL = 'INFO' LOG_AGGREGATOR_MAX_DISK_USAGE_GB = 1 LOG_AGGREGATOR_MAX_DISK_USAGE_PATH = '/var/lib/awx' LOG_AGGREGATOR_RSYSLOGD_DEBUG = False +LOG_AGGREGATOR_RSYSLOGD_ERROR_LOG_FILE = '/var/log/tower/rsyslog.err' # The number of retry attempts for websocket session establishment # If you're encountering issues establishing websockets in clustered Tower, @@ -852,38 +871,30 @@ LOGGING = { }, 'tower_warnings': { # don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL - 'class': 'logging.handlers.RotatingFileHandler', + 'class': 'logging.handlers.WatchedFileHandler', 'filters': ['require_debug_false', 'dynamic_level_filter'], 'filename': os.path.join(LOG_ROOT, 'tower.log'), - 'maxBytes': 1024 * 1024 * 5, # 5 MB - 'backupCount': 5, 'formatter':'simple', }, 'callback_receiver': { # don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL - 'class': 'logging.handlers.RotatingFileHandler', + 'class': 'logging.handlers.WatchedFileHandler', 'filters': ['require_debug_false', 'dynamic_level_filter'], 'filename': os.path.join(LOG_ROOT, 'callback_receiver.log'), - 'maxBytes': 1024 * 1024 * 5, # 5 MB - 'backupCount': 5, 'formatter':'simple', }, 'dispatcher': { # don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL - 'class': 'logging.handlers.RotatingFileHandler', + 'class': 'logging.handlers.WatchedFileHandler', 'filters': ['require_debug_false', 'dynamic_level_filter'], 'filename': os.path.join(LOG_ROOT, 'dispatcher.log'), - 'maxBytes': 1024 * 1024 * 5, # 5 MB - 'backupCount': 5, 'formatter':'dispatcher', }, 'wsbroadcast': { # don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL - 'class': 'logging.handlers.RotatingFileHandler', + 'class': 'logging.handlers.WatchedFileHandler', 'filters': ['require_debug_false', 'dynamic_level_filter'], 'filename': os.path.join(LOG_ROOT, 'wsbroadcast.log'), - 'maxBytes': 1024 * 1024 * 5, # 5 MB - 'backupCount': 5, 'formatter':'simple', }, 'celery.beat': { @@ -897,38 +908,36 @@ LOGGING = { }, 'task_system': { # don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL - 'class': 'logging.handlers.RotatingFileHandler', + 'class': 'logging.handlers.WatchedFileHandler', 'filters': ['require_debug_false', 'dynamic_level_filter'], 'filename': os.path.join(LOG_ROOT, 'task_system.log'), - 'maxBytes': 1024 * 1024 * 5, # 5 MB - 'backupCount': 5, 'formatter':'simple', }, 'management_playbooks': { 'level': 'DEBUG', - 'class':'logging.handlers.RotatingFileHandler', + 'class':'logging.handlers.WatchedFileHandler', 'filters': ['require_debug_false'], 'filename': os.path.join(LOG_ROOT, 'management_playbooks.log'), - 'maxBytes': 1024 * 1024 * 5, # 5 MB - 'backupCount': 5, 'formatter':'simple', }, 'system_tracking_migrations': { 'level': 'WARNING', - 'class':'logging.handlers.RotatingFileHandler', + 'class':'logging.handlers.WatchedFileHandler', 'filters': ['require_debug_false'], 'filename': os.path.join(LOG_ROOT, 'tower_system_tracking_migrations.log'), - 'maxBytes': 1024 * 1024 * 5, # 5 MB - 'backupCount': 5, 'formatter':'simple', }, 'rbac_migrations': { 'level': 'WARNING', - 'class':'logging.handlers.RotatingFileHandler', + 'class':'logging.handlers.WatchedFileHandler', 'filters': ['require_debug_false'], 'filename': os.path.join(LOG_ROOT, 'tower_rbac_migrations.log'), - 'maxBytes': 1024 * 1024 * 5, # 5 MB - 'backupCount': 5, + 'formatter':'simple', + }, + 'isolated_manager': { + 'level': 'WARNING', + 'class':'logging.handlers.WatchedFileHandler', + 'filename': os.path.join(LOG_ROOT, 'isolated_manager.log'), 'formatter':'simple', }, }, @@ -980,6 +989,11 @@ LOGGING = { 'awx.main.wsbroadcast': { 'handlers': ['wsbroadcast'], }, + 'awx.isolated.manager': { + 'level': 'WARNING', + 'handlers': ['console', 'file', 'isolated_manager'], + 'propagate': True + }, 'awx.isolated.manager.playbooks': { 'handlers': ['management_playbooks'], 'propagate': False diff --git a/awx/settings/development.py b/awx/settings/development.py index 108767b98c..6181d16ec6 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -148,9 +148,9 @@ include(optional('/etc/tower/settings.py'), scope=locals()) include(optional('/etc/tower/conf.d/*.py'), scope=locals()) # Installed differently in Dockerfile compared to production versions -AWX_ANSIBLE_COLLECTIONS_PATHS = '/vendor/awx_ansible_collections' +AWX_ANSIBLE_COLLECTIONS_PATHS = '/var/lib/awx/vendor/awx_ansible_collections' -BASE_VENV_PATH = "/venv/" +BASE_VENV_PATH = "/var/lib/awx/venv/" ANSIBLE_VENV_PATH = os.path.join(BASE_VENV_PATH, "ansible") AWX_VENV_PATH = os.path.join(BASE_VENV_PATH, "awx") @@ -158,7 +158,10 @@ AWX_VENV_PATH = os.path.join(BASE_VENV_PATH, "awx") # default settings for development. If not present, we can still run using # only the defaults. try: - include(optional('local_*.py'), scope=locals()) + if os.getenv('AWX_KUBE_DEVEL', False): + include(optional('minikube.py'), scope=locals()) + else: + include(optional('local_*.py'), scope=locals()) except ImportError: traceback.print_exc() sys.exit(1) diff --git a/awx/settings/local_settings.py.docker_compose b/awx/settings/local_settings.py.docker_compose index 5ffbc9db1c..88ef90fd64 100644 --- a/awx/settings/local_settings.py.docker_compose +++ b/awx/settings/local_settings.py.docker_compose @@ -48,56 +48,12 @@ if "pytest" in sys.modules: } } -# Absolute filesystem path to the directory to host projects (with playbooks). -# This directory should NOT be web-accessible. -PROJECTS_ROOT = '/var/lib/awx/projects/' - # Location for cross-development of inventory plugins -AWX_ANSIBLE_COLLECTIONS_PATHS = '/vendor/awx_ansible_collections' - -# Absolute filesystem path to the directory for job status stdout -# This directory should not be web-accessible -JOBOUTPUT_ROOT = os.path.join(BASE_DIR, 'job_status') +AWX_ANSIBLE_COLLECTIONS_PATHS = '/var/lib/awx/vendor/awx_ansible_collections' # The UUID of the system, for HA. SYSTEM_UUID = '00000000-0000-0000-0000-000000000000' -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -USE_TZ = True -TIME_ZONE = 'UTC' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -# SECURITY WARNING: keep the secret key used in production secret! -# Hardcoded values can leak through source control. Consider loading -# the secret key from an environment variable or a file instead. -SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y' - -# HTTP headers and meta keys to search to determine remote host name or IP. Add -# additional items to this list, such as "HTTP_X_FORWARDED_FOR", if behind a -# reverse proxy. -REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] - -# If Tower is behind a reverse proxy/load balancer, use this setting to -# whitelist the proxy IP addresses from which Tower should trust custom -# REMOTE_HOST_HEADERS header values -# REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST'] -# PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101'] -# If this setting is an empty list (the default), the headers specified by -# REMOTE_HOST_HEADERS will be trusted unconditionally') -PROXY_IP_WHITELIST = [] - -# Define additional environment variables to be passed to ansible subprocesses -#AWX_TASK_ENV['FOO'] = 'BAR' - # If set, use -vvv for project updates instead of -v for more output. # PROJECT_UPDATE_VVV=True @@ -108,40 +64,6 @@ PROXY_IP_WHITELIST = [] # Enable logging to syslog. Setting level to ERROR captures 500 errors, # WARNING also logs 4xx responses. -LOGGING['handlers']['syslog'] = { - 'level': 'WARNING', - 'filters': ['require_debug_false'], - 'class': 'logging.NullHandler', - 'formatter': 'simple', -} - -LOGGING['loggers']['django.request']['handlers'] = ['console'] -LOGGING['loggers']['rest_framework.request']['handlers'] = ['console'] -LOGGING['loggers']['awx']['handlers'] = ['console', 'external_logger'] -LOGGING['loggers']['awx.main.commands.run_callback_receiver']['handlers'] = [] # propogates to awx -LOGGING['loggers']['awx.main.tasks']['handlers'] = ['console', 'external_logger'] -LOGGING['loggers']['awx.main.scheduler']['handlers'] = ['console', 'external_logger'] -LOGGING['loggers']['django_auth_ldap']['handlers'] = ['console'] -LOGGING['loggers']['social']['handlers'] = ['console'] -LOGGING['loggers']['system_tracking_migrations']['handlers'] = ['console'] -LOGGING['loggers']['rbac_migrations']['handlers'] = ['console'] -LOGGING['loggers']['awx.isolated.manager.playbooks']['handlers'] = ['console'] -LOGGING['handlers']['callback_receiver'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['fact_receiver'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['task_system'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['tower_warnings'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['rbac_migrations'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['system_tracking_migrations'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['management_playbooks'] = {'class': 'logging.NullHandler'} - - -# Enable the following lines to also log to a file. -#LOGGING['handlers']['file'] = { -# 'class': 'logging.FileHandler', -# 'filename': os.path.join(BASE_DIR, 'awx.log'), -# 'formatter': 'simple', -#} - # Enable the following lines to turn on lots of permissions-related logging. #LOGGING['loggers']['awx.main.access']['level'] = 'DEBUG' #LOGGING['loggers']['awx.main.signals']['level'] = 'DEBUG' @@ -154,81 +76,6 @@ LOGGING['handlers']['management_playbooks'] = {'class': 'logging.NullHandler'} #LOGGING['loggers']['django_auth_ldap']['handlers'] = ['console'] #LOGGING['loggers']['django_auth_ldap']['level'] = 'DEBUG' -############################################################################### -# SCM TEST SETTINGS -############################################################################### - -# Define these variables to enable more complete testing of project support for -# SCM updates. The test repositories listed do not have to contain any valid -# playbooks. - -try: - path = os.path.expanduser(os.path.expandvars('~/.ssh/id_rsa')) - TEST_SSH_KEY_DATA = open(path, 'rb').read() -except IOError: - TEST_SSH_KEY_DATA = '' - -TEST_GIT_USERNAME = '' -TEST_GIT_PASSWORD = '' -TEST_GIT_KEY_DATA = TEST_SSH_KEY_DATA -TEST_GIT_PUBLIC_HTTPS = 'https://github.com/ansible/ansible.github.com.git' -TEST_GIT_PRIVATE_HTTPS = 'https://github.com/ansible/product-docs.git' -TEST_GIT_PRIVATE_SSH = 'git@github.com:ansible/product-docs.git' - -TEST_HG_USERNAME = '' -TEST_HG_PASSWORD = '' -TEST_HG_KEY_DATA = TEST_SSH_KEY_DATA -TEST_HG_PUBLIC_HTTPS = 'https://bitbucket.org/cchurch/django-hotrunner' -TEST_HG_PRIVATE_HTTPS = '' -TEST_HG_PRIVATE_SSH = '' - -TEST_SVN_USERNAME = '' -TEST_SVN_PASSWORD = '' -TEST_SVN_PUBLIC_HTTPS = 'https://github.com/ansible/ansible.github.com' -TEST_SVN_PRIVATE_HTTPS = 'https://github.com/ansible/product-docs' - -# To test repo access via SSH login to localhost. -import getpass -try: - TEST_SSH_LOOPBACK_USERNAME = getpass.getuser() -except KeyError: - TEST_SSH_LOOPBACK_USERNAME = 'root' -TEST_SSH_LOOPBACK_PASSWORD = '' - -############################################################################### -# INVENTORY IMPORT TEST SETTINGS -############################################################################### - -# Define these variables to enable more complete testing of inventory import -# from cloud providers. - -# EC2 credentials -TEST_AWS_ACCESS_KEY_ID = '' -TEST_AWS_SECRET_ACCESS_KEY = '' -TEST_AWS_REGIONS = 'all' -# Check IAM STS credentials -TEST_AWS_SECURITY_TOKEN = '' - -# Rackspace credentials -TEST_RACKSPACE_USERNAME = '' -TEST_RACKSPACE_API_KEY = '' -TEST_RACKSPACE_REGIONS = 'all' - -# VMware credentials -TEST_VMWARE_HOST = '' -TEST_VMWARE_USER = '' -TEST_VMWARE_PASSWORD = '' - -# OpenStack credentials -TEST_OPENSTACK_HOST = '' -TEST_OPENSTACK_USER = '' -TEST_OPENSTACK_PASSWORD = '' -TEST_OPENSTACK_PROJECT = '' - -# Azure credentials. -TEST_AZURE_USERNAME = '' -TEST_AZURE_KEY_DATA = '' - BROADCAST_WEBSOCKET_SECRET = '🤖starscream🤖' BROADCAST_WEBSOCKET_PORT = 8013 BROADCAST_WEBSOCKET_VERIFY_CERT = False diff --git a/awx/settings/local_settings.py.example b/awx/settings/local_settings.py.example deleted file mode 100644 index d9de08cc5a..0000000000 --- a/awx/settings/local_settings.py.example +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. (formerly AnsibleWorks, Inc.) -# All Rights Reserved. - -# Local Django settings for AWX project. Rename to "local_settings.py" and -# edit as needed for your development environment. - -# All variables defined in awx/settings/development.py will already be loaded -# into the global namespace before this file is loaded, to allow for reading -# and updating the default settings as needed. - -############################################################################### -# MISC PROJECT SETTINGS -############################################################################### - -# Database settings to use PostgreSQL for development. -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'awx-dev', - 'USER': 'awx-dev', - 'PASSWORD': 'AWXsome1', - 'HOST': 'localhost', - 'PORT': '', - } -} - -# Use SQLite for unit tests instead of PostgreSQL. If the lines below are -# commented out, Django will create the test_awx-dev database in PostgreSQL to -# run unit tests. -if is_testing(sys.argv): - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'awx.sqlite3'), - 'TEST': { - # Test database cannot be :memory: for tests. - 'NAME': os.path.join(BASE_DIR, 'awx_test.sqlite3'), - }, - } - } - -# AMQP configuration. -BROKER_URL = 'amqp://guest:guest@localhost:5672' - -# Absolute filesystem path to the directory to host projects (with playbooks). -# This directory should NOT be web-accessible. -PROJECTS_ROOT = os.path.join(BASE_DIR, 'projects') - -# Absolute filesystem path to the directory for job status stdout -# This directory should not be web-accessible -JOBOUTPUT_ROOT = os.path.join(BASE_DIR, 'job_status') - -# The UUID of the system, for HA. -SYSTEM_UUID = '00000000-0000-0000-0000-000000000000' - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = None - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -# SECURITY WARNING: keep the secret key used in production secret! -# Hardcoded values can leak through source control. Consider loading -# the secret key from an environment variable or a file instead. -SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y' - -# HTTP headers and meta keys to search to determine remote host name or IP. Add -# additional items to this list, such as "HTTP_X_FORWARDED_FOR", if behind a -# reverse proxy. -REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] - -# If Tower is behind a reverse proxy/load balancer, use this setting to -# whitelist the proxy IP addresses from which Tower should trust custom -# REMOTE_HOST_HEADERS header values -# REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST'] -# PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101'] -# If this setting is an empty list (the default), the headers specified by -# REMOTE_HOST_HEADERS will be trusted unconditionally') -PROXY_IP_WHITELIST = [] - -# Define additional environment variables to be passed to ansible subprocesses -#AWX_TASK_ENV['FOO'] = 'BAR' - -# If set, use -vvv for project updates instead of -v for more output. -# PROJECT_UPDATE_VVV=True - -############################################################################### -# LOGGING SETTINGS -############################################################################### - -# Enable logging to syslog. Setting level to ERROR captures 500 errors, -# WARNING also logs 4xx responses. -LOGGING['handlers']['syslog'] = { - 'level': 'WARNING', - 'filters': [], - 'class': 'logging.handlers.SysLogHandler', - 'address': '/dev/log', - 'facility': 'local0', - 'formatter': 'simple', -} - -# Enable the following lines to also log to a file. -#LOGGING['handlers']['file'] = { -# 'class': 'logging.FileHandler', -# 'filename': os.path.join(BASE_DIR, 'awx.log'), -# 'formatter': 'simple', -#} - -# Enable the following lines to turn on lots of permissions-related logging. -#LOGGING['loggers']['awx.main.access']['level'] = 'DEBUG' -#LOGGING['loggers']['awx.main.signals']['level'] = 'DEBUG' -#LOGGING['loggers']['awx.main.permissions']['level'] = 'DEBUG' - -# Enable the following line to turn on database settings logging. -#LOGGING['loggers']['awx.conf']['level'] = 'DEBUG' - -# Enable the following lines to turn on LDAP auth logging. -#LOGGING['loggers']['django_auth_ldap']['handlers'] = ['console'] -#LOGGING['loggers']['django_auth_ldap']['level'] = 'DEBUG' - -############################################################################### -# SCM TEST SETTINGS -############################################################################### - -# Define these variables to enable more complete testing of project support for -# SCM updates. The test repositories listed do not have to contain any valid -# playbooks. - -try: - path = os.path.expanduser(os.path.expandvars('~/.ssh/id_rsa')) - TEST_SSH_KEY_DATA = file(path, 'rb').read() -except IOError: - TEST_SSH_KEY_DATA = '' - -TEST_GIT_USERNAME = '' -TEST_GIT_PASSWORD = '' -TEST_GIT_KEY_DATA = TEST_SSH_KEY_DATA -TEST_GIT_PUBLIC_HTTPS = 'https://github.com/ansible/ansible.github.com.git' -TEST_GIT_PRIVATE_HTTPS = 'https://github.com/ansible/product-docs.git' -TEST_GIT_PRIVATE_SSH = 'git@github.com:ansible/product-docs.git' - -TEST_HG_USERNAME = '' -TEST_HG_PASSWORD = '' -TEST_HG_KEY_DATA = TEST_SSH_KEY_DATA -TEST_HG_PUBLIC_HTTPS = 'https://bitbucket.org/cchurch/django-hotrunner' -TEST_HG_PRIVATE_HTTPS = '' -TEST_HG_PRIVATE_SSH = '' - -TEST_SVN_USERNAME = '' -TEST_SVN_PASSWORD = '' -TEST_SVN_PUBLIC_HTTPS = 'https://github.com/ansible/ansible.github.com' -TEST_SVN_PRIVATE_HTTPS = 'https://github.com/ansible/product-docs' - -# To test repo access via SSH login to localhost. -import getpass -TEST_SSH_LOOPBACK_USERNAME = getpass.getuser() -TEST_SSH_LOOPBACK_PASSWORD = '' - -############################################################################### -# INVENTORY IMPORT TEST SETTINGS -############################################################################### - -# Define these variables to enable more complete testing of inventory import -# from cloud providers. - -# EC2 credentials -TEST_AWS_ACCESS_KEY_ID = '' -TEST_AWS_SECRET_ACCESS_KEY = '' -TEST_AWS_REGIONS = 'all' -# Check IAM STS credentials -TEST_AWS_SECURITY_TOKEN = '' - - -# Rackspace credentials -TEST_RACKSPACE_USERNAME = '' -TEST_RACKSPACE_API_KEY = '' -TEST_RACKSPACE_REGIONS = 'all' - -# VMware credentials -TEST_VMWARE_HOST = '' -TEST_VMWARE_USER = '' -TEST_VMWARE_PASSWORD = '' - -# OpenStack credentials -TEST_OPENSTACK_HOST = '' -TEST_OPENSTACK_USER = '' -TEST_OPENSTACK_PASSWORD = '' -TEST_OPENSTACK_PROJECT = '' - -# Azure credentials. -TEST_AZURE_USERNAME = '' -TEST_AZURE_KEY_DATA = '' diff --git a/awx/settings/minikube.py b/awx/settings/minikube.py new file mode 100644 index 0000000000..0ac81875bc --- /dev/null +++ b/awx/settings/minikube.py @@ -0,0 +1,4 @@ +BROADCAST_WEBSOCKET_SECRET = '🤖starscream🤖' +BROADCAST_WEBSOCKET_PORT = 8013 +BROADCAST_WEBSOCKET_VERIFY_CERT = False +BROADCAST_WEBSOCKET_PROTOCOL = 'http' diff --git a/awx/settings/production.py b/awx/settings/production.py index fb24b7087f..02681265e6 100644 --- a/awx/settings/production.py +++ b/awx/settings/production.py @@ -30,10 +30,6 @@ SECRET_KEY = None # See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = [] -# Absolute filesystem path to the directory for job status stdout -# This directory should not be web-accessible -JOBOUTPUT_ROOT = '/var/lib/awx/job_status/' - # The heartbeat file for the tower scheduler SCHEDULE_METADATA_LOCATION = '/var/lib/awx/.tower_cycle' @@ -46,15 +42,6 @@ AWX_VENV_PATH = os.path.join(BASE_VENV_PATH, "awx") AWX_ISOLATED_USERNAME = 'awx' -LOGGING['handlers']['tower_warnings']['filename'] = '/var/log/tower/tower.log' # noqa -LOGGING['handlers']['callback_receiver']['filename'] = '/var/log/tower/callback_receiver.log' # noqa -LOGGING['handlers']['dispatcher']['filename'] = '/var/log/tower/dispatcher.log' # noqa -LOGGING['handlers']['wsbroadcast']['filename'] = '/var/log/tower/wsbroadcast.log' # noqa -LOGGING['handlers']['task_system']['filename'] = '/var/log/tower/task_system.log' # noqa -LOGGING['handlers']['management_playbooks']['filename'] = '/var/log/tower/management_playbooks.log' # noqa -LOGGING['handlers']['system_tracking_migrations']['filename'] = '/var/log/tower/tower_system_tracking_migrations.log' # noqa -LOGGING['handlers']['rbac_migrations']['filename'] = '/var/log/tower/tower_rbac_migrations.log' # noqa - # Store a snapshot of default settings at this point before loading any # customizable config files. DEFAULTS_SNAPSHOT = {} diff --git a/awx/sso/conf.py b/awx/sso/conf.py index 5f595517cc..8f21eb4d0a 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -842,6 +842,298 @@ register( placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, ) +############################################################################### +# GITHUB ENTERPRISE OAUTH2 AUTHENTICATION SETTINGS +############################################################################### + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_CALLBACK_URL', + field_class=fields.CharField, + read_only=True, + default=SocialAuthCallbackURL('github-enterprise'), + label=_('GitHub Enterprise OAuth2 Callback URL'), + help_text=_('Provide this URL as the callback URL for your application as part ' + 'of your registration process. Refer to the Ansible Tower ' + 'documentation for more detail.'), + category=_('GitHub Enterprise OAuth2'), + category_slug='github-enterprise', + depends_on=['TOWER_URL_BASE'], +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise URL'), + help_text=_('The URL for your Github Enterprise instance, e.g.: http(s)://hostname/. Refer to Github Enterprise ' + 'documentation for more details.'), + category=_('GitHub Enterprise OAuth2'), + category_slug='github-enterprise', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise API URL'), + help_text=_('The API URL for your GitHub Enterprise instance, e.g.: http(s)://hostname/api/v3/. Refer to Github ' + 'Enterprise documentation for more details.'), + category=_('GitHub Enterprise OAuth2'), + category_slug='github-enterprise', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise OAuth2 Key'), + help_text=_('The OAuth2 key (Client ID) from your GitHub Enterprise developer application.'), + category=_('GitHub Enterprise OAuth2'), + category_slug='github-enterprise', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise OAuth2 Secret'), + help_text=_('The OAuth2 secret (Client Secret) from your GitHub Enterprise developer application.'), + category=_('GitHub OAuth2'), + category_slug='github-enterprise', + encrypted=True, +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP', + field_class=SocialOrganizationMapField, + allow_null=True, + default=None, + label=_('GitHub Enterprise OAuth2 Organization Map'), + help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, + category=_('GitHub Enterprise OAuth2'), + category_slug='github-enterprise', + placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP', + field_class=SocialTeamMapField, + allow_null=True, + default=None, + label=_('GitHub Enterprise OAuth2 Team Map'), + help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, + category=_('GitHub Enterprise OAuth2'), + category_slug='github-enterprise', + placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, +) + +############################################################################### +# GITHUB ENTERPRISE ORG OAUTH2 AUTHENTICATION SETTINGS +############################################################################### + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_CALLBACK_URL', + field_class=fields.CharField, + read_only=True, + default=SocialAuthCallbackURL('github-enterprise-org'), + label=_('GitHub Enterprise Organization OAuth2 Callback URL'), + help_text=_('Provide this URL as the callback URL for your application as part ' + 'of your registration process. Refer to the Ansible Tower ' + 'documentation for more detail.'), + category=_('GitHub Enterprise Organization OAuth2'), + category_slug='github-enterprise-org', + depends_on=['TOWER_URL_BASE'], +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise Organization URL'), + help_text=_('The URL for your Github Enterprise instance, e.g.: http(s)://hostname/. Refer to Github Enterprise ' + 'documentation for more details.'), + category=_('GitHub Enterprise OAuth2'), + category_slug='github-enterprise-org', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise Organization API URL'), + help_text=_('The API URL for your GitHub Enterprise instance, e.g.: http(s)://hostname/api/v3/. Refer to Github ' + 'Enterprise documentation for more details.'), + category=_('GitHub Enterprise OAuth2'), + category_slug='github-enterprise-org', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise Organization OAuth2 Key'), + help_text=_('The OAuth2 key (Client ID) from your GitHub Enterprise organization application.'), + category=_('GitHub Enterprise Organization OAuth2'), + category_slug='github-enterprise-org', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise Organization OAuth2 Secret'), + help_text=_('The OAuth2 secret (Client Secret) from your GitHub Enterprise organization application.'), + category=_('GitHub Enterprise Organization OAuth2'), + category_slug='github-enterprise-org', + encrypted=True, +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise Organization Name'), + help_text=_('The name of your GitHub Enterprise organization, as used in your ' + 'organization\'s URL: https://github.com//.'), + category=_('GitHub Enterprise Organization OAuth2'), + category_slug='github-enterprise-org', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP', + field_class=SocialOrganizationMapField, + allow_null=True, + default=None, + label=_('GitHub Enterprise Organization OAuth2 Organization Map'), + help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, + category=_('GitHub Enterprise Organization OAuth2'), + category_slug='github-enterprise-org', + placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP', + field_class=SocialTeamMapField, + allow_null=True, + default=None, + label=_('GitHub Enterprise Organization OAuth2 Team Map'), + help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, + category=_('GitHub Enterprise Organization OAuth2'), + category_slug='github-enterprise-org', + placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, +) + +############################################################################### +# GITHUB ENTERPRISE TEAM OAUTH2 AUTHENTICATION SETTINGS +############################################################################### + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_CALLBACK_URL', + field_class=fields.CharField, + read_only=True, + default=SocialAuthCallbackURL('github-enterprise-team'), + label=_('GitHub Enterprise Team OAuth2 Callback URL'), + help_text=_('Create an organization-owned application at ' + 'https://github.com/organizations//settings/applications ' + 'and obtain an OAuth2 key (Client ID) and secret (Client Secret). ' + 'Provide this URL as the callback URL for your application.'), + category=_('GitHub Enterprise Team OAuth2'), + category_slug='github-enterprise-team', + depends_on=['TOWER_URL_BASE'], +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise Team URL'), + help_text=_('The URL for your Github Enterprise instance, e.g.: http(s)://hostname/. Refer to Github Enterprise ' + 'documentation for more details.'), + category=_('GitHub Enterprise OAuth2'), + category_slug='github-enterprise-team', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise Team API URL'), + help_text=_('The API URL for your GitHub Enterprise instance, e.g.: http(s)://hostname/api/v3/. Refer to Github ' + 'Enterprise documentation for more details.'), + category=_('GitHub Enterprise OAuth2'), + category_slug='github-enterprise-team', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise Team OAuth2 Key'), + help_text=_('The OAuth2 key (Client ID) from your GitHub Enterprise organization application.'), + category=_('GitHub Enterprise Team OAuth2'), + category_slug='github-enterprise-team', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise Team OAuth2 Secret'), + help_text=_('The OAuth2 secret (Client Secret) from your GitHub Enterprise organization application.'), + category=_('GitHub Enterprise Team OAuth2'), + category_slug='github-enterprise-team', + encrypted=True, +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID', + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('GitHub Enterprise Team ID'), + help_text=_('Find the numeric team ID using the Github Enterprise API: ' + 'http://fabian-kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/.'), + category=_('GitHub Enterprise Team OAuth2'), + category_slug='github-enterprise-team', +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP', + field_class=SocialOrganizationMapField, + allow_null=True, + default=None, + label=_('GitHub Enterprise Team OAuth2 Organization Map'), + help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, + category=_('GitHub Enterprise Team OAuth2'), + category_slug='github-enterprise-team', + placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, +) + +register( + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP', + field_class=SocialTeamMapField, + allow_null=True, + default=None, + label=_('GitHub Enterprise Team OAuth2 Team Map'), + help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, + category=_('GitHub Enterprise Team OAuth2'), + category_slug='github-enterprise-team', + placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, +) + ############################################################################### # MICROSOFT AZURE ACTIVE DIRECTORY SETTINGS ############################################################################### diff --git a/awx/sso/fields.py b/awx/sso/fields.py index e430a0a367..5df5f894c9 100644 --- a/awx/sso/fields.py +++ b/awx/sso/fields.py @@ -187,6 +187,26 @@ class AuthenticationBackendsField(fields.StringListField): 'SOCIAL_AUTH_GITHUB_TEAM_SECRET', 'SOCIAL_AUTH_GITHUB_TEAM_ID', ]), + ('social_core.backends.github_enterprise.GithubEnterpriseOAuth2', [ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET', + ]), + ('social_core.backends.github_enterprise.GithubEnterpriseOrganizationOAuth2', [ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME', + ]), + ('social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2', [ + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID', + ]), ('social_core.backends.azuread.AzureADOAuth2', [ 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET', @@ -445,7 +465,8 @@ class LDAPGroupTypeField(fields.ChoiceField, DependsOnMixin): default_error_messages = { 'type_error': _('Expected an instance of LDAPGroupType but got {input_type} instead.'), - 'missing_parameters': _('Missing required parameters in {dependency}.') + 'missing_parameters': _('Missing required parameters in {dependency}.'), + 'invalid_parameters': _('Invalid group_type parameters. Expected instance of dict but got {parameters_type} instead.') } def __init__(self, choices=None, **kwargs): @@ -465,7 +486,6 @@ class LDAPGroupTypeField(fields.ChoiceField, DependsOnMixin): if not data: return None - params = self.get_depends_on() or {} cls = find_class_in_modules(data) if not cls: return None @@ -475,8 +495,16 @@ class LDAPGroupTypeField(fields.ChoiceField, DependsOnMixin): # Backwords compatability. Before AUTH_LDAP_GROUP_TYPE_PARAMS existed # MemberDNGroupType was the only group type, of the underlying lib, that # took a parameter. + params = self.get_depends_on() or {} params_sanitized = dict() - for attr in inspect.getargspec(cls.__init__).args[1:]: + + cls_args = inspect.getargspec(cls.__init__).args[1:] + + if cls_args: + if not isinstance(params, dict): + self.fail('invalid_parameters', parameters_type=type(params)) + + for attr in cls_args: if attr in params: params_sanitized[attr] = params[attr] diff --git a/awx/sso/views.py b/awx/sso/views.py index fa248f634f..ddbc2cbd59 100644 --- a/awx/sso/views.py +++ b/awx/sso/views.py @@ -25,7 +25,7 @@ class BaseRedirectView(RedirectView): def get_redirect_url(self, *args, **kwargs): last_path = self.request.COOKIES.get('lastPath', '') last_path = urllib.parse.quote(urllib.parse.unquote(last_path).strip('"')) - url = reverse('ui:index') + url = reverse('ui_next:index') if last_path: return '%s#%s' % (url, last_path) else: diff --git a/awx/ui/context_processors.py b/awx/ui/context_processors.py new file mode 100644 index 0000000000..87c071c285 --- /dev/null +++ b/awx/ui/context_processors.py @@ -0,0 +1,8 @@ +import base64 +import os + + +def csp(request): + return { + 'csp_nonce': base64.encodebytes(os.urandom(32)).decode().rstrip(), + } diff --git a/awx/ui_next/.eslintignore b/awx/ui_next/.eslintignore index 096662151e..eb4a217ff6 100644 --- a/awx/ui_next/.eslintignore +++ b/awx/ui_next/.eslintignore @@ -6,4 +6,5 @@ coverage build node_modules dist -images \ No newline at end of file +images +instrumented \ No newline at end of file diff --git a/awx/ui_next/.eslintrc b/awx/ui_next/.eslintrc index 7f535d012d..c464701811 100644 --- a/awx/ui_next/.eslintrc +++ b/awx/ui_next/.eslintrc @@ -8,8 +8,14 @@ "modules": true } }, - "plugins": ["react-hooks"], - "extends": ["airbnb", "prettier", "prettier/react"], + "plugins": ["react-hooks", "jsx-a11y", "i18next"], + "extends": [ + "airbnb", + "prettier", + "prettier/react", + "plugin:jsx-a11y/strict", + "plugin:i18next/recommended" + ], "settings": { "react": { "version": "16.5.2" @@ -24,6 +30,70 @@ "window": true }, "rules": { + "i18next/no-literal-string": [ + 2, + { + "markupOnly": true, + "ignoreAttribute": [ + "to", + "streamType", + "path", + "component", + "variant", + "key", + "position", + "promptName", + "color", + "promptId", + "headingLevel", + "size", + "target", + "autoComplete", + "trigger", + "from", + "name", + "fieldId", + "css", + "gutter", + "dataCy", + "tooltipMaxWidth", + "mode", + "aria-labelledby", + "aria-hidden", + "sortKey", + "ouiaId", + "credentialTypeNamespace", + "link", + "value", + "credentialTypeKind", + "linkTo", + "scrollToAlignment", + "displayKey", + "sortedColumnKey", + "maxHeight", + "role", + "aria-haspopup", + "dropDirection", + "resizeOrientation", + "src", + "theme", + "gridColumns" + ], + "ignore": ["Ansible", "Tower", "JSON", "YAML", "lg", "START"], + "ignoreComponent": [ + "code", + "Omit", + "PotentialLink", + "TypeRedirect", + "Radio", + "RunOnRadio", + "NodeTypeLetter", + "SelectableItem", + "Dash" + ], + "ignoreCallee": ["describe"] + } + ], "camelcase": "off", "arrow-parens": "off", "comma-dangle": "off", diff --git a/awx/ui_next/CONTRIBUTING.md b/awx/ui_next/CONTRIBUTING.md index 575e08e913..3471c6b086 100644 --- a/awx/ui_next/CONTRIBUTING.md +++ b/awx/ui_next/CONTRIBUTING.md @@ -57,12 +57,12 @@ The UI is built using [ReactJS](https://reactjs.org/docs/getting-started.html) a The AWX UI requires the following: -- Node 10.x LTS +- Node 14.x LTS - NPM 6.x LTS Run the following to install all the dependencies: ```bash -(host) $ npm run install +(host) $ npm install ``` #### Build the User Interface diff --git a/awx/ui_next/Dockerfile b/awx/ui_next/Dockerfile index 713fb207d8..a710a820cd 100644 --- a/awx/ui_next/Dockerfile +++ b/awx/ui_next/Dockerfile @@ -1,4 +1,4 @@ -FROM node:10 +FROM node:14 ARG NPMRC_FILE=.npmrc ENV NPMRC_FILE=${NPMRC_FILE} ARG TARGET='https://awx:8043' diff --git a/awx/ui_next/README.md b/awx/ui_next/README.md index 3c83889641..2755f136d9 100644 --- a/awx/ui_next/README.md +++ b/awx/ui_next/README.md @@ -1,7 +1,7 @@ # AWX-PF ## Requirements -- node 10.x LTS, npm 6.x LTS, make, git +- node 14.x LTS, npm 6.x LTS, make, git ## Development The API development server will need to be running. See [CONTRIBUTING.md](../../CONTRIBUTING.md). @@ -15,6 +15,19 @@ npm --prefix=awx/ui_next install npm --prefix=awx/ui_next start ``` +### Build for the Development Containers +If you just want to build a ui for the container-based awx development +environment, use these make targets: + +```shell +# The ui will be reachable at https://localhost:8043 or +# http://localhost:8013 +make ui-devel + +# clean up +make clean-ui +``` + ### Using an External Server If you normally run awx on an external host/server (in this example, `awx.local`), you'll need use the `TARGET` environment variable when starting the ui development diff --git a/awx/ui_next/docs/APP_ARCHITECTURE.md b/awx/ui_next/docs/APP_ARCHITECTURE.md new file mode 100644 index 0000000000..4be18f17d2 --- /dev/null +++ b/awx/ui_next/docs/APP_ARCHITECTURE.md @@ -0,0 +1,27 @@ +# Application Architecture + +## Local Storage Integration +The `useStorage` hook integrates with the browser's localStorage api. +It accepts a localStorage key as its only argument and returns a state +variable and setter function for that state variable. The hook enables +bidirectional data transfer between tabs via an event listener that +is registered with the Web Storage api. + + +![Sequence Diagram for useStorage](images/useStorage.png) + +The `useStorage` hook currently lives in the `AppContainer` component. It +can be relocated to a more general location should and if the need +ever arise + +## Session Expiration +Session timeout state is communicated to the client in the HTTP(S) +response headers. Every HTTP(S) response is intercepted to read the +session expiration time before being passed into the rest of the +application. A timeout date is computed from the intercepted HTTP(S) +headers and is pushed into local storage, where it can be read using +standard Web Storage apis or other utilities, such as `useStorage`. + + +![Sequence Diagram for session expiration](images/sessionExpiration.png) + diff --git a/awx/ui_next/docs/images/sessionExpiration.png b/awx/ui_next/docs/images/sessionExpiration.png new file mode 100644 index 0000000000..fa740c44a5 Binary files /dev/null and b/awx/ui_next/docs/images/sessionExpiration.png differ diff --git a/awx/ui_next/docs/images/useStorage.png b/awx/ui_next/docs/images/useStorage.png new file mode 100644 index 0000000000..712b477121 Binary files /dev/null and b/awx/ui_next/docs/images/useStorage.png differ diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json index 9e9d09304a..70e18e4165 100644 --- a/awx/ui_next/package-lock.json +++ b/awx/ui_next/package-lock.json @@ -5,47 +5,46 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "requires": { - "@babel/highlight": "^7.8.3" + "@babel/highlight": "^7.10.4" } }, "@babel/compat-data": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.1.tgz", - "integrity": "sha512-725AQupWJZ8ba0jbKceeFblZTY90McUBWMwHhkFQ9q1zKPJ95GUktljFcgcsIVwRnTnRKlcYzfiNImg5G9m6ZQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz", + "integrity": "sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==", "dev": true }, "@babel/core": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", - "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz", + "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==", "dev": true, "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.0", - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helpers": "^7.9.0", - "@babel/parser": "^7.9.0", - "@babel/template": "^7.8.6", - "@babel/traverse": "^7.9.0", - "@babel/types": "^7.9.0", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.10", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.10", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", "json5": "^2.1.2", - "lodash": "^4.17.13", - "resolve": "^1.3.2", + "lodash": "^4.17.19", "semver": "^5.4.1", "source-map": "^0.5.0" }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -66,29 +65,21 @@ } }, "@babel/generator": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz", - "integrity": "sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", "requires": { - "@babel/types": "^7.9.6", + "@babel/types": "^7.12.11", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } } }, "@babel/helper-annotate-as-pure": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz", - "integrity": "sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", + "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.10" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { @@ -99,25 +90,6 @@ "requires": { "@babel/helper-explode-assignable-expression": "^7.10.4", "@babel/types": "^7.10.4" - }, - "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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-builder-react-jsx": { @@ -128,93 +100,28 @@ "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "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.4" - } - }, - "@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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@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.12.4", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.4.tgz", - "integrity": "sha512-AjEa0jrQqNk7eDQOo0pTfUOwQBMF+xVqrausQwT9/rTKy0g04ggFNaJpaE09IQMn9yExluigWMJcj0WC7bq+Og==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.11.tgz", + "integrity": "sha512-4oGVOekPI8dh9JphkPXC68iIuP6qp/RPbaPmorRmEFbRAHZjSqxPjqHudn18GVDPgCuFM/KdFXc63C17Ygfa9w==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-module-imports": "^7.12.1", - "@babel/types": "^7.12.1" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "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.4" - } - }, - "@babel/helper-module-imports": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.1.tgz", - "integrity": "sha512-ZeC1TlMSvikvJNy1v/wPIazCu3NdOwgYZLIkmIyAsGhqkNpiDoQQRmaCK8YP4Pq3GPTLPV9WXaPCJKvx06JxKA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1" - } - }, - "@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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } + "@babel/helper-annotate-as-pure": "^7.12.10", + "@babel/helper-module-imports": "^7.12.5", + "@babel/types": "^7.12.11" } }, "@babel/helper-compilation-targets": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.1.tgz", - "integrity": "sha512-jtBEif7jsPwP27GPHs06v4WBV0KrE8a/P7n0N0sSvHn2hwUCYnolP/CLmz51IzAW4NlN+HuoBtb9QcwnRo9F/g==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz", + "integrity": "sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==", "dev": true, "requires": { - "@babel/compat-data": "^7.12.1", + "@babel/compat-data": "^7.12.5", "@babel/helper-validator-option": "^7.12.1", - "browserslist": "^4.12.0", + "browserslist": "^4.14.5", "semver": "^5.5.0" }, "dependencies": { @@ -237,130 +144,16 @@ "@babel/helper-optimise-call-expression": "^7.10.4", "@babel/helper-replace-supers": "^7.12.1", "@babel/helper-split-export-declaration": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "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.4" - } - }, - "@babel/helper-function-name": { - "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.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "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.4" - } - }, - "@babel/helper-split-export-declaration": { - "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.11.0" - } - }, - "@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/highlight": { - "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.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz", - "integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==", - "dev": true - }, - "@babel/template": { - "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.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@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.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.1.tgz", - "integrity": "sha512-rsZ4LGvFTZnzdNZR5HZdmJVuXK8834R5QkF3WvcnBhrlVtF0HSIUC6zbreL9MgjTywhKokn8RIYRiq99+DLAxA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz", + "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-regex": "^7.10.4", "regexpu-core": "^4.7.1" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "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.4" - } - }, - "@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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-define-map": { @@ -372,82 +165,6 @@ "@babel/helper-function-name": "^7.10.4", "@babel/types": "^7.10.5", "lodash": "^4.17.19" - }, - "dependencies": { - "@babel/code-frame": { - "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.4" - } - }, - "@babel/helper-function-name": { - "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.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "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.4" - } - }, - "@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/highlight": { - "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.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz", - "integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==", - "dev": true - }, - "@babel/template": { - "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.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-explode-assignable-expression": { @@ -457,43 +174,24 @@ "dev": true, "requires": { "@babel/types": "^7.12.1" - }, - "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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" } }, "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.10" } }, "@babel/helper-hoist-variables": { @@ -503,61 +201,23 @@ "dev": true, "requires": { "@babel/types": "^7.10.4" - }, - "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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@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.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz", - "integrity": "sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", "dev": true, "requires": { - "@babel/types": "^7.12.1" - }, - "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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } + "@babel/types": "^7.12.7" } }, "@babel/helper-module-imports": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", - "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.5" } }, "@babel/helper-module-transforms": { @@ -575,171 +235,15 @@ "@babel/traverse": "^7.12.1", "@babel/types": "^7.12.1", "lodash": "^4.17.19" - }, - "dependencies": { - "@babel/code-frame": { - "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.4" - } - }, - "@babel/generator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.1.tgz", - "integrity": "sha512-DB+6rafIdc9o72Yc3/Ph5h+6hUjeOp66pF0naQBgUFFuPqzQwIlPTm3xZR7YNvduIMtkDIj2t21LSQwnbCrXvg==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "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.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "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.4" - } - }, - "@babel/helper-module-imports": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.1.tgz", - "integrity": "sha512-ZeC1TlMSvikvJNy1v/wPIazCu3NdOwgYZLIkmIyAsGhqkNpiDoQQRmaCK8YP4Pq3GPTLPV9WXaPCJKvx06JxKA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1" - } - }, - "@babel/helper-split-export-declaration": { - "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.11.0" - } - }, - "@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/highlight": { - "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.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz", - "integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==", - "dev": true - }, - "@babel/template": { - "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.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.1.tgz", - "integrity": "sha512-MA3WPoRt1ZHo2ZmoGKNqi20YnPt0B1S0GTZEPhhd+hw2KGUzBlHuVunj6K4sNuK+reEvyiPwtp0cpaqLzJDmAw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.1", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.1", - "@babel/types": "^7.12.1", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "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-optimise-call-expression": { - "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==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", "dev": true, "requires": { - "@babel/types": "^7.10.4" - }, - "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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } + "@babel/types": "^7.12.10" } }, "@babel/helper-plugin-utils": { @@ -748,15 +252,6 @@ "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true }, - "@babel/helper-regex": { - "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.19" - } - }, "@babel/helper-remap-async-to-generator": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", @@ -766,174 +261,18 @@ "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-wrap-function": "^7.10.4", "@babel/types": "^7.12.1" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "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.4" - } - }, - "@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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-replace-supers": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.1.tgz", - "integrity": "sha512-zJjTvtNJnCFsCXVi5rUInstLd/EIVNmIKA1Q9ynESmMBWPWd+7sdR+G4/wdu+Mppfep0XLyG2m7EBPvjCeFyrw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.12.1", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.12.1", - "@babel/types": "^7.12.1" - }, - "dependencies": { - "@babel/code-frame": { - "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.4" - } - }, - "@babel/generator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.1.tgz", - "integrity": "sha512-DB+6rafIdc9o72Yc3/Ph5h+6hUjeOp66pF0naQBgUFFuPqzQwIlPTm3xZR7YNvduIMtkDIj2t21LSQwnbCrXvg==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "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.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "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.4" - } - }, - "@babel/helper-split-export-declaration": { - "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.11.0" - } - }, - "@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/highlight": { - "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.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz", - "integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==", - "dev": true - }, - "@babel/template": { - "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.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.1.tgz", - "integrity": "sha512-MA3WPoRt1ZHo2ZmoGKNqi20YnPt0B1S0GTZEPhhd+hw2KGUzBlHuVunj6K4sNuK+reEvyiPwtp0cpaqLzJDmAw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.1", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.1", - "@babel/types": "^7.12.1", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "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-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" } }, "@babel/helper-simple-access": { @@ -943,25 +282,6 @@ "dev": true, "requires": { "@babel/types": "^7.12.1" - }, - "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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-skip-transparent-expression-wrappers": { @@ -971,44 +291,25 @@ "dev": true, "requires": { "@babel/types": "^7.12.1" - }, - "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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.11" } }, "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" }, "@babel/helper-validator-option": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz", - "integrity": "sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz", + "integrity": "sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==", "dev": true }, "@babel/helper-wrap-function": { @@ -1021,289 +322,33 @@ "@babel/template": "^7.10.4", "@babel/traverse": "^7.10.4", "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "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.4" - } - }, - "@babel/generator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.1.tgz", - "integrity": "sha512-DB+6rafIdc9o72Yc3/Ph5h+6hUjeOp66pF0naQBgUFFuPqzQwIlPTm3xZR7YNvduIMtkDIj2t21LSQwnbCrXvg==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "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.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "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.4" - } - }, - "@babel/helper-split-export-declaration": { - "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.11.0" - } - }, - "@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/highlight": { - "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.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz", - "integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==", - "dev": true - }, - "@babel/template": { - "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.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.1.tgz", - "integrity": "sha512-MA3WPoRt1ZHo2ZmoGKNqi20YnPt0B1S0GTZEPhhd+hw2KGUzBlHuVunj6K4sNuK+reEvyiPwtp0cpaqLzJDmAw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.1", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.1", - "@babel/types": "^7.12.1", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "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/helpers": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.1.tgz", - "integrity": "sha512-9JoDSBGoWtmbay98efmT2+mySkwjzeFeAL9BuWNoVQpkPFQF8SIIFUfY5os9u8wVzglzoiPRSW7cuJmBDUt43g==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", "dev": true, "requires": { "@babel/template": "^7.10.4", - "@babel/traverse": "^7.12.1", - "@babel/types": "^7.12.1" - }, - "dependencies": { - "@babel/code-frame": { - "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.4" - } - }, - "@babel/generator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.1.tgz", - "integrity": "sha512-DB+6rafIdc9o72Yc3/Ph5h+6hUjeOp66pF0naQBgUFFuPqzQwIlPTm3xZR7YNvduIMtkDIj2t21LSQwnbCrXvg==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "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.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "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.4" - } - }, - "@babel/helper-split-export-declaration": { - "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.11.0" - } - }, - "@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/highlight": { - "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.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz", - "integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==", - "dev": true - }, - "@babel/template": { - "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.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.1.tgz", - "integrity": "sha512-MA3WPoRt1ZHo2ZmoGKNqi20YnPt0B1S0GTZEPhhd+hw2KGUzBlHuVunj6K4sNuK+reEvyiPwtp0cpaqLzJDmAw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.1", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.1", - "@babel/types": "^7.12.1", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "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/traverse": "^7.12.5", + "@babel/types": "^7.12.5" } }, "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "requires": { - "@babel/helper-validator-identifier": "^7.9.0", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz", - "integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==" + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==" }, "@babel/plugin-proposal-async-generator-functions": { "version": "7.12.1", @@ -1388,9 +433,9 @@ } }, "@babel/plugin-proposal-numeric-separator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.1.tgz", - "integrity": "sha512-MR7Ok+Af3OhNTCxYVjJZHS0t97ydnJZt/DbR4WISO39iDnhiD8XHrY12xuSJ90FFEGjir0Fzyyn7g/zY6hxbxA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz", + "integrity": "sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", @@ -1419,9 +464,9 @@ } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.1.tgz", - "integrity": "sha512-c2uRpY6WzaVDzynVY9liyykS+kVU+WRZPMPYpkelXH8KBt1oXoI89kPbZKKG/jDT5UK92FTW2fZkZaJhdiBabw==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz", + "integrity": "sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", @@ -1611,34 +656,6 @@ "@babel/helper-module-imports": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-remap-async-to-generator": "^7.12.1" - }, - "dependencies": { - "@babel/helper-module-imports": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.1.tgz", - "integrity": "sha512-ZeC1TlMSvikvJNy1v/wPIazCu3NdOwgYZLIkmIyAsGhqkNpiDoQQRmaCK8YP4Pq3GPTLPV9WXaPCJKvx06JxKA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1" - } - }, - "@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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-block-scoped-functions": { @@ -1651,9 +668,9 @@ } }, "@babel/plugin-transform-block-scoping": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz", - "integrity": "sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.11.tgz", + "integrity": "sha512-atR1Rxc3hM+VPg/NvNvfYw0npQEAcHuJ+MGZnFn6h3bo+1U3BWXMdFMlvVRApBTWKQMX7SOwRJZA5FBF/JQbvA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" @@ -1673,100 +690,6 @@ "@babel/helper-replace-supers": "^7.12.1", "@babel/helper-split-export-declaration": "^7.10.4", "globals": "^11.1.0" - }, - "dependencies": { - "@babel/code-frame": { - "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.4" - } - }, - "@babel/helper-annotate-as-pure": { - "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.4" - } - }, - "@babel/helper-function-name": { - "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.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "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.4" - } - }, - "@babel/helper-split-export-declaration": { - "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.11.0" - } - }, - "@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/highlight": { - "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.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz", - "integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==", - "dev": true - }, - "@babel/template": { - "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.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-computed-properties": { @@ -1843,82 +766,6 @@ "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "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.4" - } - }, - "@babel/helper-function-name": { - "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.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "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.4" - } - }, - "@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/highlight": { - "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.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz", - "integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==", - "dev": true - }, - "@babel/template": { - "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.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-literals": { @@ -1973,14 +820,6 @@ "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-validator-identifier": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "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/plugin-transform-modules-umd": { @@ -2058,24 +897,24 @@ } }, "@babel/plugin-transform-react-jsx": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.1.tgz", - "integrity": "sha512-RmKejwnT0T0QzQUzcbP5p1VWlpnP8QHtdhEtLG55ZDQnJNalbF3eeDyu3dnGKvGzFIQiBzFhBYTwvv435p9Xpw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.11.tgz", + "integrity": "sha512-5nWOw6mTylaFU72BdZfa0dP1HsGdY3IMExpxn8LBE8dNmkQjB+W+sR+JwIdtbzkPvVuFviT3zyNbSUkuVTVxbw==", "dev": true, "requires": { "@babel/helper-builder-react-jsx": "^7.10.4", - "@babel/helper-builder-react-jsx-experimental": "^7.12.1", + "@babel/helper-builder-react-jsx-experimental": "^7.12.11", "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-jsx": "^7.12.1" } }, "@babel/plugin-transform-react-jsx-development": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.1.tgz", - "integrity": "sha512-IilcGWdN1yNgEGOrB96jbTplRh+V2Pz1EoEwsKsHfX1a/L40cUYuD71Zepa7C+ujv7kJIxnDftWeZbKNEqZjCQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.11.tgz", + "integrity": "sha512-5MvsGschXeXJsbzQGR/BH89ATMzCsM7rx95n+R7/852cGoK2JgMbacDw/A9Pmrfex4tArdMab0L5SBV4SB/Nxg==", "dev": true, "requires": { - "@babel/helper-builder-react-jsx-experimental": "^7.12.1", + "@babel/helper-builder-react-jsx-experimental": "^7.12.11", "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-jsx": "^7.12.1" } @@ -2106,34 +945,6 @@ "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "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.4" - } - }, - "@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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-regenerator": { @@ -2194,13 +1005,12 @@ } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.1.tgz", - "integrity": "sha512-CiUgKQ3AGVk7kveIaPEET1jNDhZZEl1RPMWdTBE1799bdz++SwqDHStmxfCtDfBhQgCl38YRiSnrMuUMZIWSUQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz", + "integrity": "sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-regex": "^7.10.4" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-template-literals": { @@ -2213,9 +1023,9 @@ } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz", - "integrity": "sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz", + "integrity": "sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" @@ -2252,9 +1062,9 @@ } }, "@babel/polyfill": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.8.7.tgz", - "integrity": "sha512-LeSfP9bNZH2UOZgcGcZ0PIHUt1ZuHub1L3CVmEyqLxCeDLm4C5Gi8jRH8ZX2PNpDhQCo0z6y/+DIs2JlliXW8w==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz", + "integrity": "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==", "dev": true, "requires": { "core-js": "^2.6.5", @@ -2262,24 +1072,24 @@ }, "dependencies": { "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 } } }, "@babel/preset-env": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.1.tgz", - "integrity": "sha512-H8kxXmtPaAGT7TyBvSSkoSTUK6RHh61So05SyEbpmr0MCZrsNYn7mGMzzeYoOUCdHzww61k8XBft2TaES+xPLg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.11.tgz", + "integrity": "sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw==", "dev": true, "requires": { - "@babel/compat-data": "^7.12.1", - "@babel/helper-compilation-targets": "^7.12.1", - "@babel/helper-module-imports": "^7.12.1", + "@babel/compat-data": "^7.12.7", + "@babel/helper-compilation-targets": "^7.12.5", + "@babel/helper-module-imports": "^7.12.5", "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-validator-option": "^7.12.1", + "@babel/helper-validator-option": "^7.12.11", "@babel/plugin-proposal-async-generator-functions": "^7.12.1", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-dynamic-import": "^7.12.1", @@ -2287,10 +1097,10 @@ "@babel/plugin-proposal-json-strings": "^7.12.1", "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", - "@babel/plugin-proposal-numeric-separator": "^7.12.1", + "@babel/plugin-proposal-numeric-separator": "^7.12.7", "@babel/plugin-proposal-object-rest-spread": "^7.12.1", "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", - "@babel/plugin-proposal-optional-chaining": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.7", "@babel/plugin-proposal-private-methods": "^7.12.1", "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", "@babel/plugin-syntax-async-generators": "^7.8.0", @@ -2308,7 +1118,7 @@ "@babel/plugin-transform-arrow-functions": "^7.12.1", "@babel/plugin-transform-async-to-generator": "^7.12.1", "@babel/plugin-transform-block-scoped-functions": "^7.12.1", - "@babel/plugin-transform-block-scoping": "^7.12.1", + "@babel/plugin-transform-block-scoping": "^7.12.11", "@babel/plugin-transform-classes": "^7.12.1", "@babel/plugin-transform-computed-properties": "^7.12.1", "@babel/plugin-transform-destructuring": "^7.12.1", @@ -2332,43 +1142,17 @@ "@babel/plugin-transform-reserved-words": "^7.12.1", "@babel/plugin-transform-shorthand-properties": "^7.12.1", "@babel/plugin-transform-spread": "^7.12.1", - "@babel/plugin-transform-sticky-regex": "^7.12.1", + "@babel/plugin-transform-sticky-regex": "^7.12.7", "@babel/plugin-transform-template-literals": "^7.12.1", - "@babel/plugin-transform-typeof-symbol": "^7.12.1", + "@babel/plugin-transform-typeof-symbol": "^7.12.10", "@babel/plugin-transform-unicode-escapes": "^7.12.1", "@babel/plugin-transform-unicode-regex": "^7.12.1", "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.12.1", - "core-js-compat": "^3.6.2", + "@babel/types": "^7.12.11", + "core-js-compat": "^3.8.0", "semver": "^5.5.0" }, "dependencies": { - "@babel/helper-module-imports": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.1.tgz", - "integrity": "sha512-ZeC1TlMSvikvJNy1v/wPIazCu3NdOwgYZLIkmIyAsGhqkNpiDoQQRmaCK8YP4Pq3GPTLPV9WXaPCJKvx06JxKA==", - "dev": true, - "requires": { - "@babel/types": "^7.12.1" - } - }, - "@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.12.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", - "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -2391,17 +1175,15 @@ } }, "@babel/preset-react": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.1.tgz", - "integrity": "sha512-euCExymHCi0qB9u5fKw7rvlw7AZSjw/NaB9h7EkdTt5+yHRrXdiRTh7fkG3uBPpJg82CqLfp1LHLqWGSCrab+g==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.10.tgz", + "integrity": "sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-transform-react-display-name": "^7.12.1", - "@babel/plugin-transform-react-jsx": "^7.12.1", - "@babel/plugin-transform-react-jsx-development": "^7.12.1", - "@babel/plugin-transform-react-jsx-self": "^7.12.1", - "@babel/plugin-transform-react-jsx-source": "^7.12.1", + "@babel/plugin-transform-react-jsx": "^7.12.10", + "@babel/plugin-transform-react-jsx-development": "^7.12.7", "@babel/plugin-transform-react-pure-annotations": "^7.12.1" } }, @@ -2416,24 +1198,24 @@ } }, "@babel/runtime": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz", - "integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", "requires": { "regenerator-runtime": "^0.13.4" }, "dependencies": { "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==" } } }, "@babel/runtime-corejs3": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.6.tgz", - "integrity": "sha512-6toWAfaALQjt3KMZQc6fABqZwUDDuWzz+cAfPhqyEnzxvdWOAkjwPNxgF8xlmo7OWLsSjaKjsskpKHRLaMArOA==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz", + "integrity": "sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==", "dev": true, "requires": { "core-js-pure": "^3.0.0", @@ -2441,45 +1223,45 @@ }, "dependencies": { "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 } } }, "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" } }, "@babel/traverse": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz", - "integrity": "sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz", + "integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==", "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.6", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.6", - "@babel/types": "^7.9.6", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.10", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.10", + "@babel/types": "^7.12.10", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -2490,12 +1272,12 @@ } }, "@babel/types": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz", - "integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "requires": { - "@babel/helper-validator-identifier": "^7.9.5", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -2521,21 +1303,52 @@ "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==", "dev": true }, + "@cypress/instrument-cra": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@cypress/instrument-cra/-/instrument-cra-1.4.0.tgz", + "integrity": "sha512-gXf540xL0jcUXkWyrA2Ug9rzs+jRkc9EPhnRi8XfbnRjdF4lvnn108N6x0lgTApMTbbpCDbVuskHGXDmIuD3CQ==", + "dev": true, + "requires": { + "babel-plugin-istanbul": "6.0.0", + "debug": "4.2.0", + "find-yarn-workspace-root": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "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 + } + } + }, "@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", "requires": { "@emotion/memoize": "0.7.4" - }, - "dependencies": { - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" - } } }, + "@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -2575,6 +1388,25 @@ "@hapi/hoek": "^8.3.0" } }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, "@jest/console": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", @@ -2622,11 +1454,135 @@ "strip-ansi": "^5.0.0" }, "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } } } }, @@ -2682,6 +1638,27 @@ "string-length": "^2.0.0" }, "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2701,6 +1678,12 @@ "source-map": "^0.6.0" }, "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2756,11 +1739,253 @@ "write-file-atomic": "2.4.1" }, "dependencies": { + "babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "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" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "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" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "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" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } } } }, @@ -2792,9 +2017,9 @@ "dev": true }, "@lingui/babel-plugin-transform-react": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@lingui/babel-plugin-transform-react/-/babel-plugin-transform-react-2.9.1.tgz", - "integrity": "sha512-3btgAUpH5OSlZf/c4bXsDtvPity+H+7Mm1GJmWEwQSjl5kQNLisNVHpWPKvESuNbfQEtmtK5iIMVUYx8GD/Sig==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@lingui/babel-plugin-transform-react/-/babel-plugin-transform-react-2.9.2.tgz", + "integrity": "sha512-bxvrepiS6J9vZqRtpRiAgBIASQscjvu7aFmPqH4Y6001TDXrYuyhhNRt1BI3k2E6C2SckHh5vRtSpsqpjEiY3A==", "dev": true }, "@lingui/cli": { @@ -2827,123 +2052,6 @@ "pofile": "^1.0.11", "pseudolocale": "^1.1.0", "ramda": "^0.26.1" - }, - "dependencies": { - "@lingui/babel-plugin-transform-react": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@lingui/babel-plugin-transform-react/-/babel-plugin-transform-react-2.9.2.tgz", - "integrity": "sha512-bxvrepiS6J9vZqRtpRiAgBIASQscjvu7aFmPqH4Y6001TDXrYuyhhNRt1BI3k2E6C2SckHh5vRtSpsqpjEiY3A==", - "dev": true - }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - } - }, - "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 - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "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": { - "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" - } - } - } - } } }, "@lingui/conf": { @@ -2960,9 +2068,9 @@ } }, "@lingui/core": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@lingui/core/-/core-2.9.1.tgz", - "integrity": "sha512-oygikacnRlfR2p8Q30q9z7jBASvVd8JXgoq7q2Ao/NdZyk3YIYbDhBICxAtwlCbm1hSROh2xMS2Ygx3Ezwzp1Q==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@lingui/core/-/core-2.9.2.tgz", + "integrity": "sha512-prrEGlhbvqbyvHMfgKXaaGDM4cGCofca1lOfIOEEX1rZreRjG7Y+cga0oEEQJ9xS59uMht9GGFOwQJsGHYIU0g==", "requires": { "babel-runtime": "^6.26.0", "make-plural": "^4.1.1", @@ -2970,20 +2078,20 @@ } }, "@lingui/macro": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@lingui/macro/-/macro-2.9.1.tgz", - "integrity": "sha512-iuT4rNl57h574vUfO9sYWstpbNMMaBxdscyOnY/R+HIPX0cPSjqaDmKTDInfI1gFpYznM2OOhOpqxMMUGt2+Gw==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@lingui/macro/-/macro-2.9.2.tgz", + "integrity": "sha512-IFv9h3LL/vjMz98JiDpckIsAlIcnCNuCD4+/C8KK33qGsU9MbCF+zjIztdEWlm3JlnFNm1/qIw7CkrtdoH0RpA==", "dev": true, "requires": { - "@lingui/babel-plugin-transform-react": "2.9.1" + "@lingui/babel-plugin-transform-react": "2.9.2" } }, "@lingui/react": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@lingui/react/-/react-2.9.1.tgz", - "integrity": "sha512-LUpsw7zDUfS/iJWrBfmaVKcxjyNUQkMjBRuLVEzLHp32A6pK2CGFlDouBsEl1zbJn8gFdcBwWhTsh/FcXZCmXg==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@lingui/react/-/react-2.9.2.tgz", + "integrity": "sha512-grlarLgJt6vu9wkvGetLunbrImRlL5bsnc4CdtXCNwm0r+8srI+mow5PimtK+MrtOSjXSqLdt/giMYFBE3MOSw==", "requires": { - "@lingui/core": "2.9.1", + "@lingui/core": "2.9.2", "babel-runtime": "^6.26.0", "hash-sum": "^1.0.2", "hoist-non-react-statics": "3.3.0", @@ -3013,45 +2121,68 @@ "dev": true }, "@patternfly/patternfly": { - "version": "4.59.1", - "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.59.1.tgz", - "integrity": "sha512-zk3aqg62JXMTzzJMJsyVgt5fXlcxUUkRKkaxUv/hwpjhGiyLexZ1l3Gupb9ziYl74p38KzbbfcfdnlFCwJZfgg==" + "version": "4.70.2", + "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.70.2.tgz", + "integrity": "sha512-XKCHnOjx1JThY3s98AJhsApSsGHPvEdlY7r+b18OecqUnmThVGw3nslzYYrwfCGlJ/xQtV5so29SduH2/uhHzA==" }, "@patternfly/react-core": { - "version": "4.75.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.75.2.tgz", - "integrity": "sha512-hoCeTahVgl5I0jA74/so2bZL1eabSD2Uwt8uYgz+W+ShYAC4rJreLOVn43I15J5VPpwIvxrbubccpBqCGL2+wA==", + "version": "4.84.3", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.84.3.tgz", + "integrity": "sha512-VeCv/r09ay6yIER7Eb8Dp5ZhbDu6SCW9smwgUTNp80kt83wIfKvGvQOKg+/7cev/GC6VzfgPHhiS04Jm/N5loA==", "requires": { - "@patternfly/react-icons": "^4.7.16", - "@patternfly/react-styles": "^4.7.12", - "@patternfly/react-tokens": "^4.9.16", - "focus-trap": "4.0.2", + "@patternfly/react-icons": "^4.7.22", + "@patternfly/react-styles": "^4.7.22", + "@patternfly/react-tokens": "^4.9.22", + "focus-trap": "6.2.2", "react-dropzone": "9.0.0", "tippy.js": "5.1.2", "tslib": "1.13.0" - }, - "dependencies": { - "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" - } } }, "@patternfly/react-icons": { - "version": "4.7.16", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.7.16.tgz", - "integrity": "sha512-g1RsaGy/FRsHINdHpHR9RNhC72qs5W/0wIUQVB0HPtCrXdr1UfJCh03RRIhixfGPQDYgw9Ou3j3rARljhwUPUw==" + "version": "4.7.22", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.7.22.tgz", + "integrity": "sha512-JDsnebr9CNIyrv9yjaGFQ56OChbV6KcxMYBIpNc8/sZdU4TXHWNC7P7rlUM/BuGpbWvyaOJtscRuf5uteIKX3A==" }, "@patternfly/react-styles": { - "version": "4.7.12", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.7.12.tgz", - "integrity": "sha512-+IfOc0E440Q61IYQZbux7pwcLf7JWQd1rOs2DDWnoVaw82zr/BZMtGh9T2kcklcH7TUHavSyF6tlCgU40IQ/4g==" + "version": "4.7.22", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.7.22.tgz", + "integrity": "sha512-ojNuSNJx6CkNtsSFseZ2SJEVyzPMFYh0jOs204ICzYM1+fn9acsIi3Co0bcskFRzw8F6e2/x+8uVNx6QI8elxg==" + }, + "@patternfly/react-table": { + "version": "4.19.45", + "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.19.45.tgz", + "integrity": "sha512-HFOefgyZ8SGbLflGlEqefHrdxrV+X+x7+bBRvsB+zFZAYWDVau3Z6Pkv8gY5JselBsMywZYPb5juHBoBkzuezg==", + "requires": { + "@patternfly/patternfly": "4.70.2", + "@patternfly/react-core": "^4.84.4", + "@patternfly/react-icons": "^4.7.22", + "@patternfly/react-styles": "^4.7.22", + "@patternfly/react-tokens": "^4.9.22", + "lodash": "^4.17.19", + "tslib": "1.13.0" + }, + "dependencies": { + "@patternfly/react-core": { + "version": "4.84.4", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.84.4.tgz", + "integrity": "sha512-GIv6zJl+NGYIrSm2pATQFggRt4ZFYUKVNbLSsna2x8+eYeZscqIUFSPmBM+eNpQkGaoJk254Lu09pgm4hM/b4g==", + "requires": { + "@patternfly/react-icons": "^4.7.22", + "@patternfly/react-styles": "^4.7.22", + "@patternfly/react-tokens": "^4.9.22", + "focus-trap": "6.2.2", + "react-dropzone": "9.0.0", + "tippy.js": "5.1.2", + "tslib": "1.13.0" + } + } + } }, "@patternfly/react-tokens": { - "version": "4.9.16", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.9.16.tgz", - "integrity": "sha512-gqwhMDYHB2R01PlmT+CwaIQiXvc8s7ZywpLpO63rWjy+37cgtgBMApIxZx2tALTuJDYTmu9F/SUce5CUQIgwkQ==" + "version": "4.9.22", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.9.22.tgz", + "integrity": "sha512-hN/8u7mFR62naFB2hdO7nl1p/0lCXtNq+VY+BAbp4UFC2/QyjNP0IOPBR+mR9Pbj5JwxrURI7G5blLp+k9RLvQ==" }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", @@ -3177,9 +2308,9 @@ } }, "@types/babel__core": { - "version": "7.1.10", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.10.tgz", - "integrity": "sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw==", + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", + "integrity": "sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ==", "dev": true, "requires": { "@babel/parser": "^7.1.0", @@ -3199,9 +2330,9 @@ } }, "@types/babel__template": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.3.tgz", - "integrity": "sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", + "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", "dev": true, "requires": { "@babel/parser": "^7.1.0", @@ -3209,19 +2340,22 @@ } }, "@types/babel__traverse": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.15.tgz", - "integrity": "sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.0.tgz", + "integrity": "sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg==", "dev": true, "requires": { "@babel/types": "^7.3.0" } }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "@types/cheerio": { + "version": "0.22.23", + "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.23.tgz", + "integrity": "sha512-QfHLujVMlGqcS/ePSf3Oe5hK3H8wi/yN2JYuxSB1U10VvW1fO3K8C+mURQesFYS1Hn7lspOsTT75SKq/XtydQg==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "@types/eslint-visitor-keys": { "version": "1.0.0", @@ -3249,9 +2383,9 @@ } }, "@types/istanbul-lib-coverage": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", - "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", "dev": true }, "@types/istanbul-lib-report": { @@ -3264,9 +2398,9 @@ } }, "@types/istanbul-reports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", - "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "*", @@ -3279,6 +2413,12 @@ "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", "dev": true }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -3286,9 +2426,9 @@ "dev": true }, "@types/node": { - "version": "13.13.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.5.tgz", - "integrity": "sha512-3ySmiBYJPqgjiHA7oEaIo2Rzz0HrOZ7yrNO5HWyaE5q0lQ3BppDZ3N53Miz8bw2I7gh1/zir2MGVZBvpb1zq9g==", + "version": "14.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz", + "integrity": "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==", "dev": true }, "@types/parse-json": { @@ -3310,9 +2450,9 @@ "dev": true }, "@types/yargs": { - "version": "13.0.8", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz", - "integrity": "sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA==", + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -3334,6 +2474,14 @@ "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", "tsutils": "^3.17.1" + }, + "dependencies": { + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + } } }, "@typescript-eslint/experimental-utils": { @@ -3346,6 +2494,17 @@ "@typescript-eslint/typescript-estree": "2.34.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + } } }, "@typescript-eslint/parser": { @@ -3376,9 +2535,9 @@ }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -3391,10 +2550,13 @@ "dev": true }, "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } } } }, @@ -3603,9 +2765,9 @@ } }, "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true }, "acorn-globals": { @@ -3627,9 +2789,9 @@ } }, "acorn-jsx": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", - "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "dev": true }, "acorn-walk": { @@ -3678,27 +2840,26 @@ } }, "airbnb-prop-types": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz", - "integrity": "sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", + "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==", "dev": true, "requires": { - "array.prototype.find": "^2.1.0", - "function.prototype.name": "^1.1.1", - "has": "^1.0.3", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", + "array.prototype.find": "^2.1.1", + "function.prototype.name": "^1.1.2", + "is-regex": "^1.1.0", + "object-is": "^1.1.2", "object.assign": "^4.1.0", - "object.entries": "^1.1.0", + "object.entries": "^1.1.2", "prop-types": "^15.7.2", "prop-types-exact": "^1.2.0", - "react-is": "^16.9.0" + "react-is": "^16.13.1" } }, "ajv": { - "version": "6.12.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", - "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -3732,21 +2893,10 @@ "dev": true }, "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - }, - "dependencies": { - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true - } - } + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true }, "ansi-html": { "version": "0.0.7", @@ -3755,10 +2905,9 @@ "dev": true }, "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "ansi-styles": { "version": "3.2.1", @@ -3784,6 +2933,123 @@ "requires": { "micromatch": "^3.1.4", "normalize-path": "^2.1.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } } }, "aproba": { @@ -3800,6 +3066,16 @@ "sprintf-js": "~1.0.2" } }, + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + }, "arity-n": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz", @@ -3849,13 +3125,15 @@ "dev": true }, "array-includes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", - "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.2.tgz", + "integrity": "sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", + "es-abstract": "^1.18.0-next.1", + "get-intrinsic": "^1.0.1", "is-string": "^1.0.5" } }, @@ -3888,16 +3166,50 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.4" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "array.prototype.flat": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", - "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz", + "integrity": "sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" + "es-abstract": "^1.18.0-next.1" + } + }, + "array.prototype.flatmap": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", + "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "function-bind": "^1.1.1" } }, "arrify": { @@ -3922,31 +3234,50 @@ } }, "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "dev": true, "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" }, "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==", + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", "dev": true } } }, "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", "dev": true, "requires": { + "object-assign": "^4.1.1", "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } } }, "assert-plus": { @@ -4027,6 +3358,14 @@ "num2fraction": "^1.2.2", "postcss": "^7.0.32", "postcss-value-parser": "^4.1.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + } } }, "aws-sign2": { @@ -4036,24 +3375,36 @@ "dev": true }, "aws4": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", - "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "axe-core": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz", + "integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==", "dev": true }, "axios": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", - "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" + "follow-redirects": "^1.10.0" + }, + "dependencies": { + "follow-redirects": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", + "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" + } } }, "axobject-query": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.2.tgz", - "integrity": "sha512-ICt34ZmrVt8UQnvPl6TVyDTkmhXmAyAT4Jh5ugfGUX4MOrZ+U/ZY6/sdylRw3qGNr9Ub5AJsaHeDMzNLehRdOQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", "dev": true }, "babel-code-frame": { @@ -4190,6 +3541,135 @@ "babel-preset-jest": "^24.9.0", "chalk": "^2.4.2", "slash": "^2.0.0" + }, + "dependencies": { + "babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + } + }, + "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" + } + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "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" + } + }, + "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" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + } + } } }, "babel-loader": { @@ -4203,14 +3683,6 @@ "mkdirp": "^0.5.3", "pify": "^4.0.1", "schema-utils": "^2.6.5" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } } }, "babel-messages": { @@ -4232,60 +3704,16 @@ } }, "babel-plugin-istanbul": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", - "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", - "find-up": "^3.0.0", - "istanbul-lib-instrument": "^3.3.0", - "test-exclude": "^5.2.3" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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 - } + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" } }, "babel-plugin-jest-hoist": { @@ -4321,6 +3749,16 @@ "yaml": "^1.7.2" } }, + "import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, "parse-json": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", @@ -4338,19 +3776,25 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true } } }, "babel-plugin-named-asset-import": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz", - "integrity": "sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz", + "integrity": "sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw==", "dev": true }, "babel-plugin-styled-components": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.7.tgz", - "integrity": "sha512-MBMHGcIA22996n9hZRf/UJLVVgkEOITuR2SvjHLb5dSTUyR4ZRGn+ngITapes36FI3WLxZHfRhkA1ffHxihOrg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.12.0.tgz", + "integrity": "sha512-FEiD7l5ZABdJPpLssKXjBUJMYqzbcNzBowfXDCdJhOpbhWiewapUaY+LZGT8R4Jg2TwOjGjG4RKeyrO5p9sBkA==", "requires": { "@babel/helper-annotate-as-pure": "^7.0.0", "@babel/helper-module-imports": "^7.0.0", @@ -4360,7 +3804,7 @@ }, "babel-plugin-syntax-jsx": { "version": "6.18.0", - "resolved": "http://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" }, "babel-plugin-syntax-object-rest-spread": { @@ -4418,6 +3862,30 @@ "babel-plugin-transform-react-remove-prop-types": "0.4.24" }, "dependencies": { + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, "@babel/plugin-proposal-class-properties": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", @@ -4558,6 +4026,21 @@ "regenerator-runtime": "^0.13.4" } }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "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 + }, "regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", @@ -4675,9 +4158,9 @@ } }, "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, "batch": { @@ -4687,9 +4170,9 @@ "dev": true }, "bcp-47": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-1.0.7.tgz", - "integrity": "sha512-XywQRckEigetKCTuxsaecL/68psvr7ayWsPq6LLwoz5k+qwpwnpcTMyU/Gs+JO3u8J+BxofouYCS+s9ACiNyrw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-1.0.8.tgz", + "integrity": "sha512-Y9y1QNBBtYtv7hcmoX0tR+tUNSFZGZ6OL6vKPObq8BbOhkCoyayF6ogfLTgAli/KuAEbsYHYUNq2AQuY6IuLag==", "dev": true, "requires": { "is-alphabetical": "^1.0.0", @@ -4718,6 +4201,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", @@ -4725,9 +4218,9 @@ "dev": true }, "bn.js": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz", - "integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", "dev": true }, "body-parser": { @@ -4802,32 +4295,12 @@ } }, "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "fill-range": "^7.0.1" } }, "brorand": { @@ -4861,7 +4334,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -4897,64 +4370,37 @@ } }, "browserify-rsa": { - "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", "dev": true, "requires": { - "bn.js": "^4.1.0", + "bn.js": "^5.0.0", "randombytes": "^2.0.1" - }, - "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 - } } }, "browserify-sign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.1.0.tgz", - "integrity": "sha512-VYxo7cDCeYUoBZ0ZCy4UyEUCP3smyBd4DRQM5nrFS1jJjPJjX7rP3oLRpPoWfkhQfyJ0I9ZbHbKafrFD/SGlrg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", "dev": true, "requires": { "bn.js": "^5.1.1", "browserify-rsa": "^4.0.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.2", + "elliptic": "^6.5.3", "inherits": "^2.0.4", "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0" + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" }, "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } } } }, @@ -4968,15 +4414,16 @@ } }, "browserslist": { - "version": "4.14.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz", - "integrity": "sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.0.tgz", + "integrity": "sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001135", - "electron-to-chromium": "^1.3.571", - "escalade": "^3.1.0", - "node-releases": "^1.1.61" + "caniuse-lite": "^1.0.30001165", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.621", + "escalade": "^3.1.1", + "node-releases": "^1.1.67" } }, "bser": { @@ -5063,6 +4510,15 @@ "unique-filename": "^1.1.1" }, "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -5071,6 +4527,12 @@ "requires": { "glob": "^7.1.3" } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -5091,6 +4553,16 @@ "unset-value": "^1.0.0" } }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + } + }, "call-me-maybe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", @@ -5104,14 +4576,6 @@ "dev": true, "requires": { "callsites": "^2.0.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - } } }, "caller-path": { @@ -5124,19 +4588,27 @@ } }, "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", "dev": true }, "camel-case": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", - "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", "dev": true, "requires": { - "pascal-case": "^3.1.1", - "tslib": "^1.10.0" + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } } }, "camelcase": { @@ -5163,9 +4635,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001150", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001150.tgz", - "integrity": "sha512-kiNKvihW0m36UhAFnl7bOAv0i1K1f6wpfVtTF5O5O82XzgtBnb05V0XeV3oZ968vfg2sRNChsHw8ASH2hDfoYQ==", + "version": "1.0.30001168", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001168.tgz", + "integrity": "sha512-P2zmX7swIXKu+GMMR01TWa4csIKELTNnZKc+f1CjebmZJQtTAEXmpQSoKVJVVcvPGAA0TEYTOUp3VehavZSFPQ==", "dev": true }, "capture-exit": { @@ -5217,55 +4689,6 @@ "htmlparser2": "^3.9.1", "lodash": "^4.15.0", "parse5": "^3.0.1" - }, - "dependencies": { - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - } - }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true - }, - "dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "dev": true, - "requires": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "parse5": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", - "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", - "dev": true, - "requires": { - "@types/node": "*" - } - } } }, "chokidar": { @@ -5294,44 +4717,11 @@ "picomatch": "^2.0.4" } }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } } } }, @@ -5413,27 +4803,28 @@ "dev": true }, "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", "dev": true, "requires": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^2.0.0" } }, "cli-spinners": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.4.0.tgz", - "integrity": "sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.5.0.tgz", + "integrity": "sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==", "dev": true }, "cli-table": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.4.tgz", + "integrity": "sha512-1vinpnX/ZERcmE443i3SZTmU5DF0rPO9DrL4I2iVAllhxzCM9SzPlHnz19fsZB78htkKZvYBvj6SZ6vXnaxmTA==", "dev": true, "requires": { - "colors": "1.0.3" + "chalk": "^2.4.1", + "string-width": "^4.2.0" } }, "cli-width": { @@ -5453,6 +4844,12 @@ "wrap-ansi": "^5.1.0" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -5475,6 +4872,15 @@ "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, @@ -5498,9 +4904,9 @@ } }, "clsx": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz", - "integrity": "sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==" }, "co": { "version": "4.6.0", @@ -5520,9 +4926,9 @@ } }, "codemirror": { - "version": "5.53.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.53.2.tgz", - "integrity": "sha512-wvSQKS4E+P8Fxn/AQ+tQtJnF1qH5UOlxtugFLpubEZ5jcdH2iXTVinb+Xc/4QjshuOxRm4fUsU2QPF1JJKiyXA==" + "version": "5.58.3", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.58.3.tgz", + "integrity": "sha512-KBhB+juiyOOgn0AqtRmWyAT3yoElkuvWTI6hsHa9E6GQrl6bk/fdAYcvuqW1/upO9T9rtEtapWdw4XYcNiVDEA==" }, "collection-visit": { "version": "1.0.0", @@ -5573,12 +4979,6 @@ "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "dev": true }, - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", - "dev": true - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5671,12 +5071,44 @@ "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "confusing-browser-globals": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz", - "integrity": "sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz", + "integrity": "sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA==", "dev": true }, "connect-history-api-fallback": { @@ -5760,17 +5192,17 @@ "dev": true }, "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" }, "core-js-compat": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", - "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.1.tgz", + "integrity": "sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ==", "dev": true, "requires": { - "browserslist": "^4.8.5", + "browserslist": "^4.15.0", "semver": "7.0.0" }, "dependencies": { @@ -5783,9 +5215,9 @@ } }, "core-js-pure": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", - "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.8.1.tgz", + "integrity": "sha512-Se+LaxqXlVXGvmexKGPvnUIYC1jwXu1H6Pkyb3uBM5d8/NELMYCHs/4/roD7721NxrTLyv7e5nXd5/QLBO+10g==", "dev": true }, "core-util-is": { @@ -5804,47 +5236,29 @@ "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" - }, - "dependencies": { - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } } }, "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "dev": true, "requires": { "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "elliptic": "^6.5.3" }, "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==", + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", "dev": true } } }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -5857,7 +5271,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -6013,6 +5427,12 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true } } }, @@ -6026,15 +5446,15 @@ } }, "css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "dev": true, "requires": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" } }, "css-select-base-adapter": { @@ -6051,13 +5471,6 @@ "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^3.3.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } } }, "css-tree": { @@ -6079,9 +5492,9 @@ } }, "css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", "dev": true }, "cssdb": { @@ -6174,28 +5587,28 @@ "dev": true }, "csso": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz", - "integrity": "sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", "dev": true, "requires": { - "css-tree": "1.0.0-alpha.39" + "css-tree": "^1.1.2" }, "dependencies": { "css-tree": { - "version": "1.0.0-alpha.39", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz", - "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz", + "integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==", "dev": true, "requires": { - "mdn-data": "2.0.6", + "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "mdn-data": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz", - "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "dev": true }, "source-map": { @@ -6222,9 +5635,9 @@ } }, "csstype": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.10.tgz", - "integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==" }, "cyclist": { "version": "1.0.1", @@ -6291,9 +5704,9 @@ "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" }, "d3-brush": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.5.tgz", - "integrity": "sha512-rEaJ5gHlgLxXugWjIkolTA0OyMvw8UWU1imYXy1v642XyyswmI1ybKOv05Ft+ewq+TFmdliD3VuK0pRp1VT/5A==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", "requires": { "d3-dispatch": "1", "d3-drag": "1", @@ -6354,14 +5767,14 @@ } }, "d3-ease": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.6.tgz", - "integrity": "sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" }, "d3-fetch": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz", - "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", "requires": { "d3-dsv": "1" } @@ -6378,14 +5791,14 @@ } }, "d3-format": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.4.tgz", - "integrity": "sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw==" + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" }, "d3-geo": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.0.tgz", - "integrity": "sha512-NalZVW+6/SpbKcnl+BCO67m8gX+nGeJdo6oGL9H6BRUGUL1e+AtPcP4vE4TwCQ/gl8y5KE7QvBzrLn+HsKIl+w==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", "requires": { "d3-array": "1" } @@ -6446,9 +5859,9 @@ } }, "d3-selection": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.1.tgz", - "integrity": "sha512-BTIbRjv/m5rcVTfBs4AMBLKs4x8XaaLkwm28KWu9S2vKNqXkXt2AH2Qf0sdPZHjFxcWg/YL53zcqAz+3g4/7PA==" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" }, "d3-shape": { "version": "1.3.7", @@ -6464,9 +5877,9 @@ "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" }, "d3-time-format": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz", - "integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", "requires": { "d3-time": "1" } @@ -6564,6 +5977,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -6631,14 +6045,6 @@ "dev": true, "requires": { "object-keys": "^1.0.12" - }, - "dependencies": { - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - } } }, "define-property": { @@ -6729,12 +6135,6 @@ "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 } } }, @@ -6816,7 +6216,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -6826,9 +6226,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==", + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", "dev": true } } @@ -6841,6 +6241,23 @@ "requires": { "arrify": "^1.0.1", "path-type": "^3.0.0" + }, + "dependencies": { + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } } }, "discontinuous-range": { @@ -6893,36 +6310,22 @@ } }, "dom-helpers": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz", - "integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", + "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", "requires": { "@babel/runtime": "^7.8.7", - "csstype": "^2.6.7" + "csstype": "^3.0.2" } }, "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", "dev": true, "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", - "dev": true - }, - "entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", - "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", - "dev": true - } + "domelementtype": "^1.3.0", + "entities": "^1.1.1" } }, "domain-browser": { @@ -6956,9 +6359,9 @@ } }, "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "dev": true, "requires": { "dom-serializer": "0", @@ -6966,13 +6369,21 @@ } }, "dot-case": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz", - "integrity": "sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dev": true, "requires": { - "no-case": "^3.0.3", - "tslib": "^1.10.0" + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } } }, "dot-prop": { @@ -7012,6 +6423,38 @@ "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "ecc-jsbn": { @@ -7031,9 +6474,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.582", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.582.tgz", - "integrity": "sha512-0nCJ7cSqnkMC+kUuPs0YgklFHraWGl/xHqtZWWtOeVtyi+YqkoAOMGuZQad43DscXCQI/yizcTa3u6B5r+BLww==", + "version": "1.3.628", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.628.tgz", + "integrity": "sha512-fmhO4YGo/kapy+xL9Eq/cZwDASaTHZu3psIFYo4yc+RY1LzbZr84xjKlDImDrlrmWhOxsrDi98nX097U/xK/cQ==", "dev": true }, "elliptic": { @@ -7087,26 +6530,14 @@ } }, "enhanced-resolve": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", - "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", + "integrity": "sha1-TW5omzcl+GCQknzMhs2fFjW4ni4=", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "dependencies": { - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - } + "memory-fs": "^0.2.0", + "tapable": "^0.1.8" } }, "entities": { @@ -7145,18 +6576,18 @@ } }, "enzyme-adapter-react-16": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.2.tgz", - "integrity": "sha512-SkvDrb8xU3lSxID8Qic9rB8pvevDbLybxPK6D/vW7PrT0s2Cl/zJYuXvsd1EBTz0q4o3iqG3FJhpYz3nUNpM2Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.5.tgz", + "integrity": "sha512-33yUJGT1nHFQlbVI5qdo5Pfqvu/h4qPwi1o0a6ZZsjpiqq92a3HjynDhwd1IeED+Su60HDWV8mxJqkTnLYdGkw==", "dev": true, "requires": { - "enzyme-adapter-utils": "^1.13.0", - "enzyme-shallow-equal": "^1.0.1", + "enzyme-adapter-utils": "^1.13.1", + "enzyme-shallow-equal": "^1.0.4", "has": "^1.0.3", "object.assign": "^4.1.0", "object.values": "^1.1.1", "prop-types": "^15.7.2", - "react-is": "^16.12.0", + "react-is": "^16.13.1", "react-test-renderer": "^16.0.0-0", "semver": "^5.7.0" }, @@ -7170,15 +6601,16 @@ } }, "enzyme-adapter-utils": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.0.tgz", - "integrity": "sha512-YuEtfQp76Lj5TG1NvtP2eGJnFKogk/zT70fyYHXK2j3v6CtuHqc8YmgH/vaiBfL8K1SgVVbQXtTcgQZFwzTVyQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz", + "integrity": "sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg==", "dev": true, "requires": { - "airbnb-prop-types": "^2.15.0", - "function.prototype.name": "^1.1.2", - "object.assign": "^4.1.0", - "object.fromentries": "^2.0.2", + "airbnb-prop-types": "^2.16.0", + "function.prototype.name": "^1.1.3", + "has": "^1.0.3", + "object.assign": "^4.1.2", + "object.fromentries": "^2.0.3", "prop-types": "^15.7.2", "semver": "^5.7.1" }, @@ -7192,29 +6624,30 @@ } }, "enzyme-shallow-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.1.tgz", - "integrity": "sha512-hGA3i1so8OrYOZSM9whlkNmVHOicJpsjgTzC+wn2JMJXhq1oO4kA4bJ5MsfzSIcC71aLDKzJ6gZpIxrqt3QTAQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz", + "integrity": "sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==", "dev": true, "requires": { "has": "^1.0.3", - "object-is": "^1.0.2" + "object-is": "^1.1.2" } }, "enzyme-to-json": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.4.4.tgz", - "integrity": "sha512-50LELP/SCPJJGic5rAARvU7pgE3m1YaNj7JLM+Qkhl5t7PAs6fiyc8xzc50RnkKPFQCv0EeFVjEWdIFRGPWMsA==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.1.tgz", + "integrity": "sha512-15tXuONeq5ORoZjV/bUo2gbtZrN2IH+Z6DvL35QmZyKHgbY1ahn6wcnLd9Xv9OjiwbAXiiP8MRZwbZrCv1wYNg==", "dev": true, "requires": { + "@types/cheerio": "^0.22.22", "lodash": "^4.17.15", "react-is": "^16.12.0" } }, "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "dev": true, "requires": { "prr": "~1.0.1" @@ -7230,30 +6663,23 @@ } }, "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - }, - "dependencies": { - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - } + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" } }, "es-to-primitive": { @@ -7383,22 +6809,84 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", "dev": true, "requires": { - "ms": "^2.1.1" + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } } }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "color-convert": "^2.0.1" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" } }, "globals": { @@ -7410,16 +6898,137 @@ "type-fest": "^0.8.1" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "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 }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true } } @@ -7465,9 +7074,9 @@ } }, "eslint-import-resolver-node": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz", - "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", "dev": true, "requires": { "debug": "^2.6.9", @@ -7512,34 +7121,11 @@ "ms": "2.0.0" } }, - "enhanced-resolve": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", - "integrity": "sha1-TW5omzcl+GCQknzMhs2fFjW4ni4=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.2.0", - "tapable": "^0.1.8" - } - }, - "memory-fs": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", - "integrity": "sha1-8rslNovBIeORwlIN6Slpyu4KApA=", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true - }, - "tapable": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", - "integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=", - "dev": true } } }, @@ -7574,15 +7160,6 @@ "requires": { "ms": "2.0.0" } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } } } }, @@ -7595,24 +7172,34 @@ "lodash": "^4.17.15" } }, - "eslint-plugin-import": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", - "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", + "eslint-plugin-i18next": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-i18next/-/eslint-plugin-i18next-5.0.0.tgz", + "integrity": "sha512-ixbgSMrSb0dZsO6WPElg4JvPiQKLDA3ZpBuayxToADan1TKcbzKXT2A42Vyc0lEDhJRPL6uZnmm8vPjODDJypg==", "dev": true, "requires": { - "array-includes": "^3.0.3", - "array.prototype.flat": "^1.2.1", + "requireindex": "~1.1.0" + } + }, + "eslint-plugin-import": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", "contains-path": "^0.1.0", "debug": "^2.6.9", "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.1", + "eslint-import-resolver-node": "^0.3.4", + "eslint-module-utils": "^2.6.0", "has": "^1.0.3", "minimatch": "^3.0.4", - "object.values": "^1.1.0", + "object.values": "^1.1.1", "read-pkg-up": "^2.0.0", - "resolve": "^1.12.0" + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" }, "dependencies": { "debug": { @@ -7639,119 +7226,53 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true, - "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - } } } }, "eslint-plugin-jsx-a11y": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", - "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz", + "integrity": "sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==", "dev": true, "requires": { - "@babel/runtime": "^7.4.5", - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", + "@babel/runtime": "^7.11.2", + "aria-query": "^4.2.2", + "array-includes": "^3.1.1", "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.2", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^7.0.2", + "axe-core": "^4.0.2", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.6", + "emoji-regex": "^9.0.0", "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1" + "jsx-ast-utils": "^3.1.0", + "language-tags": "^1.0.5" }, "dependencies": { - "aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.0.tgz", + "integrity": "sha512-DNc3KFPK18bPdElMJnf/Pkv5TXhxFU3YFDEuGLDRtPmV4rkmCjBkCSEp22u6rBHdSN9Vlp/GK7k98prmE1Jgug==", "dev": true } } }, "eslint-plugin-react": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz", - "integrity": "sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==", + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.21.5.tgz", + "integrity": "sha512-8MaEggC2et0wSF6bUeywF7qQ46ER81irOdWS4QWxnnlAEsnzeBevk1sWh7fhpCghPpXb+8Ks7hvaft6L/xsR6g==", "dev": true, "requires": { "array-includes": "^3.1.1", + "array.prototype.flatmap": "^1.2.3", "doctrine": "^2.1.0", "has": "^1.0.3", - "jsx-ast-utils": "^2.2.3", - "object.entries": "^1.1.1", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "object.entries": "^1.1.2", "object.fromentries": "^2.0.2", "object.values": "^1.1.1", "prop-types": "^15.7.2", - "resolve": "^1.15.1", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.2", - "xregexp": "^4.3.0" + "resolve": "^1.18.1", + "string.prototype.matchall": "^4.0.2" }, "dependencies": { "doctrine": { @@ -7772,28 +7293,28 @@ "dev": true }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { - "esrecurse": "^4.1.0", + "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" } }, "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true }, "espree": { @@ -7822,20 +7343,28 @@ }, "dependencies": { "estraverse": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", - "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", "dev": true } } }, "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { - "estraverse": "^4.1.0" + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "estraverse": { @@ -7857,15 +7386,15 @@ "dev": true }, "eventemitter3": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", - "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true }, "events": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", - "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", "dev": true }, "eventsource": { @@ -8172,9 +7701,9 @@ "dev": true }, "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "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 }, "fast-glob": { @@ -8191,6 +7720,58 @@ "micromatch": "^3.1.10" }, "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -8211,6 +7792,69 @@ } } } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } } } }, @@ -8251,9 +7895,9 @@ "dev": true }, "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" @@ -8279,9 +7923,9 @@ } }, "file-selector": { - "version": "0.1.18", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.18.tgz", - "integrity": "sha512-MUY65bNFSE+VraxNpxAz04vDlPuh5qENA0WtfgDWoDnKV7ZN2InI8AbSp0F/3aHoJJVKmZ+cqachhVoBNGifWA==", + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz", + "integrity": "sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==", "requires": { "tslib": "^2.0.1" }, @@ -8293,6 +7937,13 @@ } } }, + "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", @@ -8300,26 +7951,12 @@ "dev": true }, "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "to-regex-range": "^5.0.1" } }, "finalhandler": { @@ -8357,6 +7994,51 @@ "commondir": "^1.0.1", "make-dir": "^2.0.0", "pkg-dir": "^3.0.0" + }, + "dependencies": { + "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" + } + }, + "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" + } + }, + "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" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } } }, "find-root": { @@ -8366,12 +8048,22 @@ "dev": true }, "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { - "locate-path": "^2.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "requires": { + "micromatch": "^4.0.2" } }, "flat-cache": { @@ -8405,21 +8097,53 @@ "requires": { "inherits": "^2.0.3", "readable-stream": "^2.3.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "focus-trap": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-4.0.2.tgz", - "integrity": "sha512-HtLjfAK7Hp2qbBtLS6wEznID1mPT+48ZnP2nkHzgjpL4kroYHg0CdqJ5cTXk+UO5znAxF5fRUkhdyfgrhh8Lzw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.2.2.tgz", + "integrity": "sha512-qWovH9+LGoKqREvJaTCzJyO0hphQYGz+ap5Hc4NqXHNhZBdxCi5uBPPcaOUw66fHmzXLVwvETLvFgpwPILqKpg==", "requires": { - "tabbable": "^3.1.2", - "xtend": "^4.0.1" + "tabbable": "^5.1.4" } }, "follow-redirects": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dev": true, "requires": { "debug": "=3.1.0" } @@ -8461,11 +8185,132 @@ "worker-rpc": "^0.1.0" }, "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } } } }, @@ -8481,16 +8326,15 @@ } }, "formik": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/formik/-/formik-2.1.4.tgz", - "integrity": "sha512-oKz8S+yQBzuQVSEoxkqqJrKQS5XJASWGVn6mrs+oTWrBoHgByVwwI1qHiVc9GKDpZBU9vAxXYAKz2BvujlwunA==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.2.6.tgz", + "integrity": "sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA==", "requires": { "deepmerge": "^2.1.1", "hoist-non-react-statics": "^3.3.0", "lodash": "^4.17.14", "lodash-es": "^4.17.14", "react-fast-compare": "^2.0.1", - "scheduler": "^0.18.0", "tiny-warning": "^1.0.2", "tslib": "^1.10.0" } @@ -8524,6 +8368,38 @@ "requires": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "fs-extra": { @@ -8556,6 +8432,38 @@ "iferr": "^0.1.5", "imurmurhash": "^0.1.4", "readable-stream": "1 || 2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "fs.realpath": { @@ -8578,14 +8486,15 @@ "dev": true }, "function.prototype.name": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.2.tgz", - "integrity": "sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.3.tgz", + "integrity": "sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "functions-have-names": "^1.2.0" + "es-abstract": "^1.18.0-next.1", + "functions-have-names": "^1.2.1" } }, "functional-red-black-tree": { @@ -8595,9 +8504,9 @@ "dev": true }, "functions-have-names": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.1.tgz", - "integrity": "sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.2.tgz", + "integrity": "sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==", "dev": true }, "fuzzaldrin": { @@ -8607,9 +8516,9 @@ "dev": true }, "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, "get-caller-file": { @@ -8618,12 +8527,29 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-own-enumerable-property-symbols": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "dev": true }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, "get-stdin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", @@ -8737,6 +8663,12 @@ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", "dev": true }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", @@ -8765,11 +8697,6 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "dev": true }, - "gud": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", - "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" - }, "gzip-size": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", @@ -8778,14 +8705,6 @@ "requires": { "duplexer": "^0.1.1", "pify": "^4.0.1" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } } }, "handle-thing": { @@ -8808,20 +8727,6 @@ "requires": { "ajv": "^6.12.3", "har-schema": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "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": { @@ -8845,13 +8750,6 @@ "integrity": "sha1-Ngd+8dFfMzSEqn+neihgbxxlWzc=", "requires": { "ansi-regex": "^3.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=" - } } }, "has-flag": { @@ -8892,6 +8790,26 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", @@ -8914,31 +8832,11 @@ "safe-buffer": "^5.2.0" }, "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } } } }, @@ -9017,6 +8915,38 @@ "obuf": "^1.0.0", "readable-stream": "^2.0.1", "wbuf": "^1.1.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "hsl-regex": { @@ -9056,9 +8986,9 @@ } }, "html-entities": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", - "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.3.tgz", + "integrity": "sha512-/VulV3SYni1taM7a4RMdceqzJWR39gpZHjBwUnsCFKWV/GJkD14CJ5F7eWcZozmHJK0/f/H5U3b3SiPkuvxMgg==" }, "html-escaper": { "version": "2.0.2", @@ -9103,6 +9033,12 @@ "util.promisify": "1.0.0" }, "dependencies": { + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, "util.promisify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", @@ -9127,34 +9063,6 @@ "entities": "^1.1.1", "inherits": "^2.0.1", "readable-stream": "^3.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - } } }, "http-deceiver": { @@ -9196,61 +9104,16 @@ } }, "http-proxy-middleware": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.3.tgz", - "integrity": "sha512-GHvPeBD+A357zS5tHjzj6ISrVOjjCiy0I92bdyTJz0pNmIjFxO0NX/bX+xkGgnclKQE/5hHAB9JEQ7u9Pw4olg==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.6.tgz", + "integrity": "sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg==", "dev": true, "requires": { - "@types/http-proxy": "^1.17.3", - "http-proxy": "^1.18.0", + "@types/http-proxy": "^1.17.4", + "http-proxy": "^1.18.1", "is-glob": "^4.0.1", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "micromatch": "^4.0.2" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } } }, "http-signature": { @@ -9297,9 +9160,9 @@ } }, "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true }, "iferr": { @@ -9330,13 +9193,21 @@ } }, "import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", "dev": true, "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } } }, "import-from": { @@ -9364,6 +9235,51 @@ "requires": { "pkg-dir": "^3.0.0", "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "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" + } + }, + "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" + } + }, + "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" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } } }, "imurmurhash": { @@ -9407,95 +9323,74 @@ "dev": true }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, "inquirer": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", - "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", "dev": true, "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "cli-cursor": "^3.1.0", + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.15", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.5.3", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", "through": "^2.3.6" }, "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "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 }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "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": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "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" + } + } } }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } } } } @@ -9519,12 +9414,33 @@ "es-abstract": "^1.17.0-next.1", "has": "^1.0.3", "side-channel": "^1.0.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, "invariant": { @@ -9586,10 +9502,13 @@ } }, "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", + "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0" + } }, "is-arrayish": { "version": "0.2.1", @@ -9607,20 +9526,18 @@ } }, "is-boolean-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", - "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", - "dev": true - }, - "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", + "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", + "dev": true, + "requires": { + "call-bind": "^1.0.0" + } }, "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", "dev": true }, "is-ci": { @@ -9646,6 +9563,15 @@ "rgba-regex": "^1.0.0" } }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -9737,14 +9663,17 @@ "is-extglob": "^2.1.1" } }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true + }, "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true }, "is-number-object": { "version": "1.0.4", @@ -9798,12 +9727,12 @@ } }, "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { - "has": "^1.0.3" + "has-symbols": "^1.0.1" } }, "is-regexp": { @@ -9867,9 +9796,9 @@ "dev": true }, "is-what": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.8.0.tgz", - "integrity": "sha512-UKeBoQfV8bjlM4pmx1FLDHdxslW/1mTksEs8ReVsilPmUv5cORd4+2/wFcviI3cUjrLybxCjzc8DnodAzJ/Wrg==" + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.12.0.tgz", + "integrity": "sha512-2ilQz5/f/o9V7WRWJQmpFYNmQFZ9iM+OXRonZKcYgTkCzjb949Vi4h282PD1UfmgHk666rcWonbRJ++KI41VGw==" }, "is-windows": { "version": "1.0.2", @@ -9907,24 +9836,21 @@ "dev": true }, "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", "dev": true }, "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" } }, "istanbul-lib-report": { @@ -9938,6 +9864,12 @@ "supports-color": "^6.1.0" }, "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, "supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", @@ -9963,14 +9895,20 @@ }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" } }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -10061,6 +9999,123 @@ "micromatch": "^3.1.10", "pretty-format": "^24.9.0", "realpath-native": "^1.1.0" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } } }, "jest-diff": { @@ -10232,12 +10287,131 @@ "walker": "^1.0.7" }, "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.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 + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } } } }, @@ -10301,6 +10475,123 @@ "micromatch": "^3.1.10", "slash": "^2.0.0", "stack-utils": "^1.0.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } } }, "jest-mock": { @@ -10453,6 +10744,12 @@ "source-map": "^0.6.0" }, "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -10490,6 +10787,21 @@ "strip-ansi": "^5.0.0" }, "dependencies": { + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -10505,6 +10817,21 @@ "astral-regex": "^1.0.0", "strip-ansi": "^5.2.0" } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true } } }, @@ -10521,20 +10848,12 @@ "chalk": "^2.0.1", "jest-util": "^24.9.0", "string-length": "^2.0.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - } } }, "jest-websocket-mock": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jest-websocket-mock/-/jest-websocket-mock-2.0.2.tgz", - "integrity": "sha512-SFTUI8O/LDGqROOMnfAzbrrX5gQ8GDhRqkzVrt8Y67evnFKccRPFI3ymS05tKcMONvVfxumat4pX/LRjM/CjVg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jest-websocket-mock/-/jest-websocket-mock-2.2.0.tgz", + "integrity": "sha512-lc3wwXOEyNa4ZpcgJtUG3mmKMAq5FAsKYiZph0p/+PAJrAPuX4JCIfJMdJ/urRsLBG51fwm/wlVPNbR6s2nzNw==", "dev": true }, "jest-worker": { @@ -10564,9 +10883,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -10617,6 +10936,12 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", "dev": true + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true } } }, @@ -10713,13 +11038,13 @@ } }, "jsx-ast-utils": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", - "integrity": "sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", + "integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==", "dev": true, "requires": { - "array-includes": "^3.0.3", - "object.assign": "^4.1.0" + "array-includes": "^3.1.2", + "object.assign": "^4.1.2" } }, "killable": { @@ -10751,6 +11076,21 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "language-subtag-registry": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", + "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", + "dev": true + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "dev": true, + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, "last-call-webpack-plugin": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", @@ -10805,15 +11145,16 @@ "dev": true }, "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", + "graceful-fs": "^4.1.15", "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" } }, "loader-fs-cache": { @@ -10896,19 +11237,18 @@ } }, "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "p-locate": "^4.1.0" } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash-es": { "version": "4.17.15", @@ -10986,9 +11326,9 @@ } }, "loglevel": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz", - "integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", "dev": true }, "loose-envify": { @@ -11000,35 +11340,35 @@ } }, "lower-case": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", - "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "dev": true, "requires": { - "tslib": "^1.10.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" + "tslib": "^2.0.3" }, "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", "dev": true } } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "luxon": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.24.1.tgz", - "integrity": "sha512-CgnIMKAWT0ghcuWFfCWBnWGOddM0zu6c4wZAWmD0NN7MZTnro0+833DF6tJep+xlxRPg4KtsYEHYLfTMBQKwYg==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.25.0.tgz", + "integrity": "sha512-hEgLurSH8kQRjY6i4YLey+mcKVAWXbDNlZRmM6AgWDJ1cY3atl8Ztf5wEY7VBReFbmGnwQPz7KYJblL8B2k0jQ==", "optional": true }, "make-dir": { @@ -11041,12 +11381,6 @@ "semver": "^5.6.0" }, "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -11122,14 +11456,10 @@ "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" }, "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", + "integrity": "sha1-8rslNovBIeORwlIN6Slpyu4KApA=", + "dev": true }, "merge-anything": { "version": "2.4.4", @@ -11186,32 +11516,13 @@ "dev": true }, "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", "dev": true, "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "braces": "^3.0.1", + "picomatch": "^2.0.5" } }, "miller-rabin": { @@ -11225,17 +11536,17 @@ }, "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==", + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", "dev": true } } }, "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz", + "integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==", "dev": true }, "mime-db": { @@ -11254,19 +11565,18 @@ } }, "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true }, "mini-create-react-context": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz", - "integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", "requires": { - "@babel/runtime": "^7.4.0", - "gud": "^1.0.0", - "tiny-warning": "^1.0.2" + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" } }, "mini-css-extract-plugin": { @@ -11454,7 +11764,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "multicast-dns": { "version": "6.2.3", @@ -11473,11 +11784,18 @@ "dev": true }, "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -11512,24 +11830,15 @@ "dev": true }, "nearley": { - "version": "2.19.3", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.3.tgz", - "integrity": "sha512-FpAy1PmTsUpOtgxr23g4jRNvJHYzZEW2PixXeSzksLR/ykPfwKhAodc2+9wQhY+JneWLcvkDw6q7FJIsIdF/aQ==", + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", "dev": true, "requires": { "commander": "^2.19.0", "moo": "^0.5.0", "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6", - "semver": "^5.4.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } + "randexp": "0.4.6" } }, "negotiator": { @@ -11557,13 +11866,21 @@ "dev": true }, "no-case": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", - "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dev": true, "requires": { - "lower-case": "^2.0.1", - "tslib": "^1.10.0" + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } } }, "node-forge": { @@ -11646,40 +11963,6 @@ } } } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "dev": true - } - } - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } - } } } }, @@ -11711,9 +11994,9 @@ } }, "node-releases": { - "version": "1.1.64", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.64.tgz", - "integrity": "sha512-Iec8O9166/x2HRMJyLLLWkd0sFFLrFNy+Xf+JQfSQsdBJzPcHpNl3JQ9gD4j+aJxmCa25jNsIbM4bmACtSbkSg==", + "version": "1.1.67", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz", + "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==", "dev": true }, "normalize-package-data": { @@ -11833,19 +12116,19 @@ "dev": true }, "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", "dev": true }, "object-is": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", - "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz", + "integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==", "dev": true, "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" } }, "object-keys": { @@ -11864,57 +12147,50 @@ } }, "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - }, - "dependencies": { - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - } + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" } }, "object.entries": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", - "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz", + "integrity": "sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", + "es-abstract": "^1.18.0-next.1", "has": "^1.0.3" } }, "object.fromentries": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz", - "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz", + "integrity": "sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", + "es-abstract": "^1.18.0-next.1", "has": "^1.0.3" } }, "object.getownpropertydescriptors": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", - "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", + "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" + "es-abstract": "^1.18.0-next.1" } }, "object.pick": { @@ -11927,14 +12203,14 @@ } }, "object.values": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", - "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz", + "integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", + "es-abstract": "^1.18.0-next.1", "has": "^1.0.3" } }, @@ -11969,12 +12245,12 @@ } }, "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", "dev": true, "requires": { - "mimic-fn": "^2.1.0" + "mimic-fn": "^1.0.0" } }, "open": { @@ -12045,38 +12321,19 @@ "wcwidth": "^1.0.1" }, "dependencies": { - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "ansi-regex": "^4.1.0" } } } @@ -12118,21 +12375,21 @@ "dev": true }, "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "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": "^1.0.0" + "p-try": "^2.0.0" } }, "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "requires": { - "p-limit": "^1.1.0" + "p-limit": "^2.2.0" } }, "p-map": { @@ -12160,9 +12417,9 @@ } }, "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "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 }, "pako": { @@ -12180,16 +12437,56 @@ "cyclist": "^1.0.1", "inherits": "^2.0.3", "readable-stream": "^2.1.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "param-case": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz", - "integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", "dev": true, "requires": { - "dot-case": "^3.0.3", - "tslib": "^1.10.0" + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } } }, "parent-module": { @@ -12199,17 +12496,24 @@ "dev": true, "requires": { "callsites": "^3.0.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + } } }, "parse-asn1": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", - "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", "dev": true, "requires": { - "asn1.js": "^4.0.0", + "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", "pbkdf2": "^3.0.3", "safe-buffer": "^5.1.1" @@ -12226,10 +12530,13 @@ } }, "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", - "dev": true + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "parseurl": { "version": "1.3.3", @@ -12238,13 +12545,21 @@ "dev": true }, "pascal-case": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.1.tgz", - "integrity": "sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dev": true, "requires": { - "no-case": "^3.0.3", - "tslib": "^1.10.0" + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } } }, "pascalcase": { @@ -12266,9 +12581,9 @@ "dev": true }, "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, "path-is-absolute": { @@ -12304,18 +12619,26 @@ } }, "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", "dev": true, "requires": { - "pify": "^3.0.0" + "pify": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } } }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", "dev": true, "requires": { "create-hash": "^1.1.2", @@ -12338,9 +12661,9 @@ "dev": true }, "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, "pinkie": { @@ -12386,19 +12709,6 @@ "locate-path": "^3.0.0" } }, - "load-json-file": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", - "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "parse-json": "^4.0.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0", - "type-fest": "^0.3.0" - } - }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -12409,15 +12719,6 @@ "path-exists": "^3.0.0" } }, - "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", @@ -12427,76 +12728,70 @@ "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 - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true } } }, "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", "dev": true, "requires": { - "find-up": "^3.0.0" + "find-up": "^2.1.0" }, "dependencies": { "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^2.0.0" } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { - "p-locate": "^3.0.0", + "p-locate": "^2.0.0", "path-exists": "^3.0.0" } }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "requires": { - "p-try": "^2.0.0" + "p-try": "^1.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==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { - "p-limit": "^2.0.0" + "p-limit": "^1.1.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==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true } } @@ -12529,15 +12824,6 @@ "path-exists": "^3.0.0" } }, - "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", @@ -12547,10 +12833,10 @@ "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==", + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true } } @@ -12593,18 +12879,18 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "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==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true } } @@ -12671,6 +12957,14 @@ "postcss": "^7.0.27", "postcss-selector-parser": "^6.0.2", "postcss-value-parser": "^4.0.2" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + } } }, "postcss-color-functional-notation": { @@ -12736,14 +13030,6 @@ "has": "^1.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-convert-values": { @@ -12754,14 +13040,6 @@ "requires": { "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-custom-media": { @@ -12925,9 +13203,9 @@ } }, "postcss-font-variant": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.0.tgz", - "integrity": "sha512-M8BFYKOvCrI2aITzDad7kWuXXTm0YhGdP9Q8HanmN4EF1Hmcgs1KK5rSHylt/lUJe8yLxiSwWAHdScoEiIxztg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz", + "integrity": "sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA==", "dev": true, "requires": { "postcss": "^7.0.2" @@ -13036,14 +13314,6 @@ "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0", "stylehacks": "^4.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-merge-rules": { @@ -13081,14 +13351,6 @@ "requires": { "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-minify-gradients": { @@ -13101,14 +13363,6 @@ "is-color-stop": "^1.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-minify-params": { @@ -13123,14 +13377,6 @@ "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0", "uniqs": "^2.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-minify-selectors": { @@ -13177,6 +13423,14 @@ "postcss": "^7.0.32", "postcss-selector-parser": "^6.0.2", "postcss-value-parser": "^4.1.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + } } }, "postcss-modules-scope": { @@ -13239,14 +13493,6 @@ "cssnano-util-get-match": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-normalize-positions": { @@ -13259,14 +13505,6 @@ "has": "^1.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-normalize-repeat-style": { @@ -13279,14 +13517,6 @@ "cssnano-util-get-match": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-normalize-string": { @@ -13298,14 +13528,6 @@ "has": "^1.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-normalize-timing-functions": { @@ -13317,14 +13539,6 @@ "cssnano-util-get-match": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-normalize-unicode": { @@ -13336,14 +13550,6 @@ "browserslist": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-normalize-url": { @@ -13363,12 +13569,6 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", "dev": true - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true } } }, @@ -13380,14 +13580,6 @@ "requires": { "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-ordered-values": { @@ -13399,14 +13591,6 @@ "cssnano-util-get-arguments": "^4.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-overflow-shorthand": { @@ -13533,14 +13717,6 @@ "has": "^1.0.0", "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-replace-overflow-wrap": { @@ -13603,14 +13779,6 @@ "postcss": "^7.0.0", "postcss-value-parser": "^3.0.0", "svgo": "^1.0.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } } }, "postcss-unique-selectors": { @@ -13625,10 +13793,9 @@ } }, "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" }, "postcss-values-parser": { "version": "2.0.1", @@ -13673,14 +13840,6 @@ "requires": { "lodash": "^4.17.20", "renderkid": "^2.0.4" - }, - "dependencies": { - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - } } }, "pretty-format": { @@ -13693,6 +13852,14 @@ "ansi-regex": "^4.0.0", "ansi-styles": "^3.2.0", "react-is": "^16.8.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } } }, "process": { @@ -13729,13 +13896,13 @@ "dev": true }, "prompts": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", - "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", + "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", "dev": true, "requires": { "kleur": "^3.0.3", - "sisteransi": "^1.0.4" + "sisteransi": "^1.0.5" } }, "prop-types": { @@ -13785,9 +13952,9 @@ "dev": true }, "pseudolocale": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pseudolocale/-/pseudolocale-1.1.0.tgz", - "integrity": "sha512-OZ8I/hwYEJ3beN3IEcNnt8EpcqblH0/x23hulKBXjs+WhTTEle+ijCHCkh2bd+cIIeCuCwSCbBe93IthGG6hLw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pseudolocale/-/pseudolocale-1.2.0.tgz", + "integrity": "sha512-k0OQFvIlvpRdzR0dPVrrbWX7eE9EaZ6gpZtTlFSDi1Gf9tMy9wiANCNu7JZ0drcKgUri/39a2mBbH0goiQmrmQ==", "dev": true, "requires": { "commander": "*" @@ -13814,9 +13981,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==", + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", "dev": true } } @@ -13895,9 +14062,9 @@ "dev": true }, "querystringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", - "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, "raf": { @@ -13977,9 +14144,9 @@ } }, "react": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", - "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -14001,9 +14168,9 @@ }, "dependencies": { "core-js": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", - "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.1.tgz", + "integrity": "sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg==", "dev": true }, "regenerator-runtime": { @@ -14051,6 +14218,30 @@ "text-table": "0.2.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, "browserslist": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.10.0.tgz", @@ -14063,6 +14254,15 @@ "pkg-up": "^3.1.0" } }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, "cross-spawn": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", @@ -14086,14 +14286,21 @@ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "escape-string-regexp": "^1.0.5" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + } } }, "inquirer": { @@ -14148,51 +14355,43 @@ "json5": "^1.0.1" } }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "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": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.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==", + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -14208,22 +14407,11 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - } - } + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true }, "which": { "version": "2.0.2", @@ -14237,25 +14425,14 @@ } }, "react-dom": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", - "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", "scheduler": "^0.19.1" - }, - "dependencies": { - "scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - } } }, "react-dropzone": { @@ -14270,9 +14447,9 @@ } }, "react-error-overlay": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", - "integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.8.tgz", + "integrity": "sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==", "dev": true }, "react-fast-compare": { @@ -14291,15 +14468,15 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "react-router": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", - "integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", + "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "requires": { "@babel/runtime": "^7.1.2", "history": "^4.9.0", "hoist-non-react-statics": "^3.1.0", "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.3.0", + "mini-create-react-context": "^0.4.0", "path-to-regexp": "^1.7.0", "prop-types": "^15.6.2", "react-is": "^16.6.0", @@ -14308,15 +14485,15 @@ } }, "react-router-dom": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz", - "integrity": "sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", + "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", "requires": { "@babel/runtime": "^7.1.2", "history": "^4.9.0", "loose-envify": "^1.3.1", "prop-types": "^15.6.2", - "react-router": "5.1.2", + "react-router": "5.2.0", "tiny-invariant": "^1.0.2", "tiny-warning": "^1.0.0" } @@ -14382,12 +14559,196 @@ "workbox-webpack-plugin": "4.3.1" }, "dependencies": { + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "eslint-plugin-import": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", + "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "array.prototype.flat": "^1.2.1", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.1", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.0", + "read-pkg-up": "^2.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", + "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.5", + "aria-query": "^3.0.0", + "array-includes": "^3.0.3", + "ast-types-flow": "^0.0.7", + "axobject-query": "^2.0.2", + "damerau-levenshtein": "^1.0.4", + "emoji-regex": "^7.0.2", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.1" + } + }, + "eslint-plugin-react": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz", + "integrity": "sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.3", + "object.entries": "^1.1.1", + "object.fromentries": "^2.0.2", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "resolve": "^1.15.1", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.2", + "xregexp": "^4.3.0" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + } + } + }, "eslint-plugin-react-hooks": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==", "dev": true }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "jsx-ast-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", + "integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "object.assign": "^4.1.0" + } + }, + "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 + }, "resolve": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz", @@ -14400,129 +14761,140 @@ } }, "react-test-renderer": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.1.tgz", - "integrity": "sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", + "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==", "dev": true, "requires": { "object-assign": "^4.1.1", "prop-types": "^15.6.2", "react-is": "^16.8.6", "scheduler": "^0.19.1" - }, - "dependencies": { - "scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - } } }, "react-virtualized": { - "version": "9.21.2", - "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.2.tgz", - "integrity": "sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA==", + "version": "9.22.2", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.2.tgz", + "integrity": "sha512-5j4h4FhxTdOpBKtePSs1yk6LDNT4oGtUwjT7Nkh61Z8vv3fTG/XeOf8J4li1AYaexOwTXnw0HFVxsV0GBUqwRw==", "requires": { - "babel-runtime": "^6.26.0", - "clsx": "^1.0.1", - "dom-helpers": "^5.0.0", - "loose-envify": "^1.3.0", - "prop-types": "^15.6.0", + "@babel/runtime": "^7.7.2", + "clsx": "^1.0.4", + "dom-helpers": "^5.1.3", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", "react-lifecycles-compat": "^3.0.4" } }, "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", "dev": true, "requires": { - "load-json-file": "^4.0.0", + "load-json-file": "^2.0.0", "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" + "path-type": "^2.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } } }, "read-pkg-up": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", "dev": true, "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" }, "dependencies": { "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^2.0.0" } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { - "p-locate": "^3.0.0", + "p-locate": "^2.0.0", "path-exists": "^3.0.0" } }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "requires": { - "p-try": "^2.0.0" + "p-try": "^1.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==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { - "p-limit": "^2.0.0" + "p-limit": "^1.1.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==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true } } }, "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - } + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, "readdirp": { @@ -14559,9 +14931,9 @@ "dev": true }, "regenerate": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", - "integrity": "sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true }, "regenerate-unicode-properties": { @@ -14611,12 +14983,33 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, "regexpu-core": { @@ -14687,40 +15080,6 @@ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - } - }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -14813,6 +15172,12 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha1-5UBLgVV+91225JxacgBIk/4D4WI=", + "dev": true + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -14820,11 +15185,12 @@ "dev": true }, "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", "dev": true, "requires": { + "is-core-module": "^2.1.0", "path-parse": "^1.0.6" } }, @@ -14846,9 +15212,9 @@ } }, "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, "resolve-pathname": { @@ -14935,12 +15301,12 @@ } }, "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", "dev": true, "requires": { - "onetime": "^5.1.0", + "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, @@ -15012,9 +15378,9 @@ } }, "rrule": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.4.tgz", - "integrity": "sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA==", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.6.tgz", + "integrity": "sha512-h6tb/hRo9SNv8xKjcvsEfdmhXvElMXsU3Yz0KmqMehUqxP6a4Qjmth2EuL1FsjdawADjajLS0eBbWfsZzn3SIw==", "requires": { "luxon": "^1.21.3", "tslib": "^1.10.0" @@ -15057,9 +15423,9 @@ "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" }, "rxjs": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", - "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -15100,6 +15466,123 @@ "micromatch": "^3.1.4", "minimist": "^1.1.1", "walker": "~1.0.5" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } } }, "sanitize.css": { @@ -15165,9 +15648,9 @@ } }, "scheduler": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz", - "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -15182,20 +15665,6 @@ "@types/json-schema": "^7.0.5", "ajv": "^6.12.4", "ajv-keywords": "^3.5.2" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "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" - } - } } }, "select-hose": { @@ -15385,7 +15854,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -15456,13 +15925,13 @@ "dev": true }, "side-channel": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", - "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", + "integrity": "sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==", "dev": true, "requires": { - "es-abstract": "^1.17.0-next.1", - "object-inspect": "^1.7.0" + "es-abstract": "^1.18.0-next.0", + "object-inspect": "^1.8.0" } }, "signal-exit": { @@ -15656,9 +16125,9 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { "ms": "^2.1.1" @@ -15674,9 +16143,9 @@ } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true } } @@ -15699,8 +16168,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { "version": "0.5.3", @@ -15740,9 +16208,9 @@ "dev": true }, "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -15756,9 +16224,9 @@ "dev": true }, "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -15766,9 +16234,9 @@ } }, "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", "dev": true }, "spdy": { @@ -15785,9 +16253,9 @@ }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -15816,9 +16284,9 @@ }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -15829,17 +16297,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } } } }, @@ -15891,10 +16348,21 @@ "dev": true }, "stack-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", - "dev": true + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.4.tgz", + "integrity": "sha512-IPDJfugEGbfizBwBZRZ3xpccMdRyP5lqsBWXGQWimVjua/ccLCeMOAVjlc1R7LxFjo5sEDhyNIXd8mo/AiDS9w==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } }, "static-extend": { "version": "0.1.2", @@ -16023,12 +16491,6 @@ "requires": { "safe-buffer": "~5.1.0" } - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true } } }, @@ -16054,12 +16516,6 @@ "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", @@ -16080,99 +16536,69 @@ "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } } }, "string.prototype.matchall": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz", - "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.3.tgz", + "integrity": "sha512-OBxYDA2ifZQ2e13cP82dWFMaCV9CGF8GzmN4fljBVw5O5wep0lu4gacm1OL6MjROoUnB8VbkWRThqkV2YFLNxw==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", + "es-abstract": "^1.18.0-next.1", "has-symbols": "^1.0.1", "internal-slot": "^1.0.2", "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.2" + "side-channel": "^1.0.3" } }, "string.prototype.trim": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz", - "integrity": "sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.3.tgz", + "integrity": "sha512-16IL9pIBA5asNOSukPfxX2W68BaBvxyiRK16H3RA/lWW9BDosh+w7f+LhomPHpXJ82QEe7w7/rY/S1CV97raLg==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1" + "es-abstract": "^1.18.0-next.1" } }, "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", "dev": true, "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "string.prototype.trimleft": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", - "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5", - "string.prototype.trimstart": "^1.0.0" - } - }, - "string.prototype.trimright": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", - "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5", - "string.prototype.trimend": "^1.0.0" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" } }, "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", "dev": true, "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" } }, "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "stringify-object": { @@ -16195,12 +16621,20 @@ } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + } } }, "strip-bom": { @@ -16226,9 +16660,9 @@ "dev": true }, "strip-json-comments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", - "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "style-loader": { @@ -16272,13 +16706,6 @@ "stylis": "^3.5.0", "stylis-rule-sheet": "^0.0.10", "supports-color": "^5.5.0" - }, - "dependencies": { - "@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - } } }, "stylehacks": { @@ -16348,6 +16775,36 @@ "stable": "^0.1.8", "unquote": "~1.1.1", "util.promisify": "~1.0.0" + }, + "dependencies": { + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "dev": true + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + } } }, "symbol-tree": { @@ -16357,9 +16814,9 @@ "dev": true }, "tabbable": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-3.1.2.tgz", - "integrity": "sha512-wjB6puVXTYO0BSFtCmWQubA/KIn7Xvajw0x0l6eJUudMG/EAiJvIUnyNX6xO4NpGrJ16lbD0eUseB9WxW0vlpQ==" + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.1.5.tgz", + "integrity": "sha512-oVAPrWgLLqrbvQE8XqcU7CVBq6SQbaIbHkhOca3u7/jzuQvyZycrUKPCGr04qpEIUslmUlULbSeN+m3QrKEykA==" }, "table": { "version": "5.4.6", @@ -16373,6 +16830,12 @@ "string-width": "^3.0.0" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -16395,13 +16858,22 @@ "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", + "integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=", "dev": true }, "terser": { @@ -16451,16 +16923,6 @@ "pkg-dir": "^4.1.0" } }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -16477,15 +16939,6 @@ "supports-color": "^7.0.0" } }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -16495,36 +16948,6 @@ "semver": "^6.0.0" } }, - "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": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.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 - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -16552,15 +16975,14 @@ } }, "test-exclude": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", - "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "requires": { - "glob": "^7.1.3", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^2.0.0" + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" } }, "text-table": { @@ -16577,7 +16999,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, @@ -16589,6 +17011,38 @@ "requires": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "thunky": { @@ -16598,9 +17052,9 @@ "dev": true }, "timers-browserify": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", - "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", "dev": true, "requires": { "setimmediate": "^1.0.4" @@ -16678,13 +17132,12 @@ } }, "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "is-number": "^7.0.0" } }, "toidentifier": { @@ -16724,10 +17177,33 @@ "integrity": "sha512-CrG5GqAAzMT7144Cl+UIFP7mz/iIhiy+xQ6GGcnjTezhALT02uPMRw7tgDSESgB5MsfKt55+GPWw4ir1kVtMIQ==", "dev": true }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, "tslib": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==" + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" }, "tsutils": { "version": "3.17.1", @@ -16775,9 +17251,9 @@ } }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", "dev": true }, "type-is": { @@ -16937,9 +17413,9 @@ "dev": true }, "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", "dev": true, "requires": { "punycode": "^2.1.0" @@ -16997,18 +17473,18 @@ "dev": true }, "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dev": true, "requires": { - "inherits": "2.0.1" + "inherits": "2.0.3" }, "dependencies": { "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true } } @@ -17029,6 +17505,27 @@ "es-abstract": "^1.17.2", "has-symbols": "^1.0.1", "object.getownpropertydescriptors": "^2.1.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "utila": { @@ -17050,9 +17547,9 @@ "dev": true }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", "dev": true }, "validate-npm-package-license": { @@ -17137,27 +17634,53 @@ } }, "watchpack": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", - "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", "dev": true, "requires": { "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", "neo-async": "^2.5.0", - "watchpack-chokidar2": "^2.0.0" + "watchpack-chokidar2": "^2.0.1" } }, "watchpack-chokidar2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz", - "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", + "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", "dev": true, "optional": true, "requires": { "chokidar": "^2.1.8" }, "dependencies": { + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "optional": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -17168,11 +17691,208 @@ "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", "upath": "^1.1.1" } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.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, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true, + "optional": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "optional": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "optional": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "optional": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } } } }, @@ -17237,6 +17957,35 @@ "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "cacache": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", @@ -17260,6 +18009,29 @@ "y18n": "^4.0.0" } }, + "enhanced-resolve": { + "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", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + } + } + }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -17270,6 +18042,122 @@ "estraverse": "^4.1.1" } }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -17296,6 +18184,21 @@ "figgy-pudding": "^3.5.1" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, "terser-webpack-plugin": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", @@ -17312,13 +18215,29 @@ "webpack-sources": "^1.4.0", "worker-farm": "^1.7.0" } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, "webpack-dev-middleware": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", - "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", + "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", "dev": true, "requires": { "memory-fs": "^0.4.1", @@ -17326,6 +18245,48 @@ "mkdirp": "^0.5.1", "range-parser": "^1.2.1", "webpack-log": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "webpack-dev-server": { @@ -17381,6 +18342,24 @@ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -17402,20 +18381,45 @@ } }, "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" } }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.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 + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -17465,6 +18469,69 @@ "binary-extensions": "^1.0.0" } }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -17477,6 +18544,21 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", @@ -17499,6 +18581,15 @@ "ajv-keywords": "^3.1.0" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -17517,6 +18608,16 @@ "has-flag": "^3.0.0" } }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, "ws": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", @@ -17560,6 +18661,12 @@ "jsonfile": "^4.0.0", "universalify": "^0.1.0" } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true } } }, @@ -17606,9 +18713,9 @@ } }, "whatwg-fetch": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz", - "integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.5.0.tgz", + "integrity": "sha512-jXkLtsR42xhXg7akoDKvKWE40eJeI+2KZqcp2h3NsOrRnDvtWX36KcKl30dy+hxECivdk2BVUHVNrPtoMBUx6A==", "dev": true }, "whatwg-mimetype": { @@ -17856,6 +18963,12 @@ "strip-ansi": "^5.0.0" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -17878,6 +18991,15 @@ "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, @@ -17929,23 +19051,24 @@ "dev": true }, "xregexp": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", - "integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.4.1.tgz", + "integrity": "sha512-2u9HwfadaJaY9zHtRRnH6BY6CQVNQKkYm3oLtC9gJXXzfsbACg5X5e4EZZGVAH+YIfa+QA9lsFQTTe3HURF3ag==", "dev": true, "requires": { - "@babel/runtime-corejs3": "^7.8.3" + "@babel/runtime-corejs3": "^7.12.1" } }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { @@ -17978,6 +19101,12 @@ "yargs-parser": "^13.1.2" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -18009,15 +19138,6 @@ "path-exists": "^3.0.0" } }, - "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", @@ -18027,10 +19147,10 @@ "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==", + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true }, "string-width": { @@ -18043,6 +19163,15 @@ "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json index a76a5a5c25..7eb3759e74 100644 --- a/awx/ui_next/package.json +++ b/awx/ui_next/package.json @@ -3,15 +3,16 @@ "version": "0.1.0", "private": true, "engines": { - "node": "10.x" + "node": "14.x" }, "dependencies": { "@lingui/react": "^2.9.1", - "@patternfly/patternfly": "4.59.1", - "@patternfly/react-core": "4.75.2", - "@patternfly/react-icons": "4.7.16", + "@patternfly/patternfly": "4.70.2", + "@patternfly/react-core": "4.84.3", + "@patternfly/react-icons": "4.7.22", + "@patternfly/react-table": "^4.19.15", "ansi-to-html": "^0.6.11", - "axios": "^0.18.1", + "axios": "^0.21.1", "codemirror": "^5.47.0", "d3": "^5.12.0", "dagre": "^0.8.4", @@ -30,6 +31,7 @@ }, "devDependencies": { "@babel/polyfill": "^7.8.7", + "@cypress/instrument-cra": "^1.4.0", "@lingui/cli": "^2.9.2", "@lingui/macro": "^2.9.1", "@nteract/mockument": "^1.0.4", @@ -41,8 +43,9 @@ "eslint-config-airbnb": "^17.1.0", "eslint-config-prettier": "^5.0.0", "eslint-import-resolver-webpack": "0.11.1", + "eslint-plugin-i18next": "^5.0.0", "eslint-plugin-import": "^2.14.0", - "eslint-plugin-jsx-a11y": "^6.1.1", + "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.11.1", "eslint-plugin-react-hooks": "^2.2.0", "http-proxy-middleware": "^1.0.3", @@ -53,7 +56,8 @@ }, "scripts": { "start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start", - "build": "react-scripts build", + "start-instrumented": "DEBUG=instrument-cra PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts -r @cypress/instrument-cra start", + "build": "INLINE_RUNTIME_CHUNK=false react-scripts build", "test": "TZ='UTC' react-scripts test --coverage --watchAll=false", "test-watch": "TZ='UTC' react-scripts test", "eject": "react-scripts eject", diff --git a/awx/ui_next/public/index.html b/awx/ui_next/public/index.html index 2d7ff373b7..dc5174aa7c 100644 --- a/awx/ui_next/public/index.html +++ b/awx/ui_next/public/index.html @@ -1,6 +1,15 @@ + <% if (process.env.NODE_ENV === 'production') { %> + + + <% } %> @@ -12,6 +21,10 @@ -
+ <% if (process.env.NODE_ENV === 'production') { %> +
+ <% } else { %> +
+ <% } %> diff --git a/awx/ui_next/public/installing.html b/awx/ui_next/public/installing.html new file mode 100644 index 0000000000..6495b7091c --- /dev/null +++ b/awx/ui_next/public/installing.html @@ -0,0 +1,25 @@ + + + + + + + + + + +
+ +

AWX is installing.

+

This page will refresh when complete.

+
+
+ + diff --git a/awx/ui_next/src/App.jsx b/awx/ui_next/src/App.jsx index ffafd07dba..5833714d89 100644 --- a/awx/ui_next/src/App.jsx +++ b/awx/ui_next/src/App.jsx @@ -30,7 +30,12 @@ const ProtectedRoute = ({ children, ...rest }) => function App() { const catalogs = { en, ja }; - const language = getLanguageWithoutRegionCode(navigator); + let language = getLanguageWithoutRegionCode(navigator); + if (!Object.keys(catalogs).includes(language)) { + // If there isn't a string catalog available for the browser's + // preferred language, default to one that has strings. + language = 'en'; + } const match = useRouteMatch(); const { hash, search, pathname } = useLocation(); diff --git a/awx/ui_next/src/App.test.jsx b/awx/ui_next/src/App.test.jsx index c2d667df3b..a6fe414b39 100644 --- a/awx/ui_next/src/App.test.jsx +++ b/awx/ui_next/src/App.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; - +import { act } from 'react-dom/test-utils'; import { mountWithContexts } from '../testUtils/enzymeHelpers'; import App from './App'; @@ -7,8 +7,11 @@ import App from './App'; jest.mock('./api'); describe('', () => { - test('renders ok', () => { - const wrapper = mountWithContexts(); + test('renders ok', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts(); + }); expect(wrapper.length).toBe(1); }); }); diff --git a/awx/ui_next/src/api/Base.js b/awx/ui_next/src/api/Base.js index 56492715fb..cd0a76c1ec 100644 --- a/awx/ui_next/src/api/Base.js +++ b/awx/ui_next/src/api/Base.js @@ -1,6 +1,13 @@ import axios from 'axios'; +import { SESSION_TIMEOUT_KEY } from '../constants'; import { encodeQueryString } from '../util/qs'; +import debounce from '../util/debounce'; + +const updateStorage = debounce((key, val) => { + window.localStorage.setItem(key, val); + window.dispatchEvent(new Event('storage')); +}, 500); const defaultHttp = axios.create({ xsrfCookieName: 'csrftoken', @@ -10,6 +17,15 @@ const defaultHttp = axios.create({ }, }); +defaultHttp.interceptors.response.use(response => { + const timeout = response?.headers['session-timeout']; + if (timeout) { + const timeoutDate = new Date().getTime() + timeout * 1000; + updateStorage(SESSION_TIMEOUT_KEY, String(timeoutDate)); + } + return response; +}); + class Base { constructor(http = defaultHttp, baseURL) { this.http = http; diff --git a/awx/ui_next/src/api/index.js b/awx/ui_next/src/api/index.js index b3abd8e3bc..cddf01e259 100644 --- a/awx/ui_next/src/api/index.js +++ b/awx/ui_next/src/api/index.js @@ -1,5 +1,7 @@ +import ActivityStream from './models/ActivityStream'; import AdHocCommands from './models/AdHocCommands'; import Applications from './models/Applications'; +import Auth from './models/Auth'; import Config from './models/Config'; import CredentialInputSources from './models/CredentialInputSources'; import CredentialTypes from './models/CredentialTypes'; @@ -32,13 +34,16 @@ import Tokens from './models/Tokens'; import UnifiedJobTemplates from './models/UnifiedJobTemplates'; import UnifiedJobs from './models/UnifiedJobs'; import Users from './models/Users'; +import WorkflowApprovals from './models/WorkflowApprovals'; import WorkflowApprovalTemplates from './models/WorkflowApprovalTemplates'; import WorkflowJobTemplateNodes from './models/WorkflowJobTemplateNodes'; import WorkflowJobTemplates from './models/WorkflowJobTemplates'; import WorkflowJobs from './models/WorkflowJobs'; +const ActivityStreamAPI = new ActivityStream(); const AdHocCommandsAPI = new AdHocCommands(); const ApplicationsAPI = new Applications(); +const AuthAPI = new Auth(); const ConfigAPI = new Config(); const CredentialInputSourcesAPI = new CredentialInputSources(); const CredentialTypesAPI = new CredentialTypes(); @@ -71,14 +76,17 @@ const TokensAPI = new Tokens(); const UnifiedJobTemplatesAPI = new UnifiedJobTemplates(); const UnifiedJobsAPI = new UnifiedJobs(); const UsersAPI = new Users(); +const WorkflowApprovalsAPI = new WorkflowApprovals(); const WorkflowApprovalTemplatesAPI = new WorkflowApprovalTemplates(); const WorkflowJobTemplateNodesAPI = new WorkflowJobTemplateNodes(); const WorkflowJobTemplatesAPI = new WorkflowJobTemplates(); const WorkflowJobsAPI = new WorkflowJobs(); export { + ActivityStreamAPI, AdHocCommandsAPI, ApplicationsAPI, + AuthAPI, ConfigAPI, CredentialInputSourcesAPI, CredentialTypesAPI, @@ -111,6 +119,7 @@ export { UnifiedJobTemplatesAPI, UnifiedJobsAPI, UsersAPI, + WorkflowApprovalsAPI, WorkflowApprovalTemplatesAPI, WorkflowJobTemplateNodesAPI, WorkflowJobTemplatesAPI, diff --git a/awx/ui_next/src/api/models/ActivityStream.js b/awx/ui_next/src/api/models/ActivityStream.js new file mode 100644 index 0000000000..99b65bc634 --- /dev/null +++ b/awx/ui_next/src/api/models/ActivityStream.js @@ -0,0 +1,10 @@ +import Base from '../Base'; + +class ActivityStream extends Base { + constructor(http) { + super(http); + this.baseUrl = '/api/v2/activity_stream/'; + } +} + +export default ActivityStream; diff --git a/awx/ui_next/src/api/models/Auth.js b/awx/ui_next/src/api/models/Auth.js new file mode 100644 index 0000000000..5743b4f3d5 --- /dev/null +++ b/awx/ui_next/src/api/models/Auth.js @@ -0,0 +1,10 @@ +import Base from '../Base'; + +class Auth extends Base { + constructor(http) { + super(http); + this.baseUrl = '/api/v2/auth/'; + } +} + +export default Auth; diff --git a/awx/ui_next/src/api/models/Jobs.js b/awx/ui_next/src/api/models/Jobs.js index 9c43509f9e..fc9bbb2334 100644 --- a/awx/ui_next/src/api/models/Jobs.js +++ b/awx/ui_next/src/api/models/Jobs.js @@ -36,6 +36,10 @@ class Jobs extends RelaunchMixin(Base) { return this.http.post(`/api/v2${getBaseURL(type)}${id}/cancel/`); } + readCredentials(id, type) { + return this.http.get(`/api/v2${getBaseURL(type)}${id}/credentials/`); + } + readDetail(id, type) { return this.http.get(`/api/v2${getBaseURL(type)}${id}/`); } diff --git a/awx/ui_next/src/api/models/WorkflowApprovals.js b/awx/ui_next/src/api/models/WorkflowApprovals.js new file mode 100644 index 0000000000..4674d338c5 --- /dev/null +++ b/awx/ui_next/src/api/models/WorkflowApprovals.js @@ -0,0 +1,18 @@ +import Base from '../Base'; + +class WorkflowApprovals extends Base { + constructor(http) { + super(http); + this.baseUrl = '/api/v2/workflow_approvals/'; + } + + approve(id) { + return this.http.post(`${this.baseUrl}${id}/approve/`); + } + + deny(id) { + return this.http.post(`${this.baseUrl}${id}/deny/`); + } +} + +export default WorkflowApprovals; diff --git a/awx/ui_next/src/api/models/WorkflowJobTemplateNodes.js b/awx/ui_next/src/api/models/WorkflowJobTemplateNodes.js index 512316a1ab..b628312d03 100644 --- a/awx/ui_next/src/api/models/WorkflowJobTemplateNodes.js +++ b/awx/ui_next/src/api/models/WorkflowJobTemplateNodes.js @@ -55,6 +55,19 @@ class WorkflowJobTemplateNodes extends Base { readCredentials(id) { return this.http.get(`${this.baseUrl}${id}/credentials/`); } + + associateCredentials(id, credentialId) { + return this.http.post(`${this.baseUrl}${id}/credentials/`, { + id: credentialId, + }); + } + + disassociateCredentials(id, credentialId) { + return this.http.post(`${this.baseUrl}${id}/credentials/`, { + id: credentialId, + disassociate: true, + }); + } } export default WorkflowJobTemplateNodes; diff --git a/awx/ui_next/src/components/About/About.jsx b/awx/ui_next/src/components/About/About.jsx index 8444797830..f68f75b613 100644 --- a/awx/ui_next/src/components/About/About.jsx +++ b/awx/ui_next/src/components/About/About.jsx @@ -12,8 +12,8 @@ import { import { BrandName } from '../../variables'; import brandLogoImg from './brand-logo.svg'; -class About extends React.Component { - static createSpeechBubble(version) { +function About({ ansible_version, version, isOpen, onClose, i18n }) { + const createSpeechBubble = () => { let text = `${BrandName} ${version}`; let top = ''; let bottom = ''; @@ -28,31 +28,22 @@ class About extends React.Component { bottom = ` --${bottom}-- `; return top + text + bottom; - } + }; - constructor(props) { - super(props); + const speechBubble = createSpeechBubble(); - this.createSpeechBubble = this.constructor.createSpeechBubble.bind(this); - } - - render() { - const { ansible_version, version, isOpen, onClose, i18n } = this.props; - - const speechBubble = this.createSpeechBubble(version); - - return ( - -
-          {speechBubble}
-          {`
+  return (
+    
+      
+        {speechBubble}
+        {`
           \\
           \\   ^__^
               (oo)\\_______
@@ -60,18 +51,17 @@ class About extends React.Component {
                   ||----w |
                   ||     ||
                     `}
-        
- - - - {i18n._(t`Ansible Version`)} - - {ansible_version} - - -
- ); - } +
+ + + + {i18n._(t`Ansible Version`)} + + {ansible_version} + + +
+ ); } About.propTypes = { diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx index 48fc566e2c..89387b9e8b 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx @@ -57,7 +57,7 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) { fetchData(); }, [fetchData]); const { - isloading: isLaunchLoading, + isLoading: isLaunchLoading, error: launchError, request: launchAdHocCommands, } = useRequest( diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx index fa6f931c24..e95f0b05cb 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx @@ -58,7 +58,7 @@ function AdHocCredentialStep({ i18n, credentialTypeId, onEnableLaunch }) { return ; } if (isLoading) { - return ; + return ; } return (
diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index 95cb910295..e339142b52 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.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'; @@ -17,95 +17,57 @@ const readTeams = async queryParams => TeamsAPI.read(queryParams); const readTeamsOptions = async () => TeamsAPI.readOptions(); -class AddResourceRole extends React.Component { - constructor(props) { - super(props); - - this.state = { - selectedResource: null, - selectedResourceRows: [], - selectedRoleRows: [], - currentStepId: 1, - maxEnabledStep: 1, - }; - - this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind( - this - ); - this.handleResourceSelect = this.handleResourceSelect.bind(this); - this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this); - this.handleWizardNext = this.handleWizardNext.bind(this); - this.handleWizardSave = this.handleWizardSave.bind(this); - this.handleWizardGoToStep = this.handleWizardGoToStep.bind(this); - } - - handleResourceCheckboxClick(user) { - const { selectedResourceRows, currentStepId } = this.state; +function AddResourceRole({ onSave, onClose, roles, i18n, resource }) { + const [selectedResource, setSelectedResource] = useState(null); + const [selectedResourceRows, setSelectedResourceRows] = useState([]); + const [selectedRoleRows, setSelectedRoleRows] = useState([]); + const [currentStepId, setCurrentStepId] = useState(1); + const [maxEnabledStep, setMaxEnabledStep] = useState(1); + const handleResourceCheckboxClick = user => { const selectedIndex = selectedResourceRows.findIndex( selectedRow => selectedRow.id === user.id ); - if (selectedIndex > -1) { selectedResourceRows.splice(selectedIndex, 1); - const stateToUpdate = { selectedResourceRows }; if (selectedResourceRows.length === 0) { - stateToUpdate.maxEnabledStep = currentStepId; + setMaxEnabledStep(currentStepId); } - this.setState(stateToUpdate); + setSelectedRoleRows(selectedResourceRows); } else { - this.setState(prevState => ({ - selectedResourceRows: [...prevState.selectedResourceRows, user], - })); + setSelectedResourceRows([...selectedResourceRows, user]); } - } - - handleRoleCheckboxClick(role) { - const { selectedRoleRows } = this.state; + }; + const handleRoleCheckboxClick = role => { const selectedIndex = selectedRoleRows.findIndex( selectedRow => selectedRow.id === role.id ); if (selectedIndex > -1) { selectedRoleRows.splice(selectedIndex, 1); - this.setState({ selectedRoleRows }); + setSelectedRoleRows(selectedRoleRows); } else { - this.setState(prevState => ({ - selectedRoleRows: [...prevState.selectedRoleRows, role], - })); + setSelectedRoleRows([...selectedRoleRows, role]); } - } + }; - handleResourceSelect(resourceType) { - this.setState({ - selectedResource: resourceType, - selectedResourceRows: [], - selectedRoleRows: [], - }); - } + const handleResourceSelect = resourceType => { + setSelectedResource(resourceType); + setSelectedResourceRows([]); + setSelectedRoleRows([]); + }; - handleWizardNext(step) { - this.setState({ - currentStepId: step.id, - maxEnabledStep: step.id, - }); - } + const handleWizardNext = step => { + setCurrentStepId(step.id); + setMaxEnabledStep(step.id); + }; - handleWizardGoToStep(step) { - this.setState({ - currentStepId: step.id, - }); - } - - async handleWizardSave() { - const { onSave } = this.props; - const { - selectedResourceRows, - selectedRoleRows, - selectedResource, - } = this.state; + const handleWizardGoToStep = step => { + setCurrentStepId(step.id); + }; + const handleWizardSave = async () => { try { const roleRequests = []; @@ -134,205 +96,198 @@ class AddResourceRole extends React.Component { } catch (err) { // TODO: handle this error } + }; + + // Object roles can be user only, so we remove them when + // showing role choices for team access + const selectableRoles = { ...roles }; + if (selectedResource === 'teams') { + Object.keys(roles).forEach(key => { + if (selectableRoles[key].user_only) { + delete selectableRoles[key]; + } + }); } - render() { - const { - selectedResource, - selectedResourceRows, - selectedRoleRows, - currentStepId, - maxEnabledStep, - } = this.state; - const { onClose, roles, i18n } = this.props; + const userSearchColumns = [ + { + name: i18n._(t`Username`), + key: 'username__icontains', + isDefault: true, + }, + { + name: i18n._(t`First Name`), + key: 'first_name__icontains', + }, + { + name: i18n._(t`Last Name`), + key: 'last_name__icontains', + }, + ]; + const userSortColumns = [ + { + name: i18n._(t`Username`), + key: 'username', + }, + { + name: i18n._(t`First Name`), + key: 'first_name', + }, + { + name: i18n._(t`Last Name`), + key: 'last_name', + }, + ]; + const teamSearchColumns = [ + { + name: i18n._(t`Name`), + key: 'name', + isDefault: true, + }, + { + name: i18n._(t`Created By (Username)`), + key: 'created_by__username', + }, + { + name: i18n._(t`Modified By (Username)`), + key: 'modified_by__username', + }, + ]; - // Object roles can be user only, so we remove them when - // showing role choices for team access - const selectableRoles = { ...roles }; - if (selectedResource === 'teams') { - Object.keys(roles).forEach(key => { - if (selectableRoles[key].user_only) { - delete selectableRoles[key]; - } - }); - } + const teamSortColumns = [ + { + name: i18n._(t`Name`), + key: 'name', + }, + ]; - const userSearchColumns = [ - { - name: i18n._(t`Username`), - key: 'username__icontains', - isDefault: true, - }, - { - name: i18n._(t`First Name`), - key: 'first_name__icontains', - }, - { - name: i18n._(t`Last Name`), - key: 'last_name__icontains', - }, - ]; + let wizardTitle = ''; - const userSortColumns = [ - { - name: i18n._(t`Username`), - key: 'username', - }, - { - name: i18n._(t`First Name`), - key: 'first_name', - }, - { - name: i18n._(t`Last Name`), - key: 'last_name', - }, - ]; + switch (selectedResource) { + case 'users': + wizardTitle = i18n._(t`Add User Roles`); + break; + case 'teams': + wizardTitle = i18n._(t`Add Team Roles`); + break; + default: + wizardTitle = i18n._(t`Add Roles`); + } - const teamSearchColumns = [ - { - name: i18n._(t`Name`), - key: 'name', - isDefault: true, - }, - { - name: i18n._(t`Created By (Username)`), - key: 'created_by__username', - }, - { - name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', - }, - ]; - - const teamSortColumns = [ - { - name: i18n._(t`Name`), - key: 'name', - }, - ]; - - let wizardTitle = ''; - - switch (selectedResource) { - case 'users': - wizardTitle = i18n._(t`Add User Roles`); - break; - case 'teams': - wizardTitle = i18n._(t`Add Team Roles`); - break; - default: - wizardTitle = i18n._(t`Add Roles`); - } - - const steps = [ - { - id: 1, - name: i18n._(t`Select a Resource Type`), - component: ( -
-
- {i18n._( - t`Choose the type of resource that will be receiving new roles. For example, if you'd like to add new roles to a set of users please choose Users and click Next. You'll be able to select the specific resources in the next step.` - )} -
- this.handleResourceSelect('users')} - /> + const steps = [ + { + id: 1, + name: i18n._(t`Select a Resource Type`), + component: ( +
+
+ {i18n._( + t`Choose the type of resource that will be receiving new roles. For example, if you'd like to add new roles to a set of users please choose Users and click Next. You'll be able to select the specific resources in the next step.` + )} +
+ handleResourceSelect('users')} + /> + {resource?.type === 'credential' && !resource?.organization ? null : ( this.handleResourceSelect('teams')} + onClick={() => handleResourceSelect('teams')} /> -
- ), - enableNext: selectedResource !== null, - }, - { - id: 2, - name: i18n._(t`Select Items from List`), - component: ( - - {selectedResource === 'users' && ( - - )} - {selectedResource === 'teams' && ( - - )} - - ), - enableNext: selectedResourceRows.length > 0, - canJumpTo: maxEnabledStep >= 2, - }, - { - id: 3, - name: i18n._(t`Select Roles to Apply`), - component: ( - - ), - nextButtonText: i18n._(t`Save`), - enableNext: selectedRoleRows.length > 0, - canJumpTo: maxEnabledStep >= 3, - }, - ]; + )} +
+ ), + enableNext: selectedResource !== null, + }, + { + id: 2, + name: i18n._(t`Select Items from List`), + component: ( + + {selectedResource === 'users' && ( + + )} + {selectedResource === 'teams' && ( + + )} + + ), + enableNext: selectedResourceRows.length > 0, + canJumpTo: maxEnabledStep >= 2, + }, + { + id: 3, + name: i18n._(t`Select Roles to Apply`), + component: ( + + ), + nextButtonText: i18n._(t`Save`), + enableNext: selectedRoleRows.length > 0, + canJumpTo: maxEnabledStep >= 3, + }, + ]; - const currentStep = steps.find(step => step.id === currentStepId); + const currentStep = steps.find(step => step.id === currentStepId); - // TODO: somehow internationalize steps and currentStep.nextButtonText - return ( - - ); - } + // TODO: somehow internationalize steps and currentStep.nextButtonText + return ( + handleWizardGoToStep(step)} + steps={steps} + title={wizardTitle} + nextButtonText={currentStep.nextButtonText || undefined} + backButtonText={i18n._(t`Back`)} + cancelButtonText={i18n._(t`Cancel`)} + /> + ); } AddResourceRole.propTypes = { onClose: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired, roles: PropTypes.shape(), + resource: PropTypes.shape(), }; AddResourceRole.defaultProps = { roles: {}, + resource: {}, }; export { AddResourceRole as _AddResourceRole }; diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.test.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.test.jsx index 76f5dbb87e..264f7cdb28 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.test.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.test.jsx @@ -1,22 +1,46 @@ /* eslint-disable react/jsx-pascal-case */ import React from 'react'; import { shallow } from 'enzyme'; -import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; + +import { + mountWithContexts, + waitForElement, +} from '../../../testUtils/enzymeHelpers'; import AddResourceRole, { _AddResourceRole } from './AddResourceRole'; import { TeamsAPI, UsersAPI } from '../../api'; -jest.mock('../../api'); +jest.mock('../../api/models/Teams'); +jest.mock('../../api/models/Users'); + +// TODO: Once error handling is functional in +// this component write tests for it describe('<_AddResourceRole />', () => { UsersAPI.read.mockResolvedValue({ data: { count: 2, results: [ - { id: 1, username: 'foo' }, - { id: 2, username: 'bar' }, + { id: 1, username: 'foo', url: '' }, + { id: 2, username: 'bar', url: '' }, ], }, }); + UsersAPI.readOptions.mockResolvedValue({ + data: { related: {}, actions: { GET: {} } }, + }); + TeamsAPI.read.mockResolvedValue({ + data: { + count: 2, + results: [ + { id: 1, name: 'Team foo', url: '' }, + { id: 2, name: 'Team bar', url: '' }, + ], + }, + }); + TeamsAPI.readOptions.mockResolvedValue({ + data: { related: {}, actions: { GET: {} } }, + }); const roles = { admin_role: { description: 'Can manage all aspects of the organization', @@ -39,186 +63,180 @@ describe('<_AddResourceRole />', () => { /> ); }); - test('handleRoleCheckboxClick properly updates state', () => { - const wrapper = shallow( - <_AddResourceRole - onClose={() => {}} - onSave={() => {}} - roles={roles} - i18n={{ _: val => val.toString() }} - /> - ); - wrapper.setState({ - selectedRoleRows: [ - { - description: 'Can manage all aspects of the organization', - name: 'Admin', - id: 1, - }, - ], + test('should save properly', async () => { + let wrapper; + act(() => { + wrapper = mountWithContexts( + {}} onSave={() => {}} roles={roles} />, + { context: { network: { handleHttpError: () => {} } } } + ); }); - wrapper.instance().handleRoleCheckboxClick({ - description: 'Can manage all aspects of the organization', - name: 'Admin', - id: 1, - }); - expect(wrapper.state('selectedRoleRows')).toEqual([]); - wrapper.instance().handleRoleCheckboxClick({ - description: 'Can manage all aspects of the organization', - name: 'Admin', - id: 1, - }); - expect(wrapper.state('selectedRoleRows')).toEqual([ - { - description: 'Can manage all aspects of the organization', - name: 'Admin', - id: 1, - }, - ]); - }); - test('handleResourceCheckboxClick properly updates state', () => { - const wrapper = shallow( - <_AddResourceRole - onClose={() => {}} - onSave={() => {}} - roles={roles} - i18n={{ _: val => val.toString() }} - /> - ); - wrapper.setState({ - selectedResourceRows: [ - { - id: 1, - username: 'foobar', - }, - ], - }); - wrapper.instance().handleResourceCheckboxClick({ - id: 1, - username: 'foobar', - }); - expect(wrapper.state('selectedResourceRows')).toEqual([]); - wrapper.instance().handleResourceCheckboxClick({ - id: 1, - username: 'foobar', - }); - expect(wrapper.state('selectedResourceRows')).toEqual([ - { - id: 1, - username: 'foobar', - }, - ]); - }); - test('clicking user/team cards updates state', () => { - const spy = jest.spyOn(_AddResourceRole.prototype, 'handleResourceSelect'); - const wrapper = mountWithContexts( - {}} onSave={() => {}} roles={roles} />, - { context: { network: { handleHttpError: () => {} } } } - ).find('AddResourceRole'); + wrapper.update(); + + // Step 1 const selectableCardWrapper = wrapper.find('SelectableCard'); expect(selectableCardWrapper.length).toBe(2); - selectableCardWrapper.first().simulate('click'); - expect(spy).toHaveBeenCalledWith('users'); - expect(wrapper.state('selectedResource')).toBe('users'); - selectableCardWrapper.at(1).simulate('click'); - expect(spy).toHaveBeenCalledWith('teams'); - expect(wrapper.state('selectedResource')).toBe('teams'); + act(() => wrapper.find('SelectableCard[label="Users"]').prop('onClick')()); + wrapper.update(); + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + wrapper.update(); + + // Step 2 + await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); + act(() => + wrapper.find('DataListCheck[name="foo"]').invoke('onChange')(true) + ); + wrapper.update(); + expect(wrapper.find('DataListCheck[name="foo"]').prop('checked')).toBe( + true + ); + act(() => wrapper.find('Button[type="submit"]').prop('onClick')()); + wrapper.update(); + + // Step 3 + act(() => + wrapper.find('Checkbox[aria-label="Admin"]').invoke('onChange')(true) + ); + wrapper.update(); + expect(wrapper.find('Checkbox[aria-label="Admin"]').prop('isChecked')).toBe( + true + ); + + // Save + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + expect(UsersAPI.associateRole).toBeCalledWith(1, 1); }); - test('handleResourceSelect clears out selected lists and sets selectedResource', () => { - const wrapper = shallow( - <_AddResourceRole + + test('should successfuly click user/team cards', async () => { + let wrapper; + act(() => { + wrapper = mountWithContexts( + {}} onSave={() => {}} roles={roles} />, + { context: { network: { handleHttpError: () => {} } } } + ); + }); + wrapper.update(); + + const selectableCardWrapper = wrapper.find('SelectableCard'); + expect(selectableCardWrapper.length).toBe(2); + act(() => wrapper.find('SelectableCard[label="Users"]').prop('onClick')()); + wrapper.update(); + + await waitForElement( + wrapper, + 'SelectableCard[label="Users"]', + el => el.prop('isSelected') === true + ); + act(() => wrapper.find('SelectableCard[label="Teams"]').prop('onClick')()); + wrapper.update(); + + await waitForElement( + wrapper, + 'SelectableCard[label="Teams"]', + el => el.prop('isSelected') === true + ); + }); + + test('should reset values with resource type changes', async () => { + let wrapper; + act(() => { + wrapper = mountWithContexts( + {}} onSave={() => {}} roles={roles} />, + { context: { network: { handleHttpError: () => {} } } } + ); + }); + wrapper.update(); + + // Step 1 + const selectableCardWrapper = wrapper.find('SelectableCard'); + expect(selectableCardWrapper.length).toBe(2); + act(() => wrapper.find('SelectableCard[label="Users"]').prop('onClick')()); + wrapper.update(); + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + wrapper.update(); + + // Step 2 + await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); + act(() => + wrapper.find('DataListCheck[name="foo"]').invoke('onChange')(true) + ); + wrapper.update(); + expect(wrapper.find('DataListCheck[name="foo"]').prop('checked')).toBe( + true + ); + act(() => wrapper.find('Button[type="submit"]').prop('onClick')()); + wrapper.update(); + + // Step 3 + act(() => + wrapper.find('Checkbox[aria-label="Admin"]').invoke('onChange')(true) + ); + wrapper.update(); + expect(wrapper.find('Checkbox[aria-label="Admin"]').prop('isChecked')).toBe( + true + ); + + // Go back to step 1 + act(() => { + wrapper + .find('WizardNavItem[content="Select a Resource Type"]') + .find('button') + .prop('onClick')({ id: 1 }); + }); + wrapper.update(); + expect( + wrapper + .find('WizardNavItem[content="Select a Resource Type"]') + .prop('isCurrent') + ).toBe(true); + + // Go back to step 1 and this time select teams. Doing so should clear following steps + act(() => wrapper.find('SelectableCard[label="Teams"]').prop('onClick')()); + wrapper.update(); + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + wrapper.update(); + + // Make sure no teams have been selected + await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); + wrapper + .find('DataListCheck') + .map(item => expect(item.prop('checked')).toBe(false)); + act(() => wrapper.find('Button[type="submit"]').prop('onClick')()); + wrapper.update(); + + // Make sure that no roles have been selected + wrapper + .find('Checkbox') + .map(card => expect(card.prop('isChecked')).toBe(false)); + + // Make sure the save button is disabled + expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true); + }); + + test('should not display team as a choice in case credential does not have organization', () => { + const wrapper = mountWithContexts( + {}} onSave={() => {}} roles={roles} - i18n={{ _: val => val.toString() }} - /> - ); - wrapper.setState({ - selectedResource: 'teams', - selectedResourceRows: [ - { - id: 1, - username: 'foobar', - }, - ], - selectedRoleRows: [ - { - description: 'Can manage all aspects of the organization', - id: 1, - name: 'Admin', - }, - ], - }); - wrapper.instance().handleResourceSelect('users'); - expect(wrapper.state()).toEqual({ - selectedResource: 'users', - selectedResourceRows: [], - selectedRoleRows: [], - currentStepId: 1, - maxEnabledStep: 1, - }); - wrapper.instance().handleResourceSelect('teams'); - expect(wrapper.state()).toEqual({ - selectedResource: 'teams', - selectedResourceRows: [], - selectedRoleRows: [], - currentStepId: 1, - maxEnabledStep: 1, - }); - }); - test('handleWizardSave makes correct api calls, calls onSave when done', async () => { - const handleSave = jest.fn(); - const wrapper = mountWithContexts( - {}} onSave={handleSave} roles={roles} />, + resource={{ type: 'credential', organization: null }} + />, { context: { network: { handleHttpError: () => {} } } } - ).find('AddResourceRole'); - wrapper.setState({ - selectedResource: 'users', - selectedResourceRows: [ - { - id: 1, - username: 'foobar', - }, - ], - selectedRoleRows: [ - { - description: 'Can manage all aspects of the organization', - id: 1, - name: 'Admin', - }, - { - description: 'May run any executable resources in the organization', - id: 2, - name: 'Execute', - }, - ], - }); - await wrapper.instance().handleWizardSave(); - expect(UsersAPI.associateRole).toHaveBeenCalledTimes(2); - expect(handleSave).toHaveBeenCalled(); - wrapper.setState({ - selectedResource: 'teams', - selectedResourceRows: [ - { - id: 1, - name: 'foobar', - }, - ], - selectedRoleRows: [ - { - description: 'Can manage all aspects of the organization', - id: 1, - name: 'Admin', - }, - { - description: 'May run any executable resources in the organization', - id: 2, - name: 'Execute', - }, - ], - }); - await wrapper.instance().handleWizardSave(); - expect(TeamsAPI.associateRole).toHaveBeenCalledTimes(2); - expect(handleSave).toHaveBeenCalled(); + ); + + expect(wrapper.find('SelectableCard').length).toBe(1); + wrapper.find('SelectableCard[label="Users"]').simulate('click'); + wrapper.update(); + expect( + wrapper.find('SelectableCard[label="Users"]').prop('isSelected') + ).toBe(true); }); }); diff --git a/awx/ui_next/src/components/AddRole/SelectRoleStep.jsx b/awx/ui_next/src/components/AddRole/SelectRoleStep.jsx index 826c2d52aa..32f0e6a96c 100644 --- a/awx/ui_next/src/components/AddRole/SelectRoleStep.jsx +++ b/awx/ui_next/src/components/AddRole/SelectRoleStep.jsx @@ -7,59 +7,55 @@ import { t } from '@lingui/macro'; import CheckboxCard from './CheckboxCard'; import SelectedList from '../SelectedList'; -class RolesStep extends React.Component { - render() { - const { - onRolesClick, - roles, - selectedListKey, - selectedListLabel, - selectedResourceRows, - selectedRoleRows, - i18n, - } = this.props; - - return ( - -
- {i18n._( - t`Choose roles to apply to the selected resources. Note that all selected roles will be applied to all selected resources.` - )} -
-
- {selectedResourceRows.length > 0 && ( - - )} -
-
- {Object.keys(roles).map(role => ( - item.id === roles[role].id - )} - key={roles[role].id} - name={roles[role].name} - onSelect={() => onRolesClick(roles[role])} - /> - ))} -
-
- ); - } +function RolesStep({ + onRolesClick, + roles, + selectedListKey, + selectedListLabel, + selectedResourceRows, + selectedRoleRows, + i18n, +}) { + return ( + +
+ {i18n._( + t`Choose roles to apply to the selected resources. Note that all selected roles will be applied to all selected resources.` + )} +
+
+ {selectedResourceRows.length > 0 && ( + + )} +
+
+ {Object.keys(roles).map(role => ( + item.id === roles[role].id + )} + key={roles[role].id} + name={roles[role].name} + onSelect={() => onRolesClick(roles[role])} + /> + ))} +
+
+ ); } RolesStep.propTypes = { diff --git a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx index 4f49268c0a..62b8983eb4 100644 --- a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx +++ b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx @@ -12,52 +12,44 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { FormSelect, FormSelectOption } from '@patternfly/react-core'; -class AnsibleSelect extends React.Component { - constructor(props) { - super(props); - this.onSelectChange = this.onSelectChange.bind(this); - } - - onSelectChange(val, event) { - const { onChange, name } = this.props; +function AnsibleSelect({ + id, + data, + i18n, + isValid, + onBlur, + value, + className, + isDisabled, + onChange, + name, +}) { + const onSelectChange = (val, event) => { event.target.name = name; onChange(event, val); - } + }; - render() { - const { - id, - data, - i18n, - isValid, - onBlur, - value, - className, - isDisabled, - } = this.props; - - return ( - - {data.map(option => ( - - ))} - - ); - } + return ( + + {data.map(option => ( + + ))} + + ); } const Option = shape({ diff --git a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.test.jsx b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.test.jsx index d9bd7c669d..bb671c8823 100644 --- a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.test.jsx +++ b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.test.jsx @@ -1,21 +1,22 @@ import React from 'react'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; -import AnsibleSelect, { _AnsibleSelect } from './AnsibleSelect'; +import AnsibleSelect from './AnsibleSelect'; const mockData = [ { key: 'baz', label: 'Baz', - value: '/venv/baz/', + value: '/var/lib/awx/venv/baz/', }, { key: 'default', label: 'Default', - value: '/venv/ansible/', + value: '/var/lib/awx/venv/ansible/', }, ]; describe('', () => { + const onChange = jest.fn(); test('initially renders succesfully', async () => { mountWithContexts( ', () => { }); test('calls "onSelectChange" on dropdown select change', () => { - const spy = jest.spyOn(_AnsibleSelect.prototype, 'onSelectChange'); const wrapper = mountWithContexts( {}} + onChange={onChange} data={mockData} /> ); - expect(spy).not.toHaveBeenCalled(); + expect(onChange).not.toHaveBeenCalled(); wrapper.find('select').simulate('change'); - expect(spy).toHaveBeenCalled(); + expect(onChange).toHaveBeenCalled(); }); test('Returns correct select options', () => { diff --git a/awx/ui_next/src/components/AppContainer/AppContainer.jsx b/awx/ui_next/src/components/AppContainer/AppContainer.jsx index b107fd0f40..0abd198c07 100644 --- a/awx/ui_next/src/components/AppContainer/AppContainer.jsx +++ b/awx/ui_next/src/components/AppContainer/AppContainer.jsx @@ -1,6 +1,7 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useRef } from 'react'; import { useHistory, useLocation, withRouter } from 'react-router-dom'; import { + Button, Nav, NavList, Page, @@ -13,6 +14,8 @@ import styled from 'styled-components'; import { ConfigAPI, MeAPI, RootAPI } from '../../api'; import { ConfigProvider } from '../../contexts/Config'; +import { SESSION_TIMEOUT_KEY } from '../../constants'; +import { isAuthenticated } from '../../util/auth'; import About from '../About'; import AlertModal from '../AlertModal'; import ErrorDetail from '../ErrorDetail'; @@ -20,6 +23,17 @@ import BrandLogo from './BrandLogo'; import NavExpandableGroup from './NavExpandableGroup'; import PageHeaderToolbar from './PageHeaderToolbar'; +// The maximum supported timeout for setTimeout(), in milliseconds, +// is the highest number you can represent as a signed 32bit +// integer (approximately 25 days) +const MAX_TIMEOUT = 2 ** (32 - 1) - 1; + +// The number of seconds the session timeout warning is displayed +// before the user is logged out. Increasing this number (up to +// the total session time, which is 1800s by default) will cause +// the session timeout warning to display sooner. +const SESSION_WARNING_DURATION = 10; + const PageHeader = styled(PFPageHeader)` & .pf-c-page__header-brand-link { color: inherit; @@ -30,6 +44,45 @@ const PageHeader = styled(PFPageHeader)` } `; +/** + * The useStorage hook integrates with the browser's localStorage api. + * It accepts a storage key as its only argument and returns a state + * variable and setter function for that state variable. + * + * This utility behaves much like the standard useState hook with some + * key differences: + * 1. You don't pass it an initial value. Instead, the provided key + * is used to retrieve the initial value from local storage. If + * the key doesn't exist in local storage, null is returned. + * 2. Behind the scenes, this hook registers an event listener with + * the Web Storage api to establish a two-way binding between the + * state variable and its corresponding local storage value. This + * means that updates to the state variable with the setter + * function will produce a corresponding update to the local + * storage value and vice-versa. + * 3. When local storage is shared across browser tabs, the data + * binding is also shared across browser tabs. This means that + * updates to the state variable using the setter function on + * one tab will also update the state variable on any other tab + * using this hook with the same key and vice-versa. + */ +function useStorage(key) { + const [storageVal, setStorageVal] = useState( + window.localStorage.getItem(key) + ); + window.addEventListener('storage', () => { + const newVal = window.localStorage.getItem(key); + if (newVal !== storageVal) { + setStorageVal(newVal); + } + }); + const setValue = val => { + window.localStorage.setItem(key, val); + setStorageVal(val); + }; + return [storageVal, setValue]; +} + function AppContainer({ i18n, navRouteConfig = [], children }) { const history = useHistory(); const { pathname } = useLocation(); @@ -38,14 +91,51 @@ function AppContainer({ i18n, navRouteConfig = [], children }) { const [isAboutModalOpen, setIsAboutModalOpen] = useState(false); const [isReady, setIsReady] = useState(false); + const sessionTimeoutId = useRef(); + const sessionIntervalId = useRef(); + const [sessionTimeout, setSessionTimeout] = useStorage(SESSION_TIMEOUT_KEY); + const [timeoutWarning, setTimeoutWarning] = useState(false); + const [timeRemaining, setTimeRemaining] = useState(null); + const handleAboutModalOpen = () => setIsAboutModalOpen(true); const handleAboutModalClose = () => setIsAboutModalOpen(false); const handleConfigErrorClose = () => setConfigError(null); + const handleSessionTimeout = () => setTimeoutWarning(true); const handleLogout = useCallback(async () => { await RootAPI.logout(); - history.replace('/login'); - }, [history]); + setSessionTimeout(null); + }, [setSessionTimeout]); + + const handleSessionContinue = () => { + MeAPI.read(); + setTimeoutWarning(false); + }; + + useEffect(() => { + if (!isAuthenticated(document.cookie)) history.replace('/login'); + const calcRemaining = () => + parseInt(sessionTimeout, 10) - new Date().getTime(); + const updateRemaining = () => setTimeRemaining(calcRemaining()); + setTimeoutWarning(false); + clearTimeout(sessionTimeoutId.current); + clearInterval(sessionIntervalId.current); + sessionTimeoutId.current = setTimeout( + handleSessionTimeout, + Math.min(calcRemaining() - SESSION_WARNING_DURATION * 1000, MAX_TIMEOUT) + ); + sessionIntervalId.current = setInterval(updateRemaining, 1000); + return () => { + clearTimeout(sessionTimeoutId.current); + clearInterval(sessionIntervalId.current); + }; + }, [history, sessionTimeout]); + + useEffect(() => { + if (timeRemaining !== null && timeRemaining <= 1) { + handleLogout(); + } + }, [handleLogout, timeRemaining]); useEffect(() => { const loadConfig = async () => { @@ -128,6 +218,31 @@ function AppContainer({ i18n, navRouteConfig = [], children }) { {i18n._(t`Failed to retrieve configuration.`)} + 0 && timeRemaining !== null} + onClose={handleLogout} + showClose={false} + variant="warning" + actions={[ + , + , + ]} + > + {i18n._( + t`You will be logged out in ${Number( + Math.max(Math.floor(timeRemaining / 1000), 0) + )} seconds due to inactivity.` + )} + ); } diff --git a/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx b/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx index f09bdbdf3f..1b8bb0c7e4 100644 --- a/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx +++ b/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -17,129 +17,102 @@ import { QuestionCircleIcon, UserIcon } from '@patternfly/react-icons'; const DOCLINK = 'https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html'; -class PageHeaderToolbar extends Component { - constructor(props) { - super(props); - this.state = { - isHelpOpen: false, - isUserOpen: false, - }; +function PageHeaderToolbar({ + isAboutDisabled, + onAboutClick, + onLogoutClick, + loggedInUser, + i18n, +}) { + const [isHelpOpen, setIsHelpOpen] = useState(false); + const [isUserOpen, setIsUserOpen] = useState(false); - this.handleHelpSelect = this.handleHelpSelect.bind(this); - this.handleHelpToggle = this.handleHelpToggle.bind(this); - this.handleUserSelect = this.handleUserSelect.bind(this); - this.handleUserToggle = this.handleUserToggle.bind(this); - } + const handleHelpSelect = () => { + setIsHelpOpen(!isHelpOpen); + }; - handleHelpSelect() { - const { isHelpOpen } = this.state; - - this.setState({ isHelpOpen: !isHelpOpen }); - } - - handleUserSelect() { - const { isUserOpen } = this.state; - - this.setState({ isUserOpen: !isUserOpen }); - } - - handleHelpToggle(isOpen) { - this.setState({ isHelpOpen: isOpen }); - } - - handleUserToggle(isOpen) { - this.setState({ isUserOpen: isOpen }); - } - - render() { - const { isHelpOpen, isUserOpen } = this.state; - const { - isAboutDisabled, - onAboutClick, - onLogoutClick, - loggedInUser, - i18n, - } = this.props; - - return ( - - - {i18n._(t`Info`)}}> - - - - - } - dropdownItems={[ - - {i18n._(t`Help`)} - , - - {i18n._(t`About`)} - , - ]} - /> - - - {i18n._(t`User`)}}> - - - - {loggedInUser && ( - - {loggedInUser.username} - - )} - - } - dropdownItems={[ - - {i18n._(t`User Details`)} - , - - {i18n._(t`Logout`)} - , - ]} - /> - - - - - ); - } + const handleUserSelect = () => { + setIsUserOpen(!isUserOpen); + }; + return ( + + + {i18n._(t`Info`)}}> + + + + + } + dropdownItems={[ + + {i18n._(t`Help`)} + , + + {i18n._(t`About`)} + , + ]} + /> + + + {i18n._(t`User`)}}> + + + + {loggedInUser && ( + + {loggedInUser.username} + + )} + + } + dropdownItems={[ + + {i18n._(t`User Details`)} + , + + {i18n._(t`Logout`)} + , + ]} + /> + + + + + ); } PageHeaderToolbar.propTypes = { diff --git a/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.test.jsx b/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.test.jsx index 7c151d3dc5..4dc093c6c2 100644 --- a/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.test.jsx +++ b/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.test.jsx @@ -25,6 +25,7 @@ describe('PageHeaderToolbar', () => { ); expect(wrapper.find('DropdownItem')).toHaveLength(0); @@ -37,6 +38,10 @@ describe('PageHeaderToolbar', () => { expect(wrapper.find('DropdownItem')).toHaveLength(0); wrapper.find(pageUserDropdownSelector).simulate('click'); + wrapper.update(); + expect( + wrapper.find('DropdownItem[aria-label="User details"]').prop('href') + ).toBe('/#/users/1/details'); expect(wrapper.find('DropdownItem')).toHaveLength(2); const logout = wrapper.find('DropdownItem li button'); diff --git a/awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.jsx b/awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.jsx deleted file mode 100644 index 93a9b3d7f4..0000000000 --- a/awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - PageSection as PFPageSection, - PageSectionVariants, - Breadcrumb, - BreadcrumbItem, - BreadcrumbHeading, -} from '@patternfly/react-core'; -import { Link, Route, useRouteMatch } from 'react-router-dom'; - -import styled from 'styled-components'; - -const PageSection = styled(PFPageSection)` - padding-top: 10px; - padding-bottom: 10px; -`; - -const Breadcrumbs = ({ breadcrumbConfig }) => { - const { light } = PageSectionVariants; - - return ( - - - - - - - - ); -}; - -const Crumb = ({ breadcrumbConfig, showDivider }) => { - const match = useRouteMatch(); - const crumb = breadcrumbConfig[match.url]; - - let crumbElement = ( - - {crumb} - - ); - - if (match.isExact) { - crumbElement = ( - - {crumb} - - ); - } - - if (!crumb) { - crumbElement = null; - } - - return ( - - {crumbElement} - - - - - ); -}; - -Breadcrumbs.propTypes = { - breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired, -}; - -Crumb.propTypes = { - breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired, -}; - -export default Breadcrumbs; diff --git a/awx/ui_next/src/components/Breadcrumbs/index.js b/awx/ui_next/src/components/Breadcrumbs/index.js deleted file mode 100644 index 3ff68ca589..0000000000 --- a/awx/ui_next/src/components/Breadcrumbs/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Breadcrumbs'; diff --git a/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.jsx b/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.jsx index 92a9071332..a07b6feca5 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.jsx @@ -6,6 +6,7 @@ import 'codemirror/mode/javascript/javascript'; import 'codemirror/mode/yaml/yaml'; import 'codemirror/mode/jinja2/jinja2'; import 'codemirror/lib/codemirror.css'; +import 'codemirror/addon/display/placeholder'; const LINE_HEIGHT = 24; const PADDING = 12; @@ -55,6 +56,17 @@ const CodeMirror = styled(ReactCodeMirror)` background-color: var(--pf-c-form-control--disabled--BackgroundColor); } `} + ${props => + props.options && + props.options.placeholder && + ` + .CodeMirror-empty { + pre.CodeMirror-placeholder { + color: var(--pf-c-form-control--placeholder--Color); + height: 100% !important; + } + } + `} `; function CodeMirrorInput({ @@ -66,6 +78,7 @@ function CodeMirrorInput({ rows, fullHeight, className, + placeholder, }) { // Workaround for CodeMirror bug: If CodeMirror renders in a modal on the // modal's initial render, it appears as an empty box due to mis-calculated @@ -92,6 +105,7 @@ function CodeMirrorInput({ smartIndent: false, lineNumbers: true, lineWrapping: true, + placeholder, readOnly, }} fullHeight={fullHeight} diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx index feb3ebb0b7..b5501fb0b7 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx @@ -1,6 +1,7 @@ import 'styled-components/macro'; import React, { useState, useEffect } from 'react'; import { node, number, oneOfType, shape, string, arrayOf } from 'prop-types'; +import { Trans, withI18n } from '@lingui/react'; import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core'; import { DetailName, DetailValue } from '../DetailList'; import MultiButtonToggle from '../MultiButtonToggle'; @@ -111,7 +112,7 @@ function VariablesDetail({ dataCy, helpText, value, label, rows, fullHeight }) { css="color: var(--pf-global--danger-color--100); font-size: var(--pf-global--FontSize--sm" > - Error: {error.message} + Error: {error.message} )} @@ -131,4 +132,4 @@ VariablesDetail.defaultProps = { helpText: '', }; -export default VariablesDetail; +export default withI18n()(VariablesDetail); diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx index edb83d3515..9cf080e1ed 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx @@ -1,13 +1,13 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { shallow, mount } from 'enzyme'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import VariablesDetail from './VariablesDetail'; jest.mock('../../api'); describe('', () => { test('should render readonly CodeMirrorInput', () => { - const wrapper = shallow( + const wrapper = mountWithContexts( ); const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput'); @@ -18,7 +18,7 @@ describe('', () => { }); test('should detect JSON', () => { - const wrapper = shallow( + const wrapper = mountWithContexts( ); const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput'); @@ -28,7 +28,7 @@ describe('', () => { }); test('should convert between modes', () => { - const wrapper = shallow( + const wrapper = mountWithContexts( ); wrapper.find('MultiButtonToggle').invoke('onChange')('javascript'); @@ -43,7 +43,9 @@ describe('', () => { }); test('should render label and value= --- when there are no values', () => { - const wrapper = shallow(); + const wrapper = mountWithContexts( + + ); expect(wrapper.find('VariablesDetail___StyledCodeMirrorInput').length).toBe( 1 ); @@ -51,7 +53,7 @@ describe('', () => { }); test('should update value if prop changes', () => { - const wrapper = mount( + const wrapper = mountWithContexts( ); act(() => { @@ -67,13 +69,17 @@ describe('', () => { }); test('should default yaml value to "---"', () => { - const wrapper = shallow(); + const wrapper = mountWithContexts( + + ); const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput'); expect(input.prop('value')).toEqual('---'); }); test('should default empty json to "{}"', () => { - const wrapper = mount(); + const wrapper = mountWithContexts( + + ); act(() => { wrapper.find('MultiButtonToggle').invoke('onChange')('javascript'); }); diff --git a/awx/ui_next/src/components/ContentLoading/ContentLoading.jsx b/awx/ui_next/src/components/ContentLoading/ContentLoading.jsx index b1c51a6b8f..9808cc02af 100644 --- a/awx/ui_next/src/components/ContentLoading/ContentLoading.jsx +++ b/awx/ui_next/src/components/ContentLoading/ContentLoading.jsx @@ -1,22 +1,25 @@ import React from 'react'; -import { t } from '@lingui/macro'; -import { withI18n } from '@lingui/react'; + import styled from 'styled-components'; import { EmptyState as PFEmptyState, - EmptyStateBody, + EmptyStateIcon, + Spinner, } from '@patternfly/react-core'; const EmptyState = styled(PFEmptyState)` --pf-c-empty-state--m-lg--MaxWidth: none; + min-height: 250px; `; // TODO: Better loading state - skeleton lines / spinner, etc. -const ContentLoading = ({ className, i18n }) => ( - - {i18n._(t`Loading...`)} - -); +const ContentLoading = ({ className }) => { + return ( + + + + ); +}; export { ContentLoading as _ContentLoading }; -export default withI18n()(ContentLoading); +export default ContentLoading; diff --git a/awx/ui_next/src/components/CopyButton/CopyButton.jsx b/awx/ui_next/src/components/CopyButton/CopyButton.jsx index f8eeda7626..30927f22c5 100644 --- a/awx/ui_next/src/components/CopyButton/CopyButton.jsx +++ b/awx/ui_next/src/components/CopyButton/CopyButton.jsx @@ -10,12 +10,13 @@ import AlertModal from '../AlertModal'; import ErrorDetail from '../ErrorDetail'; function CopyButton({ - i18n, + id, copyItem, isDisabled, onCopyStart, onCopyFinish, helperText, + i18n, }) { const { isLoading, error: copyError, request: copyItemToAPI } = useRequest( copyItem @@ -34,7 +35,8 @@ function CopyButton({ <> - - - - - - ); - } +function ExpandCollapse({ isCompact, onCompact, onExpand, i18n }) { + return ( + + + + + + + + + ); } ExpandCollapse.propTypes = { diff --git a/awx/ui_next/src/components/FormField/PasswordInput.jsx b/awx/ui_next/src/components/FormField/PasswordInput.jsx index 8fa0977f48..1fd6979206 100644 --- a/awx/ui_next/src/components/FormField/PasswordInput.jsx +++ b/awx/ui_next/src/components/FormField/PasswordInput.jsx @@ -12,7 +12,15 @@ import { import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons'; function PasswordInput(props) { - const { id, name, validate, isRequired, isDisabled, i18n } = props; + const { + autocomplete, + id, + name, + validate, + isRequired, + isDisabled, + i18n, + } = props; const [inputType, setInputType] = useState('password'); const [field, meta] = useField({ name, validate }); @@ -38,6 +46,7 @@ function PasswordInput(props) { {}, isRequired: false, isDisabled: false, diff --git a/awx/ui_next/src/components/JobList/JobList.jsx b/awx/ui_next/src/components/JobList/JobList.jsx index ead8916fe8..30804d4b7f 100644 --- a/awx/ui_next/src/components/JobList/JobList.jsx +++ b/awx/ui_next/src/components/JobList/JobList.jsx @@ -7,7 +7,8 @@ import { Card } from '@patternfly/react-core'; import AlertModal from '../AlertModal'; import DatalistToolbar from '../DataListToolbar'; import ErrorDetail from '../ErrorDetail'; -import PaginatedDataList, { ToolbarDeleteButton } from '../PaginatedDataList'; +import { ToolbarDeleteButton } from '../PaginatedDataList'; +import PaginatedTable, { HeaderRow, HeaderCell } from '../PaginatedTable'; import useRequest, { useDeleteItems, useDismissableError, @@ -27,7 +28,7 @@ import { } from '../../api'; function JobList({ i18n, defaultParams, showTypeColumn = false }) { - const QS_CONFIG = getQSConfig( + const qsConfig = getQSConfig( 'job', { page: 1, @@ -49,7 +50,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { } = useRequest( useCallback( async () => { - const params = parseQueryString(QS_CONFIG, location.search); + const params = parseQueryString(qsConfig, location.search); const [response, actionsResponse] = await Promise.all([ UnifiedJobsAPI.read({ ...params }), UnifiedJobsAPI.readOptions(), @@ -81,7 +82,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { // TODO: update QS_CONFIG to be safe for deps array const fetchJobsById = useCallback( async ids => { - const params = parseQueryString(QS_CONFIG, location.search); + const params = parseQueryString(qsConfig, location.search); params.id__in = ids.join(','); const { data } = await UnifiedJobsAPI.read(params); return data.results; @@ -89,7 +90,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { [location.search] // eslint-disable-line react-hooks/exhaustive-deps ); - const jobs = useWsJobs(results, fetchJobsById, QS_CONFIG); + const jobs = useWsJobs(results, fetchJobsById, qsConfig); const isAllSelected = selected.length === jobs.length && selected.length > 0; @@ -145,7 +146,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { ); }, [selected]), { - qsConfig: QS_CONFIG, + qsConfig, allItemsSelected: isAllSelected, fetchItems: fetchJobs, } @@ -176,14 +177,13 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { return ( <> - + {i18n._(t`Name`)} + {i18n._(t`Status`)} + {showTypeColumn && {i18n._(t`Type`)}} + {i18n._(t`Start Time`)} + + {i18n._(t`Finish Time`)} + + {i18n._(t`Actions`)} + + } toolbarSearchableKeys={searchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( @@ -267,13 +253,13 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { showSelectAll isAllSelected={isAllSelected} onSelectAll={handleSelectAll} - qsConfig={QS_CONFIG} + qsConfig={qsConfig} additionalControls={[ , )} - renderItem={job => ( + renderRow={(job, index) => ( handleSelect(job)} isSelected={selected.some(row => row.id === job.id)} + rowIndex={index} /> )} /> diff --git a/awx/ui_next/src/components/JobList/JobListCancelButton.jsx b/awx/ui_next/src/components/JobList/JobListCancelButton.jsx index 684d088c96..ff4cf1cda6 100644 --- a/awx/ui_next/src/components/JobList/JobListCancelButton.jsx +++ b/awx/ui_next/src/components/JobList/JobListCancelButton.jsx @@ -7,8 +7,15 @@ import { KebabifiedContext } from '../../contexts/Kebabified'; import AlertModal from '../AlertModal'; import { Job } from '../../types'; -function cannotCancel(job) { - return !job.summary_fields.user_capabilities.start; +function cannotCancelBecausePermissions(job) { + return ( + !job.summary_fields.user_capabilities.start && + ['pending', 'waiting', 'running'].includes(job.status) + ); +} + +function cannotCancelBecauseNotRunning(job) { + return !['pending', 'waiting', 'running'].includes(job.status); } function JobListCancelButton({ i18n, jobsToCancel, onCancel }) { @@ -33,20 +40,40 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) { }, [isKebabified, isModalOpen, onKebabModalChange]); const renderTooltip = () => { - const jobsUnableToCancel = jobsToCancel - .filter(cannotCancel) + const cannotCancelPermissions = jobsToCancel + .filter(cannotCancelBecausePermissions) .map(job => job.name); - const numJobsUnableToCancel = jobsUnableToCancel.length; + const cannotCancelNotRunning = jobsToCancel + .filter(cannotCancelBecauseNotRunning) + .map(job => job.name); + const numJobsUnableToCancel = cannotCancelPermissions.concat( + cannotCancelNotRunning + ).length; if (numJobsUnableToCancel > 0) { return (
- {i18n._( - '{numJobsUnableToCancel, plural, one {You do not have permission to cancel the following job:} other {You do not have permission to cancel the following jobs:}}', - { - numJobsUnableToCancel, - } + {cannotCancelPermissions.length > 0 && ( +
+ {i18n._( + '{numJobsUnableToCancel, plural, one {You do not have permission to cancel the following job:} other {You do not have permission to cancel the following jobs:}}', + { + numJobsUnableToCancel: cannotCancelPermissions.length, + } + )} + {' '.concat(cannotCancelPermissions.join(', '))} +
+ )} + {cannotCancelNotRunning.length > 0 && ( +
+ {i18n._( + '{numJobsUnableToCancel, plural, one {You cannot cancel the following job because it is not running:} other {You cannot cancel the following jobs because they are not running:}}', + { + numJobsUnableToCancel: cannotCancelNotRunning.length, + } + )} + {' '.concat(cannotCancelNotRunning.join(', '))} +
)} - {' '.concat(jobsUnableToCancel.join(', '))}
); } @@ -62,7 +89,9 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) { }; const isDisabled = - jobsToCancel.length === 0 || jobsToCancel.some(cannotCancel); + jobsToCancel.length === 0 || + jobsToCancel.some(cannotCancelBecausePermissions) || + jobsToCancel.some(cannotCancelBecauseNotRunning); const cancelJobText = i18n._( '{zeroOrOneJobSelected, plural, one {Cancel job} other {Cancel jobs}}', diff --git a/awx/ui_next/src/components/JobList/JobListCancelButton.test.jsx b/awx/ui_next/src/components/JobList/JobListCancelButton.test.jsx index 43bdbe1352..b38992fcf5 100644 --- a/awx/ui_next/src/components/JobList/JobListCancelButton.test.jsx +++ b/awx/ui_next/src/components/JobList/JobListCancelButton.test.jsx @@ -30,6 +30,29 @@ describe('', () => { start: false, }, }, + status: 'running', + }, + ]} + /> + ); + expect(wrapper.find('JobListCancelButton button').props().disabled).toBe( + true + ); + }); + test('should be disabled when selected job is not running', () => { + wrapper = mountWithContexts( + @@ -51,6 +74,7 @@ describe('', () => { start: true, }, }, + status: 'running', }, ]} /> @@ -73,6 +97,7 @@ describe('', () => { start: true, }, }, + status: 'running', }, ]} onCancel={onCancel} diff --git a/awx/ui_next/src/components/JobList/JobListItem.jsx b/awx/ui_next/src/components/JobList/JobListItem.jsx index a813641a7f..a1cb09ad8a 100644 --- a/awx/ui_next/src/components/JobList/JobListItem.jsx +++ b/awx/ui_next/src/components/JobList/JobListItem.jsx @@ -1,110 +1,158 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Link } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemRow, - DataListItemCells, - Tooltip, -} from '@patternfly/react-core'; +import { Button, Chip } from '@patternfly/react-core'; +import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; import { RocketIcon } from '@patternfly/react-icons'; import styled from 'styled-components'; -import DataListCell from '../DataListCell'; +import { ActionsTd, ActionItem } from '../PaginatedTable'; import LaunchButton from '../LaunchButton'; -import StatusIcon from '../StatusIcon'; +import StatusLabel from '../StatusLabel'; +import { DetailList, Detail, LaunchedByDetail } from '../DetailList'; +import ChipGroup from '../ChipGroup'; +import CredentialChip from '../CredentialChip'; import { formatDateString } from '../../util/dates'; import { JOB_TYPE_URL_SEGMENTS } from '../../constants'; -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: 40px; -`; - +const Dash = styled.span``; function JobListItem({ i18n, job, + rowIndex, isSelected, onSelect, showTypeColumn = false, }) { const labelId = `check-action-${job.id}`; + const [isExpanded, setIsExpanded] = useState(false); const jobTypes = { project_update: i18n._(t`Source Control Update`), inventory_update: i18n._(t`Inventory Sync`), job: i18n._(t`Playbook Run`), - command: i18n._(t`Command`), + ad_hoc_command: i18n._(t`Command`), management_job: i18n._(t`Management Job`), workflow_job: i18n._(t`Workflow Job`), }; + const { credentials, inventory, labels } = job.summary_fields; + return ( - - - + + setIsExpanded(!isExpanded), + }} /> - - {job.status && } - , - - - - - {job.id} — {job.name} - - - - , - ...(showTypeColumn - ? [ - - {jobTypes[job.type]} - , - ] - : []), - - {job.finished ? formatDateString(job.finished) : ''} - , - ]} + - - {job.type !== 'system_job' && - job.summary_fields?.user_capabilities?.start ? ( - - - {({ handleRelaunch }) => ( - - )} - - - ) : ( - '' - )} - - - + + + + + {job.id} {job.name} + + + + + + {job.status && } + + {showTypeColumn && ( + {jobTypes[job.type]} + )} + + {formatDateString(job.started)} + + + {job.finished ? formatDateString(job.finished) : ''} + + + + + {({ handleRelaunch }) => ( + + )} + + + + + + + + + + + {credentials && credentials.length > 0 && ( + + {credentials.map(c => ( + + ))} + + } + /> + )} + {labels && labels.count > 0 && ( + + {labels.results.map(l => ( + + {l.name} + + ))} + + } + /> + )} + {inventory && ( + + {inventory.name} + + } + /> + )} + + + + + ); } diff --git a/awx/ui_next/src/components/JobList/JobListItem.test.jsx b/awx/ui_next/src/components/JobList/JobListItem.test.jsx index fc453c3be4..6b9a9c3ae4 100644 --- a/awx/ui_next/src/components/JobList/JobListItem.test.jsx +++ b/awx/ui_next/src/components/JobList/JobListItem.test.jsx @@ -32,7 +32,11 @@ describe('', () => { initialEntries: ['/jobs'], }); wrapper = mountWithContexts( - {}} />, + + + {}} /> + +
, { context: { router: { history } } } ); }); @@ -51,32 +55,40 @@ describe('', () => { test('launch button hidden from users without launch capabilities', () => { wrapper = mountWithContexts( - {}} - isSelected={false} - /> + + + {}} + isSelected={false} + /> + +
); expect(wrapper.find('LaunchButton').length).toBe(0); }); test('should hide type column when showTypeColumn is false', () => { - expect(wrapper.find('DataListCell[aria-label="type"]').length).toBe(0); + expect(wrapper.find('Td[dataLabel="Type"]').length).toBe(0); }); test('should show type column when showTypeColumn is true', () => { wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
); - expect(wrapper.find('DataListCell[aria-label="type"]').length).toBe(1); + expect(wrapper.find('Td[dataLabel="Type"]').length).toBe(1); }); }); diff --git a/awx/ui_next/src/components/JobList/useWsJobs.test.jsx b/awx/ui_next/src/components/JobList/useWsJobs.test.jsx index 77e5b714a3..150009c808 100644 --- a/awx/ui_next/src/components/JobList/useWsJobs.test.jsx +++ b/awx/ui_next/src/components/JobList/useWsJobs.test.jsx @@ -44,7 +44,7 @@ describe('useWsJobs hook', () => { test('should establish websocket connection', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const jobs = [{ id: 1 }]; await act(async () => { @@ -67,7 +67,7 @@ describe('useWsJobs hook', () => { test('should update job status', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const jobs = [{ id: 1, status: 'running' }]; await act(async () => { @@ -105,7 +105,7 @@ describe('useWsJobs hook', () => { test('should fetch new job', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const jobs = [{ id: 1 }]; const fetch = jest.fn(() => []); await act(async () => { diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx index e2484abb5a..a832a929f8 100644 --- a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx +++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx @@ -25,6 +25,8 @@ function canLaunchWithoutPrompt(launchData) { !launchData.ask_limit_on_launch && !launchData.ask_scm_branch_on_launch && !launchData.survey_enabled && + (!launchData.passwords_needed_to_start || + launchData.passwords_needed_to_start.length === 0) && (!launchData.variables_needed_to_start || launchData.variables_needed_to_start.length === 0) ); @@ -44,6 +46,7 @@ class LaunchButton extends React.Component { showLaunchPrompt: false, launchConfig: null, launchError: false, + surveyConfig: null, }; this.handleLaunch = this.handleLaunch.bind(this); @@ -67,15 +70,28 @@ class LaunchButton extends React.Component { resource.type === 'workflow_job_template' ? WorkflowJobTemplatesAPI.readLaunch(resource.id) : JobTemplatesAPI.readLaunch(resource.id); + const readSurvey = + resource.type === 'workflow_job_template' + ? WorkflowJobTemplatesAPI.readSurvey(resource.id) + : JobTemplatesAPI.readSurvey(resource.id); try { const { data: launchConfig } = await readLaunch; + let surveyConfig = null; + + if (launchConfig.survey_enabled) { + const { data } = await readSurvey; + + surveyConfig = data; + } + if (canLaunchWithoutPrompt(launchConfig)) { this.launchWithParams({}); } else { this.setState({ showLaunchPrompt: true, launchConfig, + surveyConfig, }); } } catch (err) { @@ -86,17 +102,20 @@ class LaunchButton extends React.Component { async launchWithParams(params) { try { const { history, resource } = this.props; - const jobPromise = - resource.type === 'workflow_job_template' - ? WorkflowJobTemplatesAPI.launch(resource.id, params || {}) - : JobTemplatesAPI.launch(resource.id, params || {}); + let jobPromise; + + if (resource.type === 'job_template') { + jobPromise = JobTemplatesAPI.launch(resource.id, params || {}); + } else if (resource.type === 'workflow_job_template') { + jobPromise = WorkflowJobTemplatesAPI.launch(resource.id, params || {}); + } else if (resource.type === 'job') { + jobPromise = JobsAPI.relaunch(resource.id, params || {}); + } else if (resource.type === 'workflow_job') { + jobPromise = WorkflowJobsAPI.relaunch(resource.id, params || {}); + } const { data: job } = await jobPromise; - history.push( - `/${ - resource.type === 'workflow_job_template' ? 'jobs/workflow' : 'jobs' - }/${job.id}/output` - ); + history.push(`/jobs/${job.id}/output`); } catch (launchError) { this.setState({ launchError }); } @@ -113,20 +132,15 @@ class LaunchButton extends React.Component { readRelaunch = InventorySourcesAPI.readLaunchUpdate( resource.inventory_source ); - relaunch = InventorySourcesAPI.launchUpdate(resource.inventory_source); } else if (resource.type === 'project_update') { // We'll need to handle the scenario where the project no longer exists readRelaunch = ProjectsAPI.readLaunchUpdate(resource.project); - relaunch = ProjectsAPI.launchUpdate(resource.project); } else if (resource.type === 'workflow_job') { readRelaunch = WorkflowJobsAPI.readRelaunch(resource.id); - relaunch = WorkflowJobsAPI.relaunch(resource.id); } else if (resource.type === 'ad_hoc_command') { readRelaunch = AdHocCommandsAPI.readRelaunch(resource.id); - relaunch = AdHocCommandsAPI.relaunch(resource.id); } else if (resource.type === 'job') { readRelaunch = JobsAPI.readRelaunch(resource.id); - relaunch = JobsAPI.relaunch(resource.id); } try { @@ -135,11 +149,22 @@ class LaunchButton extends React.Component { !relaunchConfig.passwords_needed_to_start || relaunchConfig.passwords_needed_to_start.length === 0 ) { + if (resource.type === 'inventory_update') { + relaunch = InventorySourcesAPI.launchUpdate( + resource.inventory_source + ); + } else if (resource.type === 'project_update') { + relaunch = ProjectsAPI.launchUpdate(resource.project); + } else if (resource.type === 'workflow_job') { + relaunch = WorkflowJobsAPI.relaunch(resource.id); + } else if (resource.type === 'ad_hoc_command') { + relaunch = AdHocCommandsAPI.relaunch(resource.id); + } else if (resource.type === 'job') { + relaunch = JobsAPI.relaunch(resource.id); + } const { data: job } = await relaunch; history.push(`/jobs/${job.id}/output`); } else { - // TODO: restructure (async?) to send launch command after prompts - // TODO: does relaunch need different prompt treatment than launch? this.setState({ showLaunchPrompt: true, launchConfig: relaunchConfig, @@ -151,7 +176,12 @@ class LaunchButton extends React.Component { } render() { - const { launchError, showLaunchPrompt, launchConfig } = this.state; + const { + launchError, + showLaunchPrompt, + launchConfig, + surveyConfig, + } = this.state; const { resource, i18n, children } = this.props; return ( @@ -172,7 +202,8 @@ class LaunchButton extends React.Component { )} {showLaunchPrompt && ( this.setState({ showLaunchPrompt: false })} diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx index 10fbbd1bf4..c84c7fde4d 100644 --- a/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx +++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx @@ -4,10 +4,16 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import { sleep } from '../../../testUtils/testUtils'; import LaunchButton from './LaunchButton'; -import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api'; +import { + InventorySourcesAPI, + JobsAPI, + JobTemplatesAPI, + ProjectsAPI, + WorkflowJobsAPI, + WorkflowJobTemplatesAPI, +} from '../../api'; -jest.mock('../../api/models/WorkflowJobTemplates'); -jest.mock('../../api/models/JobTemplates'); +jest.mock('../../api'); describe('LaunchButton', () => { JobTemplatesAPI.readLaunch.mockResolvedValue({ @@ -22,10 +28,14 @@ describe('LaunchButton', () => { }, }); - const children = ({ handleLaunch }) => ( + const launchButton = ({ handleLaunch }) => ( + + {searchColumns.map(({ name, key }) => ( + + {chips[key]?.chips?.map(chip => ( + + {chip.node} + + ))} + + ))} + + + ); + return ( } > - - - - {searchColumns.map(({ name, key }) => ( - - {chips[key]?.chips?.map((chip, index) => ( - - {chip.node} - - ))} - - ))} - - + {renderLookup()} + + ) : ( + renderLookup() + )} actionsResponse.data.actions?.GET[key].filterable), - canEdit: Boolean(actionsResponse.data.actions.POST), + canEdit: + Boolean(actionsResponse.data.actions.POST) || isOverrideDisabled, }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [history.location]), { inventories: [], @@ -195,11 +199,13 @@ InventoryLookup.propTypes = { value: Inventory, onChange: func.isRequired, required: bool, + isOverrideDisabled: bool, }; InventoryLookup.defaultProps = { value: null, required: false, + isOverrideDisabled: false, }; export default withI18n()(withRouter(InventoryLookup)); diff --git a/awx/ui_next/src/components/Lookup/InventoryLookup.test.jsx b/awx/ui_next/src/components/Lookup/InventoryLookup.test.jsx new file mode 100644 index 0000000000..1c7d13f488 --- /dev/null +++ b/awx/ui_next/src/components/Lookup/InventoryLookup.test.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import InventoryLookup from './InventoryLookup'; +import { InventoriesAPI } from '../../api'; + +jest.mock('../../api'); + +const mockedInventories = { + data: { + count: 2, + results: [ + { id: 2, name: 'Bar' }, + { id: 3, name: 'Baz' }, + ], + }, +}; + +describe('InventoryLookup', () => { + let wrapper; + + beforeEach(() => { + InventoriesAPI.read.mockResolvedValue(mockedInventories); + }); + + afterEach(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('should render successfully and fetch data', async () => { + InventoriesAPI.readOptions.mockReturnValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); + await act(async () => { + wrapper = mountWithContexts( {}} />); + }); + wrapper.update(); + expect(InventoriesAPI.read).toHaveBeenCalledTimes(1); + expect(wrapper.find('InventoryLookup')).toHaveLength(1); + expect(wrapper.find('Lookup').prop('isDisabled')).toBe(false); + }); + + test('inventory lookup should be enabled', async () => { + InventoriesAPI.readOptions.mockReturnValue({ + data: { + actions: { + GET: {}, + }, + related_search_fields: [], + }, + }); + await act(async () => { + wrapper = mountWithContexts( + {}} /> + ); + }); + wrapper.update(); + expect(InventoriesAPI.read).toHaveBeenCalledTimes(1); + expect(wrapper.find('InventoryLookup')).toHaveLength(1); + expect(wrapper.find('Lookup').prop('isDisabled')).toBe(false); + }); + + test('inventory lookup should be disabled', async () => { + InventoriesAPI.readOptions.mockReturnValue({ + data: { + actions: { + GET: {}, + }, + related_search_fields: [], + }, + }); + await act(async () => { + wrapper = mountWithContexts( {}} />); + }); + wrapper.update(); + expect(InventoriesAPI.read).toHaveBeenCalledTimes(1); + expect(wrapper.find('InventoryLookup')).toHaveLength(1); + expect(wrapper.find('Lookup').prop('isDisabled')).toBe(true); + }); +}); diff --git a/awx/ui_next/src/components/Lookup/Lookup.jsx b/awx/ui_next/src/components/Lookup/Lookup.jsx index 2cfc284af8..d5d86b9f4d 100644 --- a/awx/ui_next/src/components/Lookup/Lookup.jsx +++ b/awx/ui_next/src/components/Lookup/Lookup.jsx @@ -103,7 +103,7 @@ function Lookup(props) { , - , - ]} - > - {this.isTeamRole() ? ( - - {i18n._( - t`Are you sure you want to remove ${role.name} access from ${role.team_name}? Doing so affects all members of the team.` - )} -
-
- {i18n._( - t`If you only want to remove access for this particular user, please remove them from the team.` - )} -
- ) : ( - - {i18n._( - t`Are you sure you want to remove ${role.name} access from ${username}?` - )} - - )} - - ); - } + const title = i18n._( + t`Remove ${isTeamRole() ? i18n._(t`Team`) : i18n._(t`User`)} Access` + ); + return ( + + {i18n._(t`Delete`)} + , + , + ]} + > + {isTeamRole() ? ( + + {i18n._( + t`Are you sure you want to remove ${role.name} access from ${role.team_name}? Doing so affects all members of the team.` + )} +
+
+ {i18n._( + t`If you only want to remove access for this particular user, please remove them from the team.` + )} +
+ ) : ( + + {i18n._( + t`Are you sure you want to remove ${role.name} access from ${username}?` + )} + + )} +
+ ); } +DeleteRoleConfirmationModal.propTypes = { + role: Role.isRequired, + username: string, + onCancel: func.isRequired, + onConfirm: func.isRequired, +}; + +DeleteRoleConfirmationModal.defaultProps = { + username: '', +}; + export default withI18n()(DeleteRoleConfirmationModal); diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx index 4568c477e4..0f5f7c1c64 100644 --- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx +++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx @@ -144,6 +144,7 @@ function ResourceAccessList({ i18n, apiModel, resource }) { setDeletionRole(role); setShowDeleteModal(true); }} + i18n={i18n} /> )} /> @@ -155,6 +156,7 @@ function ResourceAccessList({ i18n, apiModel, resource }) { fetchAccessRecords(); }} roles={resource.summary_fields.object_roles} + resource={resource} /> )} {showDeleteModal && ( diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessListItem.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessListItem.jsx index 3fe656a3fb..d641e67e01 100644 --- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessListItem.jsx +++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessListItem.jsx @@ -24,19 +24,13 @@ const DataListItemCells = styled(PFDataListItemCells)` align-items: start; `; -class ResourceAccessListItem extends React.Component { - static propTypes = { +function ResourceAccessListItem({ accessRecord, onRoleDelete, i18n }) { + ResourceAccessListItem.propTypes = { accessRecord: AccessRecord.isRequired, onRoleDelete: func.isRequired, }; - constructor(props) { - super(props); - this.renderChip = this.renderChip.bind(this); - } - - getRoleLists() { - const { accessRecord } = this.props; + const getRoleLists = () => { const teamRoles = []; const userRoles = []; @@ -52,10 +46,9 @@ class ResourceAccessListItem extends React.Component { accessRecord.summary_fields.direct_access.map(sort); accessRecord.summary_fields.indirect_access.map(sort); return [teamRoles, userRoles]; - } + }; - renderChip(role) { - const { accessRecord, onRoleDelete } = this.props; + const renderChip = role => { return ( ); - } + }; - render() { - const { accessRecord, i18n } = this.props; - const [teamRoles, userRoles] = this.getRoleLists(); + const [teamRoles, userRoles] = getRoleLists(); - return ( - - - - {accessRecord.username && ( - - {accessRecord.id ? ( - - - {accessRecord.username} - - - ) : ( - + return ( + + + + {accessRecord.username && ( + + {accessRecord.id ? ( + + {accessRecord.username} - - )} - - )} - {accessRecord.first_name || accessRecord.last_name ? ( - - - - ) : null} - , - + + + ) : ( + + {accessRecord.username} + + )} + + )} + {accessRecord.first_name || accessRecord.last_name ? ( - {userRoles.length > 0 && ( - - {userRoles.map(this.renderChip)} - - } - /> - )} - {teamRoles.length > 0 && ( - - {teamRoles.map(this.renderChip)} - - } - /> - )} + - , - ]} - /> - - - ); - } + ) : null} + , + + + {userRoles.length > 0 && ( + + {userRoles.map(renderChip)} + + } + /> + )} + {teamRoles.length > 0 && ( + + {teamRoles.map(renderChip)} + + } + /> + )} + + , + ]} + /> + + + ); } export default withI18n()(ResourceAccessListItem); diff --git a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx index 3b93990417..c995404ddd 100644 --- a/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx +++ b/awx/ui_next/src/components/RoutedTabs/RoutedTabs.jsx @@ -1,19 +1,20 @@ import React from 'react'; import { shape, string, number, arrayOf, node, oneOfType } from 'prop-types'; import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; function RoutedTabs(props) { const { tabsArray } = props; const history = useHistory(); + const location = useLocation(); const getActiveTabId = () => { - const match = tabsArray.find(tab => tab.link === history.location.pathname); + const match = tabsArray.find(tab => tab.link === location.pathname); if (match) { return match.id; } const subpathMatch = tabsArray.find(tab => - history.location.pathname.startsWith(tab.link) + location.pathname.startsWith(tab.link) ); if (subpathMatch) { return subpathMatch.id; diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx index cf553745f0..946ac94f55 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx @@ -5,7 +5,7 @@ import { RRule, rrulestr } from 'rrule'; import styled from 'styled-components'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Chip, Title, Button } from '@patternfly/react-core'; +import { Chip, Divider, Title, Button } from '@patternfly/react-core'; import { Schedule } from '../../../types'; import AlertModal from '../../AlertModal'; import { CardBody, CardActionsRow } from '../../Card'; @@ -27,11 +27,21 @@ import ErrorDetail from '../../ErrorDetail'; import ChipGroup from '../../ChipGroup'; import { VariablesDetail } from '../../CodeMirrorInput'; +const PromptDivider = styled(Divider)` + margin-top: var(--pf-global--spacer--lg); + margin-bottom: var(--pf-global--spacer--lg); +`; + const PromptTitle = styled(Title)` + margin-top: 40px; --pf-c-title--m-md--FontWeight: 700; grid-column: 1 / -1; `; +const PromptDetailList = styled(DetailList)` + padding: 0px 20px; +`; + function ScheduleDetail({ schedule, i18n }) { const { id, @@ -41,6 +51,7 @@ function ScheduleDetail({ schedule, i18n }) { dtend, dtstart, extra_data, + inventory, job_tags, job_type, limit, @@ -52,12 +63,21 @@ function ScheduleDetail({ schedule, i18n }) { skip_tags, summary_fields, timezone, + verbosity, } = schedule; const history = useHistory(); const { pathname } = useLocation(); const pathRoot = pathname.substr(0, pathname.indexOf('schedules')); + const VERBOSITY = { + 0: i18n._(t`0 (Normal)`), + 1: i18n._(t`1 (Verbose)`), + 2: i18n._(t`2 (More Verbose)`), + 3: i18n._(t`3 (Debug)`), + 4: i18n._(t`4 (Connection Debug)`), + }; + const { request: deleteSchedule, isLoading: isDeleteLoading, @@ -140,18 +160,34 @@ function ScheduleDetail({ schedule, i18n }) { survey_enabled, } = launchData || {}; + const showCredentialsDetail = + ask_credential_on_launch && credentials.length > 0; + const showInventoryDetail = ask_inventory_on_launch && inventory; + const showVariablesDetail = + (ask_variables_on_launch || survey_enabled) && + ((typeof extra_data === 'string' && extra_data !== '') || + (typeof extra_data === 'object' && Object.keys(extra_data).length > 0)); + const showTagsDetail = ask_tags_on_launch && job_tags && job_tags.length > 0; + const showSkipTagsDetail = + ask_skip_tags_on_launch && skip_tags && skip_tags.length > 0; + const showDiffModeDetail = + ask_diff_mode_on_launch && typeof diff_mode === 'boolean'; + const showLimitDetail = ask_limit_on_launch && limit; + const showJobTypeDetail = ask_job_type_on_launch && job_type; + const showSCMBranchDetail = ask_scm_branch_on_launch && scm_branch; + const showVerbosityDetail = ask_verbosity_on_launch && VERBOSITY[verbosity]; + const showPromptedFields = - 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; + showCredentialsDetail || + showDiffModeDetail || + showInventoryDetail || + showJobTypeDetail || + showLimitDetail || + showSCMBranchDetail || + showSkipTagsDetail || + showTagsDetail || + showVerbosityDetail || + showVariablesDetail; if (isLoading) { return ; @@ -189,27 +225,34 @@ function ScheduleDetail({ schedule, i18n }) { date={modified} user={summary_fields.modified_by} /> - {showPromptedFields && ( - <> - - {i18n._(t`Prompted Fields`)} - + + {showPromptedFields && ( + <> + + {i18n._(t`Prompted Values`)} + + + {ask_job_type_on_launch && ( )} - {ask_inventory_on_launch && ( + {showInventoryDetail && ( - {summary_fields.inventory.name} - + summary_fields?.inventory ? ( + + {summary_fields?.inventory?.name} + + ) : ( + ' ' + ) } /> )} @@ -222,13 +265,19 @@ function ScheduleDetail({ schedule, i18n }) { {ask_limit_on_launch && ( )} - {ask_diff_mode_on_launch && typeof diff_mode === 'boolean' && ( + {ask_verbosity_on_launch && ( + + )} + {showDiffModeDetail && ( )} - {ask_credential_on_launch && ( + {showCredentialsDetail && ( )} - {ask_tags_on_launch && job_tags && job_tags.length > 0 && ( + {showTagsDetail && ( )} - {ask_skip_tags_on_launch && skip_tags && skip_tags.length > 0 && ( + {showSkipTagsDetail && ( )} - {(ask_variables_on_launch || survey_enabled) && ( + {showVariablesDetail && ( )} - - )} - + + + )} {summary_fields?.user_capabilities?.edit && ( - - ) : ( - '' - )} - - - + + + + ); } diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.test.jsx index f9f7ae5d32..2e01a3764e 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.test.jsx @@ -50,39 +50,47 @@ describe('ScheduleListItem', () => { describe('User has edit permissions', () => { beforeAll(() => { wrapper = mountWithContexts( - + + + + +
); }); + afterAll(() => { wrapper.unmount(); }); + test('Name correctly shown with correct link', () => { expect( wrapper - .find('DataListCell') - .first() + .find('Td') + .at(1) .text() ).toBe('Mock Schedule'); expect( wrapper - .find('DataListCell') - .first() + .find('Td') + .at(1) .find('Link') .props().to ).toBe('/templates/job_template/12/schedules/6/details'); }); + test('Type correctly shown', () => { expect( wrapper - .find('DataListCell') - .at(1) + .find('Td') + .at(2) .text() ).toBe('Playbook Run'); }); + test('Edit button shown with correct link', () => { expect(wrapper.find('PencilAltIcon').length).toBe(1); expect( @@ -92,6 +100,7 @@ describe('ScheduleListItem', () => { .props().to ).toBe('/templates/job_template/12/schedules/6/edit'); }); + test('Toggle button enabled', () => { expect( wrapper @@ -100,63 +109,74 @@ describe('ScheduleListItem', () => { .props().isDisabled ).toBe(false); }); - test('Clicking checkbox makes expected callback', () => { + + test('Clicking checkbox selects item', () => { wrapper - .find('DataListCheck') + .find('Td') .first() .find('input') .simulate('change'); expect(onSelect).toHaveBeenCalledTimes(1); }); }); + describe('User has read-only permissions', () => { beforeAll(() => { wrapper = mountWithContexts( - + + + + +
); }); + afterAll(() => { wrapper.unmount(); }); + test('Name correctly shown with correct link', () => { expect( wrapper - .find('DataListCell') - .first() + .find('Td') + .at(1) .text() ).toBe('Mock Schedule'); expect( wrapper - .find('DataListCell') - .first() + .find('Td') + .at(1) .find('Link') .props().to ).toBe('/templates/job_template/12/schedules/6/details'); }); + test('Type correctly shown', () => { expect( wrapper - .find('DataListCell') - .at(1) + .find('Td') + .at(2) .text() ).toBe('Playbook Run'); }); + test('Edit button hidden', () => { expect(wrapper.find('PencilAltIcon').length).toBe(0); }); + test('Toggle button disabled', () => { expect( wrapper diff --git a/awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx b/awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx new file mode 100644 index 0000000000..d856a430ee --- /dev/null +++ b/awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx @@ -0,0 +1,135 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + Button, + PageSection, + PageSectionVariants, + Breadcrumb, + BreadcrumbItem, + Title, + Tooltip, +} from '@patternfly/react-core'; +import { HistoryIcon } from '@patternfly/react-icons'; +import { Link, Route, useRouteMatch } from 'react-router-dom'; + +const ScreenHeader = ({ breadcrumbConfig, i18n, streamType }) => { + const { light } = PageSectionVariants; + const oneCrumbMatch = useRouteMatch({ + path: Object.keys(breadcrumbConfig)[0], + strict: true, + }); + const isOnlyOneCrumb = oneCrumbMatch && oneCrumbMatch.isExact; + + return ( + +
+
+ {!isOnlyOneCrumb && ( + + + + + + )} +
+ + + +
+
+ {streamType !== 'none' && ( +
+ + + +
+ )} +
+
+ ); +}; + +const ActualTitle = ({ breadcrumbConfig }) => { + const match = useRouteMatch(); + const title = breadcrumbConfig[match.url]; + let titleElement; + + if (match.isExact) { + titleElement = ( + + {title} + + ); + } + + if (!title) { + titleElement = null; + } + + return ( + + {titleElement} + + + + + ); +}; + +const Crumb = ({ breadcrumbConfig, showDivider }) => { + const match = useRouteMatch(); + const crumb = breadcrumbConfig[match.url]; + + let crumbElement = ( + + {crumb} + + ); + + if (match.isExact) { + crumbElement = null; + } + + if (!crumb) { + crumbElement = null; + } + return ( + + {crumbElement} + + + + + ); +}; + +ScreenHeader.propTypes = { + breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired, +}; + +Crumb.propTypes = { + breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired, +}; + +export default withI18n()(ScreenHeader); diff --git a/awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.test.jsx b/awx/ui_next/src/components/ScreenHeader/ScreenHeader.test.jsx similarity index 70% rename from awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.test.jsx rename to awx/ui_next/src/components/ScreenHeader/ScreenHeader.test.jsx index 83e5e8c3fe..64f3a92c53 100644 --- a/awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.test.jsx +++ b/awx/ui_next/src/components/ScreenHeader/ScreenHeader.test.jsx @@ -1,9 +1,15 @@ import React from 'react'; -import { mount } from 'enzyme'; import { MemoryRouter } from 'react-router-dom'; -import Breadcrumbs from './Breadcrumbs'; -describe('', () => { +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; + +import ScreenHeader from './ScreenHeader'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + +describe('', () => { let breadcrumbWrapper; let breadcrumb; let breadcrumbItem; @@ -17,15 +23,15 @@ describe('', () => { }; const findChildren = () => { - breadcrumb = breadcrumbWrapper.find('Breadcrumb'); + breadcrumb = breadcrumbWrapper.find('ScreenHeader'); breadcrumbItem = breadcrumbWrapper.find('BreadcrumbItem'); - breadcrumbHeading = breadcrumbWrapper.find('BreadcrumbHeading'); + breadcrumbHeading = breadcrumbWrapper.find('Title'); }; test('initially renders succesfully', () => { - breadcrumbWrapper = mount( + breadcrumbWrapper = mountWithContexts( - + ); @@ -51,9 +57,9 @@ describe('', () => { ]; routes.forEach(([location, crumbLength]) => { - breadcrumbWrapper = mount( + breadcrumbWrapper = mountWithContexts( - + ); diff --git a/awx/ui_next/src/components/ScreenHeader/index.js b/awx/ui_next/src/components/ScreenHeader/index.js new file mode 100644 index 0000000000..7f5ab32733 --- /dev/null +++ b/awx/ui_next/src/components/ScreenHeader/index.js @@ -0,0 +1 @@ +export { default } from './ScreenHeader'; diff --git a/awx/ui_next/src/components/Search/AdvancedSearch.jsx b/awx/ui_next/src/components/Search/AdvancedSearch.jsx index cb1d8b72bd..bc14007b32 100644 --- a/awx/ui_next/src/components/Search/AdvancedSearch.jsx +++ b/awx/ui_next/src/components/Search/AdvancedSearch.jsx @@ -92,6 +92,7 @@ function AdvancedSearch({ isOpen={isPrefixDropdownOpen} placeholderText={i18n._(t`Set type`)} maxHeight="500px" + noResultsFoundText={i18n._(t`No results found`)} > {allKeys.map(optionKey => ( @@ -148,6 +150,7 @@ function AdvancedSearch({ isOpen={isLookupDropdownOpen} placeholderText={i18n._(t`Lookup type`)} maxHeight="500px" + noResultsFoundText={i18n._(t`No results found`)} > key !== searchKey) .map(({ key, name }) => ( - + {name} )); @@ -177,6 +177,7 @@ function Search({ onSelect={handleDropdownSelect} selections={searchColumnName} isOpen={isSearchDropdownOpen} + ouiaId="simple-key-select" > {searchOptions} @@ -217,9 +218,14 @@ function Search({ })} isOpen={isFilterDropdownOpen} placeholderText={`Filter By ${name}`} + ouiaId={`filter-by-${key}`} > {options.map(([optionKey, optionLabel]) => ( - + {optionLabel} ))} @@ -234,6 +240,7 @@ function Search({ selections={chipsByKey[key].chips[0]?.label} isOpen={isFilterDropdownOpen} placeholderText={`Filter By ${name}`} + ouiaId={`filter-by-${key}`} > {booleanLabels.true || i18n._(t`Yes`)} diff --git a/awx/ui_next/src/components/SelectableCard/SelectableCard.jsx b/awx/ui_next/src/components/SelectableCard/SelectableCard.jsx index db9f5008ed..338c089c53 100644 --- a/awx/ui_next/src/components/SelectableCard/SelectableCard.jsx +++ b/awx/ui_next/src/components/SelectableCard/SelectableCard.jsx @@ -31,7 +31,14 @@ const Description = styled.p` font-size: 14px; `; -function SelectableCard({ label, description, onClick, isSelected, dataCy }) { +function SelectableCard({ + label, + description, + onClick, + isSelected, + dataCy, + ariaLabel, +}) { return ( @@ -55,12 +63,14 @@ SelectableCard.propTypes = { description: PropTypes.string, onClick: PropTypes.func.isRequired, isSelected: PropTypes.bool, + ariaLabel: PropTypes.string, }; SelectableCard.defaultProps = { label: '', description: '', isSelected: false, + ariaLabel: '', }; export default SelectableCard; diff --git a/awx/ui_next/src/components/Sort/Sort.jsx b/awx/ui_next/src/components/Sort/Sort.jsx index c0a513cd48..ee8599b531 100644 --- a/awx/ui_next/src/components/Sort/Sort.jsx +++ b/awx/ui_next/src/components/Sort/Sort.jsx @@ -1,7 +1,7 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; -import { withRouter } from 'react-router-dom'; +import { useLocation, withRouter } from 'react-router-dom'; import { t } from '@lingui/macro'; import { Button, @@ -31,140 +31,110 @@ const NoOptionDropdown = styled.div` border-bottom-color: var(--pf-global--BorderColor--200); `; -class Sort extends React.Component { - constructor(props) { - super(props); +function Sort({ columns, qsConfig, onSort, i18n }) { + const location = useLocation(); + const [isSortDropdownOpen, setIsSortDropdownOpen] = useState(false); - let sortKey; - let sortOrder; - let isNumeric; + let sortKey; + let sortOrder; + let isNumeric; - const { qsConfig, location } = this.props; - const queryParams = parseQueryString(qsConfig, location.search); - if (queryParams.order_by && queryParams.order_by.startsWith('-')) { - sortKey = queryParams.order_by.substr(1); - sortOrder = 'descending'; - } else if (queryParams.order_by) { - sortKey = queryParams.order_by; - sortOrder = 'ascending'; - } - - if (qsConfig.integerFields.find(field => field === sortKey)) { - isNumeric = true; - } else { - isNumeric = false; - } - - this.state = { - isSortDropdownOpen: false, - sortKey, - sortOrder, - isNumeric, - }; - - this.handleDropdownToggle = this.handleDropdownToggle.bind(this); - this.handleDropdownSelect = this.handleDropdownSelect.bind(this); - this.handleSort = this.handleSort.bind(this); + const queryParams = parseQueryString(qsConfig, location.search); + if (queryParams.order_by && queryParams.order_by.startsWith('-')) { + sortKey = queryParams.order_by.substr(1); + sortOrder = 'descending'; + } else if (queryParams.order_by) { + sortKey = queryParams.order_by; + sortOrder = 'ascending'; } - handleDropdownToggle(isSortDropdownOpen) { - this.setState({ isSortDropdownOpen }); + if (qsConfig.integerFields.find(field => field === sortKey)) { + isNumeric = true; + } else { + isNumeric = false; } - handleDropdownSelect({ target }) { - const { columns, onSort, qsConfig } = this.props; - const { sortOrder } = this.state; + const handleDropdownToggle = isOpen => { + setIsSortDropdownOpen(isOpen); + }; + + const handleDropdownSelect = ({ target }) => { const { innerText } = target; - const [{ key: sortKey }] = columns.filter(({ name }) => name === innerText); - - let isNumeric; - - if (qsConfig.integerFields.find(field => field === sortKey)) { + const [{ key }] = columns.filter(({ name }) => name === innerText); + sortKey = key; + if (qsConfig.integerFields.find(field => field === key)) { isNumeric = true; } else { isNumeric = false; } - this.setState({ isSortDropdownOpen: false, sortKey, isNumeric }); + setIsSortDropdownOpen(false); onSort(sortKey, sortOrder); - } + }; - handleSort() { - const { onSort } = this.props; - const { sortKey, sortOrder } = this.state; - const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending'; - this.setState({ sortOrder: newSortOrder }); - onSort(sortKey, newSortOrder); - } + const handleSort = () => { + onSort(sortKey, sortOrder === 'ascending' ? 'descending' : 'ascending'); + }; - render() { - const { up } = DropdownPosition; - const { columns, i18n } = this.props; - const { isSortDropdownOpen, sortKey, sortOrder, isNumeric } = this.state; + const { up } = DropdownPosition; - const defaultSortedColumn = columns.find(({ key }) => key === sortKey); + const defaultSortedColumn = columns.find(({ key }) => key === sortKey); - if (!defaultSortedColumn) { - throw new Error( - 'sortKey must match one of the column keys, check the sortColumns prop passed to ' - ); - } - - const sortedColumnName = defaultSortedColumn?.name; - - const sortDropdownItems = columns - .filter(({ key }) => key !== sortKey) - .map(({ key, name }) => ( - - {name} - - )); - - let SortIcon; - if (isNumeric) { - SortIcon = - sortOrder === 'ascending' - ? SortNumericDownIcon - : SortNumericDownAltIcon; - } else { - SortIcon = - sortOrder === 'ascending' ? SortAlphaDownIcon : SortAlphaDownAltIcon; - } - - return ( - - {sortedColumnName && ( - - {(sortDropdownItems.length > 0 && ( - - {sortedColumnName} - - } - dropdownItems={sortDropdownItems} - /> - )) || {sortedColumnName}} - - - )} - + if (!defaultSortedColumn) { + throw new Error( + 'sortKey must match one of the column keys, check the sortColumns prop passed to ' ); } + + const sortedColumnName = defaultSortedColumn?.name; + + const sortDropdownItems = columns + .filter(({ key }) => key !== sortKey) + .map(({ key, name }) => ( + + {name} + + )); + + let SortIcon; + if (isNumeric) { + SortIcon = + sortOrder === 'ascending' ? SortNumericDownIcon : SortNumericDownAltIcon; + } else { + SortIcon = + sortOrder === 'ascending' ? SortAlphaDownIcon : SortAlphaDownAltIcon; + } + return ( + + {sortedColumnName && ( + + {(sortDropdownItems.length > 0 && ( + + {sortedColumnName} + + } + dropdownItems={sortDropdownItems} + /> + )) || {sortedColumnName}} + + + + )} + + ); } Sort.propTypes = { diff --git a/awx/ui_next/src/components/Sort/Sort.test.jsx b/awx/ui_next/src/components/Sort/Sort.test.jsx index 1764cf0298..c6cf89ad0d 100644 --- a/awx/ui_next/src/components/Sort/Sort.test.jsx +++ b/awx/ui_next/src/components/Sort/Sort.test.jsx @@ -1,5 +1,10 @@ import React from 'react'; -import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { + mountWithContexts, + waitForElement, +} from '../../../testUtils/enzymeHelpers'; + import Sort from './Sort'; describe('', () => { @@ -105,7 +110,7 @@ describe('', () => { expect(onSort).toHaveBeenCalledWith('foo', 'ascending'); }); - test('Changing dropdown correctly passes back new sort key', () => { + test('Changing dropdown correctly passes back new sort key', async () => { const qsConfig = { namespace: 'item', defaultParams: { page: 1, page_size: 5, order_by: 'foo' }, @@ -131,44 +136,18 @@ describe('', () => { const wrapper = mountWithContexts( - ).find('Sort'); - - wrapper.instance().handleDropdownSelect({ target: { innerText: 'Bar' } }); + ); + act(() => wrapper.find('Dropdown').invoke('onToggle')(true)); + wrapper.update(); + await waitForElement(wrapper, 'Dropdown', el => el.prop('isOpen') === true); + wrapper + .find('li') + .at(0) + .prop('onClick')({ target: { innerText: 'Bar' } }); + wrapper.update(); expect(onSort).toBeCalledWith('bar', 'ascending'); }); - test('Opening dropdown correctly updates state', () => { - const qsConfig = { - namespace: 'item', - defaultParams: { page: 1, page_size: 5, order_by: 'foo' }, - integerFields: ['page', 'page_size'], - }; - - const columns = [ - { - name: 'Foo', - key: 'foo', - }, - { - name: 'Bar', - key: 'bar', - }, - { - name: 'Bakery', - key: 'bakery', - }, - ]; - - const onSort = jest.fn(); - - const wrapper = mountWithContexts( - - ).find('Sort'); - expect(wrapper.state('isSortDropdownOpen')).toEqual(false); - wrapper.instance().handleDropdownToggle(true); - expect(wrapper.state('isSortDropdownOpen')).toEqual(true); - }); - test('It displays correct sort icon', () => { const forwardNumericIconSelector = 'SortNumericDownIcon'; const reverseNumericIconSelector = 'SortNumericDownAltIcon'; diff --git a/awx/ui_next/src/components/Sparkline/Sparkline.jsx b/awx/ui_next/src/components/Sparkline/Sparkline.jsx index b76dc0b30c..c087705953 100644 --- a/awx/ui_next/src/components/Sparkline/Sparkline.jsx +++ b/awx/ui_next/src/components/Sparkline/Sparkline.jsx @@ -1,5 +1,5 @@ import React, { Fragment } from 'react'; -import { arrayOf, object } from 'prop-types'; +import { arrayOf } from 'prop-types'; import { withI18n } from '@lingui/react'; import { Link as _Link } from 'react-router-dom'; import { Tooltip } from '@patternfly/react-core'; @@ -8,6 +8,7 @@ import { t } from '@lingui/macro'; import StatusIcon from '../StatusIcon'; import { formatDateString } from '../../util/dates'; import { JOB_TYPE_URL_SEGMENTS } from '../../constants'; +import { Job } from '../../types'; /* eslint-disable react/jsx-pascal-case */ const Link = styled(props => <_Link {...props} />)` @@ -16,6 +17,7 @@ const Link = styled(props => <_Link {...props} />)` const Wrapper = styled.div` display: inline-flex; + flex-wrap: wrap; `; /* eslint-enable react/jsx-pascal-case */ @@ -51,7 +53,7 @@ const Sparkline = ({ i18n, jobs }) => { }; Sparkline.propTypes = { - jobs: arrayOf(object), + jobs: arrayOf(Job), }; Sparkline.defaultProps = { jobs: [], diff --git a/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx index 31917b9597..bd0466f5ce 100644 --- a/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx +++ b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx @@ -8,6 +8,7 @@ import { SyncAltIcon, ExclamationTriangleIcon, ClockIcon, + MinusCircleIcon, } from '@patternfly/react-icons'; import styled, { keyframes } from 'styled-components'; @@ -32,6 +33,7 @@ const colors = { running: 'blue', pending: 'blue', waiting: 'grey', + disabled: 'grey', canceled: 'orange', }; const icons = { @@ -42,6 +44,7 @@ const icons = { running: RunningIcon, pending: ClockIcon, waiting: ClockIcon, + disabled: MinusCircleIcon, canceled: ExclamationTriangleIcon, }; @@ -66,6 +69,7 @@ StatusLabel.propTypes = { 'running', 'pending', 'waiting', + 'disabled', 'canceled', ]).isRequired, }; diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/components/TemplateList/TemplateList.jsx similarity index 77% rename from awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx rename to awx/ui_next/src/components/TemplateList/TemplateList.jsx index 961178bf54..2ac347a701 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx +++ b/awx/ui_next/src/components/TemplateList/TemplateList.jsx @@ -7,29 +7,33 @@ import { JobTemplatesAPI, UnifiedJobTemplatesAPI, WorkflowJobTemplatesAPI, -} from '../../../api'; -import AlertModal from '../../../components/AlertModal'; -import DatalistToolbar from '../../../components/DataListToolbar'; -import ErrorDetail from '../../../components/ErrorDetail'; -import PaginatedDataList, { - ToolbarDeleteButton, -} from '../../../components/PaginatedDataList'; -import useRequest, { useDeleteItems } from '../../../util/useRequest'; -import { getQSConfig, parseQueryString } from '../../../util/qs'; -import useWsTemplates from '../../../util/useWsTemplates'; -import AddDropDownButton from '../../../components/AddDropDownButton'; +} from '../../api'; +import AlertModal from '../AlertModal'; +import DatalistToolbar from '../DataListToolbar'; +import ErrorDetail from '../ErrorDetail'; +import { ToolbarDeleteButton } from '../PaginatedDataList'; +import PaginatedTable, { HeaderRow, HeaderCell } from '../PaginatedTable'; +import useRequest, { useDeleteItems } from '../../util/useRequest'; +import { getQSConfig, parseQueryString } from '../../util/qs'; +import useWsTemplates from '../../util/useWsTemplates'; +import AddDropDownButton from '../AddDropDownButton'; import TemplateListItem from './TemplateListItem'; -// The type value in const QS_CONFIG below does not have a space between job_template and -// workflow_job_template so the params sent to the API match what the api expects. -const QS_CONFIG = getQSConfig('template', { - page: 1, - page_size: 20, - order_by: 'name', - type: 'job_template,workflow_job_template', -}); +function TemplateList({ defaultParams, i18n }) { + // The type value in const qsConfig below does not have a space between job_template and + // workflow_job_template so the params sent to the API match what the api expects. + const qsConfig = getQSConfig( + 'template', + { + page: 1, + page_size: 20, + order_by: 'name', + type: 'job_template,workflow_job_template', + ...defaultParams, + }, + ['id', 'page', 'page_size'] + ); -function TemplateList({ i18n }) { const location = useLocation(); const [selected, setSelected] = useState([]); @@ -47,7 +51,7 @@ function TemplateList({ i18n }) { request: fetchTemplates, } = useRequest( useCallback(async () => { - const params = parseQueryString(QS_CONFIG, location.search); + const params = parseQueryString(qsConfig, location.search); const responses = await Promise.all([ UnifiedJobTemplatesAPI.read(params), JobTemplatesAPI.readOptions(), @@ -66,7 +70,7 @@ function TemplateList({ i18n }) { responses[3].data.actions?.GET || {} ).filter(key => responses[3].data.actions?.GET[key].filterable), }; - }, [location]), + }, [location]), // eslint-disable-line react-hooks/exhaustive-deps { results: [], count: 0, @@ -105,7 +109,7 @@ function TemplateList({ i18n }) { ); }, [selected]), { - qsConfig: QS_CONFIG, + qsConfig, allItemsSelected: isAllSelected, fetchItems: fetchTemplates, } @@ -167,13 +171,13 @@ function TemplateList({ i18n }) { return ( - + {i18n._(t`Name`)} + {i18n._(t`Type`)} + + {i18n._(t`Last Ran`)} + + {i18n._(t`Actions`)} + + } renderToolbar={props => ( , ]} /> )} - renderItem={template => ( + renderRow={(template, index) => ( handleSelect(template)} isSelected={selected.some(row => row.id === template.id)} fetchTemplates={fetchTemplates} + rowIndex={index} /> )} emptyStateControls={(canAddJT || canAddWFJT) && addButton} diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.test.jsx b/awx/ui_next/src/components/TemplateList/TemplateList.test.jsx similarity index 98% rename from awx/ui_next/src/screens/Template/TemplateList/TemplateList.test.jsx rename to awx/ui_next/src/components/TemplateList/TemplateList.test.jsx index 2486ea5579..6726bd5b13 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.test.jsx +++ b/awx/ui_next/src/components/TemplateList/TemplateList.test.jsx @@ -4,15 +4,15 @@ import { JobTemplatesAPI, UnifiedJobTemplatesAPI, WorkflowJobTemplatesAPI, -} from '../../../api'; +} from '../../api'; import { mountWithContexts, waitForElement, -} from '../../../../testUtils/enzymeHelpers'; +} from '../../../testUtils/enzymeHelpers'; import TemplateList from './TemplateList'; -jest.mock('../../../api'); +jest.mock('../../api'); const mockTemplates = [ { diff --git a/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx new file mode 100644 index 0000000000..dc53622737 --- /dev/null +++ b/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx @@ -0,0 +1,278 @@ +import 'styled-components/macro'; +import React, { useState, useCallback } from 'react'; +import { Link } from 'react-router-dom'; +import { Button, Tooltip, Chip } from '@patternfly/react-core'; +import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; +import { + ExclamationTriangleIcon, + PencilAltIcon, + ProjectDiagramIcon, + RocketIcon, +} from '@patternfly/react-icons'; +import { ActionsTd, ActionItem } from '../PaginatedTable'; +import { DetailList, Detail, DeletedDetail } from '../DetailList'; +import ChipGroup from '../ChipGroup'; +import CredentialChip from '../CredentialChip'; +import { timeOfDay, formatDateString } from '../../util/dates'; + +import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api'; +import LaunchButton from '../LaunchButton'; +import Sparkline from '../Sparkline'; +import { toTitleCase } from '../../util/strings'; +import CopyButton from '../CopyButton'; + +function TemplateListItem({ + i18n, + template, + isSelected, + onSelect, + detailUrl, + fetchTemplates, + rowIndex, +}) { + const [isExpanded, setIsExpanded] = useState(false); + const [isDisabled, setIsDisabled] = useState(false); + const labelId = `check-action-${template.id}`; + + const copyTemplate = useCallback(async () => { + if (template.type === 'job_template') { + await JobTemplatesAPI.copy(template.id, { + name: `${template.name} @ ${timeOfDay()}`, + }); + } else { + await WorkflowJobTemplatesAPI.copy(template.id, { + name: `${template.name} @ ${timeOfDay()}`, + }); + } + await fetchTemplates(); + }, [fetchTemplates, template.id, template.name, template.type]); + + const handleCopyStart = useCallback(() => { + setIsDisabled(true); + }, []); + + const handleCopyFinish = useCallback(() => { + setIsDisabled(false); + }, []); + + const { + summary_fields: summaryFields, + ask_inventory_on_launch: askInventoryOnLaunch, + } = template; + + const missingResourceIcon = + template.type === 'job_template' && + (!summaryFields.project || + (!summaryFields.inventory && !askInventoryOnLaunch)); + + const inventoryValue = (kind, id) => { + const inventorykind = kind === 'smart' ? 'smart_inventory' : 'inventory'; + + return askInventoryOnLaunch ? ( + <> + + {summaryFields.inventory.name} + + {i18n._(t`(Prompt on launch)`)} + + ) : ( + + {summaryFields.inventory.name} + + ); + }; + let lastRun = ''; + const mostRecentJob = template.summary_fields.recent_jobs + ? template.summary_fields.recent_jobs[0] + : null; + if (mostRecentJob) { + lastRun = mostRecentJob.finished + ? formatDateString(mostRecentJob.finished) + : i18n._(t`Running`); + } + + return ( + <> + + setIsExpanded(!isExpanded), + }} + /> + + + + {template.name} + + {missingResourceIcon && ( + + + + + + )} + + {toTitleCase(template.type)} + {lastRun} + + + + + + + {({ handleLaunch }) => ( + + )} + + + + + + + + + + + + + + + + } + dataCy={`template-${template.id}-activity`} + /> + {summaryFields.credentials && summaryFields.credentials.length && ( + + {summaryFields.credentials.map(c => ( + + ))} + + } + dataCy={`template-${template.id}-credentials`} + /> + )} + {summaryFields.inventory ? ( + + ) : ( + !askInventoryOnLaunch && ( + + ) + )} + {summaryFields.labels && summaryFields.labels.results.length > 0 && ( + + {summaryFields.labels.results.map(l => ( + + {l.name} + + ))} + + } + dataCy={`template-${template.id}-labels`} + /> + )} + {summaryFields.project && ( + + {summaryFields.project.name} + + } + dataCy={`template-${template.id}-project`} + /> + )} + + + + + + + ); +} + +export { TemplateListItem as _TemplateListItem }; +export default withI18n()(TemplateListItem); diff --git a/awx/ui_next/src/components/TemplateList/TemplateListItem.test.jsx b/awx/ui_next/src/components/TemplateList/TemplateListItem.test.jsx new file mode 100644 index 0000000000..d9726196b2 --- /dev/null +++ b/awx/ui_next/src/components/TemplateList/TemplateListItem.test.jsx @@ -0,0 +1,323 @@ +import React from 'react'; + +import { createMemoryHistory } from 'history'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import { JobTemplatesAPI } from '../../api'; +import mockJobTemplateData from './data.job_template.json'; +import TemplateListItem from './TemplateListItem'; + +jest.mock('../../api'); + +describe('', () => { + test('launch button shown to users with start capabilities', () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('LaunchButton').exists()).toBeTruthy(); + }); + test('launch button hidden from users without start capabilities', () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('LaunchButton').exists()).toBeFalsy(); + }); + test('edit button shown to users with edit capabilities', () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); + }); + test('edit button hidden from users without edit capabilities', () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); + }); + test('missing resource icon is shown.', () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(true); + }); + test('missing resource icon is not shown when there is a project and an inventory.', () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); + }); + test('missing resource icon is not shown when inventory is prompt_on_launch, and a project', () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); + }); + test('missing resource icon is not shown type is workflow_job_template', () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); + }); + test('clicking on template from templates list navigates properly', () => { + const history = createMemoryHistory({ + initialEntries: ['/templates'], + }); + const wrapper = mountWithContexts( + + + + +
, + { context: { router: { history } } } + ); + wrapper.find('Link').simulate('click', { button: 0 }); + expect(history.location.pathname).toEqual( + '/templates/job_template/1/details' + ); + }); + test('should call api to copy template', async () => { + JobTemplatesAPI.copy.mockResolvedValue(); + + const wrapper = mountWithContexts( + + + + +
+ ); + await act(async () => + wrapper.find('Button[aria-label="Copy"]').prop('onClick')() + ); + expect(JobTemplatesAPI.copy).toHaveBeenCalled(); + jest.clearAllMocks(); + }); + + test('should render proper alert modal on copy error', async () => { + JobTemplatesAPI.copy.mockRejectedValue(new Error()); + + const wrapper = mountWithContexts( + + + + +
+ ); + await act(async () => + wrapper.find('Button[aria-label="Copy"]').prop('onClick')() + ); + wrapper.update(); + expect(wrapper.find('Modal').prop('isOpen')).toBe(true); + jest.clearAllMocks(); + }); + + test('should not render copy button', async () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('CopyButton').length).toBe(0); + }); + + test('should render visualizer button for workflow', async () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('ProjectDiagramIcon').length).toBe(1); + }); + + test('should not render visualizer button for job template', async () => { + const wrapper = mountWithContexts( + + + + +
+ ); + expect(wrapper.find('ProjectDiagramIcon').length).toBe(0); + }); +}); diff --git a/awx/ui_next/src/components/TemplateList/data.job_template.json b/awx/ui_next/src/components/TemplateList/data.job_template.json new file mode 100644 index 0000000000..804c3b72a2 --- /dev/null +++ b/awx/ui_next/src/components/TemplateList/data.job_template.json @@ -0,0 +1,181 @@ +{ + "id": 7, + "type": "job_template", + "url": "/api/v2/job_templates/7/", + "related": { + "named_url": "/api/v2/job_templates/Mike's JT/", + "created_by": "/api/v2/users/1/", + "modified_by": "/api/v2/users/1/", + "labels": "/api/v2/job_templates/7/labels/", + "inventory": "/api/v2/inventories/1/", + "project": "/api/v2/projects/6/", + "credentials": "/api/v2/job_templates/7/credentials/", + "last_job": "/api/v2/jobs/12/", + "jobs": "/api/v2/job_templates/7/jobs/", + "schedules": "/api/v2/job_templates/7/schedules/", + "activity_stream": "/api/v2/job_templates/7/activity_stream/", + "launch": "/api/v2/job_templates/7/launch/", + "notification_templates_started": "/api/v2/job_templates/7/notification_templates_started/", + "notification_templates_success": "/api/v2/job_templates/7/notification_templates_success/", + "notification_templates_error": "/api/v2/job_templates/7/notification_templates_error/", + "access_list": "/api/v2/job_templates/7/access_list/", + "survey_spec": "/api/v2/job_templates/7/survey_spec/", + "object_roles": "/api/v2/job_templates/7/object_roles/", + "instance_groups": "/api/v2/job_templates/7/instance_groups/", + "slice_workflow_jobs": "/api/v2/job_templates/7/slice_workflow_jobs/", + "copy": "/api/v2/job_templates/7/copy/", + "webhook_receiver": "/api/v2/job_templates/7/github/", + "webhook_key": "/api/v2/job_templates/7/webhook_key/" + }, + "summary_fields": { + "inventory": { + "id": 1, + "name": "Mike's Inventory", + "description": "", + "has_active_failures": false, + "total_hosts": 1, + "hosts_with_active_failures": 0, + "total_groups": 0, + "groups_with_active_failures": 0, + "has_inventory_sources": false, + "total_inventory_sources": 0, + "inventory_sources_with_failures": 0, + "organization_id": 1, + "kind": "" + }, + "project": { + "id": 6, + "name": "Mike's Project", + "description": "", + "status": "successful", + "scm_type": "git" + }, + "last_job": { + "id": 12, + "name": "Mike's JT", + "description": "", + "finished": "2019-10-01T14:34:35.142483Z", + "status": "successful", + "failed": false + }, + "last_update": { + "id": 12, + "name": "Mike's JT", + "description": "", + "status": "successful", + "failed": false + }, + "created_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "modified_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "object_roles": { + "admin_role": { + "description": "Can manage all aspects of the job template", + "name": "Admin", + "id": 24 + }, + "execute_role": { + "description": "May run the job template", + "name": "Execute", + "id": 25 + }, + "read_role": { + "description": "May view settings for the job template", + "name": "Read", + "id": 26 + } + }, + "user_capabilities": { + "edit": true, + "delete": true, + "start": true, + "schedule": true, + "copy": true + }, + "labels": { + "count": 1, + "results": [{ + "id": 91, + "name": "L_91o2" + }] + }, + "survey": { + "title": "", + "description": "" + }, + "recent_jobs": [{ + "id": 12, + "status": "successful", + "finished": "2019-10-01T14:34:35.142483Z", + "type": "job" + }], + "credentials": [{ + "id": 1, + "kind": "ssh", + "name": "Credential 1" + }, + { + "id": 2, + "kind": "awx", + "name": "Credential 2" + } + ], + "webhook_credential": { + "id": "1", + "name": "Webhook Credential" + + } + }, + "created": "2019-09-30T16:18:34.564820Z", + "modified": "2019-10-01T14:47:31.818431Z", + "name": "Mike's JT", + "description": "", + "job_type": "run", + "inventory": 1, + "project": 6, + "playbook": "ping.yml", + "scm_branch": "Foo branch", + "forks": 0, + "limit": "", + "verbosity": 0, + "extra_vars": "", + "job_tags": "T_100,T_200", + "force_handlers": false, + "skip_tags": "S_100,S_200", + "start_at_task": "", + "timeout": 0, + "use_fact_cache": true, + "last_job_run": "2019-10-01T14:34:35.142483Z", + "last_job_failed": false, + "next_job_run": null, + "status": "successful", + "host_config_key": "", + "ask_scm_branch_on_launch": false, + "ask_diff_mode_on_launch": false, + "ask_variables_on_launch": false, + "ask_limit_on_launch": false, + "ask_tags_on_launch": false, + "ask_skip_tags_on_launch": false, + "ask_job_type_on_launch": false, + "ask_verbosity_on_launch": false, + "ask_inventory_on_launch": false, + "ask_credential_on_launch": false, + "survey_enabled": true, + "become_enabled": false, + "diff_mode": false, + "allow_simultaneous": false, + "custom_virtualenv": null, + "job_slice_count": 1, + "webhook_credential": 1, + "webhook_key": "asertdyuhjkhgfd234567kjgfds", + "webhook_service": "github" +} diff --git a/awx/ui_next/src/screens/Template/TemplateList/index.js b/awx/ui_next/src/components/TemplateList/index.js similarity index 53% rename from awx/ui_next/src/screens/Template/TemplateList/index.js rename to awx/ui_next/src/components/TemplateList/index.js index 60759a849a..f5cdbd115c 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/index.js +++ b/awx/ui_next/src/components/TemplateList/index.js @@ -1,2 +1,2 @@ -export { default as TemplateList } from './TemplateList'; +export { default } from './TemplateList'; export { default as TemplateListItem } from './TemplateListItem'; diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js index 843670e1a1..9333d1770c 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js @@ -87,7 +87,6 @@ export default function getResourceAccessConfig(i18n) { options: [ [``, i18n._(t`Manual`)], [`git`, i18n._(t`Git`)], - [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], @@ -157,7 +156,6 @@ export default function getResourceAccessConfig(i18n) { options: [ [``, i18n._(t`Manual`)], [`git`, i18n._(t`Git`)], - [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], diff --git a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx index 1abf65c3a7..909a7c3d6e 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowNodeHelp.jsx @@ -34,9 +34,12 @@ const StyledExclamationTriangleIcon = styled(ExclamationTriangleIcon)` function WorkflowNodeHelp({ node, i18n }) { let nodeType; const job = node?.originalNodeObject?.summary_fields?.job; - if (node.unifiedJobTemplate || job) { - const type = node.unifiedJobTemplate - ? node.unifiedJobTemplate.unified_job_type || node.unifiedJobTemplate.type + const unifiedJobTemplate = + node?.fullUnifiedJobTemplate || + node?.originalNodeObject?.summary_fields?.unified_job_template; + if (unifiedJobTemplate || job) { + const type = unifiedJobTemplate + ? unifiedJobTemplate.unified_job_type || unifiedJobTemplate.type : job.type; switch (type) { case 'job_template': @@ -113,7 +116,7 @@ function WorkflowNodeHelp({ node, i18n }) { return ( <> - {!node.unifiedJobTemplate && (!job || job.type !== 'workflow_approval') && ( + {!unifiedJobTemplate && (!job || job.type !== 'workflow_approval') && ( <> @@ -149,12 +152,12 @@ function WorkflowNodeHelp({ node, i18n }) { )} )} - {node.unifiedJobTemplate && !job && ( + {unifiedJobTemplate && !job && (
{i18n._(t`Name`)}
-
{node.unifiedJobTemplate.name}
+
{unifiedJobTemplate.name}
{i18n._(t`Type`)}
diff --git a/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.jsx b/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.jsx index bd3f65dcac..e0eb145528 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.jsx @@ -19,16 +19,27 @@ const CenteredPauseIcon = styled(PauseIcon)` `; function WorkflowNodeTypeLetter({ node }) { + if ( + !node?.fullUnifiedJobTemplate && + !node?.originalNodeObject?.summary_fields?.unified_job_template + ) { + return null; + } + + const unifiedJobTemplate = + node?.fullUnifiedJobTemplate || + node?.originalNodeObject?.summary_fields?.unified_job_template; + let nodeTypeLetter; if ( - (node.unifiedJobTemplate && - (node.unifiedJobTemplate.type || - node.unifiedJobTemplate.unified_job_type)) || - (node.job && node.job.type) + unifiedJobTemplate.type || + unifiedJobTemplate.unified_job_type || + node?.job?.type ) { - const ujtType = node.unifiedJobTemplate - ? node.unifiedJobTemplate.type || node.unifiedJobTemplate.unified_job_type - : node.job.type; + const ujtType = + unifiedJobTemplate.type || + unifiedJobTemplate.unified_job_type || + node.job.type; switch (ujtType) { case 'job_template': case 'job': diff --git a/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.test.jsx b/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.test.jsx index 24313f1f54..ce67fe6dd7 100644 --- a/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.test.jsx +++ b/awx/ui_next/src/components/Workflow/WorkflowNodeTypeLetter.test.jsx @@ -8,7 +8,7 @@ describe('WorkflowNodeTypeLetter', () => { const wrapper = mount( ); @@ -19,7 +19,7 @@ describe('WorkflowNodeTypeLetter', () => { const wrapper = mount( ); @@ -30,7 +30,7 @@ describe('WorkflowNodeTypeLetter', () => { const wrapper = mount( ); @@ -41,7 +41,9 @@ describe('WorkflowNodeTypeLetter', () => { const wrapper = mount( ); @@ -52,7 +54,7 @@ describe('WorkflowNodeTypeLetter', () => { const wrapper = mount( ); @@ -64,7 +66,7 @@ describe('WorkflowNodeTypeLetter', () => { @@ -76,7 +78,7 @@ describe('WorkflowNodeTypeLetter', () => { const wrapper = mount( ); @@ -87,7 +89,9 @@ describe('WorkflowNodeTypeLetter', () => { const wrapper = mount( ); @@ -98,7 +102,9 @@ describe('WorkflowNodeTypeLetter', () => { const wrapper = mount( ); @@ -110,7 +116,7 @@ describe('WorkflowNodeTypeLetter', () => { diff --git a/awx/ui_next/src/components/Workflow/workflowReducer.js b/awx/ui_next/src/components/Workflow/workflowReducer.js index 36171811e4..6d5e5bf635 100644 --- a/awx/ui_next/src/components/Workflow/workflowReducer.js +++ b/awx/ui_next/src/components/Workflow/workflowReducer.js @@ -55,27 +55,60 @@ export default function visualizerReducer(state, action) { case 'SELECT_SOURCE_FOR_LINKING': return selectSourceForLinking(state, action.node); case 'SET_ADD_LINK_TARGET_NODE': - return { ...state, addLinkTargetNode: action.value }; + return { + ...state, + addLinkTargetNode: action.value, + }; case 'SET_CONTENT_ERROR': - return { ...state, contentError: action.value }; + return { + ...state, + contentError: action.value, + }; case 'SET_IS_LOADING': - return { ...state, isLoading: action.value }; + return { + ...state, + isLoading: action.value, + }; case 'SET_LINK_TO_DELETE': - return { ...state, linkToDelete: action.value }; + return { + ...state, + linkToDelete: action.value, + }; case 'SET_LINK_TO_EDIT': - return { ...state, linkToEdit: action.value }; + return { + ...state, + linkToEdit: action.value, + }; case 'SET_NODES': - return { ...state, nodes: action.value }; + return { + ...state, + nodes: action.value, + }; case 'SET_NODE_POSITIONS': - return { ...state, nodePositions: action.value }; + return { + ...state, + nodePositions: action.value, + }; case 'SET_NODE_TO_DELETE': - return { ...state, nodeToDelete: action.value }; + return { + ...state, + nodeToDelete: action.value, + }; case 'SET_NODE_TO_EDIT': - return { ...state, nodeToEdit: action.value }; + return { + ...state, + nodeToEdit: action.value, + }; case 'SET_NODE_TO_VIEW': - return { ...state, nodeToView: action.value }; + return { + ...state, + nodeToView: action.value, + }; case 'SET_SHOW_DELETE_ALL_NODES_MODAL': - return { ...state, showDeleteAllNodesModal: action.value }; + return { + ...state, + showDeleteAllNodesModal: action.value, + }; case 'START_ADD_NODE': return { ...state, @@ -113,8 +146,12 @@ function createLink(state, linkType) { }); newLinks.push({ - source: { id: addLinkSourceNode.id }, - target: { id: addLinkTargetNode.id }, + source: { + id: addLinkSourceNode.id, + }, + target: { + id: addLinkTargetNode.id, + }, linkType, }); @@ -143,8 +180,9 @@ function createNode(state, node) { newNodes.push({ id: nextNodeId, - unifiedJobTemplate: node.nodeResource, + fullUnifiedJobTemplate: node.nodeResource, isInvalidLinkTarget: false, + promptValues: node.promptValues, }); // Ensures that root nodes appear to always run @@ -154,8 +192,12 @@ function createNode(state, node) { } newLinks.push({ - source: { id: addNodeSource }, - target: { id: nextNodeId }, + source: { + id: addNodeSource, + }, + target: { + id: nextNodeId, + }, linkType: node.linkType, }); @@ -165,7 +207,9 @@ function createNode(state, node) { linkToCompare.source.id === addNodeSource && linkToCompare.target.id === addNodeTarget ) { - linkToCompare.source = { id: nextNodeId }; + linkToCompare.source = { + id: nextNodeId, + }; } }); } @@ -268,15 +312,23 @@ function addLinksFromParentsToChildren( // doesn't have any other parents if (linkParentMapping[child.id].length === 1) { newLinks.push({ - source: { id: parentId }, - target: { id: child.id }, + source: { + id: parentId, + }, + target: { + id: child.id, + }, linkType: 'always', }); } } else if (!linkParentMapping[child.id].includes(parentId)) { newLinks.push({ - source: { id: parentId }, - target: { id: child.id }, + source: { + id: parentId, + }, + target: { + id: child.id, + }, linkType: child.linkType, }); } @@ -302,7 +354,10 @@ function removeLinksFromDeletedNode( if (link.source.id === nodeId || link.target.id === nodeId) { if (link.source.id === nodeId) { - children.push({ id: link.target.id, linkType: link.linkType }); + children.push({ + id: link.target.id, + linkType: link.linkType, + }); } else if (link.target.id === nodeId) { parents.push(link.source.id); } @@ -352,7 +407,7 @@ function generateNodes(workflowNodes, i18n) { const arrayOfNodesForChart = [ { id: 1, - unifiedJobTemplate: { + fullUnifiedJobTemplate: { name: i18n._(t`START`), }, }, @@ -365,8 +420,14 @@ function generateNodes(workflowNodes, i18n) { originalNodeObject: node, }; - if (node.summary_fields.unified_job_template) { - nodeObj.unifiedJobTemplate = node.summary_fields.unified_job_template; + if ( + node.summary_fields?.unified_job_template?.unified_job_type === + 'workflow_approval' + ) { + nodeObj.fullUnifiedJobTemplate = { + ...node.summary_fields.unified_job_template, + type: 'workflow_approval_template', + }; } arrayOfNodesForChart.push(nodeObj); @@ -596,11 +657,19 @@ function updateLink(state, linkType) { function updateNode(state, editedNode) { const { nodeToEdit, nodes } = state; + const { nodeResource, launchConfig, promptValues } = editedNode; const newNodes = [...nodes]; const matchingNode = newNodes.find(node => node.id === nodeToEdit.id); - matchingNode.unifiedJobTemplate = editedNode.nodeResource; + matchingNode.fullUnifiedJobTemplate = nodeResource; matchingNode.isEdited = true; + matchingNode.launchConfig = launchConfig; + + if (promptValues) { + matchingNode.promptValues = promptValues; + } else { + delete matchingNode.promptValues; + } return { ...state, @@ -615,7 +684,15 @@ function refreshNode(state, refreshedNode) { const newNodes = [...nodes]; const matchingNode = newNodes.find(node => node.id === nodeToView.id); - matchingNode.unifiedJobTemplate = refreshedNode.nodeResource; + + if (refreshedNode.fullUnifiedJobTemplate) { + matchingNode.fullUnifiedJobTemplate = refreshedNode.fullUnifiedJobTemplate; + } + + if (refreshedNode.originalNodeCredentials) { + matchingNode.originalNodeCredentials = + refreshedNode.originalNodeCredentials; + } return { ...state, diff --git a/awx/ui_next/src/components/Workflow/workflowReducer.test.js b/awx/ui_next/src/components/Workflow/workflowReducer.test.js index cab793ee5c..60013abe85 100644 --- a/awx/ui_next/src/components/Workflow/workflowReducer.test.js +++ b/awx/ui_next/src/components/Workflow/workflowReducer.test.js @@ -197,7 +197,7 @@ describe('Workflow reducer', () => { { id: 2, isInvalidLinkTarget: false, - unifiedJobTemplate: { + fullUnifiedJobTemplate: { id: 7000, name: 'Foo JT', }, @@ -281,7 +281,7 @@ describe('Workflow reducer', () => { { id: 3, isInvalidLinkTarget: false, - unifiedJobTemplate: { + fullUnifiedJobTemplate: { id: 7000, name: 'Foo JT', }, @@ -869,10 +869,6 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 2, }, - unifiedJobTemplate: { - id: 1, - name: 'JT 1', - }, }, target: { id: 4, @@ -889,10 +885,6 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 4, }, - unifiedJobTemplate: { - id: 3, - name: 'JT 3', - }, }, }, { @@ -912,10 +904,6 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 2, }, - unifiedJobTemplate: { - id: 1, - name: 'JT 1', - }, }, target: { id: 3, @@ -932,10 +920,6 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 3, }, - unifiedJobTemplate: { - id: 2, - name: 'JT 2', - }, }, }, { @@ -955,10 +939,6 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 5, }, - unifiedJobTemplate: { - id: 4, - name: 'JT 4', - }, }, target: { id: 3, @@ -975,17 +955,13 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 3, }, - unifiedJobTemplate: { - id: 2, - name: 'JT 2', - }, }, }, { linkType: 'always', source: { id: 1, - unifiedJobTemplate: { + fullUnifiedJobTemplate: { name: undefined, }, }, @@ -1004,17 +980,13 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 2, }, - unifiedJobTemplate: { - id: 1, - name: 'JT 1', - }, }, }, { linkType: 'always', source: { id: 1, - unifiedJobTemplate: { + fullUnifiedJobTemplate: { name: undefined, }, }, @@ -1033,10 +1005,6 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 5, }, - unifiedJobTemplate: { - id: 4, - name: 'JT 4', - }, }, }, ], @@ -1044,7 +1012,7 @@ describe('Workflow reducer', () => { nodes: [ { id: 1, - unifiedJobTemplate: { + fullUnifiedJobTemplate: { name: undefined, }, }, @@ -1063,10 +1031,6 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 2, }, - unifiedJobTemplate: { - id: 1, - name: 'JT 1', - }, }, { id: 3, @@ -1083,10 +1047,6 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 3, }, - unifiedJobTemplate: { - id: 2, - name: 'JT 2', - }, }, { id: 4, @@ -1103,10 +1063,6 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 4, }, - unifiedJobTemplate: { - id: 3, - name: 'JT 3', - }, }, { id: 5, @@ -1123,10 +1079,6 @@ describe('Workflow reducer', () => { }, workflowMakerNodeId: 5, }, - unifiedJobTemplate: { - id: 4, - name: 'JT 4', - }, }, ], }); @@ -1718,7 +1670,7 @@ describe('Workflow reducer', () => { id: 2, isEdited: false, isInvalidLinkTarget: false, - unifiedJobTemplate: { + fullUnifiedJobTemplate: { id: 703, name: 'Test JT', type: 'job_template', @@ -1729,7 +1681,7 @@ describe('Workflow reducer', () => { id: 2, isEdited: false, isInvalidLinkTarget: false, - unifiedJobTemplate: { + fullUnifiedJobTemplate: { id: 703, name: 'Test JT', type: 'job_template', @@ -1757,7 +1709,7 @@ describe('Workflow reducer', () => { id: 2, isEdited: true, isInvalidLinkTarget: false, - unifiedJobTemplate: { + fullUnifiedJobTemplate: { id: 704, name: 'Other JT', type: 'job_template', diff --git a/awx/ui_next/src/constants.js b/awx/ui_next/src/constants.js index b7fa1fc83e..663814439e 100644 --- a/awx/ui_next/src/constants.js +++ b/awx/ui_next/src/constants.js @@ -7,3 +7,5 @@ export const JOB_TYPE_URL_SEGMENTS = { ad_hoc_command: 'command', workflow_job: 'workflow', }; + +export const SESSION_TIMEOUT_KEY = 'awx-session-timeout'; diff --git a/awx/ui_next/src/index.jsx b/awx/ui_next/src/index.jsx index ad616077ef..8bbeb9e866 100644 --- a/awx/ui_next/src/index.jsx +++ b/awx/ui_next/src/index.jsx @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import './setupCSP'; import '@patternfly/react-core/dist/styles/base.css'; import App from './App'; import { BrandName } from './variables'; diff --git a/awx/ui_next/src/routeConfig.js b/awx/ui_next/src/routeConfig.js index cb936764cb..a343a7d1e0 100644 --- a/awx/ui_next/src/routeConfig.js +++ b/awx/ui_next/src/routeConfig.js @@ -1,5 +1,6 @@ import { t } from '@lingui/macro'; +import ActivityStream from './screens/ActivityStream'; import Applications from './screens/Application'; import Credentials from './screens/Credential'; import CredentialTypes from './screens/CredentialType'; @@ -17,6 +18,7 @@ import Settings from './screens/Setting'; import Teams from './screens/Team'; import Templates from './screens/Template'; import Users from './screens/User'; +import WorkflowApprovals from './screens/WorkflowApproval'; // Ideally, this should just be a regular object that we export, but we // need the i18n. When lingui3 arrives, we will be able to import i18n @@ -43,6 +45,16 @@ function getRouteConfig(i18n) { path: '/schedules', screen: Schedules, }, + { + title: i18n._(t`Activity Stream`), + path: '/activity_stream', + screen: ActivityStream, + }, + { + title: i18n._(t`Workflow Approvals`), + path: '/workflow_approvals', + screen: WorkflowApprovals, + }, ], }, { diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx new file mode 100644 index 0000000000..84dd9a6066 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx @@ -0,0 +1,269 @@ +import React, { Fragment, useState, useEffect, useCallback } from 'react'; +import { useLocation, useHistory } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + Card, + PageSection, + PageSectionVariants, + SelectGroup, + Select, + SelectVariant, + SelectOption, + Title, +} from '@patternfly/react-core'; + +import DatalistToolbar from '../../components/DataListToolbar'; +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../components/PaginatedTable'; +import useRequest from '../../util/useRequest'; +import { + getQSConfig, + parseQueryString, + replaceParams, + encodeNonDefaultQueryString, +} from '../../util/qs'; +import { ActivityStreamAPI } from '../../api'; + +import ActivityStreamListItem from './ActivityStreamListItem'; + +function ActivityStream({ i18n }) { + const { light } = PageSectionVariants; + + const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState(false); + const location = useLocation(); + const history = useHistory(); + const urlParams = new URLSearchParams(location.search); + + const activityStreamType = urlParams.get('type') || 'all'; + + let typeParams = {}; + + if (activityStreamType !== 'all') { + typeParams = { + or__object1__in: activityStreamType, + or__object2__in: activityStreamType, + }; + } + + const QS_CONFIG = getQSConfig( + 'activity_stream', + { + page: 1, + page_size: 20, + order_by: '-timestamp', + }, + ['id', 'page', 'page_size'] + ); + + const { + result: { results, count, relatedSearchableKeys, searchableKeys }, + error: contentError, + isLoading, + request: fetchActivityStream, + } = useRequest( + useCallback( + async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const [response, actionsResponse] = await Promise.all([ + ActivityStreamAPI.read({ ...params, ...typeParams }), + ActivityStreamAPI.readOptions(), + ]); + return { + results: response.data.results, + count: 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), + }; + }, + [location] // eslint-disable-line react-hooks/exhaustive-deps + ), + { + results: [], + count: 0, + relatedSearchableKeys: [], + searchableKeys: [], + } + ); + useEffect(() => { + fetchActivityStream(); + }, [fetchActivityStream]); + + const pushHistoryState = urlParamsToAdd => { + let searchParams = parseQueryString(QS_CONFIG, location.search); + searchParams = replaceParams(searchParams, { page: 1 }); + const encodedParams = encodeNonDefaultQueryString(QS_CONFIG, searchParams, { + type: urlParamsToAdd.get('type'), + }); + history.push( + encodedParams + ? `${location.pathname}?${encodedParams}` + : location.pathname + ); + }; + + return ( + + + + {i18n._(t`Activity Stream`)} + + + + + + + + {i18n._(t`Time`)} + + {i18n._(t`Initiated by`)} + + {i18n._(t`Event`)} + {i18n._(t`Actions`)} + + } + renderToolbar={props => ( + + )} + renderRow={streamItem => ( + + )} + /> + + + + ); +} + +export default withI18n()(ActivityStream); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.test.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.test.jsx new file mode 100644 index 0000000000..b5afeef3d6 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.test.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; + +import ActivityStream from './ActivityStream'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + +describe('', () => { + let pageWrapper; + + beforeEach(() => { + pageWrapper = mountWithContexts(); + }); + + afterEach(() => { + pageWrapper.unmount(); + }); + + test('initially renders without crashing', () => { + expect(pageWrapper.length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.jsx new file mode 100644 index 0000000000..517491797a --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.jsx @@ -0,0 +1,584 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; + +const buildAnchor = (obj, resource, activity) => { + let url; + let name; + // try/except pattern asserts that: + // if we encounter a case where a UI url can't or + // shouldn't be generated, just supply the name of the resource + try { + // catch-all case to avoid generating urls if a resource has been deleted + // if a resource still exists, it'll be serialized in the activity's summary_fields + if (!activity.summary_fields[resource]) { + throw new Error('The referenced resource no longer exists'); + } + switch (resource) { + case 'custom_inventory_script': + url = `/inventory_scripts/${obj.id}/`; + break; + case 'group': + if ( + activity.operation === 'create' || + activity.operation === 'delete' + ) { + // the API formats the changes.inventory field as str 'myInventoryName-PrimaryKey' + const [inventory_id] = activity.changes.inventory + .split('-') + .slice(-1); + url = `/inventories/inventory/${inventory_id}/groups/${activity.changes.id}/details/`; + } else { + url = `/inventories/inventory/${ + activity.summary_fields.inventory[0].id + }/groups/${activity.changes.id || + activity.changes.object1_pk}/details/`; + } + break; + case 'host': + url = `/hosts/${obj.id}/`; + break; + case 'job': + url = `/jobs/${obj.id}/`; + break; + case 'inventory': + url = + obj?.kind === 'smart' + ? `/inventories/smart_inventory/${obj.id}/` + : `/inventories/inventory/${obj.id}/`; + break; + case 'schedule': + // schedule urls depend on the resource they're associated with + if (activity.summary_fields.job_template) { + const jt_id = activity.summary_fields.job_template[0].id; + url = `/templates/job_template/${jt_id}/schedules/${obj.id}/`; + } else if (activity.summary_fields.workflow_job_template) { + const wfjt_id = activity.summary_fields.workflow_job_template[0].id; + url = `/templates/workflow_job_template/${wfjt_id}/schedules/${obj.id}/`; + } else if (activity.summary_fields.project) { + url = `/projects/${activity.summary_fields.project[0].id}/schedules/${obj.id}/`; + } else if (activity.summary_fields.system_job_template) { + url = null; + } else { + // urls for inventory sync schedules currently depend on having + // an inventory id and group id + throw new Error( + 'activity.summary_fields to build this url not implemented yet' + ); + } + break; + case 'setting': + url = `/settings/`; + break; + case 'notification_template': + url = `/notification_templates/${obj.id}/`; + break; + case 'role': + throw new Error( + 'role object management is not consolidated to a single UI view' + ); + case 'job_template': + url = `/templates/job_template/${obj.id}/`; + break; + case 'workflow_job_template': + url = `/templates/workflow_job_template/${obj.id}/`; + break; + case 'workflow_job_template_node': { + const { + id: wfjt_id, + name: wfjt_name, + } = activity.summary_fields.workflow_job_template[0]; + url = `/templates/workflow_job_template/${wfjt_id}/`; + name = wfjt_name; + break; + } + case 'workflow_job': + url = `/jobs/workflow/${obj.id}/`; + break; + case 'label': + url = null; + break; + case 'inventory_source': { + const inventoryId = (obj.inventory || '').split('-').reverse()[0]; + url = `/inventories/inventory/${inventoryId}/sources/${obj.id}/details/`; + break; + } + case 'o_auth2_application': + url = `/applications/${obj.id}/`; + break; + case 'workflow_approval': + url = `/jobs/workflow/${activity.summary_fields.workflow_job[0].id}/output/`; + name = `${activity.summary_fields.workflow_job[0].name} | ${activity.summary_fields.workflow_approval[0].name}`; + break; + case 'workflow_approval_template': + url = `/templates/workflow_job_template/${activity.summary_fields.workflow_job_template[0].id}/visualizer/`; + name = `${activity.summary_fields.workflow_job_template[0].name} | ${activity.summary_fields.workflow_approval_template[0].name}`; + break; + default: + url = `/${resource}s/${obj.id}/`; + } + + name = name || obj.name || obj.username; + + if (url) { + return {name}; + } + + return {name}; + } catch (err) { + return {obj.name || obj.username || ''}; + } +}; + +const getPastTense = item => { + return /e$/.test(item) ? `${item}d` : `${item}ed`; +}; + +const isGroupRelationship = item => { + return ( + item.object1 === 'group' && + item.object2 === 'group' && + item.summary_fields.group.length > 1 + ); +}; + +const buildLabeledLink = (label, link) => { + return ( + + {label} {link} + + ); +}; + +function ActivityStreamDescription({ i18n, activity }) { + const labeledLinks = []; + // Activity stream objects will outlive the resources they reference + // in that case, summary_fields will not be available - show generic error text instead + try { + switch (activity.object_association) { + // explicit role dis+associations + case 'role': { + let { object1, object2 } = activity; + + // if object1 winds up being the role's resource, we need to swap the objects + // in order to make the sentence make sense. + if (activity.object_type === object1) { + object1 = activity.object2; + object2 = activity.object1; + } + + // object1 field is resource targeted by the dis+association + // object2 field is the resource the role is inherited from + // summary_field.role[0] contains ref info about the role + switch (activity.operation) { + // expected outcome: "disassociated role_name from " + case 'disassociate': + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields.group[1], + object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} from`, + buildAnchor( + activity.summary_fields.group[0], + object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields[object2][0], + object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} from`, + buildAnchor( + activity.summary_fields[object1][0], + object1, + activity + ) + ) + ); + } + break; + // expected outcome: "associated role_name to " + case 'associate': + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields.group[1], + object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} to`, + buildAnchor( + activity.summary_fields.group[0], + object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields[object2][0], + object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} to`, + buildAnchor( + activity.summary_fields[object1][0], + object1, + activity + ) + ) + ); + } + break; + default: + break; + } + break; + // inherited role dis+associations (logic identical to case 'role') + } + case 'parents': + // object1 field is resource targeted by the dis+association + // object2 field is the resource the role is inherited from + // summary_field.role[0] contains ref info about the role + switch (activity.operation) { + // expected outcome: "disassociated role_name from " + case 'disassociate': + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object2}`, + buildAnchor( + activity.summary_fields.group[1], + activity.object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `from ${activity.object1}`, + buildAnchor( + activity.summary_fields.group[0], + activity.object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields[activity.object2][0], + activity.object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} from`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + } + break; + // expected outcome: "associated role_name to " + case 'associate': + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor( + activity.summary_fields.group[0], + activity.object1, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `to ${activity.object2}`, + buildAnchor( + activity.summary_fields.group[1], + activity.object2, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields[activity.object2][0], + activity.object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} to`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + } + break; + default: + break; + } + break; + // CRUD operations / resource on resource dis+associations + default: + switch (activity.operation) { + // expected outcome: "disassociated from " + case 'disassociate': + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object2}`, + buildAnchor( + activity.summary_fields.group[1], + activity.object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `from ${activity.object1}`, + buildAnchor( + activity.summary_fields.group[0], + activity.object1, + activity + ) + ) + ); + } else if ( + activity.object1 === 'workflow_job_template_node' && + activity.object2 === 'workflow_job_template_node' + ) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} two nodes on workflow`, + buildAnchor( + activity.summary_fields[activity.object1[0]], + activity.object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object2}`, + buildAnchor( + activity.summary_fields[activity.object2][0], + activity.object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `from ${activity.object1}`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + } + break; + // expected outcome "associated to " + case 'associate': + // groups are the only resource that can be associated/disassociated into each other + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor( + activity.summary_fields.group[0], + activity.object1, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `to ${activity.object2}`, + buildAnchor( + activity.summary_fields.group[1], + activity.object2, + activity + ) + ) + ); + } else if ( + activity.object1 === 'workflow_job_template_node' && + activity.object2 === 'workflow_job_template_node' + ) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} two nodes on workflow`, + buildAnchor( + activity.summary_fields[activity.object1[0]], + activity.object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `to ${activity.object2}`, + buildAnchor( + activity.summary_fields[activity.object2][0], + activity.object2, + activity + ) + ) + ); + } + break; + case 'delete': + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor(activity.changes, activity.object1, activity) + ) + ); + break; + // expected outcome: "operation " + case 'update': + if ( + activity.object1 === 'workflow_approval' && + activity?.changes?.status?.length === 2 + ) { + let operationText = ''; + if (activity.changes.status[1] === 'successful') { + operationText = i18n._(t`approved`); + } else if (activity.changes.status[1] === 'failed') { + if ( + activity.changes.timed_out && + activity.changes.timed_out[1] === true + ) { + operationText = i18n._(t`timed out`); + } else { + operationText = i18n._(t`denied`); + } + } else { + operationText = i18n._(t`updated`); + } + labeledLinks.push( + buildLabeledLink( + `${operationText} ${activity.object1}`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + } + break; + case 'create': + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor(activity.changes, activity.object1, activity) + ) + ); + break; + default: + break; + } + break; + } + } catch (err) { + return {i18n._(t`Event summary not available`)}; + } + + return ( + + {labeledLinks.reduce( + (acc, x) => + acc === null ? ( + x + ) : ( + <> + {acc} {x} + + ), + null + )} + + ); +} + +export default withI18n()(ActivityStreamDescription); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.test.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.test.jsx new file mode 100644 index 0000000000..9f3a2982d0 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.test.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import ActivityStreamDescription from './ActivityStreamDescription'; + +describe('ActivityStreamDescription', () => { + test('initially renders succesfully', () => { + const description = mountWithContexts( + + ); + expect(description.find('span').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.jsx new file mode 100644 index 0000000000..22559831b2 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.jsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Button, Modal } from '@patternfly/react-core'; +import { SearchPlusIcon } from '@patternfly/react-icons'; + +import { formatDateString } from '../../util/dates'; + +import { DetailList, Detail } from '../../components/DetailList'; +import { VariablesDetail } from '../../components/CodeMirrorInput'; + +function ActivityStreamDetailButton({ i18n, streamItem, user, description }) { + const [isOpen, setIsOpen] = useState(false); + + const setting = streamItem?.summary_fields?.setting; + const changeRows = Math.max( + Object.keys(streamItem?.changes || []).length + 2, + 6 + ); + + return ( + <> + + setIsOpen(false)} + > + + + + + + + {streamItem?.changes && ( + + )} + + + + ); +} + +export default withI18n()(ActivityStreamDetailButton); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.test.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.test.jsx new file mode 100644 index 0000000000..40dc104117 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.test.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import ActivityStreamDetailButton from './ActivityStreamDetailButton'; + +jest.mock('../../api/models/ActivityStream'); + +describe('', () => { + test('initially renders succesfully', () => { + mountWithContexts( + Bob} + description={foo} + /> + ); + }); +}); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.jsx new file mode 100644 index 0000000000..c5463565bb --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { shape } from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { Tr, Td } from '@patternfly/react-table'; +import { t } from '@lingui/macro'; +import { Link } from 'react-router-dom'; + +import { formatDateString } from '../../util/dates'; +import { ActionsTd, ActionItem } from '../../components/PaginatedTable'; + +import ActivityStreamDetailButton from './ActivityStreamDetailButton'; +import ActivityStreamDescription from './ActivityStreamDescription'; + +function ActivityStreamListItem({ streamItem, i18n }) { + ActivityStreamListItem.propTypes = { + streamItem: shape({}).isRequired, + }; + + const buildUser = item => { + let link; + if (item?.summary_fields?.actor?.id) { + link = ( + + {item.summary_fields.actor.username} + + ); + } else if (item?.summary_fields?.actor) { + link = i18n._(t`${item.summary_fields.actor.username} (deleted)`); + } else { + link = i18n._(t`system`); + } + return link; + }; + + const labelId = `check-action-${streamItem.id}`; + const user = buildUser(streamItem); + const description = ; + + return ( + + + + {streamItem.timestamp ? formatDateString(streamItem.timestamp) : ''} + + {user} + + {description} + + + + + + + + ); +} +export default withI18n()(ActivityStreamListItem); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.test.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.test.jsx new file mode 100644 index 0000000000..c0f03d23ee --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.test.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import ActivityStreamListItem from './ActivityStreamListItem'; + +jest.mock('../../api/models/ActivityStream'); + +describe('', () => { + test('initially renders succesfully', () => { + mountWithContexts( + + + {}} + /> + +
+ ); + }); +}); diff --git a/awx/ui_next/src/screens/ActivityStream/index.js b/awx/ui_next/src/screens/ActivityStream/index.js new file mode 100644 index 0000000000..5c0c72d9ef --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/index.js @@ -0,0 +1 @@ +export { default } from './ActivityStream'; diff --git a/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx b/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx index 0537fa813c..34f40645d7 100644 --- a/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx @@ -8,7 +8,7 @@ import ApplicationForm from '../shared/ApplicationForm'; import { ApplicationsAPI } from '../../../api'; import { CardBody } from '../../../components/Card'; -function ApplicationAdd() { +function ApplicationAdd({ onSuccessfulAdd }) { const history = useHistory(); const [submitError, setSubmitError] = useState(null); @@ -53,10 +53,9 @@ function ApplicationAdd() { const handleSubmit = async ({ ...values }) => { values.organization = values.organization.id; try { - const { - data: { id }, - } = await ApplicationsAPI.create(values); - history.push(`/applications/${id}/details`); + const { data } = await ApplicationsAPI.create(values); + onSuccessfulAdd(data); + history.push(`/applications/${data.id}/details`); } catch (err) { setSubmitError(err); } diff --git a/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.test.jsx b/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.test.jsx index 2bc0eba47e..dd480e8ab9 100644 --- a/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.test.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.test.jsx @@ -39,12 +39,16 @@ const options = { }, }; +const onSuccessfulAdd = jest.fn(); + describe('', () => { let wrapper; test('should render properly', async () => { ApplicationsAPI.readOptions.mockResolvedValue(options); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts( + + ); }); expect(wrapper.find('ApplicationAdd').length).toBe(1); expect(wrapper.find('ApplicationForm').length).toBe(1); @@ -59,9 +63,12 @@ describe('', () => { ApplicationsAPI.create.mockResolvedValue({ data: { id: 8 } }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); }); await act(async () => { @@ -124,6 +131,7 @@ describe('', () => { redirect_uris: 'http://www.google.com', }); expect(history.location.pathname).toBe('/applications/8/details'); + expect(onSuccessfulAdd).toHaveBeenCalledWith({ id: 8 }); }); test('should cancel form properly', async () => { @@ -134,9 +142,12 @@ describe('', () => { ApplicationsAPI.create.mockResolvedValue({ data: { id: 8 } }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); }); await act(async () => { wrapper.find('Button[aria-label="Cancel"]').prop('onClick')(); @@ -157,7 +168,9 @@ describe('', () => { ApplicationsAPI.create.mockRejectedValue(error); ApplicationsAPI.readOptions.mockResolvedValue(options); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts( + + ); }); await act(async () => { wrapper.find('Formik').prop('onSubmit')({ @@ -181,7 +194,9 @@ describe('', () => { }) ); await act(async () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts( + + ); }); wrapper.update(); diff --git a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx index e6874b65a5..80d7a8c916 100644 --- a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx @@ -7,7 +7,11 @@ import { Button } from '@patternfly/react-core'; import useRequest, { useDismissableError } from '../../../util/useRequest'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; -import { Detail, DetailList } from '../../../components/DetailList'; +import { + Detail, + DetailList, + UserDateDetail, +} from '../../../components/DetailList'; import { ApplicationsAPI } from '../../../api'; import DeleteButton from '../../../components/DeleteButton'; import ErrorDetail from '../../../components/ErrorDetail'; @@ -58,11 +62,12 @@ function ApplicationDetails({ } + dataCy="app-detail-organization" /> + + + diff --git a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.test.jsx b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.test.jsx index 27fdb5f2f4..eebefb0ce0 100644 --- a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.test.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.test.jsx @@ -120,6 +120,9 @@ describe('', () => { expect(wrapper.find('Detail[label="Client type"]').prop('value')).toBe( 'Confidential' ); + expect(wrapper.find('Detail[label="Client ID"]').prop('value')).toBe( + 'b1dmj8xzkbFm1ZQ27ygw2ZeE9I0AXqqeL74fiyk4' + ); expect(wrapper.find('Button[aria-label="Edit"]').prop('to')).toBe( '/applications/10/edit' ); diff --git a/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx b/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx index 18268ba63a..13558bd2a0 100644 --- a/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; -import { Card, PageSection } from '@patternfly/react-core'; +import { Card } from '@patternfly/react-core'; import ApplicationForm from '../shared/ApplicationForm'; import { ApplicationsAPI } from '../../../api'; import { CardBody } from '../../../components/Card'; @@ -29,22 +29,18 @@ function ApplicationEdit({ history.push(`/applications/${id}/details`); }; return ( - <> - - - - - - - - + + + + + ); } export default ApplicationEdit; diff --git a/awx/ui_next/src/screens/Application/Applications.jsx b/awx/ui_next/src/screens/Application/Applications.jsx index 1842e5bd89..ae6fa94af1 100644 --- a/awx/ui_next/src/screens/Application/Applications.jsx +++ b/awx/ui_next/src/screens/Application/Applications.jsx @@ -1,14 +1,26 @@ import React, { useState, useCallback } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import styled from 'styled-components'; import { Route, Switch } from 'react-router-dom'; - +import { + Alert, + ClipboardCopy, + ClipboardCopyVariant, + Modal, +} from '@patternfly/react-core'; import ApplicationsList from './ApplicationsList'; import ApplicationAdd from './ApplicationAdd'; import Application from './Application'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; +import { Detail, DetailList } from '../../components/DetailList'; + +const ApplicationAlert = styled(Alert)` + margin-bottom: 20px; +`; function Applications({ i18n }) { + const [applicationModalSource, setApplicationModalSource] = useState(null); const [breadcrumbConfig, setBreadcrumbConfig] = useState({ '/applications': i18n._(t`Applications`), '/applications/add': i18n._(t`Create New Application`), @@ -33,10 +45,15 @@ function Applications({ i18n }) { return ( <> - + - + setApplicationModalSource(app)} + /> @@ -45,6 +62,57 @@ function Applications({ i18n }) { + {applicationModalSource && ( + setApplicationModalSource(null)} + > + {applicationModalSource.client_secret && ( + + )} + + + {applicationModalSource.client_id && ( + + {applicationModalSource.client_id} + + } + /> + )} + {applicationModalSource.client_secret && ( + + {applicationModalSource.client_secret} + + } + /> + )} + + + )} ); } diff --git a/awx/ui_next/src/screens/Application/Applications.test.jsx b/awx/ui_next/src/screens/Application/Applications.test.jsx index f309a2b60a..80c5f9a34c 100644 --- a/awx/ui_next/src/screens/Application/Applications.test.jsx +++ b/awx/ui_next/src/screens/Application/Applications.test.jsx @@ -1,25 +1,52 @@ import React from 'react'; - +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Applications from './Applications'; -describe('', () => { - let pageWrapper; - let pageSections; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); - beforeEach(() => { - pageWrapper = mountWithContexts(); - pageSections = pageWrapper.find('PageSection'); - }); +describe('', () => { + let wrapper; afterEach(() => { - pageWrapper.unmount(); + wrapper.unmount(); }); - test('initially renders without crashing', () => { - expect(pageWrapper.length).toBe(1); + test('renders successfully', () => { + wrapper = mountWithContexts(); + const pageSections = wrapper.find('PageSection'); + expect(wrapper.length).toBe(1); expect(pageSections.length).toBe(1); expect(pageSections.first().props().variant).toBe('light'); }); + + test('shows Application information modal after successful creation', async () => { + const history = createMemoryHistory({ + initialEntries: ['/applications/add'], + }); + wrapper = mountWithContexts(, { + context: { router: { history } }, + }); + expect(wrapper.find('Modal[title="Application information"]').length).toBe( + 0 + ); + await act(async () => { + wrapper + .find('ApplicationAdd') + .props() + .onSuccessfulAdd({ + name: 'test', + client_id: 'foobar', + client_secret: 'aaaaaaaaaaaaaaaaaaaaaaaaaa', + }); + }); + wrapper.update(); + expect(wrapper.find('Modal[title="Application information"]').length).toBe( + 1 + ); + }); }); diff --git a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.jsx b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.jsx index 470e4dc255..1a0f5c9f4e 100644 --- a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationListItem.jsx @@ -78,7 +78,7 @@ function ApplicationListItem({ ]} /> diff --git a/awx/ui_next/src/screens/Credential/Credential.jsx b/awx/ui_next/src/screens/Credential/Credential.jsx index e06dce231e..42c18a0d41 100644 --- a/awx/ui_next/src/screens/Credential/Credential.jsx +++ b/awx/ui_next/src/screens/Credential/Credential.jsx @@ -56,15 +56,12 @@ function Credential({ i18n, setBreadcrumb }) { id: 99, }, { name: i18n._(t`Details`), link: `/credentials/${id}/details`, id: 0 }, - ]; - - if (credential && credential.organization) { - tabsArray.push({ + { name: i18n._(t`Access`), link: `/credentials/${id}/access`, id: 1, - }); - } + }, + ]; let showCardHeader = true; @@ -108,14 +105,12 @@ function Credential({ i18n, setBreadcrumb }) { , - credential.organization && ( - - - - ), + + + , {!hasContentLoading && ( diff --git a/awx/ui_next/src/screens/Credential/Credential.test.jsx b/awx/ui_next/src/screens/Credential/Credential.test.jsx index 7cb192ac0c..ed3404c1a6 100644 --- a/awx/ui_next/src/screens/Credential/Credential.test.jsx +++ b/awx/ui_next/src/screens/Credential/Credential.test.jsx @@ -31,7 +31,7 @@ describe('', () => { wrapper = mountWithContexts( {}} />); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 2); + await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 3); }); test('initially renders org-based credential succesfully', async () => { diff --git a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx index 868878832a..c5df022772 100644 --- a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx @@ -43,7 +43,7 @@ function CredentialAdd({ me }) { possibleFields.forEach(field => { const input = inputs[field.id]; - if (input.credential && input.inputs) { + if (input?.credential && input?.inputs) { pluginInputs[field.id] = input; } else if (passwordPrompts[field.id]) { nonPluginInputs[field.id] = 'ASK'; diff --git a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx index 5f8066b904..55190e3a5f 100644 --- a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx @@ -78,7 +78,7 @@ function CredentialDetail({ i18n, credential }) { {} ), }; - }, [credentialId, credential_type]), + }, [credentialId, credential_type.id]), { fields: [], managedByTower: true, diff --git a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx index 0cb78ed75e..83e64e81f8 100644 --- a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx @@ -39,7 +39,7 @@ function CredentialEdit({ credential, me }) { possibleFields.forEach(field => { const input = inputs[field.id]; - if (input.credential && input.inputs) { + if (input?.credential && input?.inputs) { pluginInputs[field.id] = input; } else if (passwordPrompts[field.id]) { nonPluginInputs[field.id] = 'ASK'; diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx index 26272ee4ad..e4a10ed86c 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx @@ -26,7 +26,13 @@ function CredentialList({ i18n }) { const location = useLocation(); const { - result: { credentials, credentialCount, actions }, + result: { + credentials, + credentialCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchCredentials, @@ -37,16 +43,29 @@ function CredentialList({ i18n }) { CredentialsAPI.read(params), CredentialsAPI.readOptions(), ]); + const searchKeys = Object.keys( + credActions.data.actions?.GET || {} + ).filter(key => credActions.data.actions?.GET[key].filterable); + const item = searchKeys.indexOf('type'); + if (item) { + searchKeys[item] = 'credential_type__kind'; + } return { credentials: creds.data.results, credentialCount: creds.data.count, actions: credActions.data.actions, + relatedSearchableKeys: ( + credActions?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: searchKeys, }; }, [location]), { credentials: [], credentialCount: 0, actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -102,6 +121,8 @@ function CredentialList({ i18n }) { itemCount={credentialCount} qsConfig={QS_CONFIG} onRowClick={handleSelect} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarSearchColumns={[ { name: i18n._(t`Name`), diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx index 88266fed41..a4e436c073 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialListItem.jsx @@ -83,7 +83,7 @@ function CredentialListItem({ ]} /> diff --git a/awx/ui_next/src/screens/Credential/Credentials.jsx b/awx/ui_next/src/screens/Credential/Credentials.jsx index beca652fd5..d883aa0dca 100644 --- a/awx/ui_next/src/screens/Credential/Credentials.jsx +++ b/awx/ui_next/src/screens/Credential/Credentials.jsx @@ -3,7 +3,7 @@ import { Route, Switch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Config } from '../../contexts/Config'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; import Credential from './Credential'; import CredentialAdd from './CredentialAdd'; import { CredentialList } from './CredentialList'; @@ -34,7 +34,10 @@ function Credentials({ i18n }) { return ( <> - + {({ me }) => } diff --git a/awx/ui_next/src/screens/Credential/Credentials.test.jsx b/awx/ui_next/src/screens/Credential/Credentials.test.jsx index f63c9d9b65..e5cb618fb2 100644 --- a/awx/ui_next/src/screens/Credential/Credentials.test.jsx +++ b/awx/ui_next/src/screens/Credential/Credentials.test.jsx @@ -3,6 +3,10 @@ import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Credentials from './Credentials'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let wrapper; @@ -30,8 +34,8 @@ describe('', () => { }, }); - expect(wrapper.find('Crumb').length).toBe(1); - expect(wrapper.find('BreadcrumbHeading').text()).toBe('Credentials'); + expect(wrapper.find('Crumb').length).toBe(0); + expect(wrapper.find('Title').text()).toBe('Credentials'); }); test('should display create new credential breadcrumb heading', () => { @@ -51,8 +55,6 @@ describe('', () => { }); expect(wrapper.find('Crumb').length).toBe(2); - expect(wrapper.find('BreadcrumbHeading').text()).toBe( - 'Create New Credential' - ); + expect(wrapper.find('Title').text()).toBe('Create New Credential'); }); }); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx index c3d6856136..5d5382cbe0 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx @@ -22,12 +22,26 @@ function CredentialFormFields({ initialValues, }) { const { setFieldValue } = useFormikContext(); - const [orgField, orgMeta, orgHelpers] = useField('organization'); + const [credTypeField, credTypeMeta, credTypeHelpers] = useField({ name: 'credential_type', validate: required(i18n._(t`Select a value for this field`), i18n), }); + const isGalaxyCredential = + !!credTypeField.value && + credentialTypes[credTypeField.value].kind === 'galaxy'; + + const [orgField, orgMeta, orgHelpers] = useField({ + name: 'organization', + validate: + isGalaxyCredential && + required( + i18n._(t`Galaxy credentials must be owned by an Organization.`), + i18n + ), + }); + const credentialTypeOptions = Object.keys(credentialTypes) .map(key => { return { @@ -108,6 +122,7 @@ function CredentialFormFields({ value={orgField.value} touched={orgMeta.touched} error={orgMeta.error} + required={isGalaxyCredential} /> ', () => { gceFieldExpects(); }); + + test('should display from fields for galaxy/automation hub credentials', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1); + expect( + wrapper.find('FormGroup[label="Organization"]').prop('isRequired') + ).toBe(true); + expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1); + }); }); }); diff --git a/awx/ui_next/src/screens/Credential/shared/data.credentialTypes.json b/awx/ui_next/src/screens/Credential/shared/data.credentialTypes.json index 98db5efeab..6281f15024 100644 --- a/awx/ui_next/src/screens/Credential/shared/data.credentialTypes.json +++ b/awx/ui_next/src/screens/Credential/shared/data.credentialTypes.json @@ -275,6 +275,11 @@ "type": "string", "help_text": "OpenStack domains define administrative boundaries. It is only needed for Keystone v3 authentication URLs. Refer to Ansible Tower documentation for common scenarios." }, + { + "id": "project_region_name", + "label": "Region Name", + "type": "string" + }, { "id": "verify_ssl", "label": "Verify SSL", @@ -1269,5 +1274,54 @@ "required": ["vault_password"] }, "injectors": {} - } + }, + { + "id": 42, + "type": "credential_type", + "url": "/api/v2/credential_types/42/", + "related": { + "credentials": "/api/v2/credential_types/42/credentials/", + "activity_stream": "/api/v2/credential_types/42/activity_stream/" + }, + "summary_fields": { + "user_capabilities": { + "edit": true, + "delete": true + } + }, + "created": "2020-11-11T00:18:31.928286Z", + "modified": "2020-11-11T00:19:00.851597Z", + "name": "Ansible Galaxy/Automation Hub API Token", + "description": "", + "kind": "galaxy", + "namespace": "galaxy_api_token", + "managed_by_tower": true, + "inputs": { + "fields": [ + { + "id": "url", + "label": "Galaxy Server URL", + "type": "string", + "help_text": "The URL of the Galaxy instance to connect to." + }, + { + "id": "auth_url", + "label": "Auth Server URL", + "type": "string", + "help_text": "The URL of a Keycloak server token_endpoint, if using SSO auth." + }, + { + "id": "token", + "label": "API Token", + "type": "string", + "secret": true, + "help_text": "A token to use for authentication against the Galaxy instance." + } + ], + "required": [ + "url" + ] + }, + "injectors": {} +} ] diff --git a/awx/ui_next/src/screens/Credential/shared/data.galaxyCredential.json b/awx/ui_next/src/screens/Credential/shared/data.galaxyCredential.json new file mode 100644 index 0000000000..a8c7184548 --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/data.galaxyCredential.json @@ -0,0 +1,89 @@ +{ + "id": 5, + "type": "credential", + "url": "/api/v2/credentials/5/", + "related": { + "named_url": "/api/v2/credentials/Foo++Ansible Galaxy%2FAutomation Hub API Token+galaxy++Default/", + "created_by": "/api/v2/users/1/", + "modified_by": "/api/v2/users/1/", + "organization": "/api/v2/organizations/1/", + "activity_stream": "/api/v2/credentials/5/activity_stream/", + "access_list": "/api/v2/credentials/5/access_list/", + "object_roles": "/api/v2/credentials/5/object_roles/", + "owner_users": "/api/v2/credentials/5/owner_users/", + "owner_teams": "/api/v2/credentials/5/owner_teams/", + "copy": "/api/v2/credentials/5/copy/", + "input_sources": "/api/v2/credentials/5/input_sources/", + "credential_type": "/api/v2/credential_types/42/" + }, + "summary_fields": { + "organization": { + "id": 1, + "name": "Baz", + "description": "" + }, + "credential_type": { + "id": 42, + "name": "Ansible Galaxy/Automation Hub API Token", + "description": "" + }, + "created_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "modified_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "object_roles": { + "admin_role": { + "description": "Can manage all aspects of the credential", + "name": "Admin", + "id": 109 + }, + "use_role": { + "description": "Can use the credential in a job template", + "name": "Use", + "id": 110 + }, + "read_role": { + "description": "May view settings for the credential", + "name": "Read", + "id": 111 + } + }, + "user_capabilities": { + "edit": true, + "delete": true, + "copy": true, + "use": true + }, + "owners": [ + { + "id": 1, + "type": "organization", + "name": "Default", + "description": "", + "url": "/api/v2/organizations/1/" + } + ] + }, + "created": "2020-11-16T21:33:39.385284Z", + "modified": "2020-11-16T21:33:39.385311Z", + "name": "Foo", + "description": "Bar", + "organization": 1, + "credential_type": 42, + "managed_by_tower": false, + "inputs": { + "url": "https://localhost.com", + "auth_url": "" + }, + "kind": "galaxy_api_token", + "cloud": false, + "kubernetes": false +} \ No newline at end of file diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx index e22112e9ba..ee47d2fdbc 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx @@ -18,7 +18,7 @@ import DatalistToolbar from '../../../components/DataListToolbar'; import CredentialTypeListItem from './CredentialTypeListItem'; -const QS_CONFIG = getQSConfig('credential_type', { +const QS_CONFIG = getQSConfig('credential-type', { page: 1, page_size: 20, managed_by_tower: false, diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.jsx index 580a5f5a7e..d6dd1cf1fa 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeListItem.jsx @@ -60,7 +60,7 @@ function CredentialTypeListItem({ ]} /> diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypes.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypes.jsx index 7cdbbccb00..4eb47c1892 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypes.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypes.jsx @@ -6,7 +6,7 @@ import { Route, Switch } from 'react-router-dom'; import CredentialTypeAdd from './CredentialTypeAdd'; import CredentialTypeList from './CredentialTypeList'; import CredentialType from './CredentialType'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; function CredentialTypes({ i18n }) { const [breadcrumbConfig, setBreadcrumbConfig] = useState({ @@ -33,7 +33,10 @@ function CredentialTypes({ i18n }) { ); return ( <> - + diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypes.test.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypes.test.jsx index 468a55ac7a..bd8eff8e21 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypes.test.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypes.test.jsx @@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import CredentialTypes from './CredentialTypes'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let pageWrapper; let pageSections; diff --git a/awx/ui_next/src/screens/Dashboard/Dashboard.jsx b/awx/ui_next/src/screens/Dashboard/Dashboard.jsx index d348ce28f7..c6d029cb67 100644 --- a/awx/ui_next/src/screens/Dashboard/Dashboard.jsx +++ b/awx/ui_next/src/screens/Dashboard/Dashboard.jsx @@ -18,12 +18,12 @@ import { import useRequest from '../../util/useRequest'; import { DashboardAPI } from '../../api'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; import JobList from '../../components/JobList'; - +import ContentLoading from '../../components/ContentLoading'; import LineChart from './shared/LineChart'; import Count from './shared/Count'; -import DashboardTemplateList from './shared/DashboardTemplateList'; +import TemplateList from '../../components/TemplateList'; const Counts = styled.div` display: grid; @@ -62,6 +62,7 @@ function Dashboard({ i18n }) { const [activeTabId, setActiveTabId] = useState(0); const { + isLoading, result: { jobGraphData, countData }, request: fetchDashboardGraph, } = useRequest( @@ -105,10 +106,21 @@ function Dashboard({ i18n }) { useEffect(() => { fetchDashboardGraph(); }, [fetchDashboardGraph, periodSelection, jobTypeSelection]); - + if (isLoading) { + return ( + + + + + + ); + } return ( - + )} {activeTabId === 1 && } - {activeTabId === 2 && } + {activeTabId === 2 && ( + + )}
diff --git a/awx/ui_next/src/screens/Dashboard/Dashboard.test.jsx b/awx/ui_next/src/screens/Dashboard/Dashboard.test.jsx index 548c72a1e5..fbee03febf 100644 --- a/awx/ui_next/src/screens/Dashboard/Dashboard.test.jsx +++ b/awx/ui_next/src/screens/Dashboard/Dashboard.test.jsx @@ -7,6 +7,9 @@ import { DashboardAPI } from '../../api'; import Dashboard from './Dashboard'; jest.mock('../../api'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); describe('', () => { let pageWrapper; @@ -35,10 +38,13 @@ describe('', () => { test('renders template list when the active tab is changed', async () => { expect(pageWrapper.find('DashboardTemplateList').length).toBe(0); - pageWrapper - .find('button[aria-label="Recent Templates list tab"]') - .simulate('click'); - expect(pageWrapper.find('DashboardTemplateList').length).toBe(1); + await act(async () => { + pageWrapper + .find('button[aria-label="Recent Templates list tab"]') + .simulate('click'); + }); + pageWrapper.update(); + expect(pageWrapper.find('TemplateList').length).toBe(1); }); test('renders month-based/all job type chart by default', () => { diff --git a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.jsx b/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.jsx deleted file mode 100644 index 05d4cd3ab2..0000000000 --- a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.jsx +++ /dev/null @@ -1,286 +0,0 @@ -import React, { Fragment, useEffect, useState, useCallback } from 'react'; -import { useLocation, Link } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { Card, DropdownItem } from '@patternfly/react-core'; - -import { - JobTemplatesAPI, - UnifiedJobTemplatesAPI, - WorkflowJobTemplatesAPI, -} from '../../../api'; -import AlertModal from '../../../components/AlertModal'; -import DatalistToolbar from '../../../components/DataListToolbar'; -import ErrorDetail from '../../../components/ErrorDetail'; -import PaginatedDataList, { - ToolbarDeleteButton, -} from '../../../components/PaginatedDataList'; -import useRequest, { useDeleteItems } from '../../../util/useRequest'; -import { getQSConfig, parseQueryString } from '../../../util/qs'; -import useWsTemplates from '../../../util/useWsTemplates'; -import AddDropDownButton from '../../../components/AddDropDownButton'; - -import DashboardTemplateListItem from './DashboardTemplateListItem'; - -const QS_CONFIG = getQSConfig( - 'template', - { - page: 1, - page_size: 5, - order_by: 'name', - type: 'job_template,workflow_job_template', - }, - ['id', 'page', 'page_size'] -); - -function DashboardTemplateList({ i18n }) { - // The type value in const QS_CONFIG below does not have a space between job_template and - // workflow_job_template so the params sent to the API match what the api expects. - - const location = useLocation(); - - const [selected, setSelected] = useState([]); - - const { - result: { - results, - count, - jtActions, - wfjtActions, - relatedSearchableKeys, - searchableKeys, - }, - error: contentError, - isLoading, - request: fetchTemplates, - } = useRequest( - useCallback(async () => { - const params = parseQueryString(QS_CONFIG, location.search); - const responses = await Promise.all([ - UnifiedJobTemplatesAPI.read(params), - JobTemplatesAPI.readOptions(), - WorkflowJobTemplatesAPI.readOptions(), - UnifiedJobTemplatesAPI.readOptions(), - ]); - return { - results: responses[0].data.results, - count: responses[0].data.count, - jtActions: responses[1].data.actions, - wfjtActions: responses[2].data.actions, - relatedSearchableKeys: ( - responses[3]?.data?.related_search_fields || [] - ).map(val => val.slice(0, -8)), - searchableKeys: Object.keys( - responses[3].data.actions?.GET || {} - ).filter(key => responses[3].data.actions?.GET[key].filterable), - }; - }, [location]), - { - results: [], - count: 0, - jtActions: {}, - wfjtActions: {}, - relatedSearchableKeys: [], - searchableKeys: [], - } - ); - - useEffect(() => { - fetchTemplates(); - }, [fetchTemplates]); - - const templates = useWsTemplates(results); - - const isAllSelected = - selected.length === templates.length && selected.length > 0; - const { - isLoading: isDeleteLoading, - deleteItems: deleteTemplates, - deletionError, - clearDeletionError, - } = useDeleteItems( - useCallback(() => { - return Promise.all( - selected.map(({ type, id }) => { - if (type === 'job_template') { - return JobTemplatesAPI.destroy(id); - } - if (type === 'workflow_job_template') { - return WorkflowJobTemplatesAPI.destroy(id); - } - return false; - }) - ); - }, [selected]), - { - qsConfig: QS_CONFIG, - allItemsSelected: isAllSelected, - fetchItems: fetchTemplates, - } - ); - - const handleTemplateDelete = async () => { - await deleteTemplates(); - setSelected([]); - }; - - const handleSelectAll = isSelected => { - setSelected(isSelected ? [...templates] : []); - }; - - const handleSelect = template => { - if (selected.some(s => s.id === template.id)) { - setSelected(selected.filter(s => s.id !== template.id)); - } else { - setSelected(selected.concat(template)); - } - }; - - const canAddJT = - jtActions && Object.prototype.hasOwnProperty.call(jtActions, 'POST'); - const canAddWFJT = - wfjtActions && Object.prototype.hasOwnProperty.call(wfjtActions, 'POST'); - - const addTemplate = i18n._(t`Add job template`); - const addWFTemplate = i18n._(t`Add workflow template`); - const addButton = ( - - {addTemplate} - , - - {addWFTemplate} - , - ]} - /> - ); - - return ( - - - ( - , - ]} - /> - )} - renderItem={template => ( - handleSelect(template)} - isSelected={selected.some(row => row.id === template.id)} - fetchTemplates={fetchTemplates} - /> - )} - emptyStateControls={(canAddJT || canAddWFJT) && addButton} - /> - - - {i18n._(t`Failed to delete one or more templates.`)} - - - - ); -} - -export default withI18n()(DashboardTemplateList); diff --git a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.test.jsx b/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.test.jsx deleted file mode 100644 index 5b33d3d717..0000000000 --- a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateList.test.jsx +++ /dev/null @@ -1,336 +0,0 @@ -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { - JobTemplatesAPI, - UnifiedJobTemplatesAPI, - WorkflowJobTemplatesAPI, -} from '../../../api'; -import { - mountWithContexts, - waitForElement, -} from '../../../../testUtils/enzymeHelpers'; - -import DashboardTemplateList from './DashboardTemplateList'; - -jest.mock('../../../api'); - -const mockTemplates = [ - { - id: 1, - name: 'Job Template 1', - url: '/templates/job_template/1', - type: 'job_template', - summary_fields: { - user_capabilities: { - delete: true, - edit: true, - copy: true, - }, - }, - }, - { - id: 2, - name: 'Job Template 2', - url: '/templates/job_template/2', - type: 'job_template', - summary_fields: { - user_capabilities: { - delete: true, - }, - }, - }, - { - id: 3, - name: 'Job Template 3', - url: '/templates/job_template/3', - type: 'job_template', - summary_fields: { - user_capabilities: { - delete: true, - }, - }, - }, - { - id: 4, - name: 'Workflow Job Template 1', - url: '/templates/workflow_job_template/4', - type: 'workflow_job_template', - summary_fields: { - user_capabilities: { - delete: true, - }, - }, - }, - { - id: 5, - name: 'Workflow Job Template 2', - url: '/templates/workflow_job_template/5', - type: 'workflow_job_template', - summary_fields: { - user_capabilities: { - delete: false, - }, - }, - }, -]; - -describe('', () => { - let debug; - beforeEach(() => { - UnifiedJobTemplatesAPI.read.mockResolvedValue({ - data: { - count: mockTemplates.length, - results: mockTemplates, - }, - }); - - UnifiedJobTemplatesAPI.readOptions.mockResolvedValue({ - data: { - actions: [], - }, - }); - debug = global.console.debug; // eslint-disable-line prefer-destructuring - global.console.debug = () => {}; - }); - - afterEach(() => { - jest.clearAllMocks(); - global.console.debug = debug; - }); - - test('initially renders successfully', async () => { - await act(async () => { - mountWithContexts( - - ); - }); - }); - - test('Templates are retrieved from the api and the components finishes loading', async () => { - let wrapper; - await act(async () => { - wrapper = mountWithContexts(); - }); - expect(UnifiedJobTemplatesAPI.read).toBeCalled(); - await act(async () => { - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - expect(wrapper.find('DashboardTemplateListItem').length).toEqual(5); - }); - - test('handleSelect is called when a template list item is selected', async () => { - const wrapper = mountWithContexts(); - await act(async () => { - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - const checkBox = wrapper - .find('DashboardTemplateListItem') - .at(1) - .find('input'); - - checkBox.simulate('change', { - target: { - id: 2, - name: 'Job Template 2', - url: '/templates/job_template/2', - type: 'job_template', - summary_fields: { user_capabilities: { delete: true } }, - }, - }); - - expect( - wrapper - .find('DashboardTemplateListItem') - .at(1) - .prop('isSelected') - ).toBe(true); - }); - - test('handleSelectAll is called when a template list item is selected', async () => { - const wrapper = mountWithContexts(); - await act(async () => { - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - expect(wrapper.find('Checkbox#select-all').prop('isChecked')).toBe(false); - - const toolBarCheckBox = wrapper.find('Checkbox#select-all'); - act(() => { - toolBarCheckBox.prop('onChange')(true); - }); - wrapper.update(); - expect(wrapper.find('Checkbox#select-all').prop('isChecked')).toBe(true); - }); - - test('delete button is disabled if user does not have delete capabilities on a selected template', async () => { - const wrapper = mountWithContexts(); - await act(async () => { - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - const deleteableItem = wrapper - .find('DashboardTemplateListItem') - .at(0) - .find('input'); - const nonDeleteableItem = wrapper - .find('DashboardTemplateListItem') - .at(4) - .find('input'); - - deleteableItem.simulate('change', { - id: 1, - name: 'Job Template 1', - url: '/templates/job_template/1', - type: 'job_template', - summary_fields: { - user_capabilities: { - delete: true, - }, - }, - }); - - expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( - false - ); - deleteableItem.simulate('change', { - id: 1, - name: 'Job Template 1', - url: '/templates/job_template/1', - type: 'job_template', - summary_fields: { - user_capabilities: { - delete: true, - }, - }, - }); - expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( - true - ); - nonDeleteableItem.simulate('change', { - id: 5, - name: 'Workflow Job Template 2', - url: '/templates/workflow_job_template/5', - type: 'workflow_job_template', - summary_fields: { - user_capabilities: { - delete: false, - }, - }, - }); - expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( - true - ); - }); - - test('api is called to delete templates for each selected template.', async () => { - const wrapper = mountWithContexts(); - await act(async () => { - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - const jobTemplate = wrapper - .find('DashboardTemplateListItem') - .at(1) - .find('input'); - const workflowJobTemplate = wrapper - .find('DashboardTemplateListItem') - .at(3) - .find('input'); - - jobTemplate.simulate('change', { - target: { - id: 2, - name: 'Job Template 2', - url: '/templates/job_template/2', - type: 'job_template', - summary_fields: { user_capabilities: { delete: true } }, - }, - }); - - workflowJobTemplate.simulate('change', { - target: { - id: 4, - name: 'Workflow Job Template 1', - url: '/templates/workflow_job_template/4', - type: 'workflow_job_template', - summary_fields: { - user_capabilities: { - delete: true, - }, - }, - }, - }); - - await act(async () => { - wrapper.find('button[aria-label="Delete"]').prop('onClick')(); - }); - wrapper.update(); - await act(async () => { - await wrapper - .find('button[aria-label="confirm delete"]') - .prop('onClick')(); - }); - expect(JobTemplatesAPI.destroy).toBeCalledWith(2); - expect(WorkflowJobTemplatesAPI.destroy).toBeCalledWith(4); - }); - - test('error is shown when template not successfully deleted from api', async () => { - JobTemplatesAPI.destroy.mockRejectedValue( - new Error({ - response: { - config: { - method: 'delete', - url: '/api/v2/job_templates/1', - }, - data: 'An error occurred', - }, - }) - ); - const wrapper = mountWithContexts(); - await act(async () => { - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - const checkBox = wrapper - .find('DashboardTemplateListItem') - .at(1) - .find('input'); - - checkBox.simulate('change', { - target: { - id: 'a', - name: 'Job Template 2', - url: '/templates/job_template/2', - type: 'job_template', - summary_fields: { user_capabilities: { delete: true } }, - }, - }); - await act(async () => { - wrapper.find('button[aria-label="Delete"]').prop('onClick')(); - }); - wrapper.update(); - await act(async () => { - await wrapper - .find('button[aria-label="confirm delete"]') - .prop('onClick')(); - }); - - await waitForElement( - wrapper, - 'Modal[aria-label="Deletion Error"]', - el => el.props().isOpen === true && el.props().title === 'Error!' - ); - }); - test('should properly copy template', async () => { - JobTemplatesAPI.copy.mockResolvedValue({}); - const wrapper = mountWithContexts(); - await act(async () => { - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - await act(async () => - wrapper.find('Button[aria-label="Copy"]').prop('onClick')() - ); - expect(JobTemplatesAPI.copy).toHaveBeenCalled(); - expect(UnifiedJobTemplatesAPI.read).toHaveBeenCalled(); - wrapper.update(); - }); -}); diff --git a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.jsx b/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.jsx deleted file mode 100644 index f06cf71aeb..0000000000 --- a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.jsx +++ /dev/null @@ -1,179 +0,0 @@ -import 'styled-components/macro'; -import React, { useState, useCallback } from 'react'; -import { Link } from 'react-router-dom'; -import { - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemRow, - DataListItemCells, - Tooltip, -} from '@patternfly/react-core'; -import { t } from '@lingui/macro'; -import { withI18n } from '@lingui/react'; -import { - ExclamationTriangleIcon, - PencilAltIcon, - ProjectDiagramIcon, - RocketIcon, -} from '@patternfly/react-icons'; -import styled from 'styled-components'; - -import DataListCell from '../../../components/DataListCell'; -import { timeOfDay } from '../../../util/dates'; -import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../../api'; -import LaunchButton from '../../../components/LaunchButton'; -import Sparkline from '../../../components/Sparkline'; -import { toTitleCase } from '../../../util/strings'; -import CopyButton from '../../../components/CopyButton'; - -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: repeat(4, 40px); -`; - -function DashboardTemplateListItem({ - i18n, - template, - isSelected, - onSelect, - detailUrl, - fetchTemplates, -}) { - const [isDisabled, setIsDisabled] = useState(false); - const labelId = `check-action-${template.id}`; - - const copyTemplate = useCallback(async () => { - if (template.type === 'job_template') { - await JobTemplatesAPI.copy(template.id, { - name: `${template.name} @ ${timeOfDay()}`, - }); - } else { - await WorkflowJobTemplatesAPI.copy(template.id, { - name: `${template.name} @ ${timeOfDay()}`, - }); - } - await fetchTemplates(); - }, [fetchTemplates, template.id, template.name, template.type]); - - const handleCopyStart = useCallback(() => { - setIsDisabled(true); - }, []); - - const handleCopyFinish = useCallback(() => { - setIsDisabled(false); - }, []); - - const missingResourceIcon = - template.type === 'job_template' && - (!template.summary_fields.project || - (!template.summary_fields.inventory && - !template.ask_inventory_on_launch)); - return ( - - - - - - - {template.name} - - - {missingResourceIcon && ( - - - - - - )} - , - - {toTitleCase(template.type)} - , - - - , - ]} - /> - - {template.type === 'workflow_job_template' && ( - - - - )} - {template.summary_fields.user_capabilities.start && ( - - - {({ handleLaunch }) => ( - - )} - - - )} - {template.summary_fields.user_capabilities.edit && ( - - - - )} - {template.summary_fields.user_capabilities.copy && ( - - )} - - - - ); -} - -export { DashboardTemplateListItem as _TemplateListItem }; -export default withI18n()(DashboardTemplateListItem); diff --git a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.test.jsx b/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.test.jsx deleted file mode 100644 index 571ef260c0..0000000000 --- a/awx/ui_next/src/screens/Dashboard/shared/DashboardTemplateListItem.test.jsx +++ /dev/null @@ -1,268 +0,0 @@ -import React from 'react'; -import { createMemoryHistory } from 'history'; -import { act } from 'react-dom/test-utils'; - -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; -import { JobTemplatesAPI } from '../../../api'; - -import mockJobTemplateData from './data.job_template.json'; -import DashboardTemplateListItem from './DashboardTemplateListItem'; - -jest.mock('../../../api'); - -describe('', () => { - test('launch button shown to users with start capabilities', () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('LaunchButton').exists()).toBeTruthy(); - }); - test('launch button hidden from users without start capabilities', () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('LaunchButton').exists()).toBeFalsy(); - }); - test('edit button shown to users with edit capabilities', () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); - }); - test('edit button hidden from users without edit capabilities', () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); - }); - test('missing resource icon is shown.', () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(true); - }); - test('missing resource icon is not shown when there is a project and an inventory.', () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); - }); - test('missing resource icon is not shown when inventory is prompt_on_launch, and a project', () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); - }); - test('missing resource icon is not shown type is workflow_job_template', () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false); - }); - test('clicking on template from templates list navigates properly', () => { - const history = createMemoryHistory({ - initialEntries: ['/templates'], - }); - const wrapper = mountWithContexts( - , - { context: { router: { history } } } - ); - wrapper.find('Link').simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual( - '/templates/job_template/1/details' - ); - }); - test('should call api to copy template', async () => { - JobTemplatesAPI.copy.mockResolvedValue(); - - const wrapper = mountWithContexts( - - ); - await act(async () => - wrapper.find('Button[aria-label="Copy"]').prop('onClick')() - ); - expect(JobTemplatesAPI.copy).toHaveBeenCalled(); - jest.clearAllMocks(); - }); - - test('should render proper alert modal on copy error', async () => { - JobTemplatesAPI.copy.mockRejectedValue(new Error()); - - const wrapper = mountWithContexts( - - ); - await act(async () => - wrapper.find('Button[aria-label="Copy"]').prop('onClick')() - ); - wrapper.update(); - expect(wrapper.find('Modal').prop('isOpen')).toBe(true); - jest.clearAllMocks(); - }); - - test('should not render copy button', async () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('CopyButton').length).toBe(0); - }); - - test('should render visualizer button for workflow', async () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('ProjectDiagramIcon').length).toBe(1); - }); - - test('should not render visualizer button for job template', async () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.find('ProjectDiagramIcon').length).toBe(0); - }); -}); diff --git a/awx/ui_next/src/screens/Host/Host.jsx b/awx/ui_next/src/screens/Host/Host.jsx index 33832e71ee..18a7f80edd 100644 --- a/awx/ui_next/src/screens/Host/Host.jsx +++ b/awx/ui_next/src/screens/Host/Host.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { @@ -20,29 +20,22 @@ import HostDetail from './HostDetail'; import HostEdit from './HostEdit'; import HostGroups from './HostGroups'; import { HostsAPI } from '../../api'; +import useRequest from '../../util/useRequest'; function Host({ i18n, setBreadcrumb }) { - const [host, setHost] = useState(null); - const [contentError, setContentError] = useState(null); - const [hasContentLoading, setHasContentLoading] = useState(true); - const location = useLocation(); const match = useRouteMatch('/hosts/:id'); + const { error, isLoading, result: host, request: fetchHost } = useRequest( + useCallback(async () => { + const { data } = await HostsAPI.readDetail(match.params.id); + setBreadcrumb(data); + return data; + }, [match.params.id, setBreadcrumb]) + ); useEffect(() => { - (async () => { - setContentError(null); - try { - const { data } = await HostsAPI.readDetail(match.params.id); - setHost(data); - setBreadcrumb(data); - } catch (error) { - setContentError(error); - } finally { - setHasContentLoading(false); - } - })(); - }, [match.params.id, location, setBreadcrumb]); + fetchHost(); + }, [fetchHost, location]); const tabsArray = [ { @@ -77,7 +70,7 @@ function Host({ i18n, setBreadcrumb }) { }, ]; - if (hasContentLoading) { + if (isLoading) { return ( @@ -87,12 +80,12 @@ function Host({ i18n, setBreadcrumb }) { ); } - if (contentError) { + if (error) { return ( - - {contentError?.response?.status === 404 && ( + + {error?.response?.status === 404 && ( {i18n._(t`Host not found.`)}{' '} {i18n._(t`View all Hosts.`)} diff --git a/awx/ui_next/src/screens/Host/Host.test.jsx b/awx/ui_next/src/screens/Host/Host.test.jsx index 26862760e9..a875d8777e 100644 --- a/awx/ui_next/src/screens/Host/Host.test.jsx +++ b/awx/ui_next/src/screens/Host/Host.test.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Route } from 'react-router-dom'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { HostsAPI } from '../../api'; @@ -28,7 +29,11 @@ describe('', () => { beforeEach(async () => { await act(async () => { - wrapper = mountWithContexts( {}} />); + wrapper = mountWithContexts( + + {}} /> + + ); }); }); diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupItem.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupItem.jsx index 7df50c323f..1e235ee4c2 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupItem.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupItem.jsx @@ -42,7 +42,7 @@ function HostGroupItem({ i18n, group, inventoryId, isSelected, onSelect }) { ]} /> diff --git a/awx/ui_next/src/screens/Host/HostList/HostList.jsx b/awx/ui_next/src/screens/Host/HostList/HostList.jsx index 1f5e0833bf..0fabce1b60 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostList.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostList.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { useLocation, useRouteMatch } from 'react-router-dom'; +import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Card, PageSection } from '@patternfly/react-core'; @@ -13,9 +13,14 @@ import PaginatedDataList, { ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; import useRequest, { useDeleteItems } from '../../../util/useRequest'; -import { getQSConfig, parseQueryString } from '../../../util/qs'; +import { + encodeQueryString, + getQSConfig, + parseQueryString, +} from '../../../util/qs'; import HostListItem from './HostListItem'; +import SmartInventoryButton from './SmartInventoryButton'; const QS_CONFIG = getQSConfig('host', { page: 1, @@ -24,9 +29,21 @@ const QS_CONFIG = getQSConfig('host', { }); function HostList({ i18n }) { + const history = useHistory(); const location = useLocation(); const match = useRouteMatch(); const [selected, setSelected] = useState([]); + const parsedQueryStrings = parseQueryString(QS_CONFIG, location.search); + const nonDefaultSearchParams = {}; + + Object.keys(parsedQueryStrings).forEach(key => { + if (!QS_CONFIG.defaultParams[key]) { + nonDefaultSearchParams[key] = parsedQueryStrings[key]; + } + }); + + const hasNonDefaultSearchParams = + Object.keys(nonDefaultSearchParams).length > 0; const { result: { hosts, count, actions, relatedSearchableKeys, searchableKeys }, @@ -99,6 +116,14 @@ function HostList({ i18n }) { } }; + const handleSmartInventoryClick = () => { + history.push( + `/inventories/smart_inventory/add?host_filter=${encodeURIComponent( + encodeQueryString(nonDefaultSearchParams) + )}` + ); + }; + const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); @@ -157,6 +182,14 @@ function HostList({ i18n }) { itemsToDelete={selected} pluralizedItemName={i18n._(t`Hosts`)} />, + ...(canAdd + ? [ + handleSmartInventoryClick()} + />, + ] + : []), ]} /> )} diff --git a/awx/ui_next/src/screens/Host/HostList/HostList.test.jsx b/awx/ui_next/src/screens/Host/HostList/HostList.test.jsx index b65c98b67b..5b502979e1 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostList.test.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostList.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; import { HostsAPI } from '../../../api'; import { mountWithContexts, @@ -257,7 +258,7 @@ describe('', () => { expect(modal.prop('title')).toEqual('Error!'); }); - test('should show Add button according to permissions', async () => { + test('should show Add and Smart Inventory buttons according to permissions', async () => { let wrapper; await act(async () => { wrapper = mountWithContexts(); @@ -265,9 +266,10 @@ describe('', () => { await waitForLoaded(wrapper); expect(wrapper.find('ToolbarAddButton').length).toBe(1); + expect(wrapper.find('Button[aria-label="Smart Inventory"]').length).toBe(1); }); - test('should hide Add button according to permissions', async () => { + test('should hide Add and Smart Inventory buttons according to permissions', async () => { HostsAPI.readOptions.mockResolvedValue({ data: { actions: { @@ -282,5 +284,44 @@ describe('', () => { await waitForLoaded(wrapper); expect(wrapper.find('ToolbarAddButton').length).toBe(0); + expect(wrapper.find('Button[aria-label="Smart Inventory"]').length).toBe(0); + }); + + test('Smart Inventory button should be disabled when no search params are present', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts(); + }); + await waitForLoaded(wrapper); + expect( + wrapper.find('Button[aria-label="Smart Inventory"]').props().isDisabled + ).toBe(true); + }); + + test('Clicking Smart Inventory button should navigate to smart inventory form with correct query param', async () => { + let wrapper; + const history = createMemoryHistory({ + initialEntries: ['/hosts?host.name__icontains=foo'], + }); + await act(async () => { + wrapper = mountWithContexts(, { + context: { router: { history } }, + }); + }); + + await waitForLoaded(wrapper); + expect( + wrapper.find('Button[aria-label="Smart Inventory"]').props().isDisabled + ).toBe(false); + await act(async () => { + wrapper.find('Button[aria-label="Smart Inventory"]').simulate('click'); + }); + wrapper.update(); + expect(history.location.pathname).toEqual( + '/inventories/smart_inventory/add' + ); + expect(history.location.search).toEqual( + '?host_filter=name__icontains%3Dfoo' + ); }); }); diff --git a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx index 377fb453ab..d920568f96 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx @@ -64,7 +64,7 @@ function HostListItem({ i18n, host, isSelected, onSelect, detailUrl }) { ]} /> diff --git a/awx/ui_next/src/screens/Host/HostList/SmartInventoryButton.jsx b/awx/ui_next/src/screens/Host/HostList/SmartInventoryButton.jsx new file mode 100644 index 0000000000..9e90bbf531 --- /dev/null +++ b/awx/ui_next/src/screens/Host/HostList/SmartInventoryButton.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { func } from 'prop-types'; +import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { useKebabifiedMenu } from '../../../contexts/Kebabified'; + +function SmartInventoryButton({ onClick, i18n, isDisabled }) { + const { isKebabified } = useKebabifiedMenu(); + + if (isKebabified) { + return ( + + {i18n._(t`Smart Inventory`)} + + ); + } + + return ( + +
+ +
+
+ ); +} +SmartInventoryButton.propTypes = { + onClick: func.isRequired, +}; + +export default withI18n()(SmartInventoryButton); diff --git a/awx/ui_next/src/screens/Host/HostList/SmartInventoryButton.test.jsx b/awx/ui_next/src/screens/Host/HostList/SmartInventoryButton.test.jsx new file mode 100644 index 0000000000..b8f47725f5 --- /dev/null +++ b/awx/ui_next/src/screens/Host/HostList/SmartInventoryButton.test.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import SmartInventoryButton from './SmartInventoryButton'; + +describe('', () => { + test('should render button', () => { + const onClick = jest.fn(); + const wrapper = mountWithContexts( + + ); + const button = wrapper.find('button'); + expect(button).toHaveLength(1); + button.simulate('click'); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/awx/ui_next/src/screens/Host/Hosts.jsx b/awx/ui_next/src/screens/Host/Hosts.jsx index f66a28aa94..4c128bc202 100644 --- a/awx/ui_next/src/screens/Host/Hosts.jsx +++ b/awx/ui_next/src/screens/Host/Hosts.jsx @@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Config } from '../../contexts/Config'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import HostList from './HostList'; import HostAdd from './HostAdd'; @@ -37,7 +37,7 @@ function Hosts({ i18n }) { return ( <> - + diff --git a/awx/ui_next/src/screens/Host/Hosts.test.jsx b/awx/ui_next/src/screens/Host/Hosts.test.jsx index ba199f842f..1c0b9821c0 100644 --- a/awx/ui_next/src/screens/Host/Hosts.test.jsx +++ b/awx/ui_next/src/screens/Host/Hosts.test.jsx @@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Hosts from './Hosts'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { test('initially renders succesfully', () => { mountWithContexts(); @@ -27,7 +31,7 @@ describe('', () => { }, }, }); - expect(wrapper.find('BreadcrumbHeading').length).toBe(1); + expect(wrapper.find('Title').length).toBe(1); wrapper.unmount(); }); diff --git a/awx/ui_next/src/screens/Host/data.hostFacts.json b/awx/ui_next/src/screens/Host/data.hostFacts.json index a8427e0003..2507d267e3 100644 --- a/awx/ui_next/src/screens/Host/data.hostFacts.json +++ b/awx/ui_next/src/screens/Host/data.hostFacts.json @@ -83,7 +83,7 @@ "PWD": "/tmp/awx_13_r1ffeqze/project", "HOME": "/var/lib/awx", "LANG": "\"en-us\"", - "PATH": "/venv/ansible/bin:/venv/awx/bin:/venv/awx/bin:/usr/local/n/versions/node/10.15.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PATH": "/var/lib/awx/venv/ansible/bin:/var/lib/awx/venv/awx/bin:/var/lib/awx/venv/awx/bin:/usr/local/n/versions/node/10.15.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "SHLVL": "4", "JOB_ID": "13", "LC_ALL": "en_US.UTF-8", @@ -96,9 +96,9 @@ "SDB_PORT": "7899", "MAKEFLAGS": "w", "MAKELEVEL": "2", - "PYTHONPATH": "/venv/ansible/lib/python3.6/site-packages:/awx_devel/awx/lib:/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", + "PYTHONPATH": "/var/lib/awx/venv/ansible/lib/python3.6/site-packages:/awx_devel/awx/lib:/var/lib/awx/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", "CURRENT_UID": "501", - "VIRTUAL_ENV": "/venv/ansible", + "VIRTUAL_ENV": "/var/lib/awx/venv/ansible", "INVENTORY_ID": "1", "MAX_EVENT_RES": "700000", "PROOT_TMP_DIR": "/tmp", @@ -106,7 +106,7 @@ "SDB_NOTIFY_HOST": "docker.for.mac.host.internal", "AWX_GROUP_QUEUES": "tower", "PROJECT_REVISION": "9e2cd25bfb26ba82f40cf31276e1942bf38b3a30", - "ANSIBLE_VENV_PATH": "/venv/ansible", + "ANSIBLE_VENV_PATH": "/var/lib/awx/venv/ansible", "ANSIBLE_ROLES_PATH": "/tmp/awx_13_r1ffeqze/requirements_roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles", "RUNNER_OMIT_EVENTS": "False", "SUPERVISOR_ENABLED": "1", @@ -119,7 +119,7 @@ "DJANGO_SETTINGS_MODULE": "awx.settings.development", "ANSIBLE_STDOUT_CALLBACK": "awx_display", "SUPERVISOR_PROCESS_NAME": "awx-dispatcher", - "ANSIBLE_CALLBACK_PLUGINS": "/awx_devel/awx/plugins/callback:/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", + "ANSIBLE_CALLBACK_PLUGINS": "/awx_devel/awx/plugins/callback:/var/lib/awx/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", "ANSIBLE_COLLECTIONS_PATHS": "/tmp/awx_13_r1ffeqze/requirements_collections:~/.ansible/collections:/usr/share/ansible/collections", "ANSIBLE_HOST_KEY_CHECKING": "False", "RUNNER_ONLY_FAILED_EVENTS": "False", diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx index d63996dde8..937aa15adb 100644 --- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx @@ -123,7 +123,7 @@ describe('', () => { }); test('called InstanceGroupsAPI.readOptions', async () => { - expect(InstanceGroupsAPI.readOptions).toHaveBeenCalledTimes(1); + expect(InstanceGroupsAPI.readOptions).toHaveBeenCalled(); }); test('handleCancel returns the user to container group detail', async () => { diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx index 5e56d18f73..6b3b43f637 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx @@ -18,7 +18,7 @@ import AddDropDownButton from '../../../components/AddDropDownButton'; import InstanceGroupListItem from './InstanceGroupListItem'; -const QS_CONFIG = getQSConfig('instance_group', { +const QS_CONFIG = getQSConfig('instance-group', { page: 1, page_size: 20, }); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx index 2430affa35..670835c405 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx @@ -173,7 +173,7 @@ function InstanceGroupListItem({ ]} /> diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx index 4fbdd5d9b2..a133bd91f2 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx @@ -9,7 +9,7 @@ import InstanceGroup from './InstanceGroup'; import ContainerGroupAdd from './ContainerGroupAdd'; import ContainerGroup from './ContainerGroup'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; function InstanceGroups({ i18n }) { const [breadcrumbConfig, setBreadcrumbConfig] = useState({ @@ -54,7 +54,10 @@ function InstanceGroups({ i18n }) { ); return ( <> - + diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.test.jsx index 321b6ca71b..db32e7e4eb 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.test.jsx @@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import InstanceGroups from './InstanceGroups'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let pageWrapper; let pageSections; diff --git a/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceListItem.jsx index a26012a738..d6176b73fa 100644 --- a/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceListItem.jsx @@ -113,7 +113,7 @@ function InstanceListItem({ ]} /> diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index cf286c05eb..17ee02b3be 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -1,10 +1,10 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useRef } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Route, Switch } from 'react-router-dom'; import { Config } from '../../contexts/Config'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import { InventoryList } from './InventoryList'; import Inventory from './Inventory'; import SmartInventory from './SmartInventory'; @@ -12,14 +12,34 @@ import InventoryAdd from './InventoryAdd'; import SmartInventoryAdd from './SmartInventoryAdd'; function Inventories({ i18n }) { - const [breadcrumbConfig, setBreadcrumbConfig] = useState({ + const initScreenHeader = useRef({ '/inventories': i18n._(t`Inventories`), '/inventories/inventory/add': i18n._(t`Create new inventory`), '/inventories/smart_inventory/add': i18n._(t`Create new smart inventory`), }); - const buildBreadcrumbConfig = useCallback( - (inventory, nested, schedule) => { + const [breadcrumbConfig, setScreenHeader] = useState( + initScreenHeader.current + ); + + const [inventory, setInventory] = useState(); + const [nestedObject, setNestedGroup] = useState(); + const [schedule, setSchedule] = useState(); + + const setBreadcrumbConfig = useCallback( + (passedInventory, passedNestedObject, passedSchedule) => { + if (passedInventory && passedInventory.name !== inventory?.name) { + setInventory(passedInventory); + } + if ( + passedNestedObject && + passedNestedObject.name !== nestedObject?.name + ) { + setNestedGroup(passedNestedObject); + } + if (passedSchedule && passedSchedule.name !== schedule?.name) { + setSchedule(passedSchedule); + } if (!inventory) { return; } @@ -32,13 +52,8 @@ function Inventories({ i18n }) { const inventoryGroupsPath = `${inventoryPath}/groups`; const inventorySourcesPath = `${inventoryPath}/sources`; - setBreadcrumbConfig({ - '/inventories': i18n._(t`Inventories`), - '/inventories/inventory/add': i18n._(t`Create new inventory`), - '/inventories/smart_inventory/add': i18n._( - t`Create new smart inventory` - ), - + setScreenHeader({ + ...initScreenHeader.current, [inventoryPath]: `${inventory.name}`, [`${inventoryPath}/access`]: i18n._(t`Access`), [`${inventoryPath}/completed_jobs`]: i18n._(t`Completed jobs`), @@ -47,55 +62,74 @@ function Inventories({ i18n }) { [inventoryHostsPath]: i18n._(t`Hosts`), [`${inventoryHostsPath}/add`]: i18n._(t`Create new host`), - [`${inventoryHostsPath}/${nested?.id}`]: `${nested?.name}`, - [`${inventoryHostsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`), - [`${inventoryHostsPath}/${nested?.id}/details`]: i18n._( + [`${inventoryHostsPath}/${nestedObject?.id}`]: `${nestedObject?.name}`, + [`${inventoryHostsPath}/${nestedObject?.id}/edit`]: i18n._( + t`Edit details` + ), + [`${inventoryHostsPath}/${nestedObject?.id}/details`]: i18n._( t`Host details` ), - [`${inventoryHostsPath}/${nested?.id}/completed_jobs`]: i18n._( + [`${inventoryHostsPath}/${nestedObject?.id}/completed_jobs`]: i18n._( t`Completed jobs` ), - [`${inventoryHostsPath}/${nested?.id}/facts`]: i18n._(t`Facts`), - [`${inventoryHostsPath}/${nested?.id}/groups`]: i18n._(t`Groups`), + [`${inventoryHostsPath}/${nestedObject?.id}/facts`]: i18n._(t`Facts`), + [`${inventoryHostsPath}/${nestedObject?.id}/groups`]: i18n._(t`Groups`), [inventoryGroupsPath]: i18n._(t`Groups`), [`${inventoryGroupsPath}/add`]: i18n._(t`Create new group`), - [`${inventoryGroupsPath}/${nested?.id}`]: `${nested?.name}`, - [`${inventoryGroupsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`), - [`${inventoryGroupsPath}/${nested?.id}/details`]: i18n._( + [`${inventoryGroupsPath}/${nestedObject?.id}`]: `${nestedObject?.name}`, + [`${inventoryGroupsPath}/${nestedObject?.id}/edit`]: i18n._( + t`Edit details` + ), + [`${inventoryGroupsPath}/${nestedObject?.id}/details`]: i18n._( t`Group details` ), - [`${inventoryGroupsPath}/${nested?.id}/nested_hosts`]: i18n._(t`Hosts`), - [`${inventoryGroupsPath}/${nested?.id}/nested_hosts/add`]: i18n._( + [`${inventoryGroupsPath}/${nestedObject?.id}/nested_hosts`]: i18n._( + t`Hosts` + ), + [`${inventoryGroupsPath}/${nestedObject?.id}/nested_hosts/add`]: i18n._( t`Create new host` ), - [`${inventoryGroupsPath}/${nested?.id}/nested_groups`]: i18n._( - t`Groups` + [`${inventoryGroupsPath}/${nestedObject?.id}/nested_groups`]: i18n._( + t`Related Groups` ), - [`${inventoryGroupsPath}/${nested?.id}/nested_groups/add`]: i18n._( + [`${inventoryGroupsPath}/${nestedObject?.id}/nested_groups/add`]: i18n._( t`Create new group` ), [`${inventorySourcesPath}`]: i18n._(t`Sources`), [`${inventorySourcesPath}/add`]: i18n._(t`Create new source`), - [`${inventorySourcesPath}/${nested?.id}`]: `${nested?.name}`, - [`${inventorySourcesPath}/${nested?.id}/details`]: i18n._(t`Details`), - [`${inventorySourcesPath}/${nested?.id}/edit`]: i18n._(t`Edit details`), - [`${inventorySourcesPath}/${nested?.id}/schedules`]: i18n._( + [`${inventorySourcesPath}/${nestedObject?.id}`]: `${nestedObject?.name}`, + [`${inventorySourcesPath}/${nestedObject?.id}/details`]: i18n._( + t`Details` + ), + [`${inventorySourcesPath}/${nestedObject?.id}/edit`]: i18n._( + t`Edit details` + ), + [`${inventorySourcesPath}/${nestedObject?.id}/schedules`]: i18n._( t`Schedules` ), - [`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}`]: `${schedule?.name}`, - [`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}/details`]: i18n._( + [`${inventorySourcesPath}/${nestedObject?.id}/schedules/${schedule?.id}`]: `${schedule?.name}`, + [`${inventorySourcesPath}/${nestedObject?.id}/schedules/add`]: i18n._( + t`Create New Schedule` + ), + [`${inventorySourcesPath}/${nestedObject?.id}/schedules/${schedule?.id}/details`]: i18n._( t`Schedule details` ), + [`${inventorySourcesPath}/${nestedObject?.id}/notifications`]: i18n._( + t`Notifcations` + ), }); }, - [i18n] + [i18n, inventory, nestedObject, schedule] ); return ( <> - + @@ -106,12 +140,12 @@ function Inventories({ i18n }) { {({ me }) => ( - + )} - + diff --git a/awx/ui_next/src/screens/Inventory/Inventories.test.jsx b/awx/ui_next/src/screens/Inventory/Inventories.test.jsx index ca452f877a..35e1ee5da0 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.test.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.test.jsx @@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Inventories from './Inventories'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let pageWrapper; diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx index fc17602df1..09dfe7ed54 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx @@ -80,19 +80,21 @@ function InventoryDetail({ inventory, i18n }) { } /> - - {instanceGroups.map(ig => ( - - {ig.name} - - ))} - - } - /> + {instanceGroups && instanceGroups.length > 0 && ( + + {instanceGroups.map(ig => ( + + {ig.name} + + ))} + + } + /> + )} ', () => { test('should render details', async () => { + InventoriesAPI.readInstanceGroups.mockResolvedValue({ + data: { + results: associatedInstanceGroups, + }, + }); + let wrapper; await act(async () => { wrapper = mountWithContexts( @@ -105,6 +106,12 @@ describe('', () => { }); test('should load instance groups', async () => { + InventoriesAPI.readInstanceGroups.mockResolvedValue({ + data: { + results: associatedInstanceGroups, + }, + }); + let wrapper; await act(async () => { wrapper = mountWithContexts( @@ -119,4 +126,24 @@ describe('', () => { expect(chip.prop('isReadOnly')).toEqual(true); expect(chip.prop('children')).toEqual('Foo'); }); + + test('should not load instance groups', async () => { + InventoriesAPI.readInstanceGroups.mockResolvedValue({ + data: { + results: [], + }, + }); + + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledWith( + mockInventory.id + ); + expect(wrapper.find(`Detail[label="Instance Groups"]`)).toHaveLength(0); + }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx index 39c1b4dfbf..f8ce4879c8 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -17,7 +17,7 @@ import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal'; function InventoryGroupDetail({ i18n, inventoryGroup }) { const { - summary_fields: { created_by, modified_by }, + summary_fields: { created_by, modified_by, user_capabilities }, created, modified, name, @@ -54,24 +54,28 @@ function InventoryGroupDetail({ i18n, inventoryGroup }) { /> - - - history.push(`/inventories/inventory/${params.id}/groups`) - } - /> + {user_capabilities?.edit && ( + + )} + {user_capabilities?.delete && ( + + history.push(`/inventories/inventory/${params.id}/groups`) + } + /> + )} {error && ( ', () => { let wrapper; let history; - beforeEach(async () => { - await act(async () => { - history = createMemoryHistory({ - initialEntries: ['/inventories/inventory/1/groups/1/details'], - }); - wrapper = mountWithContexts( - - - , - { - context: { - router: { - history, - route: { - location: history.location, - match: { params: { id: 1 } }, + + describe('User has full permissions', () => { + beforeEach(async () => { + await act(async () => { + history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/1/details'], + }); + wrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 1 } }, + }, }, }, - }, - } + } + ); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + }); + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + test('InventoryGroupDetail renders successfully', () => { + expect(wrapper.length).toBe(1); + }); + + test('should open delete modal and then call api to delete the group', async () => { + await act(async () => { + wrapper.find('button[aria-label="Delete"]').simulate('click'); + }); + await waitForElement(wrapper, 'Modal', el => el.length === 1); + expect(wrapper.find('Modal').length).toBe(1); + await act(async () => { + wrapper.find('Radio[id="radio-delete"]').invoke('onChange')(); + }); + wrapper.update(); + expect( + wrapper.find('Button[aria-label="Confirm Delete"]').prop('isDisabled') + ).toBe(false); + await act(() => + wrapper.find('Button[aria-label="Confirm Delete"]').prop('onClick')() ); - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(GroupsAPI.destroy).toBeCalledWith(1); + }); + + test('should navigate user to edit form on edit button click', async () => { + wrapper.find('button[aria-label="Edit"]').simulate('click'); + expect(history.location.pathname).toEqual( + '/inventories/inventory/1/groups/1/edit' + ); + }); + + test('details should render with the proper values and action buttons shown', () => { + expect(wrapper.find('Detail[label="Name"]').prop('value')).toBe('Foo'); + expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe( + 'Bar' + ); + expect(wrapper.find('Detail[label="Created"]').length).toBe(1); + expect(wrapper.find('Detail[label="Last Modified"]').length).toBe(1); + expect(wrapper.find('VariablesDetail').prop('value')).toBe('bizz: buzz'); + + expect(wrapper.find('button[aria-label="Edit"]').length).toBe(1); + expect(wrapper.find('button[aria-label="Delete"]').length).toBe(1); }); }); - afterEach(() => { - wrapper.unmount(); - jest.clearAllMocks(); - }); - test('InventoryGroupDetail renders successfully', () => { - expect(wrapper.length).toBe(1); - }); - test('should open delete modal and then call api to delete the group', async () => { - await act(async () => { - wrapper.find('button[aria-label="Delete"]').simulate('click'); + describe('User has read-only permissions', () => { + test('should hide edit/delete buttons', async () => { + const readOnlyGroup = { + ...inventoryGroup, + summary_fields: { + ...inventoryGroup.summary_fields, + user_capabilities: { + delete: false, + edit: false, + }, + }, + }; + + await act(async () => { + history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/1/details'], + }); + wrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 1 } }, + }, + }, + }, + } + ); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + expect(wrapper.find('button[aria-label="Edit"]').length).toBe(0); + expect(wrapper.find('button[aria-label="Delete"]').length).toBe(0); + + wrapper.unmount(); }); - await waitForElement(wrapper, 'Modal', el => el.length === 1); - expect(wrapper.find('Modal').length).toBe(1); - await act(async () => { - wrapper.find('Radio[id="radio-delete"]').invoke('onChange')(); - }); - wrapper.update(); - expect( - wrapper.find('Button[aria-label="Confirm Delete"]').prop('isDisabled') - ).toBe(false); - await act(() => - wrapper.find('Button[aria-label="Confirm Delete"]').prop('onClick')() - ); - expect(GroupsAPI.destroy).toBeCalledWith(1); - }); - - test('should navigate user to edit form on edit button click', async () => { - wrapper.find('button[aria-label="Edit"]').simulate('click'); - expect(history.location.pathname).toEqual( - '/inventories/inventory/1/groups/1/edit' - ); - }); - - test('details should render with the proper values', () => { - expect(wrapper.find('Detail[label="Name"]').prop('value')).toBe('Foo'); - expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe( - 'Bar' - ); - expect(wrapper.find('Detail[label="Created"]').length).toBe(1); - expect(wrapper.find('Detail[label="Last Modified"]').length).toBe(1); - expect(wrapper.find('VariablesDetail').prop('value')).toBe('bizz: buzz'); }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.jsx index 98b5e5fd17..6557db5c3e 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostListItem.jsx @@ -66,7 +66,7 @@ function InventoryGroupHostListItem({ ]} /> diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx index ae52ca6f4d..d2d26dce79 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupItem.jsx @@ -48,7 +48,7 @@ function InventoryGroupItem({ ]} /> diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupItem.jsx index 1a8f14216a..d4ed2b4166 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupItem.jsx @@ -48,7 +48,7 @@ function InventoryHostGroupItem({ ]} /> diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostItem.jsx index 61d2c493cb..2fa80ede52 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostItem.jsx @@ -59,7 +59,7 @@ function InventoryHostItem(props) { ]} /> diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx index 6bb721a427..e152e7cde8 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx @@ -8,9 +8,11 @@ import useRequest, { useDeleteItems } from '../../../util/useRequest'; import AlertModal from '../../../components/AlertModal'; import DatalistToolbar from '../../../components/DataListToolbar'; import ErrorDetail from '../../../components/ErrorDetail'; -import PaginatedDataList, { - ToolbarDeleteButton, -} from '../../../components/PaginatedDataList'; +import { ToolbarDeleteButton } from '../../../components/PaginatedDataList'; +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import useWsInventories from './useWsInventories'; import AddDropDownButton from '../../../components/AddDropDownButton'; @@ -149,17 +151,17 @@ function InventoryList({ i18n }) { ]} /> ); + return ( - + {i18n._(t`Name`)} + {i18n._(t`Status`)} + {i18n._(t`Type`)} + {i18n._(t`Organization`)} + {i18n._(t`Actions`)} + + } renderToolbar={props => ( )} - renderItem={inventory => ( + renderRow={(inventory, index) => ( - - - - - , - - {inventory.pending_deletion ? ( - {inventory.name} - ) : ( - - {inventory.name} - - )} - , - - {inventory.kind === 'smart' - ? i18n._(t`Smart Inventory`) - : i18n._(t`Inventory`)} - , - - {i18n._(t`Organization`)} - - {inventory.summary_fields.organization.name} - - , - - - {i18n._(t`Groups`)} - {inventory.total_groups} - - - {i18n._(t`Hosts`)} - {inventory.total_hosts} - - - {i18n._(t`Sources`)} - {inventory.total_inventory_sources} - - , - inventory.pending_deletion && ( - - - - ), - ]} - /> - {!inventory.pending_deletion && ( - - {inventory.summary_fields.user_capabilities.edit ? ( - - - - ) : ( - '' - )} - {inventory.summary_fields.user_capabilities.copy && ( - - )} - + + + + {inventory.pending_deletion ? ( + {inventory.name} + ) : ( + + {inventory.name} + )} - - + + + {inventory.kind !== 'smart' && } + + + {inventory.kind === 'smart' + ? i18n._(t`Smart Inventory`) + : i18n._(t`Inventory`)} + + + + {inventory?.summary_fields?.organization?.name} + + + {inventory.pending_deletion ? ( + + + + ) : ( + + + + + + + + + )} + ); } export default withI18n()(InventoryListItem); diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.test.jsx index b23878a651..6be246df39 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.test.jsx @@ -9,143 +9,167 @@ jest.mock('../../../api/models/Inventories'); describe('', () => { test('initially renders succesfully', () => { mountWithContexts( - + + {}} - /> + name: 'Inventory', + summary_fields: { + organization: { + id: 1, + name: 'Default', + }, + user_capabilities: { + edit: true, + }, + }, + }} + detailUrl="/inventories/inventory/1" + isSelected + onSelect={() => {}} + /> + + ); }); test('should render prompt list item data', () => { const wrapper = mountWithContexts( - + + {}} - /> + name: 'Inventory', + kind: '', + summary_fields: { + organization: { + id: 1, + name: 'Default', + }, + user_capabilities: { + edit: true, + }, + }, + }} + detailUrl="/inventories/inventory/1" + isSelected + onSelect={() => {}} + /> + + ); + expect(wrapper.find('StatusLabel').length).toBe(1); expect( wrapper - .find('DataListCell') + .find('Td') .at(1) .text() ).toBe('Inventory'); expect( wrapper - .find('DataListCell') + .find('Td') .at(2) .text() + ).toBe('Disabled'); + expect( + wrapper + .find('Td') + .at(3) + .text() ).toBe('Inventory'); expect( wrapper - .find('DataListCell') - .at(3) - .text() - ).toBe('OrganizationDefault'); - expect( - wrapper - .find('DataListCell') + .find('Td') .at(4) .text() - ).toBe('GroupsHostsSources'); + ).toBe('Default'); }); test('edit button shown to users with edit capabilities', () => { const wrapper = mountWithContexts( - + + {}} - /> + name: 'Inventory', + summary_fields: { + organization: { + id: 1, + name: 'Default', + }, + user_capabilities: { + edit: true, + }, + }, + }} + detailUrl="/inventories/inventory/1" + isSelected + onSelect={() => {}} + /> + + ); expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); }); + test('edit button hidden from users without edit capabilities', () => { const wrapper = mountWithContexts( - + + {}} - /> + name: 'Inventory', + summary_fields: { + organization: { + id: 1, + name: 'Default', + }, + user_capabilities: { + edit: false, + }, + }, + }} + detailUrl="/inventories/inventory/1" + isSelected + onSelect={() => {}} + /> + + ); expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); }); + test('should call api to copy inventory', async () => { InventoriesAPI.copy.mockResolvedValue(); const wrapper = mountWithContexts( - + + {}} - /> + name: 'Inventory', + summary_fields: { + organization: { + id: 1, + name: 'Default', + }, + user_capabilities: { + edit: false, + copy: true, + }, + }, + }} + detailUrl="/inventories/inventory/1" + isSelected + onSelect={() => {}} + /> + + ); await act(async () => @@ -159,25 +183,29 @@ describe('', () => { InventoriesAPI.copy.mockRejectedValue(new Error()); const wrapper = mountWithContexts( - + + {}} - /> + name: 'Inventory', + summary_fields: { + organization: { + id: 1, + name: 'Default', + }, + user_capabilities: { + edit: false, + copy: true, + }, + }, + }} + detailUrl="/inventories/inventory/1" + isSelected + onSelect={() => {}} + /> + + ); await act(async () => wrapper.find('Button[aria-label="Copy"]').prop('onClick')() @@ -189,25 +217,29 @@ describe('', () => { test('should not render copy button', async () => { const wrapper = mountWithContexts( - + + {}} - /> + name: 'Inventory', + summary_fields: { + organization: { + id: 1, + name: 'Default', + }, + user_capabilities: { + edit: false, + copy: false, + }, + }, + }} + detailUrl="/inventories/inventory/1" + isSelected + onSelect={() => {}} + /> + + ); expect(wrapper.find('CopyButton').length).toBe(0); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.test.jsx index fa18e5b175..7841d71fa9 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.test.jsx @@ -59,7 +59,7 @@ describe('useWsInventories hook', () => { test('should establish websocket connection', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const inventories = [{ id: 1 }]; await act(async () => { @@ -83,7 +83,7 @@ describe('useWsInventories hook', () => { test('should update inventory sync status', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const inventories = [{ id: 1 }]; await act(async () => { @@ -121,7 +121,7 @@ describe('useWsInventories hook', () => { test('should fetch fresh inventory after sync runs', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const inventories = [{ id: 1 }]; const fetchInventories = jest.fn(() => []); const fetchInventoriesById = jest.fn(() => []); @@ -152,7 +152,7 @@ describe('useWsInventories hook', () => { test('should update inventory pending_deletion', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const inventories = [{ id: 1, pending_deletion: false }]; await act(async () => { @@ -190,7 +190,7 @@ describe('useWsInventories hook', () => { test('should refetch inventories after an inventory is deleted', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const inventories = [{ id: 1 }, { id: 2 }]; const fetchInventories = jest.fn(() => []); const fetchInventoriesById = jest.fn(() => []); diff --git a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.jsx index 67f543c9a7..731e1f8e59 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupListItem.jsx @@ -56,7 +56,7 @@ function InventoryRelatedGroupListItem({ ]} /> diff --git a/awx/ui_next/src/screens/Inventory/InventorySource/InventorySource.jsx b/awx/ui_next/src/screens/Inventory/InventorySource/InventorySource.jsx index 7d5705347d..dec808d20e 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySource/InventorySource.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySource/InventorySource.jsx @@ -62,14 +62,19 @@ function InventorySource({ i18n, inventory, setBreadcrumb, me }) { } }, [inventory, source, setBreadcrumb]); - const loadSchedules = params => - InventorySourcesAPI.readSchedules(source?.id, params); + const loadSchedules = useCallback( + params => { + return InventorySourcesAPI.readSchedules(source?.id, params); + }, + [source] + ); const createSchedule = data => InventorySourcesAPI.createSchedule(source?.id, data); - const loadScheduleOptions = () => - InventorySourcesAPI.readScheduleOptions(source?.id); + const loadScheduleOptions = useCallback(() => { + return InventorySourcesAPI.readScheduleOptions(source?.id); + }, [source]); const tabsArray = [ { diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx index cb26155a05..f3fcdebfca 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx @@ -214,10 +214,14 @@ function InventorySourceDetail({ inventorySource, i18n }) { } /> )} - + {source === 'scm' ? ( + + ) : null} { assertDetail(wrapper, 'Description', 'mock description'); assertDetail(wrapper, 'Source', 'Sourced from a Project'); assertDetail(wrapper, 'Organization', 'Mock Org'); - assertDetail(wrapper, 'Ansible environment', '/venv/custom'); + assertDetail(wrapper, 'Ansible environment', '/var/lib/awx/venv/custom'); assertDetail(wrapper, 'Project', 'Mock Project'); assertDetail(wrapper, 'Inventory file', 'foo'); assertDetail(wrapper, 'Verbosity', '2 (Debug)'); diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx index a85ea121d1..94c21eef73 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx @@ -88,7 +88,7 @@ function InventorySourceListItem({ {source.summary_fields.user_capabilities.start && ( diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.test.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.test.jsx index b0e5668624..dcca078a5e 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.test.jsx @@ -43,7 +43,7 @@ describe('useWsInventorySources hook', () => { test('should establish websocket connection', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const sources = [{ id: 1 }]; await act(async () => { @@ -65,7 +65,7 @@ describe('useWsInventorySources hook', () => { test('should update last job status', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const sources = [ { diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryDetail/SmartInventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryDetail/SmartInventoryDetail.jsx index 3b23b1a2b7..8aee3165e5 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventoryDetail/SmartInventoryDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryDetail/SmartInventoryDetail.jsx @@ -152,7 +152,7 @@ function SmartInventoryDetail({ inventory, i18n }) { {user_capabilities?.edit && ( @@ -136,6 +158,18 @@ function NotificationTemplateListItem({ ) : (
)} + {template.summary_fields.user_capabilities.copy && ( + + )} diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx index 58878d3b96..65f959522c 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx @@ -13,6 +13,7 @@ const template = { summary_fields: { user_capabilities: { edit: true, + copy: true, }, recent_notifications: [ { @@ -63,4 +64,56 @@ describe('', () => { .text() ).toEqual('Running'); }); + + test('should call api to copy inventory', async () => { + NotificationTemplatesAPI.copy.mockResolvedValue(); + + const wrapper = mountWithContexts( + + ); + + await act(async () => + wrapper.find('Button[aria-label="Copy"]').prop('onClick')() + ); + expect(NotificationTemplatesAPI.copy).toHaveBeenCalled(); + jest.clearAllMocks(); + }); + + test('should render proper alert modal on copy error', async () => { + NotificationTemplatesAPI.copy.mockRejectedValue(new Error()); + + const wrapper = mountWithContexts( + + ); + await act(async () => + wrapper.find('Button[aria-label="Copy"]').prop('onClick')() + ); + wrapper.update(); + expect(wrapper.find('Modal').prop('isOpen')).toBe(true); + jest.clearAllMocks(); + }); + + test('should not render copy button', async () => { + const wrapper = mountWithContexts( + + ); + expect(wrapper.find('CopyButton').length).toBe(0); + }); }); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx index 2ae913202f..9d41166d1d 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx @@ -5,7 +5,7 @@ import { t } from '@lingui/macro'; import NotificationTemplateList from './NotificationTemplateList'; import NotificationTemplateAdd from './NotificationTemplateAdd'; import NotificationTemplate from './NotificationTemplate'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; function NotificationTemplates({ i18n }) { const match = useRouteMatch(); @@ -32,7 +32,10 @@ function NotificationTemplates({ i18n }) { return ( <> - + diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx index 9333850cf9..f8b02d4735 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx @@ -2,6 +2,10 @@ import React from 'react'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import NotificationTemplates from './NotificationTemplates'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let pageWrapper; let pageSections; diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx index 044c5adafd..b452ed4b62 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/CustomMessagesSubForm.jsx @@ -1,6 +1,6 @@ import 'styled-components/macro'; import React, { useEffect, useRef } from 'react'; -import { withI18n } from '@lingui/react'; +import { Trans, withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { useField, useFormikContext } from 'formik'; import { Switch, Text } from '@patternfly/react-core'; @@ -69,9 +69,11 @@ function CustomMessagesSubForm({ defaultMessages, type, i18n }) { css="margin-bottom: var(--pf-c-content--MarginBottom)" > - Use custom messages to change the content of notifications sent - when a job starts, succeeds, or fails. Use curly braces to access - information about the job:{' '} + + Use custom messages to change the content of notifications sent + when a job starts, succeeds, or fails. Use curly braces to + access information about the job:{' '} + {'{{'} job_friendly_name {'}}'} @@ -79,12 +81,15 @@ function CustomMessagesSubForm({ defaultMessages, type, i18n }) { {'{{'} url {'}}'} - , or attributes of the job such as{' '} + , or attributes of the job such as{' '} {'{{'} job.status {'}}'} - . You may apply a number of possible variables in the message. - Refer to the{' '} + .{' '} + + You may apply a number of possible variables in the message. + Refer to the{' '} + {' '} Ansible Tower documentation {' '} - for more details. + for more details. diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx index 14a38e8f32..97b1dcc76c 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/TypeInputsSubForm.jsx @@ -487,7 +487,7 @@ function WebhookFields({ i18n }) { validated={ !methodMeta.touched || !methodMeta.error ? 'default' : 'error' } - label={i18n._(t`E-mail options`)} + label={i18n._(t`HTTP Method`)} > ', () => { .find('FormSelectOption') .first() .prop('value') - ).toEqual('/venv/ansible/'); + ).toEqual('/var/lib/awx/venv/ansible/'); }); test('AnsibleSelect component does not render if there are 0 virtual environments', async () => { diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx index 9b4dca7413..03b062a83d 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx @@ -9,10 +9,14 @@ import useRequest, { useDeleteItems } from '../../../util/useRequest'; import AlertModal from '../../../components/AlertModal'; import DataListToolbar from '../../../components/DataListToolbar'; import ErrorDetail from '../../../components/ErrorDetail'; -import PaginatedDataList, { +import { ToolbarAddButton, ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../../components/PaginatedTable'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import OrganizationListItem from './OrganizationListItem'; @@ -117,14 +121,13 @@ function OrganizationsList({ i18n }) { <> - + {i18n._(t`Name`)} + {i18n._(t`Members`)} + {i18n._(t`Teams`)} + {i18n._(t`Actions`)} + + } renderToolbar={props => ( , ]} /> )} - renderItem={o => ( + renderRow={(o, index) => ( row.id === o.id)} onSelect={() => handleSelect(o)} diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.test.jsx index 963b205d62..34e9b93e33 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.test.jsx @@ -103,7 +103,7 @@ describe('', () => { }); test('Item appears selected after selecting it', async () => { - const itemCheckboxInput = 'input#select-organization-1'; + const itemCheckboxInput = 'tr#org-row-1 input[type="checkbox"]'; await act(async () => { wrapper = mountWithContexts(); }); @@ -115,7 +115,6 @@ describe('', () => { await act(async () => { wrapper .find(itemCheckboxInput) - .closest('DataListCheck') .props() .onChange(); }); @@ -128,9 +127,9 @@ describe('', () => { test('All items appear selected after select-all and unselected after unselect-all', async () => { const itemCheckboxInputs = [ - 'input#select-organization-1', - 'input#select-organization-2', - 'input#select-organization-3', + 'tr#org-row-1 input[type="checkbox"]', + 'tr#org-row-2 input[type="checkbox"]', + 'tr#org-row-3 input[type="checkbox"]', ]; await act(async () => { wrapper = mountWithContexts(); @@ -227,7 +226,7 @@ describe('', () => { }); test('Error dialog shown for failed deletion', async () => { - const itemCheckboxInput = 'input#select-organization-1'; + const itemCheckboxInput = 'tr#org-row-1 input[type="checkbox"]'; OrganizationsAPI.destroy.mockRejectedValue( new Error({ response: { @@ -250,7 +249,6 @@ describe('', () => { await act(async () => { wrapper .find(itemCheckboxInput) - .closest('DataListCheck') .props() .onChange(); }); diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx index 37d01a9e0a..dc727dba47 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx @@ -2,105 +2,61 @@ import React from 'react'; import { string, bool, func } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - Badge as PFBadge, - Button, - DataListAction as _DataListAction, - DataListCheck, - DataListItem, - DataListItemCells, - DataListItemRow, - Tooltip, -} from '@patternfly/react-core'; - +import { Button } from '@patternfly/react-core'; +import { Tr, Td } from '@patternfly/react-table'; import { Link } from 'react-router-dom'; -import styled from 'styled-components'; import { PencilAltIcon } from '@patternfly/react-icons'; -import DataListCell from '../../../components/DataListCell'; +import { ActionsTd, ActionItem } from '../../../components/PaginatedTable'; import { Organization } from '../../../types'; -const Badge = styled(PFBadge)` - margin-left: 8px; -`; - -const ListGroup = styled.span` - margin-left: 24px; - - &:first-of-type { - margin-left: 0; - } -`; - -const DataListAction = styled(_DataListAction)` - align-items: center; - display: grid; - grid-gap: 16px; - grid-template-columns: 40px; -`; - function OrganizationListItem({ organization, isSelected, onSelect, + rowIndex, detailUrl, i18n, }) { const labelId = `check-action-${organization.id}`; return ( - - - - - - {organization.name} - - , - - - {i18n._(t`Members`)} - - {organization.summary_fields.related_field_counts.users} - - - - {i18n._(t`Teams`)} - - {organization.summary_fields.related_field_counts.teams} - - - , - ]} - /> - - {organization.summary_fields.user_capabilities.edit ? ( - - - - ) : ( - '' - )} - - - + + + + + {organization.name} + + + + {organization.summary_fields.related_field_counts.users} + + + {organization.summary_fields.related_field_counts.teams} + + + + + + + ); } diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.test.jsx index e439714dfe..1e3bce02e4 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.test.jsx @@ -11,77 +11,91 @@ describe('', () => { mountWithContexts( - {}} - /> + + + {}} + /> + +
); }); + test('edit button shown to users with edit capabilities', () => { const wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
); expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); }); + test('edit button hidden from users without edit capabilities', () => { const wrapper = mountWithContexts( - {}} - /> + + + {}} + /> + +
); diff --git a/awx/ui_next/src/screens/Organization/OrganizationTeams/OrganizationTeamListItem.jsx b/awx/ui_next/src/screens/Organization/OrganizationTeams/OrganizationTeamListItem.jsx index b352e3dda9..7ffec8c2c0 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationTeams/OrganizationTeamListItem.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationTeams/OrganizationTeamListItem.jsx @@ -32,7 +32,7 @@ function OrganizationTeamListItem({ i18n, team, detailUrl }) { ]} /> diff --git a/awx/ui_next/src/screens/Organization/Organizations.jsx b/awx/ui_next/src/screens/Organization/Organizations.jsx index 8ed5f21f1d..6c7b17dc69 100644 --- a/awx/ui_next/src/screens/Organization/Organizations.jsx +++ b/awx/ui_next/src/screens/Organization/Organizations.jsx @@ -1,80 +1,68 @@ -import React, { Component, Fragment } from 'react'; -import { Route, withRouter, Switch } from 'react-router-dom'; +import React, { useCallback, useState, Fragment } from 'react'; +import { Route, withRouter, Switch, useRouteMatch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Config } from '../../contexts/Config'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import OrganizationsList from './OrganizationList/OrganizationList'; import OrganizationAdd from './OrganizationAdd/OrganizationAdd'; import Organization from './Organization'; -class Organizations extends Component { - constructor(props) { - super(props); +function Organizations({ i18n }) { + const match = useRouteMatch(); + const [breadcrumbConfig, setBreadcrumbConfig] = useState({ + '/organizations': i18n._(t`Organizations`), + '/organizations/add': i18n._(t`Create New Organization`), + }); - const { i18n } = props; + const setBreadcrumb = useCallback( + organization => { + if (!organization) { + return; + } - this.state = { - breadcrumbConfig: { + const breadcrumb = { '/organizations': i18n._(t`Organizations`), '/organizations/add': i18n._(t`Create New Organization`), - }, - }; - } + [`/organizations/${organization.id}`]: `${organization.name}`, + [`/organizations/${organization.id}/edit`]: i18n._(t`Edit Details`), + [`/organizations/${organization.id}/details`]: i18n._(t`Details`), + [`/organizations/${organization.id}/access`]: i18n._(t`Access`), + [`/organizations/${organization.id}/teams`]: i18n._(t`Teams`), + [`/organizations/${organization.id}/notifications`]: i18n._( + t`Notifications` + ), + }; + setBreadcrumbConfig(breadcrumb); + }, + [i18n] + ); - setBreadcrumbConfig = organization => { - const { i18n } = this.props; - - if (!organization) { - return; - } - - const breadcrumbConfig = { - '/organizations': i18n._(t`Organizations`), - '/organizations/add': i18n._(t`Create New Organization`), - [`/organizations/${organization.id}`]: `${organization.name}`, - [`/organizations/${organization.id}/edit`]: i18n._(t`Edit Details`), - [`/organizations/${organization.id}/details`]: i18n._(t`Details`), - [`/organizations/${organization.id}/access`]: i18n._(t`Access`), - [`/organizations/${organization.id}/teams`]: i18n._(t`Teams`), - [`/organizations/${organization.id}/notifications`]: i18n._( - t`Notifications` - ), - }; - - this.setState({ breadcrumbConfig }); - }; - - render() { - const { match } = this.props; - const { breadcrumbConfig } = this.state; - - return ( - - - - - - - - - {({ me }) => ( - - )} - - - - - - - - ); - } + return ( + + + + + + + + + {({ me }) => ( + + )} + + + + + + + + ); } export { Organizations as _Organizations }; diff --git a/awx/ui_next/src/screens/Organization/Organizations.test.jsx b/awx/ui_next/src/screens/Organization/Organizations.test.jsx index 4f510463a4..819b86d88d 100644 --- a/awx/ui_next/src/screens/Organization/Organizations.test.jsx +++ b/awx/ui_next/src/screens/Organization/Organizations.test.jsx @@ -5,6 +5,9 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Organizations from './Organizations'; jest.mock('../../api'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); describe('', () => { test('initially renders succesfully', async () => { diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx index c78b178943..094e6ac5b6 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx @@ -31,7 +31,7 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) { const defaultVenv = { label: i18n._(t`Use Default Ansible Environment`), - value: '/venv/ansible/', + value: '/var/lib/awx/venv/ansible/', key: 'default', }; const { custom_virtualenvs } = useContext(ConfigContext); diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx index 004c7d1577..67cf0a60d6 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx @@ -200,7 +200,7 @@ describe('', () => { .find('FormSelectOption') .first() .prop('value') - ).toEqual('/venv/ansible/'); + ).toEqual('/var/lib/awx/venv/ansible/'); }); test('onSubmit associates and disassociates instance groups', async () => { diff --git a/awx/ui_next/src/screens/Project/Project.jsx b/awx/ui_next/src/screens/Project/Project.jsx index ea4c3e2761..72341a5de9 100644 --- a/awx/ui_next/src/screens/Project/Project.jsx +++ b/awx/ui_next/src/screens/Project/Project.jsx @@ -1,11 +1,22 @@ -import React, { Component } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom'; +import { + Switch, + Route, + withRouter, + Redirect, + Link, + useParams, + useLocation, +} from 'react-router-dom'; import { CaretLeftIcon } from '@patternfly/react-icons'; import { Card, PageSection } from '@patternfly/react-core'; +import { useConfig } from '../../contexts/Config'; +import useRequest from '../../util/useRequest'; import RoutedTabs from '../../components/RoutedTabs'; import ContentError from '../../components/ContentError'; +import ContentLoading from '../../components/ContentLoading'; import NotificationList from '../../components/NotificationList'; import { ResourceAccessList } from '../../components/ResourceAccessList'; import { Schedules } from '../../components/Schedule'; @@ -14,48 +25,18 @@ import ProjectEdit from './ProjectEdit'; import ProjectJobTemplatesList from './ProjectJobTemplatesList'; import { OrganizationsAPI, ProjectsAPI } from '../../api'; -class Project extends Component { - constructor(props) { - super(props); +function Project({ i18n, setBreadcrumb }) { + const { me = {} } = useConfig(); + const { id } = useParams(); + const location = useLocation(); - this.state = { - project: null, - hasContentLoading: true, - contentError: null, - isInitialized: false, - isNotifAdmin: false, - }; - this.createSchedule = this.createSchedule.bind(this); - this.loadProject = this.loadProject.bind(this); - this.loadProjectAndRoles = this.loadProjectAndRoles.bind(this); - this.loadSchedules = this.loadSchedules.bind(this); - this.loadScheduleOptions = this.loadScheduleOptions.bind(this); - } - - async componentDidMount() { - await this.loadProjectAndRoles(); - this.setState({ isInitialized: true }); - } - - async componentDidUpdate(prevProps) { - const { location, match } = this.props; - const url = `/projects/${match.params.id}/`; - - if ( - prevProps.location.pathname.startsWith(url) && - prevProps.location !== location && - location.pathname === `${url}details` - ) { - await this.loadProject(); - } - } - - async loadProjectAndRoles() { - const { match, setBreadcrumb } = this.props; - const id = parseInt(match.params.id, 10); - - this.setState({ contentError: null, hasContentLoading: true }); - try { + const { + request: fetchProjectAndRoles, + result: { project, isNotifAdmin }, + isLoading: hasContentLoading, + error: contentError, + } = useRequest( + useCallback(async () => { const [{ data }, notifAdminRes] = await Promise.all([ ProjectsAPI.readDetail(id), OrganizationsAPI.read({ @@ -63,188 +44,171 @@ class Project extends Component { role_level: 'notification_admin_role', }), ]); - setBreadcrumb(data); - this.setState({ + + if (data.summary_fields.credentials) { + const params = { + page: 1, + page_size: 200, + order_by: 'name', + }; + const { + data: { results }, + } = await ProjectsAPI.readCredentials(data.id, params); + + data.summary_fields.credentials = results; + } + return { project: data, isNotifAdmin: notifAdminRes.data.results.length > 0, - }); - } catch (err) { - this.setState({ contentError: err }); - } finally { - this.setState({ hasContentLoading: false }); + }; + }, [id]), + { + project: null, + notifAdminRes: null, } - } + ); - async loadProject() { - const { match, setBreadcrumb } = this.props; - const id = parseInt(match.params.id, 10); + useEffect(() => { + fetchProjectAndRoles(); + }, [fetchProjectAndRoles, location.pathname]); - this.setState({ contentError: null, hasContentLoading: true }); - try { - const { data } = await ProjectsAPI.readDetail(id); - setBreadcrumb(data); - this.setState({ project: data }); - } catch (err) { - this.setState({ contentError: err }); - } finally { - this.setState({ hasContentLoading: false }); + useEffect(() => { + if (project) { + setBreadcrumb(project); } - } + }, [project, setBreadcrumb]); - createSchedule(data) { - const { project } = this.state; + function createSchedule(data) { return ProjectsAPI.createSchedule(project.id, data); } - loadScheduleOptions() { - const { project } = this.state; + const loadScheduleOptions = useCallback(() => { return ProjectsAPI.readScheduleOptions(project.id); - } + }, [project]); - loadSchedules(params) { - const { project } = this.state; - return ProjectsAPI.readSchedules(project.id, params); - } + const loadSchedules = useCallback( + params => { + return ProjectsAPI.readSchedules(project.id, params); + }, + [project] + ); - render() { - const { location, match, me, i18n, setBreadcrumb } = this.props; - - const { - project, - contentError, - hasContentLoading, - isInitialized, - isNotifAdmin, - } = this.state; - const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin; - const canToggleNotifications = isNotifAdmin; - - const tabsArray = [ - { - name: ( - <> - - {i18n._(t`Back to Projects`)} - - ), - link: `/projects`, - id: 99, - }, - { name: i18n._(t`Details`), link: `${match.url}/details` }, - { name: i18n._(t`Access`), link: `${match.url}/access` }, - ]; - - if (canSeeNotificationsTab) { - tabsArray.push({ - name: i18n._(t`Notifications`), - link: `${match.url}/notifications`, - }); - } + const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin; + const canToggleNotifications = isNotifAdmin; + const tabsArray = [ + { + name: ( + <> + + {i18n._(t`Back to Projects`)} + + ), + link: `/projects`, + id: 99, + }, + { name: i18n._(t`Details`), link: `/projects/${id}/details` }, + { name: i18n._(t`Access`), link: `/projects/${id}/access` }, + ]; + if (canSeeNotificationsTab) { tabsArray.push({ - name: i18n._(t`Job Templates`), - link: `${match.url}/job_templates`, + name: i18n._(t`Notifications`), + link: `/projects/${id}/notifications`, }); + } - if (project?.scm_type) { - tabsArray.push({ - name: i18n._(t`Schedules`), - link: `${match.url}/schedules`, - }); - } + tabsArray.push({ + name: i18n._(t`Job Templates`), + link: `/projects/${id}/job_templates`, + }); - tabsArray.forEach((tab, n) => { - tab.id = n; + if (project?.scm_type) { + tabsArray.push({ + name: i18n._(t`Schedules`), + link: `/projects/${id}/schedules`, }); + } - let showCardHeader = true; - - if ( - !isInitialized || - location.pathname.endsWith('edit') || - location.pathname.includes('schedules/') - ) { - showCardHeader = false; - } - - if (!hasContentLoading && contentError) { - return ( - - - - {contentError.response.status === 404 && ( - - {i18n._(t`Project not found.`)}{' '} - {i18n._(t`View all Projects.`)} - - )} - - - - ); - } + tabsArray.forEach((tab, n) => { + tab.id = n; + }); + if (contentError) { return ( - {showCardHeader && } + + {contentError.response.status === 404 && ( + + {i18n._(t`Project not found.`)}{' '} + {i18n._(t`View all Projects.`)} + + )} + + + + ); + } + + const showCardHeader = !( + location.pathname.endsWith('edit') || + location.pathname.includes('schedules/') + ); + + return ( + + + {showCardHeader && } + {hasContentLoading && } + {!hasContentLoading && project && ( - {project && ( - - - - )} - {project && ( - - - - )} - {project && ( - - - - )} + + + + + + + + + {canSeeNotificationsTab && ( )} - + {project?.scm_type && project.scm_type !== '' && ( )} - {!hasContentLoading && ( - - {match.params.id && ( - - {i18n._(t`View Project Details`)} - - )} - - )} + + {id && ( + + {i18n._(t`View Project Details`)} + + )} + - , - - - ); - } + )} +
+
+ ); } export default withI18n()(withRouter(Project)); diff --git a/awx/ui_next/src/screens/Project/Project.test.jsx b/awx/ui_next/src/screens/Project/Project.test.jsx index 7d8080e8a9..100cdabe97 100644 --- a/awx/ui_next/src/screens/Project/Project.test.jsx +++ b/awx/ui_next/src/screens/Project/Project.test.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { OrganizationsAPI, ProjectsAPI } from '../../api'; import { @@ -10,6 +11,14 @@ import mockDetails from './data.project.json'; import Project from './Project'; jest.mock('../../api'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useRouteMatch: () => ({ + pathname: '/projects/1/details', + url: '/projects/1', + }), + useParams: () => ({ id: 1 }), +})); const mockMe = { is_super_user: true, @@ -28,29 +37,34 @@ async function getOrganizations() { } describe('', () => { - test('initially renders succesfully', () => { + let wrapper; + + test('initially renders successfully', async () => { ProjectsAPI.readDetail.mockResolvedValue({ data: mockDetails }); OrganizationsAPI.read.mockImplementation(getOrganizations); - mountWithContexts( {}} me={mockMe} />); + await act(async () => { + mountWithContexts( {}} me={mockMe} />); + }); }); - test('notifications tab shown for admins', async done => { + test('notifications tab shown for admins', async () => { ProjectsAPI.readDetail.mockResolvedValue({ data: mockDetails }); OrganizationsAPI.read.mockImplementation(getOrganizations); - const wrapper = mountWithContexts( - {}} me={mockMe} /> - ); + await act(async () => { + wrapper = mountWithContexts( + {}} me={mockMe} /> + ); + }); const tabs = await waitForElement( wrapper, - '.pf-c-tabs__item', + '.pf-c-tabs__item-text', el => el.length === 6 ); expect(tabs.at(3).text()).toEqual('Notifications'); - done(); }); - test('notifications tab hidden with reduced permissions', async done => { + test('notifications tab hidden with reduced permissions', async () => { ProjectsAPI.readDetail.mockResolvedValue({ data: mockDetails }); OrganizationsAPI.read.mockResolvedValue({ count: 0, @@ -58,20 +72,20 @@ describe('', () => { previous: null, data: { results: [] }, }); - - const wrapper = mountWithContexts( - {}} me={mockMe} /> - ); + await act(async () => { + wrapper = mountWithContexts( + {}} me={mockMe} /> + ); + }); const tabs = await waitForElement( wrapper, - '.pf-c-tabs__item', + '.pf-c-tabs__item-text', el => el.length === 5 ); tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications')); - done(); }); - test('schedules tab shown for scm based projects.', async done => { + test('schedules tab shown for scm based projects.', async () => { ProjectsAPI.readDetail.mockResolvedValue({ data: mockDetails }); OrganizationsAPI.read.mockResolvedValue({ count: 0, @@ -80,19 +94,20 @@ describe('', () => { data: { results: [] }, }); - const wrapper = mountWithContexts( - {}} me={mockMe} /> - ); + await act(async () => { + wrapper = mountWithContexts( + {}} me={mockMe} /> + ); + }); const tabs = await waitForElement( wrapper, '.pf-c-tabs__item', el => el.length === 5 ); expect(tabs.at(4).text()).toEqual('Schedules'); - done(); }); - test('schedules tab hidden for manual projects.', async done => { + test('schedules tab hidden for manual projects.', async () => { const manualDetails = Object.assign(mockDetails, { scm_type: '' }); ProjectsAPI.readDetail.mockResolvedValue({ data: manualDetails }); OrganizationsAPI.read.mockResolvedValue({ @@ -102,40 +117,43 @@ describe('', () => { data: { results: [] }, }); - const wrapper = mountWithContexts( - {}} me={mockMe} /> - ); + await act(async () => { + wrapper = mountWithContexts( + {}} me={mockMe} /> + ); + }); const tabs = await waitForElement( wrapper, '.pf-c-tabs__item', el => el.length === 4 ); tabs.forEach(tab => expect(tab.text()).not.toEqual('Schedules')); - done(); }); test('should show content error when user attempts to navigate to erroneous route', async () => { const history = createMemoryHistory({ initialEntries: ['/projects/1/foobar'], }); - const wrapper = mountWithContexts( - {}} me={mockMe} />, - { - context: { - router: { - history, - route: { - location: history.location, - match: { - params: { id: 1 }, - url: '/projects/1/foobar', - path: '/project/1/foobar', + await act(async () => { + wrapper = mountWithContexts( + {}} me={mockMe} />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { + params: { id: 1 }, + url: '/projects/1/foobar', + path: '/project/1/foobar', + }, }, }, }, - }, - } - ); + } + ); + }); await waitForElement(wrapper, 'ContentError', el => el.length === 1); }); }); diff --git a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx index 5caa143c09..8bc136b889 100644 --- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx @@ -24,7 +24,7 @@ describe('', () => { scm_update_on_launch: true, scm_update_cache_timeout: 3, allow_override: false, - custom_virtualenv: '/venv/custom-env', + custom_virtualenv: '/var/lib/awx/venv/custom-env', }; const projectOptionsResolve = { @@ -35,7 +35,6 @@ describe('', () => { choices: [ ['', 'Manual'], ['git', 'Git'], - ['hg', 'Mercurial'], ['svn', 'Subversion'], ['archive', 'Remote Archive'], ['insights', 'Red Hat Insights'], diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx index 380196d950..4c92c9695e 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx @@ -19,6 +19,7 @@ import CredentialChip from '../../../components/CredentialChip'; import { ProjectsAPI } from '../../../api'; import { toTitleCase } from '../../../util/strings'; import useRequest, { useDismissableError } from '../../../util/useRequest'; +import ProjectSyncButton from '../shared/ProjectSyncButton'; function ProjectDetail({ project, i18n }) { const { @@ -148,27 +149,28 @@ function ProjectDetail({ project, i18n }) { /> - {summary_fields.user_capabilities && - summary_fields.user_capabilities.edit && ( - - )} - {summary_fields.user_capabilities && - summary_fields.user_capabilities.delete && ( - - {i18n._(t`Delete`)} - - )} + {summary_fields.user_capabilities?.edit && ( + + )} + {summary_fields.user_capabilities?.start && ( + + )} + {summary_fields.user_capabilities?.delete && ( + + {i18n._(t`Delete`)} + + )} {/* Update delete modal to show dependencies https://github.com/ansible/awx/issues/5546 */} {error && ( diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx index 3139e2b14c..52e45e7d28 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx @@ -9,7 +9,12 @@ import { ProjectsAPI } from '../../../api'; import ProjectDetail from './ProjectDetail'; jest.mock('../../../api'); - +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useRouteMatch: () => ({ + url: '/projects/1/details', + }), +})); describe('', () => { const mockProject = { id: 1, @@ -139,13 +144,19 @@ describe('', () => { ); }); - test('should show edit button for users with edit permission', async () => { + test('should show edit and sync button for users with edit permission', async () => { const wrapper = mountWithContexts(); const editButton = await waitForElement( wrapper, 'ProjectDetail Button[aria-label="edit"]' ); + + const syncButton = await waitForElement( + wrapper, + 'ProjectDetail Button[aria-label="Sync Project"]' + ); expect(editButton.text()).toEqual('Edit'); + expect(syncButton.text()).toEqual('Sync'); expect(editButton.prop('to')).toBe(`/projects/${mockProject.id}/edit`); }); @@ -166,6 +177,9 @@ describe('', () => { expect(wrapper.find('ProjectDetail Button[aria-label="edit"]').length).toBe( 0 ); + expect(wrapper.find('ProjectDetail Button[aria-label="sync"]').length).toBe( + 0 + ); }); test('edit button should navigate to project edit', () => { @@ -180,6 +194,17 @@ describe('', () => { expect(history.location.pathname).toEqual('/projects/1/edit'); }); + test('sync button should call api to syn project', async () => { + ProjectsAPI.readSync.mockResolvedValue({ data: { can_update: true } }); + const wrapper = mountWithContexts(); + await act(() => + wrapper + .find('ProjectDetail Button[aria-label="Sync Project"]') + .prop('onClick')(1) + ); + expect(ProjectsAPI.sync).toHaveBeenCalledTimes(1); + }); + test('expected api calls are made for delete', async () => { const wrapper = mountWithContexts(); await waitForElement(wrapper, 'ProjectDetail Button[aria-label="Delete"]'); diff --git a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx index 5171e0f532..1a62a3f2f0 100644 --- a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx @@ -25,7 +25,7 @@ describe('', () => { scm_update_on_launch: true, scm_update_cache_timeout: 3, allow_override: false, - custom_virtualenv: '/venv/custom-env', + custom_virtualenv: '/var/lib/awx/venv/custom-env', summary_fields: { credential: { id: 100, @@ -47,7 +47,6 @@ describe('', () => { choices: [ ['', 'Manual'], ['git', 'Git'], - ['hg', 'Mercurial'], ['svn', 'Subversion'], ['archive', 'Remote Archive'], ['insights', 'Red Hat Insights'], diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx index dafaf9675a..df2fe6630f 100644 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx @@ -86,7 +86,7 @@ function ProjectJobTemplateListItem({ ]} /> diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx index 0fff5472da..a1f6d36f41 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx @@ -140,7 +140,6 @@ function ProjectList({ i18n }) { options: [ [``, i18n._(t`Manual`)], [`git`, i18n._(t`Git`)], - [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index b2539e5f87..1382df00ca 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -14,7 +14,7 @@ import { import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; -import { PencilAltIcon, SyncIcon } from '@patternfly/react-icons'; +import { PencilAltIcon } from '@patternfly/react-icons'; import styled from 'styled-components'; import { formatDateString, timeOfDay } from '../../../util/dates'; import { ProjectsAPI } from '../../../api'; @@ -149,27 +149,14 @@ function ProjectListItem({ ]} /> - {project.summary_fields.user_capabilities.start ? ( + {project.summary_fields.user_capabilities.start && ( - - {handleSync => ( - - )} - + - ) : ( - '' )} {project.summary_fields.user_capabilities.edit ? ( diff --git a/awx/ui_next/src/screens/Project/ProjectList/useWsProjects.test.jsx b/awx/ui_next/src/screens/Project/ProjectList/useWsProjects.test.jsx index e32a31ab70..0a41c39689 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/useWsProjects.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/useWsProjects.test.jsx @@ -36,7 +36,7 @@ describe('useWsProjects', () => { test('should establish websocket connection', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const projects = [{ id: 1 }]; await act(async () => { @@ -58,7 +58,7 @@ describe('useWsProjects', () => { test('should update project status', async () => { global.document.cookie = 'csrftoken=abc123'; - const mockServer = new WS('wss://localhost/websocket/'); + const mockServer = new WS('ws://localhost/websocket/'); const projects = [ { diff --git a/awx/ui_next/src/screens/Project/Projects.jsx b/awx/ui_next/src/screens/Project/Projects.jsx index 0bb53de606..8063d4c1e6 100644 --- a/awx/ui_next/src/screens/Project/Projects.jsx +++ b/awx/ui_next/src/screens/Project/Projects.jsx @@ -1,90 +1,64 @@ -import React, { Component, Fragment } from 'react'; +import React, { useState, useCallback } from 'react'; import { Route, withRouter, Switch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Config } from '../../contexts/Config'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import ProjectsList from './ProjectList/ProjectList'; import ProjectAdd from './ProjectAdd/ProjectAdd'; import Project from './Project'; -class Projects extends Component { - constructor(props) { - super(props); +function Projects({ i18n }) { + const [breadcrumbConfig, setBreadcrumbConfig] = useState({ + '/projects': i18n._(t`Projects`), + '/projects/add': i18n._(t`Create New Project`), + }); - const { i18n } = props; - - this.state = { - breadcrumbConfig: { + const buildBreadcrumbConfig = useCallback( + (project, nested) => { + if (!project) { + return; + } + const projectSchedulesPath = `/projects/${project.id}/schedules`; + setBreadcrumbConfig({ '/projects': i18n._(t`Projects`), '/projects/add': i18n._(t`Create New Project`), - }, - }; - } + [`/projects/${project.id}`]: `${project.name}`, + [`/projects/${project.id}/edit`]: i18n._(t`Edit Details`), + [`/projects/${project.id}/details`]: i18n._(t`Details`), + [`/projects/${project.id}/access`]: i18n._(t`Access`), + [`/projects/${project.id}/notifications`]: i18n._(t`Notifications`), + [`/projects/${project.id}/job_templates`]: i18n._(t`Job Templates`), - setBreadcrumbConfig = (project, nested) => { - const { i18n } = this.props; + [`${projectSchedulesPath}`]: i18n._(t`Schedules`), + [`${projectSchedulesPath}/add`]: i18n._(t`Create New Schedule`), + [`${projectSchedulesPath}/${nested?.id}`]: `${nested?.name}`, + [`${projectSchedulesPath}/${nested?.id}/details`]: i18n._( + t`Schedule Details` + ), + [`${projectSchedulesPath}/${nested?.id}/edit`]: i18n._(t`Edit Details`), + }); + }, + [i18n] + ); - if (!project) { - return; - } - - const projectSchedulesPath = `/projects/${project.id}/schedules`; - - const breadcrumbConfig = { - '/projects': i18n._(t`Projects`), - '/projects/add': i18n._(t`Create New Project`), - [`/projects/${project.id}`]: `${project.name}`, - [`/projects/${project.id}/edit`]: i18n._(t`Edit Details`), - [`/projects/${project.id}/details`]: i18n._(t`Details`), - [`/projects/${project.id}/access`]: i18n._(t`Access`), - [`/projects/${project.id}/notifications`]: i18n._(t`Notifications`), - [`/projects/${project.id}/job_templates`]: i18n._(t`Job Templates`), - - [`${projectSchedulesPath}`]: i18n._(t`Schedules`), - [`${projectSchedulesPath}/add`]: i18n._(t`Create New Schedule`), - [`${projectSchedulesPath}/${nested?.id}`]: `${nested?.name}`, - [`${projectSchedulesPath}/${nested?.id}/details`]: i18n._( - t`Schedule Details` - ), - [`${projectSchedulesPath}/${nested?.id}/edit`]: i18n._(t`Edit Details`), - }; - - this.setState({ breadcrumbConfig }); - }; - - render() { - const { match, history, location } = this.props; - const { breadcrumbConfig } = this.state; - - return ( - - - - - - - - - {({ me }) => ( - - )} - - - - - - - - ); - } + return ( + <> + + + + + + + + + + + + + + ); } export { Projects as _Projects }; diff --git a/awx/ui_next/src/screens/Project/Projects.test.jsx b/awx/ui_next/src/screens/Project/Projects.test.jsx index b46f37ae23..4d522a3d14 100644 --- a/awx/ui_next/src/screens/Project/Projects.test.jsx +++ b/awx/ui_next/src/screens/Project/Projects.test.jsx @@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Projects from './Projects'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { test('initially renders succesfully', () => { mountWithContexts(); @@ -27,7 +31,7 @@ describe('', () => { }, }, }); - expect(wrapper.find('BreadcrumbHeading').length).toBe(1); + expect(wrapper.find('Title').length).toBe(1); wrapper.unmount(); }); }); diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx index b23e8d3982..8c52218c1e 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx @@ -21,7 +21,6 @@ import { import Popover from '../../../components/Popover'; import { GitSubForm, - HgSubForm, SvnSubForm, ArchiveSubForm, InsightsSubForm, @@ -237,13 +236,6 @@ function ProjectFormFields({ scmUpdateOnLaunch={formik.values.scm_update_on_launch} /> ), - hg: ( - - ), svn: ( datum !== '/venv/ansible/') + .filter(datum => datum !== '/var/lib/awx/venv/ansible/') .map(datum => ({ label: datum, value: datum, @@ -318,7 +310,17 @@ function ProjectForm({ i18n, project, submitError, ...props }) { const { summary_fields = {} } = project; const [contentError, setContentError] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [scmSubFormState, setScmSubFormState] = useState(null); + const [scmSubFormState, setScmSubFormState] = useState({ + scm_url: '', + scm_branch: '', + scm_refspec: '', + credential: '', + scm_clean: false, + scm_delete_on_update: false, + scm_update_on_launch: false, + allow_override: false, + scm_update_cache_timeout: 0, + }); const [scmTypeOptions, setScmTypeOptions] = useState(null); const [credentials, setCredentials] = useState({ scm: { typeId: null, value: null }, diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx index b4bbcbe9c9..7e88bc7f10 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx @@ -22,7 +22,7 @@ describe('', () => { scm_update_on_launch: true, scm_update_cache_timeout: 3, allow_override: false, - custom_virtualenv: '/venv/custom-env', + custom_virtualenv: '/var/lib/awx/venv/custom-env', summary_fields: { credential: { id: 100, @@ -45,7 +45,6 @@ describe('', () => { choices: [ ['', 'Manual'], ['git', 'Git'], - ['hg', 'Mercurial'], ['svn', 'Subversion'], ['archive', 'Remote Archive'], ['insights', 'Red Hat Insights'], @@ -296,7 +295,9 @@ describe('', () => { 'FormGroup[label="Source Control Credential Type"] FormSelect' ); await act(async () => { - scmTypeSelect.invoke('onChange')('hg', { target: { name: 'Mercurial' } }); + scmTypeSelect.invoke('onChange')('svn', { + target: { name: 'Subversion' }, + }); }); wrapper.update(); await act(async () => { diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx index ba65b0b6ff..d06d9ea84b 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx @@ -21,8 +21,16 @@ const ArchiveSubForm = ({ {i18n._(t`Example URLs for Remote Archive Source Control include:`)}
    -
  • https://github.com/username/project/archive/v0.0.1.tar.gz
  • -
  • https://github.com/username/project/archive/v0.0.2.zip
  • +
  • + + https://github.com/username/project/archive/v0.0.1.tar.gz + +
  • +
  • + + https://github.com/username/project/archive/v0.0.2.zip + +
} diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/GitSubForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/GitSubForm.jsx index a1721468dc..32d2d85565 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/GitSubForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/GitSubForm.jsx @@ -23,9 +23,15 @@ const GitSubForm = ({ {i18n._(t`Example URLs for GIT Source Control include:`)}
    -
  • https://github.com/ansible/ansible.git
  • -
  • git@github.com:ansible/ansible.git
  • -
  • git://servername.example.com/ansible.git
  • +
  • + https://github.com/ansible/ansible.git +
  • +
  • + git@github.com:ansible/ansible.git +
  • +
  • + git://servername.example.com/ansible.git +
{i18n._(t`Note: When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, do not enter a username @@ -58,8 +64,12 @@ const GitSubForm = ({
{i18n._(t`Examples include:`)}
    -
  • refs/*:refs/remotes/origin/*
  • -
  • refs/pull/62/head:refs/remotes/origin/pull/62/head
  • +
  • + refs/*:refs/remotes/origin/* +
  • +
  • + refs/pull/62/head:refs/remotes/origin/pull/62/head +
{i18n._(t`The first fetches all references. The second fetches the Github pull request number 62, in this example diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/HgSubForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/HgSubForm.jsx deleted file mode 100644 index 5640861ecc..0000000000 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/HgSubForm.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import 'styled-components/macro'; -import React from 'react'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { - UrlFormField, - BranchFormField, - ScmCredentialFormField, - ScmTypeOptions, -} from './SharedFields'; - -const HgSubForm = ({ - i18n, - credential, - onCredentialSelection, - scmUpdateOnLaunch, -}) => ( - <> - - {i18n._(t`Example URLs for Mercurial Source Control include:`)} -
    -
  • https://bitbucket.org/username/project
  • -
  • ssh://hg@bitbucket.org/username/project
  • -
  • ssh://server.example.com/path
  • -
- {i18n._(t`Note: Mercurial does not support password authentication - for SSH. Do not put the username and key in the URL. If using - Bitbucket and SSH, do not supply your Bitbucket username. - `)} -
- } - /> - - - - -); - -export default withI18n()(HgSubForm); diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SvnSubForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SvnSubForm.jsx index f9da184a17..f14d0ad649 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SvnSubForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SvnSubForm.jsx @@ -22,9 +22,15 @@ const SvnSubForm = ({ {i18n._(t`Example URLs for Subversion Source Control include:`)}
    -
  • https://github.com/ansible/ansible
  • -
  • svn://servername.example.com/path
  • -
  • svn+ssh://servername.example.com/path
  • +
  • + https://github.com/ansible/ansible +
  • +
  • + svn://servername.example.com/path +
  • +
  • + svn+ssh://servername.example.com/path +
} diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js index 8cf3e5594b..1e8b3f6044 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js @@ -1,5 +1,4 @@ export { default as GitSubForm } from './GitSubForm'; -export { default as HgSubForm } from './HgSubForm'; export { default as InsightsSubForm } from './InsightsSubForm'; export { default as ManualSubForm } from './ManualSubForm'; export { default as SvnSubForm } from './SvnSubForm'; diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx index 3ef97e9657..5a4cc23fdc 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx @@ -1,70 +1,55 @@ -import React, { Fragment } from 'react'; +import React, { useCallback } from 'react'; +import { useRouteMatch } from 'react-router-dom'; +import { Button } from '@patternfly/react-core'; +import { SyncIcon } from '@patternfly/react-icons'; + import { number } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import useRequest, { useDismissableError } from '../../../util/useRequest'; import AlertModal from '../../../components/AlertModal'; import ErrorDetail from '../../../components/ErrorDetail'; import { ProjectsAPI } from '../../../api'; -class ProjectSyncButton extends React.Component { - static propTypes = { - projectId: number.isRequired, - }; +function ProjectSyncButton({ i18n, projectId }) { + const match = useRouteMatch(); - constructor(props) { - super(props); + const { request: handleSync, error: syncError } = useRequest( + useCallback(async () => { + await ProjectsAPI.sync(projectId); + }, [projectId]), + null + ); - this.state = { - syncError: null, - }; - - this.handleSync = this.handleSync.bind(this); - this.handleSyncErrorClose = this.handleSyncErrorClose.bind(this); - } - - handleSyncErrorClose() { - this.setState({ syncError: null }); - } - - async handleSync() { - const { i18n, projectId } = this.props; - try { - const { data: syncConfig } = await ProjectsAPI.readSync(projectId); - if (syncConfig.can_update) { - await ProjectsAPI.sync(projectId); - } else { - this.setState({ - syncError: i18n._( - t`You don't have the necessary permissions to sync this project.` - ), - }); - } - } catch (err) { - this.setState({ syncError: err }); - } - } - - render() { - const { syncError } = this.state; - const { i18n, children } = this.props; - return ( - - {children(this.handleSync)} - {syncError && ( - - {i18n._(t`Failed to sync job.`)} - - - )} - - ); - } + const { error, dismissError } = useDismissableError(syncError); + const isDetailsView = match.url.endsWith('/details'); + return ( + <> + + {error && ( + + {i18n._(t`Failed to sync project.`)} + + + )} + + ); } +ProjectSyncButton.propTypes = { + projectId: number.isRequired, +}; + export default withI18n()(ProjectSyncButton); diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.test.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.test.jsx index 87b9b7bfb1..9f53dfb339 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.test.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.test.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import { sleep } from '../../../../testUtils/testUtils'; @@ -8,39 +9,39 @@ import { ProjectsAPI } from '../../../api'; jest.mock('../../../api'); describe('ProjectSyncButton', () => { - ProjectsAPI.readSync.mockResolvedValue({ - data: { - can_update: true, - }, - }); + let wrapper; const children = handleSync => ( - + {isLoading && } + {!isLoading && error && } + {!isLoading && github && ( + + {formik => ( + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} + + )} ); } -export default withI18n()(GitHubEdit); +export default GitHubEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.test.jsx index 539932c99a..f864f1f6ca 100644 --- a/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEdit/GitHubEdit.test.jsx @@ -1,16 +1,173 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; import GitHubEdit from './GitHubEdit'; +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_CALLBACK_URL: 'https://foo/complete/github/', + SOCIAL_AUTH_GITHUB_KEY: 'mock github key', + SOCIAL_AUTH_GITHUB_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP: { + Default: { + users: true, + }, + }, + }, +}); + describe('', () => { let wrapper; - beforeEach(() => { - wrapper = mountWithContexts(); - }); + let history; + afterEach(() => { wrapper.unmount(); + jest.clearAllMocks(); }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + test('initially renders without crashing', () => { expect(wrapper.find('GitHubEdit').length).toBe(1); }); + + test('should display expected form fields', async () => { + expect(wrapper.find('FormGroup[label="GitHub OAuth2 Key"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="GitHub OAuth2 Secret"]').length).toBe( + 1 + ); + expect( + wrapper.find('FormGroup[label="GitHub OAuth2 Organization Map"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub OAuth2 Team Map"]').length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_KEY: '', + SOCIAL_AUTH_GITHUB_SECRET: '', + SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper.find('input#SOCIAL_AUTH_GITHUB_KEY').simulate('change', { + target: { value: 'new key', name: 'SOCIAL_AUTH_GITHUB_KEY' }, + }); + wrapper + .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP') + .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_KEY: 'new key', + SOCIAL_AUTH_GITHUB_SECRET: '', + SOCIAL_AUTH_GITHUB_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to github default detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual('/settings/github/details'); + }); + + test('should navigate to github default detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual('/settings/github/details'); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); }); diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.jsx new file mode 100644 index 0000000000..3d6ee60c3e --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.jsx @@ -0,0 +1,151 @@ +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + EncryptedField, + InputField, + ObjectField, +} from '../../shared/SharedFields'; +import { formatJson } from '../../shared/settingUtils'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function GitHubEnterpriseEdit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { isLoading, error, request: fetchGithub, result: github } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('github-enterprise'); + const mergedData = {}; + Object.keys(data).forEach(key => { + if (!options[key]) { + return; + } + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchGithub(); + }, [fetchGithub]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/github/enterprise/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP + ), + }); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(github).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/github/enterprise/details'); + }; + + const initialValues = fields => + Object.keys(fields).reduce((acc, key) => { + if (fields[key].type === 'list' || fields[key].type === 'nested object') { + const emptyDefault = fields[key].type === 'list' ? '[]' : '{}'; + acc[key] = fields[key].value + ? JSON.stringify(fields[key].value, null, 2) + : emptyDefault; + } else { + acc[key] = fields[key].value ?? ''; + } + return acc; + }, {}); + + return ( + + {isLoading && } + {!isLoading && error && } + {!isLoading && github && ( + + {formik => ( +
+ + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} +
+ ); +} + +export default GitHubEnterpriseEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx new file mode 100644 index 0000000000..f0bb46ab23 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx @@ -0,0 +1,196 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; +import GitHubEnterpriseEdit from './GitHubEnterpriseEdit'; + +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: null, + }, +}); + +describe('', () => { + let wrapper; + let history; + + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/enterprise/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + test('initially renders without crashing', () => { + expect(wrapper.find('GitHubEnterpriseEdit').length).toBe(1); + }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="GitHub Enterprise URL"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise API URL"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise OAuth2 Key"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise OAuth2 Secret"]').length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise OAuth2 Organization Map"]' + ).length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise OAuth2 Team Map"]') + .length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper + .find('input#SOCIAL_AUTH_GITHUB_ENTERPRISE_URL') + .simulate('change', { + target: { + value: 'https://localhost', + name: 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL', + }, + }); + wrapper + .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP') + .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: 'https://localhost', + SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to github enterprise detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise/details' + ); + }); + + test('should navigate to github enterprise detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise/details' + ); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/index.js new file mode 100644 index 0000000000..5b5377e423 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/index.js @@ -0,0 +1 @@ +export { default } from './GitHubEnterpriseEdit'; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.jsx new file mode 100644 index 0000000000..272b1866ff --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.jsx @@ -0,0 +1,157 @@ +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + EncryptedField, + InputField, + ObjectField, +} from '../../shared/SharedFields'; +import { formatJson } from '../../shared/settingUtils'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function GitHubEnterpriseOrgEdit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { isLoading, error, request: fetchGithub, result: github } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('github-enterprise-org'); + const mergedData = {}; + Object.keys(data).forEach(key => { + if (!options[key]) { + return; + } + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchGithub(); + }, [fetchGithub]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/github/enterprise_organization/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP + ), + }); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(github).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/github/enterprise_organization/details'); + }; + + const initialValues = fields => + Object.keys(fields).reduce((acc, key) => { + if (fields[key].type === 'list' || fields[key].type === 'nested object') { + const emptyDefault = fields[key].type === 'list' ? '[]' : '{}'; + acc[key] = fields[key].value + ? JSON.stringify(fields[key].value, null, 2) + : emptyDefault; + } else { + acc[key] = fields[key].value ?? ''; + } + return acc; + }, {}); + + return ( + + {isLoading && } + {!isLoading && error && } + {!isLoading && github && ( + + {formik => ( +
+ + + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} +
+ ); +} + +export default GitHubEnterpriseOrgEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx new file mode 100644 index 0000000000..278dd5d37e --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx @@ -0,0 +1,212 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; +import GitHubEnterpriseOrgEdit from './GitHubEnterpriseOrgEdit'; + +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise-org/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: null, + }, +}); + +describe('', () => { + let wrapper; + let history; + + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/enterprise_organization/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + test('initially renders without crashing', () => { + expect(wrapper.find('GitHubEnterpriseOrgEdit').length).toBe(1); + }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Organization URL"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Organization API URL"]') + .length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise Organization OAuth2 Key"]' + ).length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise Organization OAuth2 Secret"]' + ).length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Organization Name"]') + .length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise Organization OAuth2 Organization Map"]' + ).length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise Organization OAuth2 Team Map"]' + ).length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper + .find('input#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL') + .simulate('change', { + target: { + value: 'https://localhost', + name: 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL', + }, + }); + wrapper + .find( + 'CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP' + ) + .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: 'https://localhost', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to github enterprise org detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise_organization/details' + ); + }); + + test('should navigate to github enterprise org detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise_organization/details' + ); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/index.js new file mode 100644 index 0000000000..e86b1f78f0 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/index.js @@ -0,0 +1 @@ +export { default } from './GitHubEnterpriseOrgEdit'; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.jsx new file mode 100644 index 0000000000..d9b725dc13 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.jsx @@ -0,0 +1,157 @@ +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + EncryptedField, + InputField, + ObjectField, +} from '../../shared/SharedFields'; +import { formatJson } from '../../shared/settingUtils'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function GitHubEnterpriseTeamEdit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { isLoading, error, request: fetchGithub, result: github } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('github-enterprise-team'); + const mergedData = {}; + Object.keys(data).forEach(key => { + if (!options[key]) { + return; + } + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchGithub(); + }, [fetchGithub]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/github/enterprise_team/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP + ), + }); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(github).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/github/enterprise_team/details'); + }; + + const initialValues = fields => + Object.keys(fields).reduce((acc, key) => { + if (fields[key].type === 'list' || fields[key].type === 'nested object') { + const emptyDefault = fields[key].type === 'list' ? '[]' : '{}'; + acc[key] = fields[key].value + ? JSON.stringify(fields[key].value, null, 2) + : emptyDefault; + } else { + acc[key] = fields[key].value ?? ''; + } + return acc; + }, {}); + + return ( + + {isLoading && } + {!isLoading && error && } + {!isLoading && github && ( + + {formik => ( +
+ + + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} +
+ ); +} + +export default GitHubEnterpriseTeamEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx new file mode 100644 index 0000000000..764c9fa377 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx @@ -0,0 +1,206 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; +import GitHubEnterpriseTeamEdit from './GitHubEnterpriseTeamEdit'; + +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise-team/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: null, + }, +}); + +describe('', () => { + let wrapper; + let history; + + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/enterprise_team/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + test('initially renders without crashing', () => { + expect(wrapper.find('GitHubEnterpriseTeamEdit').length).toBe(1); + }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team URL"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team API URL"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team OAuth2 Key"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team OAuth2 Secret"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team ID"]').length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise Team OAuth2 Organization Map"]' + ).length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team OAuth2 Team Map"]') + .length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper + .find('input#SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL') + .simulate('change', { + target: { + value: 'https://localhost', + name: 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL', + }, + }); + wrapper + .find( + 'CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP' + ) + .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: 'https://localhost', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to github enterprise team detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise_team/details' + ); + }); + + test('should navigate to github enterprise team detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise_team/details' + ); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/index.js new file mode 100644 index 0000000000..002e319910 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/index.js @@ -0,0 +1 @@ +export { default } from './GitHubEnterpriseTeamEdit'; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.jsx new file mode 100644 index 0000000000..6224acb5b7 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.jsx @@ -0,0 +1,147 @@ +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + EncryptedField, + InputField, + ObjectField, +} from '../../shared/SharedFields'; +import { formatJson } from '../../shared/settingUtils'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function GitHubOrgEdit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { isLoading, error, request: fetchGithub, result: github } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('github-org'); + const mergedData = {}; + Object.keys(data).forEach(key => { + if (!options[key]) { + return; + } + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchGithub(); + }, [fetchGithub]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/github/organization/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP + ), + }); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(github).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/github/organization/details'); + }; + + const initialValues = fields => + Object.keys(fields).reduce((acc, key) => { + if (fields[key].type === 'list' || fields[key].type === 'nested object') { + const emptyDefault = fields[key].type === 'list' ? '[]' : '{}'; + acc[key] = fields[key].value + ? JSON.stringify(fields[key].value, null, 2) + : emptyDefault; + } else { + acc[key] = fields[key].value ?? ''; + } + return acc; + }, {}); + + return ( + + {isLoading && } + {!isLoading && error && } + {!isLoading && github && ( + + {formik => ( +
+ + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} +
+ ); +} + +export default GitHubOrgEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx new file mode 100644 index 0000000000..57396c43d1 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/GitHubOrgEdit.test.jsx @@ -0,0 +1,186 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; +import GitHubOrgEdit from './GitHubOrgEdit'; + +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_ORG_CALLBACK_URL: + 'https://towerhost/sso/complete/github-org/', + SOCIAL_AUTH_GITHUB_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ORG_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: null, + }, +}); + +describe('', () => { + let wrapper; + let history; + + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/organization/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + test('initially renders without crashing', () => { + expect(wrapper.find('GitHubOrgEdit').length).toBe(1); + }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="GitHub Organization OAuth2 Key"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Organization OAuth2 Secret"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Organization Name"]').length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Organization OAuth2 Organization Map"]' + ).length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Organization OAuth2 Team Map"]') + .length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ORG_SECRET: '', + SOCIAL_AUTH_GITHUB_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ORG_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper.find('input#SOCIAL_AUTH_GITHUB_ORG_NAME').simulate('change', { + target: { value: 'new org', name: 'SOCIAL_AUTH_GITHUB_ORG_NAME' }, + }); + wrapper + .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP') + .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ORG_SECRET: '', + SOCIAL_AUTH_GITHUB_ORG_NAME: 'new org', + SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to github organization detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/organization/details' + ); + }); + + test('should navigate to github organization detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/organization/details' + ); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/index.js new file mode 100644 index 0000000000..1652804b44 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubOrgEdit/index.js @@ -0,0 +1 @@ +export { default } from './GitHubOrgEdit'; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.jsx new file mode 100644 index 0000000000..f898539283 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.jsx @@ -0,0 +1,147 @@ +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + EncryptedField, + InputField, + ObjectField, +} from '../../shared/SharedFields'; +import { formatJson } from '../../shared/settingUtils'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function GitHubTeamEdit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { isLoading, error, request: fetchGithub, result: github } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('github-team'); + const mergedData = {}; + Object.keys(data).forEach(key => { + if (!options[key]) { + return; + } + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchGithub(); + }, [fetchGithub]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/github/team/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP + ), + }); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(github).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/github/team/details'); + }; + + const initialValues = fields => + Object.keys(fields).reduce((acc, key) => { + if (fields[key].type === 'list' || fields[key].type === 'nested object') { + const emptyDefault = fields[key].type === 'list' ? '[]' : '{}'; + acc[key] = fields[key].value + ? JSON.stringify(fields[key].value, null, 2) + : emptyDefault; + } else { + acc[key] = fields[key].value ?? ''; + } + return acc; + }, {}); + + return ( + + {isLoading && } + {!isLoading && error && } + {!isLoading && github && ( + + {formik => ( +
+ + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} +
+ ); +} + +export default GitHubTeamEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx new file mode 100644 index 0000000000..bbc36f8948 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/GitHubTeamEdit.test.jsx @@ -0,0 +1,177 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; +import GitHubTeamEdit from './GitHubTeamEdit'; + +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL: + 'https://towerhost/sso/complete/github-team/', + SOCIAL_AUTH_GITHUB_TEAM_KEY: 'OAuth2 key (Client ID)', + SOCIAL_AUTH_GITHUB_TEAM_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_TEAM_ID: 'team_id', + SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP: {}, + SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: {}, + }, +}); + +describe('', () => { + let wrapper; + let history; + + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/team/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + test('initially renders without crashing', () => { + expect(wrapper.find('GitHubTeamEdit').length).toBe(1); + }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="GitHub Team OAuth2 Key"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Team OAuth2 Secret"]').length + ).toBe(1); + expect(wrapper.find('FormGroup[label="GitHub Team ID"]').length).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Team OAuth2 Organization Map"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Team OAuth2 Team Map"]').length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_TEAM_KEY: '', + SOCIAL_AUTH_GITHUB_TEAM_SECRET: '', + SOCIAL_AUTH_GITHUB_TEAM_ID: '', + SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_TEAM_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper.find('input#SOCIAL_AUTH_GITHUB_TEAM_ID').simulate('change', { + target: { value: '12345', name: 'SOCIAL_AUTH_GITHUB_TEAM_ID' }, + }); + wrapper + .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP') + .invoke('onChange')('{\n"Default":{\n"users":\ntrue\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_TEAM_KEY: 'OAuth2 key (Client ID)', + SOCIAL_AUTH_GITHUB_TEAM_SECRET: '', + SOCIAL_AUTH_GITHUB_TEAM_ID: '12345', + SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP: { + Default: { + users: true, + }, + }, + }); + }); + + test('should navigate to github team detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual('/settings/github/team/details'); + }); + + test('should navigate to github team detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual('/settings/github/team/details'); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/index.js new file mode 100644 index 0000000000..ba00e5355b --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubTeamEdit/index.js @@ -0,0 +1 @@ +export { default } from './GitHubTeamEdit'; diff --git a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2.test.jsx b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2.test.jsx index 7b7b330aec..4455605098 100644 --- a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2.test.jsx +++ b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2.test.jsx @@ -2,13 +2,28 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; -import GoogleOAuth2 from './GoogleOAuth2'; - +import { SettingsProvider } from '../../../contexts/Settings'; import { SettingsAPI } from '../../../api'; +import mockAllOptions from '../shared/data.allSettingOptions.json'; +import GoogleOAuth2 from './GoogleOAuth2'; jest.mock('../../../api/models/Settings'); SettingsAPI.readCategory.mockResolvedValue({ - data: {}, + data: { + SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL: + 'https://towerhost/sso/complete/google-oauth2/', + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: 'mock key', + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: '$encrypted$', + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: [ + 'example.com', + 'example_2.com', + ], + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: {}, + SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: { + Default: {}, + }, + SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: {}, + }, }); describe('', () => { @@ -24,9 +39,14 @@ describe('', () => { initialEntries: ['/settings/google_oauth2/details'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); expect(wrapper.find('GoogleOAuth2Detail').length).toBe(1); }); @@ -36,9 +56,14 @@ describe('', () => { initialEntries: ['/settings/google_oauth2/edit'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); expect(wrapper.find('GoogleOAuth2Edit').length).toBe(1); }); diff --git a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Detail/GoogleOAuth2Detail.jsx b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Detail/GoogleOAuth2Detail.jsx index f19f87378f..98a241f801 100644 --- a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Detail/GoogleOAuth2Detail.jsx +++ b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Detail/GoogleOAuth2Detail.jsx @@ -78,6 +78,7 @@ function GoogleOAuth2Detail({ i18n }) { - + {formik => ( +
+ + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} + + )} ); } -export default withI18n()(GoogleOAuth2Edit); +export default GoogleOAuth2Edit; diff --git a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx index 034a0def4e..68a292232c 100644 --- a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx +++ b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx @@ -1,16 +1,196 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; import GoogleOAuth2Edit from './GoogleOAuth2Edit'; +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL: + 'https://towerhost/sso/complete/google-oauth2/', + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: 'mock key', + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: '$encrypted$', + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: [ + 'example.com', + 'example_2.com', + ], + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: {}, + SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: { + Default: {}, + }, + SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: {}, + }, +}); + describe('', () => { let wrapper; - beforeEach(() => { - wrapper = mountWithContexts(); - }); + let history; + afterEach(() => { wrapper.unmount(); + jest.clearAllMocks(); }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/google_oauth2/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + test('initially renders without crashing', () => { expect(wrapper.find('GoogleOAuth2Edit').length).toBe(1); }); + + test('should display expected form fields', async () => { + expect(wrapper.find('FormGroup[label="Google OAuth2 Key"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Google OAuth2 Secret"]').length).toBe( + 1 + ); + expect( + wrapper.find('FormGroup[label="Google OAuth2 Allowed Domains"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="Google OAuth2 Extra Arguments"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="Google OAuth2 Organization Map"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="Google OAuth2 Team Map"]').length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: '', + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: '', + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: [], + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: {}, + SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper.find('input#SOCIAL_AUTH_GOOGLE_OAUTH2_KEY').simulate('change', { + target: { value: 'new key', name: 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY' }, + }); + wrapper + .find('CodeMirrorInput#SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP') + .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: 'new key', + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: '', + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: [], + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: {}, + SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: {}, + SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to Google OAuth 2.0 detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual( + '/settings/google_oauth2/details' + ); + }); + + test('should navigate to Google OAuth 2.0 detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/settings/google_oauth2/details' + ); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); }); diff --git a/awx/ui_next/src/screens/Setting/Jobs/Jobs.test.jsx b/awx/ui_next/src/screens/Setting/Jobs/Jobs.test.jsx index d0529ab483..2bc3ca1940 100644 --- a/awx/ui_next/src/screens/Setting/Jobs/Jobs.test.jsx +++ b/awx/ui_next/src/screens/Setting/Jobs/Jobs.test.jsx @@ -2,13 +2,13 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; -import Jobs from './Jobs'; - +import mockJobSettings from '../shared/data.jobSettings.json'; import { SettingsAPI } from '../../../api'; +import Jobs from './Jobs'; jest.mock('../../../api/models/Settings'); SettingsAPI.readCategory.mockResolvedValue({ - data: {}, + data: mockJobSettings, }); describe('', () => { diff --git a/awx/ui_next/src/screens/Setting/Jobs/JobsDetail/JobsDetail.jsx b/awx/ui_next/src/screens/Setting/Jobs/JobsDetail/JobsDetail.jsx index c25c221518..a29a633a2d 100644 --- a/awx/ui_next/src/screens/Setting/Jobs/JobsDetail/JobsDetail.jsx +++ b/awx/ui_next/src/screens/Setting/Jobs/JobsDetail/JobsDetail.jsx @@ -95,6 +95,7 @@ function JobsDetail({ i18n }) { - + {isLoading && } + {!isLoading && error && } + {!isLoading && jobs && ( + + {formik => { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + ); + }} +
+ )} ); } -export default withI18n()(JobsEdit); +export default JobsEdit; diff --git a/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.test.jsx b/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.test.jsx index 06f4fb2f12..14b2fb11ad 100644 --- a/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/JobsEdit.test.jsx @@ -1,16 +1,127 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import mockJobSettings from '../../shared/data.jobSettings.json'; +import mockDefaultJobSettings from './data.defaultJobSettings.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; import JobsEdit from './JobsEdit'; +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: mockJobSettings, +}); + describe('', () => { let wrapper; - beforeEach(() => { - wrapper = mountWithContexts(); - }); + let history; + afterEach(() => { wrapper.unmount(); + jest.clearAllMocks(); }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/jobs/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + test('initially renders without crashing', () => { expect(wrapper.find('JobsEdit').length).toBe(1); }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith(mockDefaultJobSettings); + }); + + test('should successfully send request to api on form submission', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + const { + ALLOW_JINJA_IN_EXTRA_VARS, + AWX_ISOLATED_KEY_GENERATION, + AWX_ISOLATED_PRIVATE_KEY, + AWX_ISOLATED_PUBLIC_KEY, + EVENT_STDOUT_MAX_BYTES_DISPLAY, + STDOUT_MAX_BYTES_DISPLAY, + ...jobRequest + } = mockJobSettings; + expect(SettingsAPI.updateAll).toHaveBeenCalledWith(jobRequest); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should navigate to job settings detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual('/settings/jobs/details'); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); }); diff --git a/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/data.defaultJobSettings.json b/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/data.defaultJobSettings.json new file mode 100644 index 0000000000..70c73869d7 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/Jobs/JobsEdit/data.defaultJobSettings.json @@ -0,0 +1,48 @@ +{ + "AD_HOC_COMMANDS": [ + "command", + "shell", + "yum", + "apt", + "apt_key", + "apt_repository", + "apt_rpm", + "service", + "group", + "user", + "mount", + "ping", + "selinux", + "setup", + "win_ping", + "win_service", + "win_updates", + "win_group", + "win_user" + ], + "ANSIBLE_FACT_CACHE_TIMEOUT": 0, + "AWX_ANSIBLE_CALLBACK_PLUGINS": [], + "AWX_COLLECTIONS_ENABLED": true, + "AWX_ISOLATED_CHECK_INTERVAL": 1, + "AWX_ISOLATED_CONNECTION_TIMEOUT": 10, + "AWX_ISOLATED_HOST_KEY_CHECKING": false, + "AWX_ISOLATED_LAUNCH_TIMEOUT": 600, + "AWX_PROOT_BASE_PATH": "/tmp", + "AWX_PROOT_ENABLED": true, + "AWX_PROOT_HIDE_PATHS": [], + "AWX_PROOT_SHOW_PATHS": [], + "AWX_RESOURCE_PROFILING_CPU_POLL_INTERVAL": 0.25, + "AWX_RESOURCE_PROFILING_ENABLED": false, + "AWX_RESOURCE_PROFILING_MEMORY_POLL_INTERVAL": 0.25, + "AWX_RESOURCE_PROFILING_PID_POLL_INTERVAL": 0.25, + "AWX_ROLES_ENABLED": true, + "AWX_SHOW_PLAYBOOK_LINKS": false, + "AWX_TASK_ENV": {}, + "DEFAULT_INVENTORY_UPDATE_TIMEOUT": 0, + "DEFAULT_JOB_TIMEOUT": 0, + "DEFAULT_PROJECT_UPDATE_TIMEOUT": 0, + "GALAXY_IGNORE_CERTS": false, + "MAX_FORKS": 200, + "PROJECT_UPDATE_VVV": false, + "SCHEDULE_MAX_JOBS": 10 +} \ No newline at end of file diff --git a/awx/ui_next/src/screens/Setting/LDAP/LDAP.test.jsx b/awx/ui_next/src/screens/Setting/LDAP/LDAP.test.jsx index bcc6c60be0..88ca337048 100644 --- a/awx/ui_next/src/screens/Setting/LDAP/LDAP.test.jsx +++ b/awx/ui_next/src/screens/Setting/LDAP/LDAP.test.jsx @@ -6,12 +6,13 @@ import { waitForElement, } from '../../../../testUtils/enzymeHelpers'; import { SettingsAPI } from '../../../api'; +import { SettingsProvider } from '../../../contexts/Settings'; +import mockAllOptions from '../shared/data.allSettingOptions.json'; +import mockLDAP from '../shared/data.ldapSettings.json'; import LDAP from './LDAP'; jest.mock('../../../api/models/Settings'); -SettingsAPI.readCategory.mockResolvedValue({ - data: {}, -}); +SettingsAPI.readCategory.mockResolvedValue({ data: mockLDAP }); describe('', () => { let wrapper; @@ -39,9 +40,14 @@ describe('', () => { initialEntries: ['/settings/ldap/default/edit'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); expect(wrapper.find('LDAPEdit').length).toBe(1); diff --git a/awx/ui_next/src/screens/Setting/LDAP/LDAPDetail/LDAPDetail.jsx b/awx/ui_next/src/screens/Setting/LDAP/LDAPDetail/LDAPDetail.jsx index 120b00fa44..74850dfbe4 100644 --- a/awx/ui_next/src/screens/Setting/LDAP/LDAPDetail/LDAPDetail.jsx +++ b/awx/ui_next/src/screens/Setting/LDAP/LDAPDetail/LDAPDetail.jsx @@ -159,6 +159,7 @@ function LDAPDetail({ i18n }) { - + {isLoading && } + {!isLoading && error && } + {!isLoading && ldap && ( + + {formik => ( +
+ + + + + + + + + + + + + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} ); } -export default withI18n()(LDAPEdit); +export default LDAPEdit; diff --git a/awx/ui_next/src/screens/Setting/LDAP/LDAPEdit/LDAPEdit.test.jsx b/awx/ui_next/src/screens/Setting/LDAP/LDAPEdit/LDAPEdit.test.jsx index 12ac75a6ed..71f998e341 100644 --- a/awx/ui_next/src/screens/Setting/LDAP/LDAPEdit/LDAPEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/LDAP/LDAPEdit/LDAPEdit.test.jsx @@ -1,16 +1,265 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { useRouteMatch } from 'react-router-dom'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import mockLDAP from '../../shared/data.ldapSettings.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; import LDAPEdit from './LDAPEdit'; +jest.mock('../../../../api/models/Settings'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useRouteMatch: jest.fn(), +})); +SettingsAPI.readCategory.mockResolvedValue({ data: mockLDAP }); + describe('', () => { let wrapper; - beforeEach(() => { - wrapper = mountWithContexts(); + let history; + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/ldap/default/edit'], + }); + useRouteMatch.mockImplementation(() => ({ + url: '/settings/ldap/default/edit', + path: '/settings/ldap/:category/edit', + params: { category: 'default' }, + })); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); + afterEach(() => { wrapper.unmount(); + jest.clearAllMocks(); }); + test('initially renders without crashing', () => { expect(wrapper.find('LDAPEdit').length).toBe(1); }); + + test('should display expected form fields', async () => { + expect(wrapper.find('FormGroup[label="LDAP Server URI"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="LDAP Bind DN"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="LDAP Bind Password"]').length).toBe( + 1 + ); + expect(wrapper.find('FormGroup[label="LDAP User Search"]').length).toBe(1); + expect( + wrapper.find('FormGroup[label="LDAP User DN Template"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="LDAP User Attribute Map"]').length + ).toBe(1); + expect(wrapper.find('FormGroup[label="LDAP Group Search"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="LDAP Group Type"]').length).toBe(1); + expect( + wrapper.find('FormGroup[label="LDAP Group Type Parameters"]').length + ).toBe(1); + expect(wrapper.find('FormGroup[label="LDAP Require Group"]').length).toBe( + 1 + ); + expect(wrapper.find('FormGroup[label="LDAP Deny Group"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="LDAP Start TLS"]').length).toBe(1); + expect( + wrapper.find('FormGroup[label="LDAP User Flags By Group"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="LDAP Organization Map"]').length + ).toBe(1); + expect(wrapper.find('FormGroup[label="LDAP Team Map"]').length).toBe(1); + expect( + wrapper.find('FormGroup[fieldId="AUTH_LDAP_SERVER_URI"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[fieldId="AUTH_LDAP_5_SERVER_URI"]').length + ).toBe(0); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + AUTH_LDAP_BIND_DN: '', + AUTH_LDAP_BIND_PASSWORD: '', + AUTH_LDAP_CONNECTION_OPTIONS: { + OPT_NETWORK_TIMEOUT: 30, + OPT_REFERRALS: 0, + }, + AUTH_LDAP_DENY_GROUP: null, + AUTH_LDAP_GROUP_SEARCH: [], + AUTH_LDAP_GROUP_TYPE: 'MemberDNGroupType', + AUTH_LDAP_GROUP_TYPE_PARAMS: { + member_attr: 'member', + name_attr: 'cn', + }, + AUTH_LDAP_ORGANIZATION_MAP: {}, + AUTH_LDAP_REQUIRE_GROUP: null, + AUTH_LDAP_SERVER_URI: '', + AUTH_LDAP_START_TLS: false, + AUTH_LDAP_TEAM_MAP: {}, + AUTH_LDAP_USER_ATTR_MAP: {}, + AUTH_LDAP_USER_DN_TEMPLATE: null, + AUTH_LDAP_USER_FLAGS_BY_GROUP: {}, + AUTH_LDAP_USER_SEARCH: [], + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="AUTH_LDAP_BIND_PASSWORD"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper + .find( + 'FormGroup[fieldId="AUTH_LDAP_BIND_DN"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper.find('input#AUTH_LDAP_SERVER_URI').simulate('change', { + target: { + value: 'ldap://mock.example.com', + name: 'AUTH_LDAP_SERVER_URI', + }, + }); + wrapper.find('CodeMirrorInput#AUTH_LDAP_TEAM_MAP').invoke('onChange')( + '{\n"LDAP Sales":{\n"organization":\n"mock org"\n}\n}' + ); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + AUTH_LDAP_BIND_DN: '', + AUTH_LDAP_BIND_PASSWORD: '', + AUTH_LDAP_DENY_GROUP: '', + AUTH_LDAP_GROUP_SEARCH: [], + AUTH_LDAP_GROUP_TYPE: 'MemberDNGroupType', + AUTH_LDAP_GROUP_TYPE_PARAMS: { name_attr: 'cn', member_attr: 'member' }, + AUTH_LDAP_ORGANIZATION_MAP: {}, + AUTH_LDAP_REQUIRE_GROUP: 'CN=Tower Users,OU=Users,DC=example,DC=com', + AUTH_LDAP_SERVER_URI: 'ldap://mock.example.com', + AUTH_LDAP_START_TLS: false, + AUTH_LDAP_USER_ATTR_MAP: {}, + AUTH_LDAP_USER_DN_TEMPLATE: 'uid=%(user)s,OU=Users,DC=example,DC=com', + AUTH_LDAP_USER_FLAGS_BY_GROUP: {}, + AUTH_LDAP_USER_SEARCH: [], + AUTH_LDAP_TEAM_MAP: { + 'LDAP Sales': { + organization: 'mock org', + }, + }, + }); + }); + + test('should navigate to ldap default detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual('/settings/ldap/default/details'); + }); + + test('should navigate to ldap default detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual('/settings/ldap/default/details'); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); + + test('should display ldap category 5 edit form', async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/ldap/5/edit'], + }); + useRouteMatch.mockImplementation(() => ({ + url: '/settings/ldap/5/edit', + path: '/settings/ldap/:category/edit', + params: { category: '5' }, + })); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect( + wrapper.find('FormGroup[fieldId="AUTH_LDAP_SERVER_URI"]').length + ).toBe(0); + expect( + wrapper.find('FormGroup[fieldId="AUTH_LDAP_5_SERVER_URI"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[fieldId="AUTH_LDAP_5_SERVER_URI"] input').props() + .value + ).toEqual('ldap://ldap5.example.com'); + }); }); diff --git a/awx/ui_next/src/screens/Setting/License/LicenseDetail/LicenseDetail.jsx b/awx/ui_next/src/screens/Setting/License/LicenseDetail/LicenseDetail.jsx index 233fb40895..05f551c512 100644 --- a/awx/ui_next/src/screens/Setting/License/LicenseDetail/LicenseDetail.jsx +++ b/awx/ui_next/src/screens/Setting/License/LicenseDetail/LicenseDetail.jsx @@ -13,6 +13,7 @@ function LicenseDetail({ i18n }) { - + {isLoading && } + {!isLoading && error && } + {!isLoading && radius && ( + + {formik => ( +
+ + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} ); } -export default withI18n()(RADIUSEdit); +export default RADIUSEdit; diff --git a/awx/ui_next/src/screens/Setting/RADIUS/RADIUSEdit/RADIUSEdit.test.jsx b/awx/ui_next/src/screens/Setting/RADIUS/RADIUSEdit/RADIUSEdit.test.jsx index 934aeb3825..94ea2f9824 100644 --- a/awx/ui_next/src/screens/Setting/RADIUS/RADIUSEdit/RADIUSEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/RADIUS/RADIUSEdit/RADIUSEdit.test.jsx @@ -1,16 +1,149 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; import RADIUSEdit from './RADIUSEdit'; +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + RADIUS_SERVER: 'radius.mock.org', + RADIUS_PORT: 1812, + RADIUS_SECRET: '$encrypted$', + }, +}); + describe('', () => { let wrapper; - beforeEach(() => { - wrapper = mountWithContexts(); - }); + let history; + afterEach(() => { wrapper.unmount(); + jest.clearAllMocks(); }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/radius/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + test('initially renders without crashing', () => { expect(wrapper.find('RADIUSEdit').length).toBe(1); }); + + test('should display expected form fields', async () => { + expect(wrapper.find('FormGroup[label="RADIUS Server"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="RADIUS Port"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="RADIUS Secret"]').length).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + RADIUS_SERVER: '', + RADIUS_PORT: 1812, + RADIUS_SECRET: '', + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper.find('input#RADIUS_SERVER').simulate('change', { + target: { value: 'radius.new_mock.org', name: 'RADIUS_SERVER' }, + }); + wrapper + .find('FormGroup[fieldId="RADIUS_SECRET"] button[aria-label="Revert"]') + .invoke('onClick')(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + RADIUS_SERVER: 'radius.new_mock.org', + RADIUS_PORT: 1812, + RADIUS_SECRET: '', + }); + }); + + test('should navigate to radius detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual('/settings/radius/details'); + }); + + test('should navigate to radius detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual('/settings/radius/details'); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); }); diff --git a/awx/ui_next/src/screens/Setting/SAML/SAML.test.jsx b/awx/ui_next/src/screens/Setting/SAML/SAML.test.jsx index 0c662fd927..70d7ae5bb2 100644 --- a/awx/ui_next/src/screens/Setting/SAML/SAML.test.jsx +++ b/awx/ui_next/src/screens/Setting/SAML/SAML.test.jsx @@ -3,11 +3,31 @@ import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import { SettingsAPI } from '../../../api'; +import { SettingsProvider } from '../../../contexts/Settings'; +import mockAllOptions from '../shared/data.allSettingOptions.json'; import SAML from './SAML'; jest.mock('../../../api/models/Settings'); SettingsAPI.readCategory.mockResolvedValue({ - data: {}, + data: { + SOCIAL_AUTH_SAML_CALLBACK_URL: 'https://towerhost/sso/complete/saml/', + SOCIAL_AUTH_SAML_METADATA_URL: 'https://towerhost/sso/metadata/saml/', + SOCIAL_AUTH_SAML_SP_ENTITY_ID: '', + SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: '', + SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: '', + SOCIAL_AUTH_SAML_ORG_INFO: {}, + SOCIAL_AUTH_SAML_TECHNICAL_CONTACT: {}, + SOCIAL_AUTH_SAML_SUPPORT_CONTACT: {}, + SOCIAL_AUTH_SAML_ENABLED_IDPS: {}, + SOCIAL_AUTH_SAML_SECURITY_CONFIG: {}, + SOCIAL_AUTH_SAML_SP_EXTRA: {}, + SOCIAL_AUTH_SAML_EXTRA_DATA: [], + SOCIAL_AUTH_SAML_ORGANIZATION_MAP: {}, + SOCIAL_AUTH_SAML_TEAM_MAP: {}, + SOCIAL_AUTH_SAML_ORGANIZATION_ATTR: {}, + SOCIAL_AUTH_SAML_TEAM_ATTR: {}, + SAML_AUTO_CREATE_OBJECTS: false, + }, }); describe('', () => { @@ -23,9 +43,14 @@ describe('', () => { initialEntries: ['/settings/saml/details'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); expect(wrapper.find('SAMLDetail').length).toBe(1); }); @@ -35,9 +60,14 @@ describe('', () => { initialEntries: ['/settings/saml/edit'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); expect(wrapper.find('SAMLEdit').length).toBe(1); }); diff --git a/awx/ui_next/src/screens/Setting/SAML/SAMLDetail/SAMLDetail.jsx b/awx/ui_next/src/screens/Setting/SAML/SAMLDetail/SAMLDetail.jsx index 0a7fac5806..f8fb104ffb 100644 --- a/awx/ui_next/src/screens/Setting/SAML/SAMLDetail/SAMLDetail.jsx +++ b/awx/ui_next/src/screens/Setting/SAML/SAMLDetail/SAMLDetail.jsx @@ -18,6 +18,7 @@ import { SettingDetail } from '../../shared'; function SAMLDetail({ i18n }) { const { me } = useConfig(); const { GET: options } = useSettings(); + options.SOCIAL_AUTH_SAML_SP_PUBLIC_CERT.type = 'certificate'; const { isLoading, error, request, result: saml } = useRequest( useCallback(async () => { @@ -78,6 +79,7 @@ function SAMLDetail({ i18n }) { - + {isLoading && } + {!isLoading && error && } + {!isLoading && saml && ( + + {formik => ( +
+ + + + + + + + + + + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} ); } -export default withI18n()(SAMLEdit); +export default SAMLEdit; diff --git a/awx/ui_next/src/screens/Setting/SAML/SAMLEdit/SAMLEdit.test.jsx b/awx/ui_next/src/screens/Setting/SAML/SAMLEdit/SAMLEdit.test.jsx index d6319d9b2e..858bf814f7 100644 --- a/awx/ui_next/src/screens/Setting/SAML/SAMLEdit/SAMLEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/SAML/SAMLEdit/SAMLEdit.test.jsx @@ -1,16 +1,251 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; import SAMLEdit from './SAMLEdit'; +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SAML_AUTO_CREATE_OBJECTS: true, + SOCIAL_AUTH_SAML_CALLBACK_URL: 'https://towerhost/sso/complete/saml/', + SOCIAL_AUTH_SAML_METADATA_URL: 'https://towerhost/sso/metadata/saml/', + SOCIAL_AUTH_SAML_SP_ENTITY_ID: 'mock_id', + SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: 'mock_cert', + SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: '$encrypted$', + SOCIAL_AUTH_SAML_ORG_INFO: {}, + SOCIAL_AUTH_SAML_TECHNICAL_CONTACT: { + givenName: 'Mock User', + emailAddress: 'mockuser@example.com', + }, + SOCIAL_AUTH_SAML_SUPPORT_CONTACT: {}, + SOCIAL_AUTH_SAML_ENABLED_IDPS: {}, + SOCIAL_AUTH_SAML_SP_EXTRA: {}, + SOCIAL_AUTH_SAML_EXTRA_DATA: [], + SOCIAL_AUTH_SAML_ORGANIZATION_MAP: {}, + SOCIAL_AUTH_SAML_TEAM_MAP: {}, + SOCIAL_AUTH_SAML_ORGANIZATION_ATTR: {}, + SOCIAL_AUTH_SAML_TEAM_ATTR: {}, + SOCIAL_AUTH_SAML_SECURITY_CONFIG: { + requestedAuthnContext: false, + }, + }, +}); + describe('', () => { let wrapper; - beforeEach(() => { - wrapper = mountWithContexts(); - }); + let history; + afterEach(() => { wrapper.unmount(); + jest.clearAllMocks(); }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/saml/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + test('initially renders without crashing', () => { expect(wrapper.find('SAMLEdit').length).toBe(1); }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="SAML Service Provider Entity ID"]').length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="Automatically Create Organizations and Teams on SAML Login"]' + ).length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="SAML Service Provider Public Certificate"]' + ).length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="SAML Service Provider Private Key"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="SAML Service Provider Organization Info"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="SAML Service Provider Technical Contact"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="SAML Service Provider Support Contact"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="SAML Enabled Identity Providers"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="SAML Organization Map"]').length + ).toBe(1); + expect(wrapper.find('FormGroup[label="SAML Team Map"]').length).toBe(1); + expect( + wrapper.find('FormGroup[label="SAML Organization Attribute Mapping"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="SAML Team Attribute Mapping"]').length + ).toBe(1); + expect(wrapper.find('FormGroup[label="SAML Security Config"]').length).toBe( + 1 + ); + expect( + wrapper.find( + 'FormGroup[label="SAML Service Provider extra configuration data"]' + ).length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="SAML IDP to extra_data attribute mapping"]' + ).length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SAML_AUTO_CREATE_OBJECTS: true, + SOCIAL_AUTH_SAML_ENABLED_IDPS: {}, + SOCIAL_AUTH_SAML_EXTRA_DATA: null, + SOCIAL_AUTH_SAML_ORGANIZATION_ATTR: {}, + SOCIAL_AUTH_SAML_ORGANIZATION_MAP: null, + SOCIAL_AUTH_SAML_ORG_INFO: {}, + SOCIAL_AUTH_SAML_SP_ENTITY_ID: '', + SOCIAL_AUTH_SAML_SP_EXTRA: null, + SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: '', + SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: '', + SOCIAL_AUTH_SAML_SUPPORT_CONTACT: {}, + SOCIAL_AUTH_SAML_TEAM_ATTR: {}, + SOCIAL_AUTH_SAML_TEAM_MAP: null, + SOCIAL_AUTH_SAML_TECHNICAL_CONTACT: {}, + SOCIAL_AUTH_SAML_SECURITY_CONFIG: { + requestedAuthnContext: false, + }, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper.find('input#SOCIAL_AUTH_SAML_SP_ENTITY_ID').simulate('change', { + target: { value: 'new_id', name: 'SOCIAL_AUTH_SAML_SP_ENTITY_ID' }, + }); + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_SAML_TECHNICAL_CONTACT"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SAML_AUTO_CREATE_OBJECTS: true, + SOCIAL_AUTH_SAML_ENABLED_IDPS: {}, + SOCIAL_AUTH_SAML_EXTRA_DATA: [], + SOCIAL_AUTH_SAML_ORGANIZATION_ATTR: {}, + SOCIAL_AUTH_SAML_ORGANIZATION_MAP: {}, + SOCIAL_AUTH_SAML_ORG_INFO: {}, + SOCIAL_AUTH_SAML_SP_ENTITY_ID: 'new_id', + SOCIAL_AUTH_SAML_SP_EXTRA: {}, + SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: '$encrypted$', + SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: 'mock_cert', + SOCIAL_AUTH_SAML_SUPPORT_CONTACT: {}, + SOCIAL_AUTH_SAML_TEAM_ATTR: {}, + SOCIAL_AUTH_SAML_TEAM_MAP: {}, + SOCIAL_AUTH_SAML_TECHNICAL_CONTACT: {}, + SOCIAL_AUTH_SAML_SECURITY_CONFIG: { + requestedAuthnContext: false, + }, + }); + }); + + test('should navigate to saml detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual('/settings/saml/details'); + }); + + test('should navigate to saml detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual('/settings/saml/details'); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); }); diff --git a/awx/ui_next/src/screens/Setting/Settings.jsx b/awx/ui_next/src/screens/Setting/Settings.jsx index ae3356f950..8b9c2db334 100644 --- a/awx/ui_next/src/screens/Setting/Settings.jsx +++ b/awx/ui_next/src/screens/Setting/Settings.jsx @@ -5,7 +5,7 @@ import { t } from '@lingui/macro'; import { PageSection, Card } from '@patternfly/react-core'; import ContentError from '../../components/ContentError'; import ContentLoading from '../../components/ContentLoading'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; import ActivityStream from './ActivityStream'; import AzureAD from './AzureAD'; import GitHub from './GitHub'; @@ -57,6 +57,17 @@ function Settings({ i18n }) { '/settings/github/team': i18n._(t`GitHub Team`), '/settings/github/team/details': i18n._(t`Details`), '/settings/github/team/edit': i18n._(t`Edit Details`), + '/settings/github/enterprise': i18n._(t`GitHub Enterprise`), + '/settings/github/enterprise/details': i18n._(t`Details`), + '/settings/github/enterprise/edit': i18n._(t`Edit Details`), + '/settings/github/enterprise_organization': i18n._( + t`GitHub Enterprise Organization` + ), + '/settings/github/enterprise_organization/details': i18n._(t`Details`), + '/settings/github/enterprise_organization/edit': i18n._(t`Edit Details`), + '/settings/github/enterprise_team': i18n._(t`GitHub Enterprise Team`), + '/settings/github/enterprise_team/details': i18n._(t`Details`), + '/settings/github/enterprise_team/edit': i18n._(t`Edit Details`), '/settings/google_oauth2': i18n._(t`Google OAuth2`), '/settings/google_oauth2/details': i18n._(t`Details`), '/settings/google_oauth2/edit': i18n._(t`Edit Details`), @@ -129,7 +140,7 @@ function Settings({ i18n }) { return ( - + diff --git a/awx/ui_next/src/screens/Setting/Settings.test.jsx b/awx/ui_next/src/screens/Setting/Settings.test.jsx index 24a82986f8..3c63750e71 100644 --- a/awx/ui_next/src/screens/Setting/Settings.test.jsx +++ b/awx/ui_next/src/screens/Setting/Settings.test.jsx @@ -13,6 +13,9 @@ jest.mock('../../api/models/Settings'); SettingsAPI.readAllOptions.mockResolvedValue({ data: mockAllOptions, }); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); describe('', () => { let wrapper; diff --git a/awx/ui_next/src/screens/Setting/TACACS/TACACS.test.jsx b/awx/ui_next/src/screens/Setting/TACACS/TACACS.test.jsx index ec3c69aed0..ad8fcdbe61 100644 --- a/awx/ui_next/src/screens/Setting/TACACS/TACACS.test.jsx +++ b/awx/ui_next/src/screens/Setting/TACACS/TACACS.test.jsx @@ -2,12 +2,20 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { SettingsProvider } from '../../../contexts/Settings'; import { SettingsAPI } from '../../../api'; +import mockAllOptions from '../shared/data.allSettingOptions.json'; import TACACS from './TACACS'; jest.mock('../../../api/models/Settings'); SettingsAPI.readCategory.mockResolvedValue({ - data: {}, + data: { + TACACSPLUS_HOST: 'mockhost', + TACACSPLUS_PORT: 49, + TACACSPLUS_SECRET: '$encrypted$', + TACACSPLUS_SESSION_TIMEOUT: 5, + TACACSPLUS_AUTH_PROTOCOL: 'ascii', + }, }); describe('', () => { @@ -23,9 +31,14 @@ describe('', () => { initialEntries: ['/settings/tacacs/details'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); expect(wrapper.find('TACACSDetail').length).toBe(1); }); @@ -35,9 +48,14 @@ describe('', () => { initialEntries: ['/settings/tacacs/edit'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); expect(wrapper.find('TACACSEdit').length).toBe(1); }); diff --git a/awx/ui_next/src/screens/Setting/TACACS/TACACSDetail/TACACSDetail.jsx b/awx/ui_next/src/screens/Setting/TACACS/TACACSDetail/TACACSDetail.jsx index 6c9f1a85de..094dbf72bd 100644 --- a/awx/ui_next/src/screens/Setting/TACACS/TACACSDetail/TACACSDetail.jsx +++ b/awx/ui_next/src/screens/Setting/TACACS/TACACSDetail/TACACSDetail.jsx @@ -79,6 +79,7 @@ function TACACSDetail({ i18n }) { aria-label={i18n._(t`Edit`)} component={Link} to="/settings/tacacs/edit" + ouiaId="edit-button" > {i18n._(t`Edit`)} diff --git a/awx/ui_next/src/screens/Setting/TACACS/TACACSEdit/TACACSEdit.jsx b/awx/ui_next/src/screens/Setting/TACACS/TACACSEdit/TACACSEdit.jsx index 8ec22acb07..a45e59d069 100644 --- a/awx/ui_next/src/screens/Setting/TACACS/TACACSEdit/TACACSEdit.jsx +++ b/awx/ui_next/src/screens/Setting/TACACS/TACACSEdit/TACACSEdit.jsx @@ -1,25 +1,130 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { Button } from '@patternfly/react-core'; -import { CardBody, CardActionsRow } from '../../../../components/Card'; +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + ChoiceField, + EncryptedField, + InputField, +} from '../../shared/SharedFields'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function TACACSEdit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { isLoading, error, request: fetchTACACS, result: tacacs } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('tacacsplus'); + const mergedData = {}; + Object.keys(data).forEach(key => { + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchTACACS(); + }, [fetchTACACS]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/tacacs/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm(form); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(tacacs).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/tacacs/details'); + }; + + const initialValues = fields => + Object.keys(fields).reduce((acc, key) => { + acc[key] = fields[key].value ?? ''; + return acc; + }, {}); -function TACACSEdit({ i18n }) { return ( - {i18n._(t`Edit form coming soon :)`)} - - - + {isLoading && } + {!isLoading && error && } + {!isLoading && tacacs && ( + + {formik => ( +
+ + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )}
); } -export default withI18n()(TACACSEdit); +export default TACACSEdit; diff --git a/awx/ui_next/src/screens/Setting/TACACS/TACACSEdit/TACACSEdit.test.jsx b/awx/ui_next/src/screens/Setting/TACACS/TACACSEdit/TACACSEdit.test.jsx index 529090a34f..a62cdbc289 100644 --- a/awx/ui_next/src/screens/Setting/TACACS/TACACSEdit/TACACSEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/TACACS/TACACSEdit/TACACSEdit.test.jsx @@ -1,16 +1,166 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; import TACACSEdit from './TACACSEdit'; +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + TACACSPLUS_HOST: 'mockhost', + TACACSPLUS_PORT: 49, + TACACSPLUS_SECRET: '$encrypted$', + TACACSPLUS_SESSION_TIMEOUT: 123, + TACACSPLUS_AUTH_PROTOCOL: 'ascii', + }, +}); + describe('', () => { let wrapper; - beforeEach(() => { - wrapper = mountWithContexts(); - }); + let history; + afterEach(() => { wrapper.unmount(); + jest.clearAllMocks(); }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/tacacs/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + test('initially renders without crashing', () => { expect(wrapper.find('TACACSEdit').length).toBe(1); }); + + test('should display expected form fields', async () => { + expect(wrapper.find('FormGroup[label="TACACS+ Server"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="TACACS+ Port"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="TACACS+ Secret"]').length).toBe(1); + expect( + wrapper.find('FormGroup[label="TACACS+ Auth Session Timeout"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="TACACS+ Authentication Protocol"]').length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + TACACSPLUS_HOST: '', + TACACSPLUS_PORT: 49, + TACACSPLUS_SECRET: '', + TACACSPLUS_SESSION_TIMEOUT: 5, + TACACSPLUS_AUTH_PROTOCOL: 'ascii', + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper.find('input#TACACSPLUS_HOST').simulate('change', { + target: { value: 'new_host', name: 'TACACSPLUS_HOST' }, + }); + wrapper.find('input#TACACSPLUS_PORT').simulate('change', { + target: { value: 999, name: 'TACACSPLUS_PORT' }, + }); + wrapper + .find( + 'FormGroup[fieldId="TACACSPLUS_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + TACACSPLUS_HOST: 'new_host', + TACACSPLUS_PORT: 999, + TACACSPLUS_SECRET: '', + TACACSPLUS_SESSION_TIMEOUT: 123, + TACACSPLUS_AUTH_PROTOCOL: 'ascii', + }); + }); + + test('should navigate to tacacs detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual('/settings/tacacs/details'); + }); + + test('should navigate to tacacs detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual('/settings/tacacs/details'); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); }); diff --git a/awx/ui_next/src/screens/Setting/UI/UI.test.jsx b/awx/ui_next/src/screens/Setting/UI/UI.test.jsx index ac7a31d608..fc5aafadcd 100644 --- a/awx/ui_next/src/screens/Setting/UI/UI.test.jsx +++ b/awx/ui_next/src/screens/Setting/UI/UI.test.jsx @@ -6,11 +6,17 @@ import { waitForElement, } from '../../../../testUtils/enzymeHelpers'; import { SettingsAPI } from '../../../api'; +import { SettingsProvider } from '../../../contexts/Settings'; +import mockAllOptions from '../shared/data.allSettingOptions.json'; import UI from './UI'; jest.mock('../../../api/models/Settings'); SettingsAPI.readCategory.mockResolvedValue({ - data: {}, + data: { + CUSTOM_LOGIN_INFO: '', + CUSTOM_LOGO: '', + PENDO_TRACKING_STATE: 'off', + }, }); describe('', () => { @@ -26,9 +32,14 @@ describe('', () => { initialEntries: ['/settings/ui/details'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); expect(wrapper.find('UIDetail').length).toBe(1); @@ -39,9 +50,14 @@ describe('', () => { initialEntries: ['/settings/ui/edit'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); expect(wrapper.find('UIEdit').length).toBe(1); diff --git a/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx b/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx index ef458e5163..e65f176b83 100644 --- a/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx +++ b/awx/ui_next/src/screens/Setting/UI/UIDetail/UIDetail.jsx @@ -94,6 +94,7 @@ function UIDetail({ i18n }) { aria-label={i18n._(t`Edit`)} component={Link} to="/settings/ui/edit" + ouiaId="edit-button" > {i18n._(t`Edit`)} diff --git a/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.jsx b/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.jsx index c8d0f4df78..937c0c3e66 100644 --- a/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.jsx +++ b/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.jsx @@ -1,25 +1,128 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { Button } from '@patternfly/react-core'; -import { CardBody, CardActionsRow } from '../../../../components/Card'; +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + ChoiceField, + FileUploadField, + TextAreaField, +} from '../../shared/SharedFields'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function UIEdit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { isLoading, error, request: fetchUI, result: uiData } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('ui'); + const mergedData = {}; + Object.keys(data).forEach(key => { + if (!options[key]) { + return; + } + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchUI(); + }, [fetchUI]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/ui/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm(form); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(uiData).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/ui/details'); + }; -function UIEdit({ i18n }) { return ( - {i18n._(t`Edit form coming soon :)`)} - - - + {formik => ( +
+ + {uiData?.PENDO_TRACKING_STATE?.value !== 'off' && ( + + )} + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} + + )}
); } -export default withI18n()(UIEdit); +export default UIEdit; diff --git a/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.test.jsx b/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.test.jsx index c51fb06fa7..adb43a788c 100644 --- a/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.test.jsx +++ b/awx/ui_next/src/screens/Setting/UI/UIEdit/UIEdit.test.jsx @@ -1,16 +1,151 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; import UIEdit from './UIEdit'; +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + CUSTOM_LOGIN_INFO: 'mock info', + CUSTOM_LOGO: 'data:mock/jpeg;', + PENDO_TRACKING_STATE: 'detailed', + }, +}); + describe('', () => { let wrapper; - beforeEach(() => { - wrapper = mountWithContexts(); - }); + let history; + afterEach(() => { wrapper.unmount(); + jest.clearAllMocks(); }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/ui/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + test('initially renders without crashing', () => { expect(wrapper.find('UIEdit').length).toBe(1); }); + + test('should display expected form fields', async () => { + expect(wrapper.find('FormGroup[label="Custom Login Info"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Custom Logo"]').length).toBe(1); + expect( + wrapper.find('FormGroup[label="User Analytics Tracking State"]').length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + CUSTOM_LOGIN_INFO: '', + CUSTOM_LOGO: '', + PENDO_TRACKING_STATE: 'off', + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper.find('textarea#CUSTOM_LOGIN_INFO').simulate('change', { + target: { value: 'new login info', name: 'CUSTOM_LOGIN_INFO' }, + }); + wrapper + .find('FormGroup[fieldId="CUSTOM_LOGO"] button[aria-label="Revert"]') + .invoke('onClick')(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + CUSTOM_LOGIN_INFO: 'new login info', + CUSTOM_LOGO: '', + PENDO_TRACKING_STATE: 'detailed', + }); + }); + + test('should navigate to ui detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual('/settings/ui/details'); + }); + + test('should navigate to ui detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual('/settings/ui/details'); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); }); diff --git a/awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx b/awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx index a1c3903f53..4ec68182fc 100644 --- a/awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx +++ b/awx/ui_next/src/screens/Setting/shared/LoggingTestAlert.jsx @@ -33,6 +33,7 @@ function LoggingTestAlert({ i18n, successResponse, errorResponse, onClose }) { {testMessage && ( } + ouiaId="logging-test-alert" title={successResponse ? i18n._(t`Success`) : i18n._(t`Error`)} variant={successResponse ? 'success' : 'danger'} > diff --git a/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx b/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx index 55b23437b9..46ed00e8d6 100644 --- a/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx +++ b/awx/ui_next/src/screens/Setting/shared/RevertAllAlert.jsx @@ -11,12 +11,14 @@ function RevertAllAlert({ i18n, onClose, onRevertAll }) { title={i18n._(t`Revert settings`)} variant="info" onClose={onClose} + ouiaId="revert-all-modal" actions={[ , @@ -25,6 +27,7 @@ function RevertAllAlert({ i18n, onClose, onRevertAll }) { variant="secondary" aria-label={i18n._(t`Cancel revert`)} onClick={onClose} + ouiaId="cancel-revert-all-button" > {i18n._(t`Cancel`)} , diff --git a/awx/ui_next/src/screens/Setting/shared/RevertButton.jsx b/awx/ui_next/src/screens/Setting/shared/RevertButton.jsx index f0fe7e8b72..a997388395 100644 --- a/awx/ui_next/src/screens/Setting/shared/RevertButton.jsx +++ b/awx/ui_next/src/screens/Setting/shared/RevertButton.jsx @@ -13,7 +13,13 @@ const ButtonWrapper = styled.div` } `; -function RevertButton({ i18n, id, defaultValue, isDisabled = false }) { +function RevertButton({ + i18n, + id, + defaultValue, + isDisabled = false, + onRevertCallback = () => null, +}) { const [field, meta, helpers] = useField(id); const initialValue = meta.initialValue ?? ''; const currentValue = field.value; @@ -30,6 +36,7 @@ function RevertButton({ i18n, id, defaultValue, isDisabled = false }) { function handleConfirm() { helpers.setValue(isRevertable ? defaultValue : initialValue); + onRevertCallback(); } const revertTooltipContent = isRevertable diff --git a/awx/ui_next/src/screens/Setting/shared/RevertFormActionGroup.jsx b/awx/ui_next/src/screens/Setting/shared/RevertFormActionGroup.jsx index 03e80d4eb1..1e707090e2 100644 --- a/awx/ui_next/src/screens/Setting/shared/RevertFormActionGroup.jsx +++ b/awx/ui_next/src/screens/Setting/shared/RevertFormActionGroup.jsx @@ -20,6 +20,7 @@ const RevertFormActionGroup = ({ variant="primary" type="button" onClick={onSubmit} + ouiaId="save-button" > {i18n._(t`Save`)} @@ -28,6 +29,7 @@ const RevertFormActionGroup = ({ variant="secondary" type="button" onClick={onRevert} + ouiaId="revert-all-button" > {i18n._(t`Revert all to default`)} @@ -37,6 +39,7 @@ const RevertFormActionGroup = ({ variant="secondary" type="button" onClick={onCancel} + ouiaId="cancel-button" > {i18n._(t`Cancel`)} diff --git a/awx/ui_next/src/screens/Setting/shared/SettingDetail.jsx b/awx/ui_next/src/screens/Setting/shared/SettingDetail.jsx index ddcb5fb44d..d58c89e721 100644 --- a/awx/ui_next/src/screens/Setting/shared/SettingDetail.jsx +++ b/awx/ui_next/src/screens/Setting/shared/SettingDetail.jsx @@ -34,6 +34,18 @@ export default withI18n()( /> ); break; + case 'certificate': + detail = ( + + ); + break; case 'image': detail = ( ( } @@ -84,6 +90,7 @@ const BooleanField = withI18n()( > { + const validate = isRequired ? required(null, i18n) : null; + const [field, meta] = useField({ name, validate }); + const isValid = !(meta.touched && meta.error); + + return config ? ( + +