From caab75121529d8068b4dcc83f2b2181dbb3e699e Mon Sep 17 00:00:00 2001 From: JoelKle <34544090+JoelKle@users.noreply.github.com> Date: Tue, 19 May 2020 09:26:03 +0200 Subject: [PATCH 001/123] Added the ability, to set the broadcast_websocket_secret variable. This is nessesary if you would like to rerun the playbook. Signed-off-by: JoelKle <34544090+JoelKle@users.noreply.github.com> --- installer/inventory | 5 +++++ installer/roles/kubernetes/tasks/main.yml | 1 + installer/roles/local_docker/tasks/main.yml | 1 + 3 files changed, 7 insertions(+) diff --git a/installer/inventory b/installer/inventory index 85a875eb0a..de001730eb 100644 --- a/installer/inventory +++ b/installer/inventory @@ -117,6 +117,11 @@ create_preload_data=True # your credentials secret_key=awxsecret +# By default a broadcast websocket secret will be generated. +# If you would like to *rerun the playbook*, you need to set a unique password. +# Otherwise it would generate a new one every playbook run. +# broadcast_websocket_secret= + # Build AWX with official logos # Requires cloning awx-logos repo as a sibling of this project. # Review the trademark guidelines at https://github.com/ansible/awx-logos/blob/master/TRADEMARKS.md diff --git a/installer/roles/kubernetes/tasks/main.yml b/installer/roles/kubernetes/tasks/main.yml index bb4065f211..6a9d9a2deb 100644 --- a/installer/roles/kubernetes/tasks/main.yml +++ b/installer/roles/kubernetes/tasks/main.yml @@ -4,6 +4,7 @@ broadcast_websocket_secret: "{{ lookup('password', '/dev/null', length=128) }}" run_once: true no_log: true + when: broadcast_websocket_secret is not defined - fail: msg: "Only set one of kubernetes_context or openshift_host" diff --git a/installer/roles/local_docker/tasks/main.yml b/installer/roles/local_docker/tasks/main.yml index ad87f16fb4..aab1260a36 100644 --- a/installer/roles/local_docker/tasks/main.yml +++ b/installer/roles/local_docker/tasks/main.yml @@ -4,6 +4,7 @@ broadcast_websocket_secret: "{{ lookup('password', '/dev/null', length=128) }}" run_once: true no_log: true + when: broadcast_websocket_secret is not defined - import_tasks: upgrade_postgres.yml when: From 699f1868904a6def505bf8b825c63b1dc3bc3c3e Mon Sep 17 00:00:00 2001 From: JoelKle <34544090+JoelKle@users.noreply.github.com> Date: Tue, 19 May 2020 09:26:30 +0200 Subject: [PATCH 002/123] Fixed a bug, where the redis.conf first would be stored with mod 0600 and in the next task changed to 0666. This has broke the ability to rerun the playbook. Signed-off-by: JoelKle <34544090+JoelKle@users.noreply.github.com> --- .../roles/local_docker/tasks/compose.yml | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/installer/roles/local_docker/tasks/compose.yml b/installer/roles/local_docker/tasks/compose.yml index 120b81cc1a..9a95ddabc3 100644 --- a/installer/roles/local_docker/tasks/compose.yml +++ b/installer/roles/local_docker/tasks/compose.yml @@ -12,22 +12,22 @@ - name: Create Docker Compose Configuration template: - src: "{{ item }}.j2" - dest: "{{ docker_compose_dir }}/{{ item }}" - mode: 0600 - with_items: - - environment.sh - - credentials.py - - docker-compose.yml - - nginx.conf - - redis.conf + src: "{{ item.file }}.j2" + dest: "{{ docker_compose_dir }}/{{ item.file }}" + mode: "{{ item.mode }}" + loop: + - file: environment.sh + mode: "0600" + - file: credentials.py + mode: "0600" + - file: docker-compose.yml + mode: "0600" + - file: nginx.conf + mode: "0600" + - file: redis.conf + mode: "0664" register: awx_compose_config -- name: Set redis config to other group readable to satisfy redis-server - file: - path: "{{ docker_compose_dir }}/redis.conf" - mode: 0666 - - name: Render SECRET_KEY file copy: content: "{{ secret_key }}" From 58da3df03ea20215a1cd445b3e6963ec79d41b09 Mon Sep 17 00:00:00 2001 From: Stefan Jakobs Date: Wed, 22 Jul 2020 14:00:05 +0200 Subject: [PATCH 003/123] Change Dockerfile to copy custom venv --- docs/custom_virtualenvs.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/custom_virtualenvs.md b/docs/custom_virtualenvs.md index 9b47c5d5ca..78bde90758 100644 --- a/docs/custom_virtualenvs.md +++ b/docs/custom_virtualenvs.md @@ -83,15 +83,17 @@ index aa8b304..eb05f91 100644 + virtualenv /opt/my-envs/my-custom-env + /opt/my-envs/my-custom-env/bin/pip install psutil + -diff --git a/installer/image_build/templates/Dockerfile.j2 b/installer/image_build/templates/Dockerfile.j2 -index d69e2c9..a08bae5 100644 ---- a/installer/image_build/templates/Dockerfile.j2 -+++ b/installer/image_build/templates/Dockerfile.j2 -@@ -34,6 +34,7 @@ RUN yum -y install epel-release && \ - pip install virtualenv supervisor && \ - VENV_BASE=/var/lib/awx/venv make requirements_ansible && \ - VENV_BASE=/var/lib/awx/venv make requirements_awx && \ -+ VENV_BASE=/var/lib/awx/venv make requirements_custom && \ +diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 +index d3b582ffcb..220ac760a3 100644 +--- a/installer/roles/image_build/templates/Dockerfile.j2 ++++ b/installer/roles/image_build/templates/Dockerfile.j2 +@@ -165,6 +165,7 @@ RUN openssl req -nodes -newkey rsa:2048 -keyout /etc/nginx/nginx.key -out /etc/n + chmod 640 /etc/nginx/nginx.{csr,key,crt} + {% else %} + COPY --from=builder /var/lib/awx /var/lib/awx ++COPY --from=builder /opt/my-envs /opt/my-envs + RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage + {% endif %} ``` Once the AWX API is available, update the `CUSTOM_VENV_PATHS` setting as described in `Preparing a New Custom Virtualenv`. From 02252f3f97083891ae3f2d91c0e8b3c9f5130e11 Mon Sep 17 00:00:00 2001 From: Andrew Gaffney Date: Sat, 11 Jul 2020 13:53:30 +0000 Subject: [PATCH 004/123] ARM image build support * upgrade `chromedriver` for ARM support * upgrade `pynacl` to fix `libsodium` build issue on ARM * remove unnecessary i686-specific `libstdc++.so.6` package * install `kubectl` and `tini` from upstream binaries for ARM support * use upstream `postgres` and `alpine` docker images for `postgresql` helm chart Fixes #7051 --- awx/ui/package-lock.json | 340 +++++++++++++++--- awx/ui/package.json | 2 +- installer/roles/image_build/defaults/main.yml | 4 + .../roles/image_build/templates/Dockerfile.j2 | 15 +- .../templates/postgresql-values.yml.j2 | 12 +- requirements/requirements_ansible.txt | 2 +- 6 files changed, 314 insertions(+), 61 deletions(-) diff --git a/awx/ui/package-lock.json b/awx/ui/package-lock.json index 24b7837312..9f6c2322e1 100644 --- a/awx/ui/package-lock.json +++ b/awx/ui/package-lock.json @@ -4,6 +4,28 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "14.0.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.26.tgz", + "integrity": "sha512-W+fpe5s91FBGE0pEa0lnqGLL4USgpLgs4nokw16SrBBco/gQxuua7KnArSEOd5iaMqbbSHV10vUDkJYJJqpXKA==", + "dev": true + }, "@uirouter/angularjs": { "version": "1.0.18", "resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.18.tgz", @@ -740,7 +762,8 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", - "dev": true + "dev": true, + "optional": true }, "axios": { "version": "0.16.2", @@ -2299,16 +2322,215 @@ "dev": true }, "chromedriver": { - "version": "2.40.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-2.40.0.tgz", - "integrity": "sha512-ewvRQ1HMk0vpFSWYCk5hKDoEz5QMPplx5w3C6/Me+03y1imr67l3Hxl9U0jn3mu2N7+c7BoC7JtNW6HzbRAwDQ==", + "version": "77.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-77.0.0.tgz", + "integrity": "sha512-mZa1IVx4HD8rDaItWbnS470mmypgiWsDiu98r0NkiT4uLm3qrANl4vOU6no6vtWtLQiW5kt1POcIbjeNpsLbXA==", "dev": true, "requires": { - "del": "^3.0.0", + "del": "^4.1.1", "extract-zip": "^1.6.7", - "kew": "^0.7.0", "mkdirp": "^0.5.1", - "request": "^2.87.0" + "request": "^2.88.0", + "tcp-port-used": "^1.0.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "aws4": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "dev": true + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "requires": { + "mime-db": "1.44.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "cipher-base": { @@ -6458,16 +6680,6 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", "dev": true }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "dev": true, - "requires": { - "ajv": "^5.1.0", - "har-schema": "^2.0.0" - } - }, "hard-source-webpack-plugin": { "version": "0.5.18", "resolved": "https://registry.npmjs.org/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.5.18.tgz", @@ -7349,6 +7561,12 @@ "integrity": "sha1-x+NWzeoiWucbNtcPLnGpK6TkJZA=", "dev": true }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, "ipaddr.js": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", @@ -7658,6 +7876,12 @@ "unc-path-regex": "^0.1.0" } }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -7676,6 +7900,17 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "dev": true }, + "is2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", + "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "ip-regex": "^2.1.0", + "is-url": "^1.2.2" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -8173,12 +8408,6 @@ } } }, - "kew": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", - "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", - "dev": true - }, "killable": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", @@ -9826,7 +10055,8 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -11196,6 +11426,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, "public-encrypt": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", @@ -11816,34 +12052,6 @@ "is-finite": "^1.0.0" } }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", - "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", - "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -13171,6 +13379,33 @@ "xtend": "^4.0.0" } }, + "tcp-port-used": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.1.tgz", + "integrity": "sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q==", + "dev": true, + "requires": { + "debug": "4.1.0", + "is2": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", + "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "test-exclude": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", @@ -13355,6 +13590,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", "dev": true, + "optional": true, "requires": { "punycode": "^1.4.1" } diff --git a/awx/ui/package.json b/awx/ui/package.json index 59cb964b5c..e546bfc891 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -43,7 +43,7 @@ "babel-loader": "^7.1.2", "babel-plugin-istanbul": "^4.1.5", "babel-preset-env": "^1.6.0", - "chromedriver": "^2.35.0", + "chromedriver": "^77.0.0", "clean-webpack-plugin": "^0.1.16", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.5", diff --git a/installer/roles/image_build/defaults/main.yml b/installer/roles/image_build/defaults/main.yml index 2618c9b40d..ab152975ce 100644 --- a/installer/roles/image_build/defaults/main.yml +++ b/installer/roles/image_build/defaults/main.yml @@ -1,3 +1,7 @@ --- create_preload_data: true build_dev: false + +# Helper vars to construct the proper download URL for the current architecture +tini_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm" }[ansible_facts.architecture] }}' +kubectl_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm" }[ansible_facts.architecture] }}' diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 index ecded902ea..c16b4b4a2c 100644 --- a/installer/roles/image_build/templates/Dockerfile.j2 +++ b/installer/roles/image_build/templates/Dockerfile.j2 @@ -35,7 +35,6 @@ RUN dnf -y update && \ glibc-langpack-en \ libcurl-devel \ libffi-devel \ - libstdc++.so.6 \ libtool-ltdl-devel \ make \ nodejs \ @@ -115,7 +114,6 @@ RUN dnf -y install \ # Install runtime requirements RUN dnf -y update && \ - dnf -y install https://github.com/krallin/tini/releases/download/v0.18.0/tini_0.18.0.rpm && \ dnf -y install epel-release 'dnf-command(config-manager)' && \ dnf module -y enable 'postgresql:10' && \ dnf config-manager --set-enabled PowerTools && \ @@ -140,12 +138,17 @@ RUN dnf -y update && \ vim-minimal \ which \ xmlsec1-openssl && \ - dnf -y --repofrompath gcloud,https://packages.cloud.google.com/yum/repos/cloud-sdk-el8-x86_64 \ - --setopt gcloud.gpgkey=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg \ - install kubectl && \ dnf -y install centos-release-stream && dnf -y install "rsyslog >= 8.1911.0" && dnf -y remove centos-release-stream && \ dnf -y clean all +# Install kubectl +RUN curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.17.8/bin/linux/{{ kubectl_architecture | default('amd64') }}/kubectl && \ + chmod a+x /usr/bin/kubectl + +# Install tini +RUN curl -L -o /usr/bin/tini https://github.com/krallin/tini/releases/download/v0.19.0/tini-{{ tini_architecture | default('amd64') }} && \ + chmod +x /usr/bin/tini + RUN python3 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if build_dev|bool %}flake8{% endif %} RUN rm -rf /root/.cache && rm -rf /tmp/* @@ -244,7 +247,7 @@ CMD ["/bin/bash"] USER 1000 EXPOSE 8052 -ENTRYPOINT ["tini", "--"] +ENTRYPOINT ["/usr/bin/tini", "--"] CMD /usr/bin/launch_awx.sh VOLUME /var/lib/nginx {% endif %} diff --git a/installer/roles/kubernetes/templates/postgresql-values.yml.j2 b/installer/roles/kubernetes/templates/postgresql-values.yml.j2 index 658b898505..ea6ba29230 100644 --- a/installer/roles/kubernetes/templates/postgresql-values.yml.j2 +++ b/installer/roles/kubernetes/templates/postgresql-values.yml.j2 @@ -34,12 +34,22 @@ master: {{ affinity | to_nice_yaml(indent=2) | indent(width=4, indentfirst=True) }} {% endif %} {% endif %} -{% if pg_image_registry is defined %} image: +{% if pg_image_registry is defined %} registry: {{ pg_image_registry }} +{% endif %} + # The default bitnami image from the chart doesn't work on ARM + repository: postgres + tag: '11' volumePermissions: image: +{% if pg_image_registry is defined %} registry: {{ pg_image_registry }} +{% endif %} + # The default bitnami image from the chart doesn't work on ARM + repository: alpine + tag: '3' +{% if pg_image_registry is defined %} metrics: image: registry: {{ pg_image_registry }} diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index 55db209e4e..5bdbaadd67 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -92,7 +92,7 @@ pycurl==7.43.0.1 # via -r /awx_devel/requirements/requirements_ansible. pygments==2.5.2 # via azure-cli-core, knack pyjwt==1.7.1 # via adal, azure-cli-core pykerberos==1.2.1 # via requests-kerberos -pynacl==1.3.0 # via paramiko +pynacl==1.4.0 # via paramiko pyopenssl==19.1.0 # via azure-cli-core, requests-credssp pyparsing==2.4.5 # via packaging python-dateutil==2.8.1 # via adal, azure-storage, botocore, kubernetes From 579604d2c666c9fc24ec9065b18fb74a6bf4f138 Mon Sep 17 00:00:00 2001 From: Rigel Di Scala Date: Wed, 29 Jul 2020 15:27:45 +0200 Subject: [PATCH 005/123] Allow YAML as a CLI import format This changset allows the import of YAML formatted resources. The CLI user can indicate which format to use with the `-f, --format` option. The CLI help text has been amended to reflect the new feature. The AWX CLI `export` subcommand offers the option of formatting the output as YAML or JSON, so it makes sense that the `import` subcommand reflects this. A simple test is also provided. In order to ease the task of testing commands that import resources by reading the stdin, the CLI has been extended to allow specifying an alternative file descriptor for stdin, similarly to stdout and stderr. --- awxkit/awxkit/cli/client.py | 3 ++- awxkit/awxkit/cli/format.py | 4 ++-- awxkit/awxkit/cli/resource.py | 11 +++++++++-- awxkit/test/cli/test_format.py | 26 ++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/awxkit/awxkit/cli/client.py b/awxkit/awxkit/cli/client.py index 8013523921..3feded89dc 100755 --- a/awxkit/awxkit/cli/client.py +++ b/awxkit/awxkit/cli/client.py @@ -70,9 +70,10 @@ class CLI(object): subparsers = {} original_action = None - def __init__(self, stdout=sys.stdout, stderr=sys.stderr): + def __init__(self, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin): self.stdout = stdout self.stderr = stderr + self.stdin = stdin def get_config(self, key): """Helper method for looking up the value of a --conf.xyz flag""" diff --git a/awxkit/awxkit/cli/format.py b/awxkit/awxkit/cli/format.py index e8c5a6903e..d35c61efbb 100644 --- a/awxkit/awxkit/cli/format.py +++ b/awxkit/awxkit/cli/format.py @@ -40,7 +40,7 @@ def add_authentication_arguments(parser, env): def add_output_formatting_arguments(parser, env): - formatting = parser.add_argument_group('output formatting') + formatting = parser.add_argument_group('input/output formatting') formatting.add_argument( '-f', @@ -49,7 +49,7 @@ def add_output_formatting_arguments(parser, env): choices=FORMATTERS.keys(), default=env.get('TOWER_FORMAT', 'json'), help=( - 'specify an output format' + 'specify a format for the input and output' ), ) formatting.add_argument( diff --git a/awxkit/awxkit/cli/resource.py b/awxkit/awxkit/cli/resource.py index f22795fab2..0f9f24dbae 100644 --- a/awxkit/awxkit/cli/resource.py +++ b/awxkit/awxkit/cli/resource.py @@ -1,8 +1,9 @@ +import yaml import json import os -import sys from awxkit import api, config +from awxkit.exceptions import ImportExportError from awxkit.utils import to_str from awxkit.api.pages import Page from awxkit.api.pages.api import EXPORTABLE_RESOURCES @@ -135,7 +136,13 @@ class Import(CustomCommand): parser.print_help() raise SystemExit() - data = json.load(sys.stdin) + format = getattr(client.args, 'conf.format') + if format == 'json': + data = json.load(client.stdin) + elif format == 'yaml': + data = yaml.safe_load(client.stdin) + else: + raise ImportExportError("Unsupported format for Import: " + format) client.authenticate() client.v2.import_assets(data) diff --git a/awxkit/test/cli/test_format.py b/awxkit/test/cli/test_format.py index 7166fb841c..5ab6e55d6c 100644 --- a/awxkit/test/cli/test_format.py +++ b/awxkit/test/cli/test_format.py @@ -1,10 +1,13 @@ +import io import json import yaml from awxkit.api.pages import Page from awxkit.api.pages.users import Users, User +from awxkit.cli import CLI from awxkit.cli.format import format_response +from awxkit.cli.resource import Import def test_json_empty_list(): @@ -44,3 +47,26 @@ def test_yaml_list(): page = Users.from_json(users) formatted = format_response(page, fmt='yaml') assert yaml.safe_load(formatted) == users + + +def test_yaml_import(): + class MockedV2: + def import_assets(self, data): + self._parsed_data = data + + def _dummy_authenticate(): + pass + + yaml_fd = io.StringIO( + """ + workflow_job_templates: + - name: Workflow1 + """ + ) + cli = CLI(stdin=yaml_fd) + cli.parse_args(['--conf.format', 'yaml']) + cli.v2 = MockedV2() + cli.authenticate = _dummy_authenticate + + Import().handle(cli, None) + assert cli.v2._parsed_data['workflow_job_templates'][0]['name'] From 5e9d372db2ad8218266dc18abf579cae71b9d96e Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 11 Aug 2020 09:32:09 -0400 Subject: [PATCH 006/123] update to a newer python-ldap to address a bug see: https://github.com/ansible/awx/issues/7868 --- CHANGELOG.md | 3 +++ requirements/requirements.in | 1 + requirements/requirements.txt | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a53e50174..63473b431e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/`. +## 14.1.0 (TBD) +- Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 + ## 14.0.0 (Aug 6, 2020) - As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to: * https://github.com/ansible/awx/commit/78229f58715fbfbf88177e54031f532543b57acc diff --git a/requirements/requirements.in b/requirements/requirements.in index cc194454a7..f8126fb081 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -36,6 +36,7 @@ pygerduty pyparsing python-radius python3-saml +python-ldap>=3.3.1 # https://github.com/python-ldap/python-ldap/issues/270 pyyaml>=5.3.1 # minimum version to pull in new pyyaml for CVE-2017-18342 schedule==0.6.0 social-auth-core==3.3.1 # see UPGRADE BLOCKERs diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8820e46b29..8408960f28 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -93,7 +93,7 @@ pyrad==2.3 # via django-radius pyrsistent==0.15.7 # via jsonschema python-daemon==2.2.4 # via ansible-runner python-dateutil==2.8.1 # via adal, kubernetes -python-ldap==3.2.0 # via django-auth-ldap +python-ldap==3.3.1 # via -r /awx_devel/requirements/requirements.in, django-auth-ldap python-radius==1.0 # via -r /awx_devel/requirements/requirements.in python-string-utils==1.0.0 # via openshift python3-openid==3.1.0 # via social-auth-core From 987c7d48a05de967f3dc4110afa070d74f902fc3 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 11 Aug 2020 09:37:12 -0400 Subject: [PATCH 007/123] minor cleanup up CLI import -f yaml support --- awxkit/awxkit/cli/resource.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/awxkit/awxkit/cli/resource.py b/awxkit/awxkit/cli/resource.py index 0f9f24dbae..8e30accad2 100644 --- a/awxkit/awxkit/cli/resource.py +++ b/awxkit/awxkit/cli/resource.py @@ -136,13 +136,13 @@ class Import(CustomCommand): parser.print_help() raise SystemExit() - format = getattr(client.args, 'conf.format') - if format == 'json': + fmt = client.get_config('format') + if fmt == 'json': data = json.load(client.stdin) - elif format == 'yaml': + elif fmt == 'yaml': data = yaml.safe_load(client.stdin) else: - raise ImportExportError("Unsupported format for Import: " + format) + raise ImportExportError("Unsupported format for Import: " + fmt) client.authenticate() client.v2.import_assets(data) From 2a0c61de630b35fd952f7574321eead45b700c83 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 11 Aug 2020 10:04:14 -0400 Subject: [PATCH 008/123] Change regex to match what is in source --- awx_collection/tools/roles/template_galaxy/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tools/roles/template_galaxy/tasks/main.yml b/awx_collection/tools/roles/template_galaxy/tasks/main.yml index 414f55b7de..96eb26413c 100644 --- a/awx_collection/tools/roles/template_galaxy/tasks/main.yml +++ b/awx_collection/tools/roles/template_galaxy/tasks/main.yml @@ -2,7 +2,7 @@ - name: Set the collection version in the tower_api.py file replace: path: "{{ collection_path }}/plugins/module_utils/tower_api.py" - regexp: '^ _COLLECTION_VERSION = "devel"' + regexp: '^ _COLLECTION_VERSION = "0.0.1-devel"' replace: ' _COLLECTION_VERSION = "{{ collection_version }}"' when: - "awx_template_version | default(True)" From e3fe680d14100e8bc9d54b7547b2ae9eac044609 Mon Sep 17 00:00:00 2001 From: nixocio Date: Mon, 10 Aug 2020 14:47:01 -0400 Subject: [PATCH 009/123] Add feature to edit instance group Add feature to edit instance group. See: https://github.com/ansible/awx/issues/7767 --- .../InstanceGroupEdit/InstanceGroupEdit.jsx | 40 ++++- .../InstanceGroupEdit.test.jsx | 140 ++++++++++++++++++ .../shared/InstanceGroupForm.jsx | 9 +- 3 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx index 2f724479ee..b2f9bbaa9a 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx @@ -1,13 +1,37 @@ -import React from 'react'; -import { Card, PageSection } from '@patternfly/react-core'; +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { CardBody } from '../../../components/Card'; +import { InstanceGroupsAPI } from '../../../api'; +import InstanceGroupForm from '../shared/InstanceGroupForm'; + +function InstanceGroupEdit({ instanceGroup }) { + const history = useHistory(); + const [submitError, setSubmitError] = useState(null); + const detailsUrl = `/instance_groups/${instanceGroup.id}/details`; + + const handleSubmit = async values => { + try { + await InstanceGroupsAPI.update(instanceGroup.id, values); + history.push(detailsUrl); + } catch (error) { + setSubmitError(error); + } + }; + + const handleCancel = () => { + history.push(detailsUrl); + }; -function InstanceGroupEdit() { return ( - - -
Edit instance group
-
-
+ + + ); } diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx new file mode 100644 index 0000000000..45b94bd17d --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { InstanceGroupsAPI } from '../../../api'; + +import InstanceGroupEdit from './InstanceGroupEdit'; + +jest.mock('../../../api'); + +const instanceGroupData = { + id: 42, + type: 'instance_group', + url: '/api/v2/instance_groups/42/', + related: { + jobs: '/api/v2/instance_groups/42/jobs/', + instances: '/api/v2/instance_groups/7/instances/', + }, + name: 'Foo', + created: '2020-07-21T18:41:02.818081Z', + modified: '2020-07-24T20:32:03.121079Z', + capacity: 24, + committed_capacity: 0, + consumed_capacity: 0, + percent_capacity_remaining: 100.0, + jobs_running: 0, + jobs_total: 0, + instances: 1, + controller: null, + is_controller: false, + is_isolated: false, + is_containerized: false, + credential: null, + policy_instance_percentage: 46, + policy_instance_minimum: 12, + policy_instance_list: [], + pod_spec_override: '', + summary_fields: { + user_capabilities: { + edit: true, + delete: true, + }, + }, +}; + +const updatedInstanceGroup = { + name: 'Bar', + policy_instance_percentage: 42, +}; + +describe('', () => { + let wrapper; + let history; + + beforeAll(async () => { + history = createMemoryHistory(); + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('tower instance group name can not be updated', async () => { + let towerWrapper; + await act(async () => { + towerWrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + expect( + towerWrapper.find('input#instance-group-name').prop('disabled') + ).toBeTruthy(); + expect( + towerWrapper.find('input#instance-group-name').prop('value') + ).toEqual('tower'); + }); + + test('handleSubmit should call the api and redirect to details page', async () => { + await act(async () => { + wrapper.find('InstanceGroupForm').invoke('onSubmit')( + updatedInstanceGroup + ); + }); + expect(InstanceGroupsAPI.update).toHaveBeenCalledWith( + 42, + updatedInstanceGroup + ); + }); + + test('should navigate to instance group details when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); + }); + expect(history.location.pathname).toEqual('/instance_groups/42/details'); + }); + + test('should navigate to instance group details after successful submission', async () => { + await act(async () => { + wrapper.find('InstanceGroupForm').invoke('onSubmit')( + updatedInstanceGroup + ); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(history.location.pathname).toEqual('/instance_groups/42/details'); + }); + + test('failed form submission should show an error message', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + InstanceGroupsAPI.update.mockImplementationOnce(() => + Promise.reject(error) + ); + await act(async () => { + wrapper.find('InstanceGroupForm').invoke('onSubmit')( + updatedInstanceGroup + ); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx b/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx index a2477d2f53..2f55092394 100644 --- a/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { func, shape } from 'prop-types'; -import { Formik } from 'formik'; +import { Formik, useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Form } from '@patternfly/react-core'; @@ -11,21 +11,24 @@ import { required, minMaxValue } from '../../../util/validators'; import { FormColumnLayout } from '../../../components/FormLayout'; function InstanceGroupFormFields({ i18n }) { + const [instanceGroupNameField, ,] = useField('name'); return ( <> Date: Mon, 20 Jul 2020 16:27:38 -0700 Subject: [PATCH 010/123] start notification template list --- .../NotificationTemplate.jsx | 5 + .../NotificationTemplateAdd.jsx | 5 + .../NotificationTemplateList.jsx | 170 ++++++++++++++++++ .../NotificationTemplateListItem.jsx | 45 +++++ .../NotificationTemplateList/index.js | 4 + .../NotificationTemplates.jsx | 62 ++++--- 6 files changed, 270 insertions(+), 21 deletions(-) create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx new file mode 100644 index 0000000000..d271962a40 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function NotificationTemplate() { + return
; +} diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx new file mode 100644 index 0000000000..bbf39b61a9 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function NotificationTemplateAdd() { + return
; +} diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx new file mode 100644 index 0000000000..50d0f3f400 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx @@ -0,0 +1,170 @@ +import React, { useCallback, useEffect } from 'react'; +import { useLocation, useRouteMatch } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Card, PageSection } from '@patternfly/react-core'; +import { NotificationTemplatesAPI } from '../../../api'; +import PaginatedDataList, { + ToolbarAddButton, + ToolbarDeleteButton, +} from '../../../components/PaginatedDataList'; +import AlertModal from '../../../components/AlertModal'; +import ErrorDetail from '../../../components/ErrorDetail'; +import DataListToolbar from '../../../components/DataListToolbar'; +import NotificationTemplateListItem from './NotificationTemplateListItem'; +import useRequest, { useDeleteItems } from '../../../util/useRequest'; +import useSelected from '../../../util/useSelected'; +import { getQSConfig, parseQueryString } from '../../../util/qs'; + +const QS_CONFIG = getQSConfig('notification-templates', { + page: 1, + page_size: 20, + order_by: 'name', +}); + +function NotificationTemplatesList({ i18n }) { + const location = useLocation(); + const match = useRouteMatch(); + + const addUrl = `${match.url}/add`; + + const { + result: { templates, count, actions }, + error: contentError, + isLoading: isTemplatesLoading, + request: fetchTemplates, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const responses = await Promise.all([ + NotificationTemplatesAPI.read(params), + NotificationTemplatesAPI.readOptions(), + ]); + return { + templates: responses[0].data.results, + count: responses[0].data.count, + actions: responses[1].data.actions, + }; + }, [location]), + { + templates: [], + count: 0, + actions: {}, + } + ); + + useEffect(() => { + fetchTemplates(); + }, [fetchTemplates]); + + const { selected, isAllSelected, handleSelect, setSelected } = useSelected( + templates + ); + + const { + isLoading: isDeleteLoading, + deleteItems: deleteTemplates, + deletionError, + clearDeletionError, + } = useDeleteItems( + useCallback(async () => { + return Promise.all( + selected.map(({ id }) => NotificationTemplatesAPI.destroy(id)) + ); + }, [selected]), + { + qsConfig: QS_CONFIG, + allItemsSelected: isAllSelected, + fetchItems: fetchTemplates, + } + ); + + const handleDelete = async () => { + await deleteTemplates(); + setSelected([]); + }; + + const canAdd = actions && actions.POST; + + return ( + <> + + + ( + setSelected([...templates])} + qsConfig={QS_CONFIG} + additionalControls={[ + ...(canAdd + ? [] + : []), + , + ]} + /> + )} + renderItem={template => ( + row.id === template.id)} + onSelect={() => handleSelect(template)} + /> + )} + emptyStateControls={ + canAdd ? : null + } + /> + + + + {i18n._(t`Failed to delete one or more organizations.`)} + + + + ); +} + +export default withI18n()(NotificationTemplatesList); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx new file mode 100644 index 0000000000..4bf773b4f8 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Link } from 'react-router-dom'; +import { + Badge as PFBadge, + Button, + DataListAction as _DataListAction, + DataListCheck, + DataListItem, + DataListItemCells, + DataListItemRow, + Tooltip, +} from '@patternfly/react-core'; +import DataListCell from '../../../components/DataListCell'; + +export default function NotificationTemplatesListItem({ + template, + detailUrl, + isSelected, + onSelect, +}) { + const labelId = `check-action-${template.id}`; + return ( + + + + + + {template.name} + + , + ]} + /> + + + ); +} diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js new file mode 100644 index 0000000000..06c347d889 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js @@ -0,0 +1,4 @@ +import NotificationTemplatesList from './NotificationTemplateList'; + +export default NotificationTemplatesList; +export { default as NotificationTemplatesListItem } from './NotificationTemplateListItem'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx index 857201bc6b..6d828175a0 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx @@ -1,28 +1,48 @@ -import React, { Component, Fragment } from 'react'; +import React, { useState } from 'react'; +import { Route, Switch, useRouteMatch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - PageSection, - PageSectionVariants, - Title, -} from '@patternfly/react-core'; +import NotificationTemplateList from './NotificationTemplateList'; +import NotificationTemplateAdd from './NotificationTemplateAdd'; +import NotificationTemplate from './NotificationTemplate'; +import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; -class NotificationTemplates extends Component { - render() { - const { i18n } = this.props; - const { light } = PageSectionVariants; +function NotificationTemplates({ i18n }) { + const match = useRouteMatch(); + const [breadcrumbConfig, setBreadcrumbConfig] = useState({ + '/notification_templates': i18n._(t`Notification Templates`), + '/notification_templates/add': i18n._(t`Create New Notification Template`), + }); - return ( - - - - {i18n._(t`Notification Templates`)} - - - - - ); - } + const updateBreadcrumbConfig = notification => { + const { id } = notification; + setBreadcrumbConfig({ + '/notification_templates': i18n._(t`Notification Templates`), + '/notification_templates/add': i18n._( + t`Create New Notification Template` + ), + [`/notification_templates/${id}`]: notification.name, + [`/notification_templates/${id}/edit`]: i18n._(t`Edit Details`), + [`/notification_templates/${id}/details`]: i18n._(t`Details`), + }); + }; + + return ( + <> + + + + + + + + + + + + + + ); } export default withI18n()(NotificationTemplates); From 182dce3dc34bf807def911a611b1e1cce55c6f70 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Tue, 21 Jul 2020 16:39:58 -0700 Subject: [PATCH 011/123] flushing out notification template detail --- .../NotificationTemplate.jsx | 59 ++++++++++++++++++- .../NotificationTemplateDetail.jsx | 48 +++++++++++++++ .../NotificationTemplateDetail/index.js | 1 + .../NotificationTemplateList.jsx | 12 ++-- .../NotificationTemplateListItem.jsx | 50 +++++++++++++++- .../OrganizationList/OrganizationListItem.jsx | 16 ++--- .../TemplateList/TemplateListItem.jsx | 8 +-- 7 files changed, 165 insertions(+), 29 deletions(-) create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx index d271962a40..5182ba645a 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx @@ -1,5 +1,58 @@ -import React from 'react'; +import React, { useEffect, useCallback } from 'react'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; +import { Card, PageSection } from '@patternfly/react-core'; +import { Link, useParams } from 'react-router-dom'; +import useRequest from '../../util/useRequest'; +import ContentError from '../../components/ContentError'; +import { NotificationTemplatesAPI } from '../../api'; +import NotificationTemplateDetail from './NotificationTemplateDetail'; -export default function NotificationTemplate() { - return
; +function NotificationTemplate({ i18n, setBreadcrumb }) { + const { id: templateId } = useParams(); + const { + result: template, + isLoading, + error, + request: fetchTemplate, + } = useRequest( + useCallback(async () => { + const { data } = await NotificationTemplatesAPI.readDetail(templateId); + return data; + }, [templateId]), + null + ); + + useEffect(() => { + fetchTemplate(); + }, [fetchTemplate]); + + if (error) { + return ( + + + + {error.response.status === 404 && ( + + {i18n._(t`Notification Template not found.`)}{' '} + + {i18n._(t`View all Notification Templates.`)} + + + )} + + + + ); + } + + return ( + + + + + + ); } + +export default withI18n()(NotificationTemplate); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx new file mode 100644 index 0000000000..5c8a592a1c --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -0,0 +1,48 @@ +import React, { Fragment, useState, useEffect, useCallback } from 'react'; +import { Link, useHistory, useParams } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { + Button, + Chip, + TextList, + TextListItem, + TextListItemVariants, + TextListVariants, + Label, +} from '@patternfly/react-core'; +import { t } from '@lingui/macro'; + +import AlertModal from '../../../components/AlertModal'; +import { CardBody, CardActionsRow } from '../../../components/Card'; +import ChipGroup from '../../../components/ChipGroup'; +import ContentError from '../../../components/ContentError'; +import ContentLoading from '../../../components/ContentLoading'; +import CredentialChip from '../../../components/CredentialChip'; +import { + Detail, + DetailList, + DeletedDetail, + UserDateDetail, +} from '../../../components/DetailList'; +import DeleteButton from '../../../components/DeleteButton'; +import ErrorDetail from '../../../components/ErrorDetail'; +import LaunchButton from '../../../components/LaunchButton'; +import { VariablesDetail } from '../../../components/CodeMirrorInput'; +import { JobTemplatesAPI } from '../../../api'; +import useRequest, { useDismissableError } from '../../../util/useRequest'; + +function NotificationTemplateDetail({ i18n, template }) { + return ( + + + + + + ); +} + +export default withI18n()(NotificationTemplateDetail); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js new file mode 100644 index 0000000000..431403014d --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js @@ -0,0 +1 @@ +export default from './NotificationTemplateDetail'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx index 50d0f3f400..3dac16f6a2 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx @@ -105,12 +105,8 @@ function NotificationTemplatesList({ i18n }) { isDefault: true, }, { - name: i18n._(t`Created By (Username)`), - key: 'created_by__username', - }, - { - name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + name: i18n._(t`Type`), + key: 'notification_type', }, ]} toolbarSortColumns={[ @@ -118,6 +114,10 @@ function NotificationTemplatesList({ i18n }) { name: i18n._(t`Name`), key: 'name', }, + { + name: i18n._(t`Type`), + key: 'notification_type', + }, ]} renderToolbar={props => ( {}; + const labelId = `template-name-${template.id}`; + return ( @@ -37,9 +48,42 @@ export default function NotificationTemplatesListItem({ {template.name} , + + {template.notification_type} + , ]} /> + + {template.summary_fields.user_capabilities.edit ? ( + + + + ) : ( +
+ )} + + + + ); } + +export default withI18n()(NotificationTemplateListItem); diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx index 51f78c173c..37d01a9e0a 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx @@ -62,12 +62,10 @@ function OrganizationListItem({ /> - - - {organization.name} - - + + + {organization.name} + , @@ -85,11 +83,7 @@ function OrganizationListItem({ , ]} /> - + {organization.summary_fields.user_capabilities.edit ? ( + )} + {summary_fields.user_capabilities && + summary_fields.user_capabilities.delete && ( + + {i18n._(t`Delete`)} + + )} + + {error && ( + + {i18n._(t`Failed to delete notification.`)} + + + )} ); } diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx new file mode 100644 index 0000000000..b089b6b89f --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; +import { CardBody } from '../../../components/Card'; +import { OrganizationsAPI } from '../../../api'; +import { Config } from '../../../contexts/Config'; + +import NotificationTemplateForm from '../shared/NotificationTemplateForm'; + +function NotificationTemplateEdit({ template }) { + const detailsUrl = `/notification_templates/${template.id}/details`; + const history = useHistory(); + const [formError, setFormError] = useState(null); + + const handleSubmit = async ( + values, + groupsToAssociate, + groupsToDisassociate + ) => { + try { + await OrganizationsAPI.update(template.id, values); + await Promise.all( + groupsToAssociate.map(id => + OrganizationsAPI.associateInstanceGroup(template.id, id) + ) + ); + await Promise.all( + groupsToDisassociate.map(id => + OrganizationsAPI.disassociateInstanceGroup(template.id, id) + ) + ); + history.push(detailsUrl); + } catch (error) { + setFormError(error); + } + }; + + const handleCancel = () => { + history.push(detailsUrl); + }; + + return ( + + + {({ me }) => ( + + )} + + + ); +} + +NotificationTemplateEdit.propTypes = { + template: PropTypes.shape().isRequired, +}; + +NotificationTemplateEdit.contextTypes = { + custom_virtualenvs: PropTypes.arrayOf(PropTypes.string), +}; + +export { NotificationTemplateEdit as _NotificationTemplateEdit }; +export default NotificationTemplateEdit; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/index.js new file mode 100644 index 0000000000..be9b40a69c --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/index.js @@ -0,0 +1,3 @@ +import NotificationTemplateEdit from './NotificationTemplateEdit'; + +export default NotificationTemplateEdit; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx index bf2ea92fb8..f5bf5f8be4 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -14,6 +14,7 @@ import { } from '@patternfly/react-core'; import { PencilAltIcon, BellIcon } from '@patternfly/react-icons'; import DataListCell from '../../../components/DataListCell'; +import { NOTIFICATION_TYPES } from '../constants'; const DataListAction = styled(_DataListAction)` align-items: center; @@ -49,7 +50,8 @@ function NotificationTemplateListItem({ , - {template.notification_type} + {NOTIFICATION_TYPES[template.notification_type] || + template.notification_type} , ]} /> diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js index 06c347d889..335e76dd6c 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js @@ -1,4 +1,4 @@ -import NotificationTemplatesList from './NotificationTemplateList'; +import NotificationTemplateList from './NotificationTemplateList'; -export default NotificationTemplatesList; -export { default as NotificationTemplatesListItem } from './NotificationTemplateListItem'; +export default NotificationTemplateList; +export { default as NotificationTemplateListItem } from './NotificationTemplateListItem'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/constants.js b/awx/ui_next/src/screens/NotificationTemplate/constants.js new file mode 100644 index 0000000000..5937e48743 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/constants.js @@ -0,0 +1,12 @@ +/* eslint-disable-next-line import/prefer-default-export */ +export const NOTIFICATION_TYPES = { + email: 'Email', + grafana: 'Grafana', + irc: 'IRC', + mattermost: 'Mattermost', + pagerduty: 'Pagerduty', + rocketchat: 'Rocket.Chat', + slack: 'Slack', + twilio: 'Twilio', + webhook: 'Webhook', +}; diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx new file mode 100644 index 0000000000..c08caaa3e5 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx @@ -0,0 +1,3 @@ +export default function NotificationTemplateForm() { + // +} From 1405f6ca51624081af40707b61947108e7865981 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 6 Aug 2020 11:37:23 -0700 Subject: [PATCH 014/123] add notification status indicator --- .../components/StatusLabel/StatusLabel.jsx | 67 +++++++++++++++++++ .../src/components/StatusLabel/index.js | 1 + .../NotificationTemplateListItem.jsx | 28 +++++--- 3 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 awx/ui_next/src/components/StatusLabel/StatusLabel.jsx create mode 100644 awx/ui_next/src/components/StatusLabel/index.js diff --git a/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx new file mode 100644 index 0000000000..95ba558cea --- /dev/null +++ b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx @@ -0,0 +1,67 @@ +import 'styled-components/macro'; +import React from 'react'; +import { oneOf } from 'prop-types'; +import { Label } from '@patternfly/react-core'; +import { + CheckCircleIcon, + ExclamationCircleIcon, + SyncAltIcon, + ExclamationTriangleIcon, + ClockIcon, +} from '@patternfly/react-icons'; +import styled, { keyframes } from 'styled-components'; + +const Spin = keyframes` + from { + transform: rotate(0); + } + to { + transform: rotate(1turn); + } +`; + +const RunningIcon = styled(SyncAltIcon)` + animation: ${Spin} 1.75s linear infinite; +`; + +const colors = { + success: 'green', + failed: 'red', + error: 'red', + running: 'blue', + pending: 'blue', + waiting: 'grey', + canceled: 'orange', +}; +const icons = { + success: CheckCircleIcon, + failed: ExclamationCircleIcon, + error: ExclamationCircleIcon, + running: RunningIcon, + pending: ClockIcon, + waiting: ClockIcon, + canceled: ExclamationTriangleIcon, +}; + +export default function StatusLabel({ status }) { + const label = status.charAt(0).toUpperCase() + status.slice(1); + const color = colors[status] || 'grey'; + const Icon = icons[status]; + + return ( + + ); +} + +StatusLabel.propTypes = { + status: oneOf([ + 'success', + 'failed', + 'error', + 'running', + 'pending', + 'canceled', + ]).isRequired, +}; diff --git a/awx/ui_next/src/components/StatusLabel/index.js b/awx/ui_next/src/components/StatusLabel/index.js new file mode 100644 index 0000000000..b9dfc8cd99 --- /dev/null +++ b/awx/ui_next/src/components/StatusLabel/index.js @@ -0,0 +1 @@ +export { default } from './StatusLabel'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx index f5bf5f8be4..102e4b9777 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -1,3 +1,4 @@ +import 'styled-components/macro'; import React from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -14,6 +15,7 @@ import { } from '@patternfly/react-core'; import { PencilAltIcon, BellIcon } from '@patternfly/react-icons'; import DataListCell from '../../../components/DataListCell'; +import StatusLabel from '../../../components/StatusLabel'; import { NOTIFICATION_TYPES } from '../constants'; const DataListAction = styled(_DataListAction)` @@ -33,6 +35,8 @@ function NotificationTemplateListItem({ const sendTestNotification = () => {}; const labelId = `template-name-${template.id}`; + const lastNotification = template.summary_fields?.recent_notifications[0]; + return ( @@ -49,13 +53,28 @@ function NotificationTemplateListItem({ {template.name} , + + {lastNotification && ( + + )} + , + {i18n._(t`Type`)} {NOTIFICATION_TYPES[template.notification_type] || template.notification_type} , ]} /> + + + {template.summary_fields.user_capabilities.edit ? ( )} - - - From 8bb1c985c05340c7566a6b7ba9ae196abe3277f2 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Fri, 7 Aug 2020 11:28:01 -0700 Subject: [PATCH 015/123] send test notifications --- .../src/api/models/NotificationTemplates.js | 4 +++ .../components/StatusLabel/StatusLabel.jsx | 1 + .../NotificationTemplateListItem.jsx | 32 +++++++++++++++---- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/awx/ui_next/src/api/models/NotificationTemplates.js b/awx/ui_next/src/api/models/NotificationTemplates.js index 7736921ad2..69cd5f4022 100644 --- a/awx/ui_next/src/api/models/NotificationTemplates.js +++ b/awx/ui_next/src/api/models/NotificationTemplates.js @@ -5,6 +5,10 @@ class NotificationTemplates extends Base { super(http); this.baseUrl = '/api/v2/notification_templates/'; } + + test(id) { + return this.http.post(`${this.baseUrl}${id}/test/`); + } } export default NotificationTemplates; diff --git a/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx index 95ba558cea..0f2be56fdc 100644 --- a/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx +++ b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx @@ -62,6 +62,7 @@ StatusLabel.propTypes = { 'error', 'running', 'pending', + 'waiting', 'canceled', ]).isRequired, }; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx index 102e4b9777..ed26638ed6 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -1,5 +1,5 @@ import 'styled-components/macro'; -import React from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; @@ -14,8 +14,10 @@ import { Tooltip, } from '@patternfly/react-core'; import { PencilAltIcon, BellIcon } from '@patternfly/react-icons'; +import { NotificationTemplatesAPI } from '../../../api'; import DataListCell from '../../../components/DataListCell'; import StatusLabel from '../../../components/StatusLabel'; +import useRequest from '../../../util/useRequest'; import { NOTIFICATION_TYPES } from '../constants'; const DataListAction = styled(_DataListAction)` @@ -32,10 +34,27 @@ function NotificationTemplateListItem({ onSelect, i18n, }) { - const sendTestNotification = () => {}; - const labelId = `template-name-${template.id}`; + const latestStatus = template.summary_fields?.recent_notifications[0]?.status; + const [status, setStatus] = useState(latestStatus); - const lastNotification = template.summary_fields?.recent_notifications[0]; + useEffect(() => { + setStatus(latestStatus); + }, [latestStatus]); + + const { request: sendTestNotification, isLoading, error } = useRequest( + useCallback(() => { + NotificationTemplatesAPI.test(template.id); + setStatus('pending'); + }, [template.id]) + ); + + useEffect(() => { + if (error) { + setStatus('error'); + } + }, [error]); + + const labelId = `template-name-${template.id}`; return ( @@ -54,9 +73,7 @@ function NotificationTemplateListItem({ , - {lastNotification && ( - - )} + {status && } , {i18n._(t`Type`)} @@ -71,6 +88,7 @@ function NotificationTemplateListItem({ aria-label={i18n._(t`Test Notification`)} variant="plain" onClick={sendTestNotification} + disabled={isLoading} > From 4c555815b35fefb2bb7d914e08395deca2cb50b8 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 10 Aug 2020 16:23:54 -0700 Subject: [PATCH 016/123] add notification list tests --- .../StatusLabel/StatusLabel.test.jsx | 61 ++++++ .../NotificationTemplateDetail.jsx | 20 +- .../NotificationTemplateList.test.jsx | 202 ++++++++++++++++++ .../NotificationTemplateListItem.jsx | 11 +- .../NotificationTemplateListItem.test.jsx | 64 ++++++ .../NotificationTemplates.test.jsx | 6 - 6 files changed, 336 insertions(+), 28 deletions(-) create mode 100644 awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx diff --git a/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx b/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx new file mode 100644 index 0000000000..58fb6c1a28 --- /dev/null +++ b/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import StatusLabel from './StatusLabel'; + +describe('StatusLabel', () => { + test('should render success', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('CheckCircleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('green'); + expect(wrapper.text()).toEqual('Success'); + }); + + test('should render failed', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('red'); + expect(wrapper.text()).toEqual('Failed'); + }); + + test('should render error', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('red'); + expect(wrapper.text()).toEqual('Error'); + }); + + test('should render running', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('SyncAltIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('blue'); + expect(wrapper.text()).toEqual('Running'); + }); + + test('should render pending', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ClockIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('blue'); + expect(wrapper.text()).toEqual('Pending'); + }); + + test('should render waiting', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ClockIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('grey'); + expect(wrapper.text()).toEqual('Waiting'); + }); + + test('should render canceled', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ExclamationTriangleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('orange'); + expect(wrapper.text()).toEqual('Canceled'); + }); +}); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx index 18f45a4c21..d7f37f9fab 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -1,33 +1,17 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { Link, useHistory } from 'react-router-dom'; import { withI18n } from '@lingui/react'; -import { - Button, - Chip, - TextList, - TextListItem, - TextListItemVariants, - TextListVariants, - Label, -} from '@patternfly/react-core'; +import { Button } from '@patternfly/react-core'; import { t } from '@lingui/macro'; - import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; -import ChipGroup from '../../../components/ChipGroup'; -import ContentError from '../../../components/ContentError'; -import ContentLoading from '../../../components/ContentLoading'; -import CredentialChip from '../../../components/CredentialChip'; import { Detail, DetailList, DeletedDetail, - UserDateDetail, } from '../../../components/DetailList'; import DeleteButton from '../../../components/DeleteButton'; import ErrorDetail from '../../../components/ErrorDetail'; -import LaunchButton from '../../../components/LaunchButton'; -import { VariablesDetail } from '../../../components/CodeMirrorInput'; import { NotificationTemplatesAPI } from '../../../api'; import useRequest, { useDismissableError } from '../../../util/useRequest'; import { NOTIFICATION_TYPES } from '../constants'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx new file mode 100644 index 0000000000..d39bffe087 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx @@ -0,0 +1,202 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { OrganizationsAPI } from '../../../api'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import NotificationTemplateList from './NotificationTemplateList'; + +jest.mock('../../../api'); + +const mockTemplates = { + data: { + count: 3, + results: [ + { + name: 'Boston', + id: 1, + url: '/notification_templates/1', + type: 'slack', + summary_fields: { + recent_notifications: [ + { + status: 'success', + }, + ], + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + name: 'Minneapolis', + id: 2, + url: '/notification_templates/2', + summary_fields: { + recent_notifications: [], + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + name: 'Philidelphia', + id: 3, + url: '/notification_templates/3', + summary_fields: { + recent_notifications: [ + { + status: 'failed', + }, + { + status: 'success', + }, + ], + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + ], + }, +}; + +describe('', () => { + let wrapper; + beforeEach(() => { + OrganizationsAPI.read.mockResolvedValue(mockTemplates); + OrganizationsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + }, + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should load notifications', async () => { + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1); + expect(wrapper.find('NotificationTemplateListItem').length).toBe(3); + }); + + test('should select item', async () => { + const itemCheckboxInput = 'input#select-template-1'; + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(false); + await act(async () => { + wrapper + .find(itemCheckboxInput) + .closest('DataListCheck') + .props() + .onChange(); + }); + wrapper.update(); + expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(true); + }); + + test('should delete notifications', async () => { + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1); + await act(async () => { + wrapper + .find('Checkbox#select-all') + .props() + .onChange(true); + }); + wrapper.update(); + await act(async () => { + wrapper.find('button[aria-label="Delete"]').simulate('click'); + wrapper.update(); + }); + const deleteButton = global.document.querySelector( + 'body div[role="dialog"] button[aria-label="confirm delete"]' + ); + expect(deleteButton).not.toEqual(null); + await act(async () => { + deleteButton.click(); + }); + expect(OrganizationsAPI.destroy).toHaveBeenCalledTimes(3); + expect(OrganizationsAPI.read).toHaveBeenCalledTimes(2); + }); + + test('should show error dialog shown for failed deletion', async () => { + const itemCheckboxInput = 'input#select-template-1'; + OrganizationsAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/organizations/1', + }, + data: 'An error occurred', + }, + }) + ); + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + await act(async () => { + wrapper + .find(itemCheckboxInput) + .closest('DataListCheck') + .props() + .onChange(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('button[aria-label="Delete"]').simulate('click'); + wrapper.update(); + }); + const deleteButton = global.document.querySelector( + 'body div[role="dialog"] button[aria-label="confirm delete"]' + ); + expect(deleteButton).not.toEqual(null); + await act(async () => { + deleteButton.click(); + }); + wrapper.update(); + + const modal = wrapper.find('Modal'); + expect(modal.prop('isOpen')).toEqual(true); + expect(modal.prop('title')).toEqual('Error!'); + }); + + test('should show add button', async () => { + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(wrapper.find('ToolbarAddButton').length).toBe(1); + }); + + test('should hide add button (rbac)', async () => { + OrganizationsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + }, + }, + }); + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(wrapper.find('ToolbarAddButton').length).toBe(0); + }); +}); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx index ed26638ed6..0087e7f9a8 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -34,7 +34,10 @@ function NotificationTemplateListItem({ onSelect, i18n, }) { - const latestStatus = template.summary_fields?.recent_notifications[0]?.status; + const recentNotifications = template.summary_fields?.recent_notifications; + const latestStatus = recentNotifications + ? recentNotifications[0]?.status + : null; const [status, setStatus] = useState(latestStatus); useEffect(() => { @@ -44,7 +47,7 @@ function NotificationTemplateListItem({ const { request: sendTestNotification, isLoading, error } = useRequest( useCallback(() => { NotificationTemplatesAPI.test(template.id); - setStatus('pending'); + setStatus('running'); }, [template.id]) ); @@ -72,11 +75,11 @@ function NotificationTemplateListItem({ {template.name} , - + {status && } , - {i18n._(t`Type`)} + {i18n._(t`Type:`)}{' '} {NOTIFICATION_TYPES[template.notification_type] || template.notification_type} , diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx new file mode 100644 index 0000000000..5a4566779e --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { NotificationTemplatesAPI } from '../../../api'; +import NotificationTemplateListItem from './NotificationTemplateListItem'; + +jest.mock('../../../api/models/NotificationTemplates'); + +const template = { + id: 3, + notification_type: 'slack', + name: 'Test Notification', + summary_fields: { + user_capabilities: { + edit: true, + }, + recent_notifications: [ + { + status: 'success', + }, + ], + }, +}; + +describe('', () => { + test('should render template row', () => { + const wrapper = mountWithContexts( + + ); + + const cells = wrapper.find('DataListCell'); + expect(cells).toHaveLength(3); + expect(cells.at(0).text()).toEqual('Test Notification'); + expect(cells.at(1).text()).toEqual('Success'); + expect(cells.at(2).text()).toEqual('Type: Slack'); + }); + + test('should send test notification', async () => { + NotificationTemplatesAPI.test.mockResolvedValue({}); + + const wrapper = mountWithContexts( + + ); + await act(async () => { + wrapper + .find('Button') + .at(0) + .invoke('onClick')(); + }); + expect(NotificationTemplatesAPI.test).toHaveBeenCalledTimes(1); + expect( + wrapper + .find('DataListCell') + .at(1) + .text() + ).toEqual('Running'); + }); +}); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx index 93babc8e06..9333850cf9 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx @@ -1,18 +1,14 @@ import React from 'react'; - import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; - import NotificationTemplates from './NotificationTemplates'; describe('', () => { let pageWrapper; let pageSections; - let title; beforeEach(() => { pageWrapper = mountWithContexts(); pageSections = pageWrapper.find('PageSection'); - title = pageWrapper.find('Title'); }); afterEach(() => { @@ -22,8 +18,6 @@ describe('', () => { test('initially renders without crashing', () => { expect(pageWrapper.length).toBe(1); expect(pageSections.length).toBe(2); - expect(title.length).toBe(1); - expect(title.props().size).toBe('2xl'); expect(pageSections.first().props().variant).toBe('light'); }); }); From 65d4c347c9152ee11efa3a5457017b914f3c1506 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 10 Aug 2020 16:48:29 -0700 Subject: [PATCH 017/123] add ObjectDetails for HTTP Headers display --- .../components/DetailList/ObjectDetail.jsx | 51 +++++++++++++++++++ .../src/components/DetailList/index.js | 1 + .../NotificationTemplateDetail.jsx | 6 ++- 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 awx/ui_next/src/components/DetailList/ObjectDetail.jsx diff --git a/awx/ui_next/src/components/DetailList/ObjectDetail.jsx b/awx/ui_next/src/components/DetailList/ObjectDetail.jsx new file mode 100644 index 0000000000..bf008866a8 --- /dev/null +++ b/awx/ui_next/src/components/DetailList/ObjectDetail.jsx @@ -0,0 +1,51 @@ +import 'styled-components/macro'; +import React from 'react'; +import { shape, node, number } from 'prop-types'; +import { TextListItemVariants } from '@patternfly/react-core'; +import { DetailName, DetailValue } from './Detail'; +import CodeMirrorInput from '../CodeMirrorInput'; + +function ObjectDetail({ value, label, rows, fullHeight }) { + return ( + <> + +
+ + {label} + +
+
+ + + + + ); +} +ObjectDetail.propTypes = { + value: shape.isRequired, + label: node.isRequired, + rows: number, +}; +ObjectDetail.defaultProps = { + rows: null, +}; + +export default ObjectDetail; diff --git a/awx/ui_next/src/components/DetailList/index.js b/awx/ui_next/src/components/DetailList/index.js index 6a12824bad..f16ed0e292 100644 --- a/awx/ui_next/src/components/DetailList/index.js +++ b/awx/ui_next/src/components/DetailList/index.js @@ -3,3 +3,4 @@ export { default as Detail, DetailName, DetailValue } from './Detail'; export { default as DeletedDetail } from './DeletedDetail'; export { default as UserDateDetail } from './UserDateDetail'; export { default as DetailBadge } from './DetailBadge'; +export { default as ObjectDetail } from './ObjectDetail'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx index d7f37f9fab..24c199836f 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -9,6 +9,7 @@ import { Detail, DetailList, DeletedDetail, + ObjectDetail, } from '../../../components/DetailList'; import DeleteButton from '../../../components/DeleteButton'; import ErrorDetail from '../../../components/ErrorDetail'; @@ -310,11 +311,12 @@ function NotificationTemplateDetail({ i18n, template }) { value={configuration.http_method} dataCy="nt-detail-webhook-http-method" /> - {/* */} + /> )} From 5fb1b1ceea9512c75a5e70e7e9027b533574ec57 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 11 Aug 2020 12:52:52 -0400 Subject: [PATCH 018/123] changelog updates for recent additions --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63473b431e..f482fe3a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,11 @@ This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/`. ## 14.1.0 (TBD) +- Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 +- Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847 - Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 +- Upgraded git-python to fix a bug that caused workflows to sometimes fail - https://github.com/ansible/awx/issues/6119 +- Fixed a bug in the AWX CLI that prevented Workflow nodes from importing properly - https://github.com/ansible/awx/issues/7793 ## 14.0.0 (Aug 6, 2020) - As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to: From d27d4e4f28d01da23a4d508d2b9cdbf5fa1a5b52 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Tue, 11 Aug 2020 10:36:39 -0700 Subject: [PATCH 019/123] workaround import/dependency bug in tests --- awx/ui_next/src/components/DetailList/index.js | 6 +++++- .../NotificationTemplateDetail.jsx | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/components/DetailList/index.js b/awx/ui_next/src/components/DetailList/index.js index f16ed0e292..8bebb27ce4 100644 --- a/awx/ui_next/src/components/DetailList/index.js +++ b/awx/ui_next/src/components/DetailList/index.js @@ -3,4 +3,8 @@ export { default as Detail, DetailName, DetailValue } from './Detail'; export { default as DeletedDetail } from './DeletedDetail'; export { default as UserDateDetail } from './UserDateDetail'; export { default as DetailBadge } from './DetailBadge'; -export { default as ObjectDetail } from './ObjectDetail'; +/* + NOTE: ObjectDetail cannot be imported here, as it causes circular + dependencies in testing environment. Import it directly from + DetailList/ObjectDetail +*/ diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx index 24c199836f..951ba5bd8b 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -9,8 +9,8 @@ import { Detail, DetailList, DeletedDetail, - ObjectDetail, } from '../../../components/DetailList'; +import ObjectDetail from '../../../components/DetailList/ObjectDetail'; import DeleteButton from '../../../components/DeleteButton'; import ErrorDetail from '../../../components/ErrorDetail'; import { NotificationTemplatesAPI } from '../../../api'; From 8a4d45ddb6efda72efe461a74c90c1527415d8c7 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Mon, 3 Aug 2020 17:46:28 -0400 Subject: [PATCH 020/123] Add smart inventory host list view --- .../src/components/HostToggle/HostToggle.jsx | 8 +- .../components/HostToggle/HostToggle.test.jsx | 14 +- .../screens/Host/HostList/HostListItem.jsx | 6 +- .../src/screens/Inventory/Inventories.jsx | 9 +- .../InventoryHostDetail.test.jsx | 13 +- .../src/screens/Inventory/SmartInventory.jsx | 2 +- .../SmartInventoryHostList.jsx | 120 ++++++++++++++++ .../SmartInventoryHostList.test.jsx | 136 ++++++++++++++++++ .../SmartInventoryHostListItem.jsx | 84 +++++++++++ .../SmartInventoryHostListItem.test.jsx | 52 +++++++ .../SmartInventoryHosts.jsx | 20 ++- .../SmartInventoryHosts.test.jsx | 28 ++++ 12 files changed, 466 insertions(+), 26 deletions(-) create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx diff --git a/awx/ui_next/src/components/HostToggle/HostToggle.jsx b/awx/ui_next/src/components/HostToggle/HostToggle.jsx index d08a15a70c..43452ee3f0 100644 --- a/awx/ui_next/src/components/HostToggle/HostToggle.jsx +++ b/awx/ui_next/src/components/HostToggle/HostToggle.jsx @@ -8,7 +8,7 @@ import ErrorDetail from '../ErrorDetail'; import useRequest from '../../util/useRequest'; import { HostsAPI } from '../../api'; -function HostToggle({ host, onToggle, className, i18n }) { +function HostToggle({ host, isDisabled = false, onToggle, className, i18n }) { const [isEnabled, setIsEnabled] = useState(host.enabled); const [showError, setShowError] = useState(false); @@ -54,7 +54,11 @@ function HostToggle({ host, onToggle, className, i18n }) { label={i18n._(t`On`)} labelOff={i18n._(t`Off`)} isChecked={isEnabled} - isDisabled={isLoading || !host.summary_fields.user_capabilities.edit} + isDisabled={ + isLoading || + isDisabled || + !host.summary_fields.user_capabilities.edit + } onChange={toggleHost} aria-label={i18n._(t`Toggle host`)} /> diff --git a/awx/ui_next/src/components/HostToggle/HostToggle.test.jsx b/awx/ui_next/src/components/HostToggle/HostToggle.test.jsx index 63dd971285..7391036bf2 100644 --- a/awx/ui_next/src/components/HostToggle/HostToggle.test.jsx +++ b/awx/ui_next/src/components/HostToggle/HostToggle.test.jsx @@ -19,7 +19,7 @@ const mockHost = { }, user_capabilities: { delete: true, - update: true, + edit: true, }, recent_jobs: [], }, @@ -68,6 +68,18 @@ describe('', () => { expect(onToggle).toHaveBeenCalledWith(true); }); + test('should be enabled', async () => { + const wrapper = mountWithContexts(); + expect(wrapper.find('Switch').prop('isDisabled')).toEqual(false); + }); + + test('should be disabled', async () => { + const wrapper = mountWithContexts( + + ); + expect(wrapper.find('Switch').prop('isDisabled')).toEqual(true); + }); + test('should show error modal', async () => { HostsAPI.update.mockImplementation(() => { throw new Error('nope'); diff --git a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx index e2751f6f06..377fb453ab 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx @@ -54,11 +54,7 @@ function HostListItem({ i18n, host, isSelected, onSelect, detailUrl }) { {i18n._(t`Inventory`)} {host.summary_fields.inventory.name} diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index 0ad93adbe9..30a46b64c3 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -105,14 +105,7 @@ function Inventories({ i18n }) { - - {({ me }) => ( - - )} - + diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.test.jsx index ae7d60b1f7..a6a7c7ef73 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.test.jsx @@ -77,11 +77,18 @@ describe('', () => { describe('User has read-only permissions', () => { beforeAll(() => { - const readOnlyHost = { ...mockHost }; + const readOnlyHost = { + ...mockHost, + summary_fields: { + ...mockHost.summary_fields, + user_capabilities: { + ...mockHost.summary_fields.user_capabilities, + }, + }, + }; readOnlyHost.summary_fields.user_capabilities.edit = false; readOnlyHost.summary_fields.recent_jobs = []; - - wrapper = mountWithContexts(); + wrapper = mountWithContexts(); }); afterAll(() => { diff --git a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx index acb08661b6..18291a2959 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx @@ -47,7 +47,7 @@ function SmartInventory({ i18n, setBreadcrumb }) { useEffect(() => { fetchInventory(); - }, [fetchInventory, location.pathname]); + }, [fetchInventory]); useEffect(() => { if (inventory) { diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx new file mode 100644 index 0000000000..aa6290669c --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx @@ -0,0 +1,120 @@ +import React, { useEffect, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Button } from '@patternfly/react-core'; +import DataListToolbar from '../../../components/DataListToolbar'; +import PaginatedDataList from '../../../components/PaginatedDataList'; +import SmartInventoryHostListItem from './SmartInventoryHostListItem'; +import useRequest from '../../../util/useRequest'; +import useSelected from '../../../util/useSelected'; +import { getQSConfig, parseQueryString } from '../../../util/qs'; +import { InventoriesAPI } from '../../../api'; +import { Inventory } from '../../../types'; + +const QS_CONFIG = getQSConfig('host', { + page: 1, + page_size: 20, + order_by: 'name', +}); + +function SmartInventoryHostList({ i18n, inventory }) { + const location = useLocation(); + + const { + result: { hosts, count }, + error: contentError, + isLoading, + request: fetchHosts, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const { data } = await InventoriesAPI.readHosts(inventory.id, params); + return { + hosts: data.results, + count: data.count, + }; + }, [location.search, inventory.id]), + { + hosts: [], + count: 0, + } + ); + + const { selected, isAllSelected, handleSelect, setSelected } = useSelected( + hosts + ); + + useEffect(() => { + fetchHosts(); + }, [fetchHosts]); + + return ( + ( + setSelected(isSelected ? [...hosts] : [])} + qsConfig={QS_CONFIG} + additionalControls={ + inventory?.summary_fields?.user_capabilities?.adhoc + ? [ + , + ] + : [] + } + /> + )} + renderItem={host => ( + row.id === host.id)} + onSelect={() => handleSelect(host)} + /> + )} + /> + ); +} + +SmartInventoryHostList.propTypes = { + inventory: Inventory.isRequired, +}; + +export default withI18n()(SmartInventoryHostList); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx new file mode 100644 index 0000000000..ae3f00d66f --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { InventoriesAPI } from '../../../api'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; +import SmartInventoryHostList from './SmartInventoryHostList'; +import mockInventory from '../shared/data.inventory.json'; +import mockHosts from '../shared/data.hosts.json'; + +jest.mock('../../../api'); + +describe('', () => { + describe('User has adhoc permissions', () => { + let wrapper; + const clonedInventory = { + ...mockInventory, + summary_fields: { + ...mockInventory.summary_fields, + user_capabilities: { + ...mockInventory.summary_fields.user_capabilities, + }, + }, + }; + + beforeAll(async () => { + InventoriesAPI.readHosts.mockResolvedValue({ + data: mockHosts, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + afterAll(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('initially renders successfully', () => { + expect(wrapper.find('SmartInventoryHostList').length).toBe(1); + }); + + test('should fetch hosts from api and render them in the list', () => { + expect(InventoriesAPI.readHosts).toHaveBeenCalled(); + expect(wrapper.find('SmartInventoryHostListItem').length).toBe(3); + }); + + test('should disable run commands button when no hosts are selected', () => { + wrapper.find('DataListCheck').forEach(el => { + expect(el.props().checked).toBe(false); + }); + const runCommandsButton = wrapper.find( + 'button[aria-label="Run commands"]' + ); + expect(runCommandsButton.length).toBe(1); + expect(runCommandsButton.prop('disabled')).toEqual(true); + }); + + test('should enable run commands button when at least one host is selected', () => { + act(() => { + wrapper.find('DataListCheck[id="select-host-2"]').invoke('onChange')( + true + ); + }); + wrapper.update(); + const runCommandsButton = wrapper.find( + 'button[aria-label="Run commands"]' + ); + expect(runCommandsButton.prop('disabled')).toEqual(false); + }); + + test('should select and deselect all items', async () => { + act(() => { + wrapper.find('DataListToolbar').invoke('onSelectAll')(true); + }); + wrapper.update(); + wrapper.find('DataListCheck').forEach(el => { + expect(el.props().checked).toEqual(true); + }); + act(() => { + wrapper.find('DataListToolbar').invoke('onSelectAll')(false); + }); + wrapper.update(); + wrapper.find('DataListCheck').forEach(el => { + expect(el.props().checked).toEqual(false); + }); + }); + + test('should show content error when api throws an error', async () => { + InventoriesAPI.readHosts.mockImplementation(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentError', el => el.length === 1); + }); + }); + + describe('User does not have adhoc permissions', () => { + let wrapper; + const clonedInventory = { + ...mockInventory, + summary_fields: { + user_capabilities: { + adhoc: false, + }, + }, + }; + + test('should hide run commands button', async () => { + InventoriesAPI.readHosts.mockResolvedValue({ + data: { results: [], count: 0 }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + const runCommandsButton = wrapper.find( + 'button[aria-label="Run commands"]' + ); + expect(runCommandsButton.length).toBe(0); + jest.clearAllMocks(); + wrapper.unmount(); + }); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx new file mode 100644 index 0000000000..960218206b --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { string, bool, func } from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import 'styled-components/macro'; + +import { + DataListAction, + DataListCheck, + DataListItem, + DataListItemCells, + DataListItemRow, +} from '@patternfly/react-core'; +import DataListCell from '../../../components/DataListCell'; +import HostToggle from '../../../components/HostToggle'; +import Sparkline from '../../../components/Sparkline'; +import { Host } from '../../../types'; + +function SmartInventoryHostListItem({ + i18n, + detailUrl, + host, + isSelected, + onSelect, +}) { + const recentPlaybookJobs = host.summary_fields.recent_jobs.map(job => ({ + ...job, + type: 'job', + })); + + const labelId = `check-action-${host.id}`; + + return ( + + + + + + {host.name} + +
, + + + , + + <> + {i18n._(t`Inventory`)} + + {host.summary_fields.inventory.name} + + + , + ]} + /> + + + + +
+ ); +} + +SmartInventoryHostListItem.propTypes = { + detailUrl: string.isRequired, + host: Host.isRequired, + isSelected: bool.isRequired, + onSelect: func.isRequired, +}; + +export default withI18n()(SmartInventoryHostListItem); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx new file mode 100644 index 0000000000..a2462a831a --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import SmartInventoryHostListItem from './SmartInventoryHostListItem'; + +const mockHost = { + id: 2, + name: 'Host Two', + url: '/api/v2/hosts/2', + inventory: 1, + summary_fields: { + inventory: { + id: 1, + name: 'Inv 1', + }, + user_capabilities: { + edit: true, + }, + recent_jobs: [], + }, +}; + +describe('', () => { + let wrapper; + + beforeEach(() => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + test('should render expected row cells', () => { + const cells = wrapper.find('DataListCell'); + expect(cells).toHaveLength(3); + expect(cells.at(0).text()).toEqual('Host Two'); + expect(cells.at(1).find('Sparkline').length).toEqual(1); + expect(cells.at(2).text()).toContain('Inv 1'); + }); + + test('should display disabled host toggle', () => { + expect(wrapper.find('HostToggle').length).toBe(1); + expect(wrapper.find('HostToggle Switch').prop('isDisabled')).toEqual(true); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx index 608e664e95..0aa24cdc59 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx @@ -1,10 +1,18 @@ -import React, { Component } from 'react'; -import { CardBody } from '../../../components/Card'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import SmartInventoryHostList from './SmartInventoryHostList'; +import { Inventory } from '../../../types'; -class SmartInventoryHosts extends Component { - render() { - return Coming soon :); - } +function SmartInventoryHosts({ inventory }) { + return ( + + + + ); } +SmartInventoryHosts.propTypes = { + inventory: Inventory.isRequired, +}; + export default SmartInventoryHosts; diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx new file mode 100644 index 0000000000..8fed1ef7f7 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { createMemoryHistory } from 'history'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import SmartInventoryHosts from './SmartInventoryHosts'; + +jest.mock('../../../api'); + +describe('', () => { + test('should render smart inventory host list', () => { + const history = createMemoryHistory({ + initialEntries: ['/inventories/smart_inventory/1/hosts'], + }); + const match = { + path: '/inventories/smart_inventory/:id/hosts', + url: '/inventories/smart_inventory/1/hosts', + isExact: true, + }; + const wrapper = mountWithContexts( + , + { + context: { router: { history, route: { match } } }, + } + ); + expect(wrapper.find('SmartInventoryHostList').length).toBe(1); + jest.clearAllMocks(); + wrapper.unmount(); + }); +}); From aace8f5032d63daf35f80e4f65611f8bbe8c8f61 Mon Sep 17 00:00:00 2001 From: nixocio Date: Tue, 11 Aug 2020 16:44:03 -0400 Subject: [PATCH 021/123] Remove undefined prop from SelectedList call Remove undefined prop from `SelectedList` call. --- awx/ui_next/src/components/OptionsList/OptionsList.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/ui_next/src/components/OptionsList/OptionsList.jsx b/awx/ui_next/src/components/OptionsList/OptionsList.jsx index a3b4b39922..b2242d7202 100644 --- a/awx/ui_next/src/components/OptionsList/OptionsList.jsx +++ b/awx/ui_next/src/components/OptionsList/OptionsList.jsx @@ -49,7 +49,6 @@ function OptionsList({ deselectItem(item)} isReadOnly={readOnly} renderItemChip={renderItemChip} From 8f04026404da9d35060cfa9ecd1aa82113713b68 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 11 Aug 2020 14:20:38 -0400 Subject: [PATCH 022/123] kebabify additional controls when advanced search is displayed --- .../AddDropDownButton/AddDropDownButton.jsx | 43 +++- .../DataListToolbar/DataListToolbar.jsx | 189 ++++++++++-------- .../PaginatedDataList/ToolbarAddButton.jsx | 18 +- .../PaginatedDataList/ToolbarDeleteButton.jsx | 106 +++++----- awx/ui_next/src/components/Search/Search.jsx | 4 +- .../src/components/Search/Search.test.jsx | 42 +++- awx/ui_next/src/contexts/Kebabified.jsx | 8 + 7 files changed, 264 insertions(+), 146 deletions(-) create mode 100644 awx/ui_next/src/contexts/Kebabified.jsx diff --git a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx index 78655e44d9..3bf1963bce 100644 --- a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx +++ b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx @@ -1,25 +1,46 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, Fragment } from 'react'; import { Link } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; import PropTypes from 'prop-types'; -import { Dropdown, DropdownPosition } from '@patternfly/react-core'; +import { + Dropdown, + DropdownPosition, + DropdownItem, +} from '@patternfly/react-core'; import { ToolbarAddButton } from '../PaginatedDataList'; +import { toTitleCase } from '../../util/strings'; +import { useKebabified } from '../../contexts/Kebabified'; -function AddDropDownButton({ dropdownItems }) { +function AddDropDownButton({ dropdownItems, i18n }) { + const { isKebabified } = useKebabified(); const [isOpen, setIsOpen] = useState(false); const element = useRef(null); - const toggle = e => { - if (!element || !element.current.contains(e.target)) { - setIsOpen(false); - } - }; - useEffect(() => { + const toggle = e => { + if (!isKebabified && (!element || !element.current.contains(e.target))) { + setIsOpen(false); + } + }; + document.addEventListener('click', toggle, false); return () => { document.removeEventListener('click', toggle); }; - }, []); + }, [isKebabified]); + + if (isKebabified) { + return ( + + {dropdownItems.map(item => ( + + {toTitleCase(`${i18n._(t`Add`)} ${item.label}`)} + + ))} + + ); + } return (
@@ -52,4 +73,4 @@ AddDropDownButton.propTypes = { }; export { AddDropDownButton as _AddDropDownButton }; -export default AddDropDownButton; +export default withI18n()(AddDropDownButton); diff --git a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx index 9e02017fdf..1ddfb57df6 100644 --- a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx +++ b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -9,103 +9,128 @@ import { ToolbarGroup, ToolbarItem, ToolbarToggleGroup, + Dropdown, + KebabToggle, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import ExpandCollapse from '../ExpandCollapse'; import Search from '../Search'; import Sort from '../Sort'; - import { SearchColumns, SortColumns, QSConfig } from '../../types'; +import { KebabifiedProvider } from '../../contexts/Kebabified'; -class DataListToolbar extends React.Component { - render() { - const { - itemCount, - clearAllFilters, - searchColumns, - searchableKeys, - relatedSearchableKeys, - sortColumns, - showSelectAll, - isAllSelected, - isCompact, - onSort, - onSearch, - onReplaceSearch, - onRemove, - onCompact, - onExpand, - onSelectAll, - additionalControls, - i18n, - qsConfig, - pagination, - } = this.props; +function DataListToolbar({ + itemCount, + clearAllFilters, + searchColumns, + searchableKeys, + relatedSearchableKeys, + sortColumns, + showSelectAll, + isAllSelected, + isCompact, + onSort, + onSearch, + onReplaceSearch, + onRemove, + onCompact, + onExpand, + onSelectAll, + additionalControls, + i18n, + qsConfig, + pagination, +}) { + const showExpandCollapse = onCompact && onExpand; + const [kebabIsOpen, setKebabIsOpen] = useState(false); + const [advancedSearchShown, setAdvancedSearchShown] = useState(false); - const showExpandCollapse = onCompact && onExpand; - return ( - - - {showSelectAll && ( - - - - - - )} - } breakpoint="lg"> + const onShowAdvancedSearch = shown => { + setAdvancedSearchShown(shown); + setKebabIsOpen(false); + }; + + return ( + + + {showSelectAll && ( + - - - - - - {showExpandCollapse && ( - - - - - - - - )} + + )} + } breakpoint="lg"> + + + + + + + + {showExpandCollapse && ( + + + + + + + + )} + {advancedSearchShown && ( + + } + isOpen={kebabIsOpen} + isPlain + dropdownItems={additionalControls.map(control => { + return ( + + {control} + + ); + })} + /> + + )} + {!advancedSearchShown && ( {additionalControls.map(control => ( {control} ))} - {pagination && itemCount > 0 && ( - {pagination} - )} - - - ); - } + )} + {!advancedSearchShown && pagination && itemCount > 0 && ( + {pagination} + )} + + + ); } DataListToolbar.propTypes = { diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx index 19ae9a68c9..589a09a64e 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx @@ -1,16 +1,32 @@ import React from 'react'; import { string, func } from 'prop-types'; import { Link } from 'react-router-dom'; -import { Button, Tooltip } from '@patternfly/react-core'; +import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import { useKebabified } from '../../contexts/Kebabified'; function ToolbarAddButton({ linkTo, onClick, i18n, isDisabled }) { + const { isKebabified } = useKebabified(); + if (!linkTo && !onClick) { throw new Error( 'ToolbarAddButton requires either `linkTo` or `onClick` prop' ); } + if (isKebabified) { + return ( + + {i18n._(t`Add`)} + + ); + } if (linkTo) { return ( diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx index 2d5625a95c..7be476dfc1 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx @@ -8,10 +8,11 @@ import { shape, checkPropTypes, } from 'prop-types'; -import { Button, Tooltip } from '@patternfly/react-core'; +import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import AlertModal from '../AlertModal'; +import { Kebabified } from '../../contexts/Kebabified'; const requireNameOrUsername = props => { const { name, username } = props; @@ -138,54 +139,69 @@ class ToolbarDeleteButton extends React.Component { // we can delete the extra
around the below. // See: https://github.com/patternfly/patternfly-react/issues/1894 return ( - - -
- -
-
- {isModalOpen && ( - + {({ isKebabified }) => ( + + {isKebabified ? ( + {i18n._(t`Delete`)} - , - +
+
+ )} + {isModalOpen && ( + + {i18n._(t`Delete`)} + , + , + ]} > - {i18n._(t`Cancel`)} - , - ]} - > -
{i18n._(t`This action will delete the following:`)}
- {itemsToDelete.map(item => ( - - {item.name || item.username} -
-
- ))} -
+
{i18n._(t`This action will delete the following:`)}
+ {itemsToDelete.map(item => ( + + {item.name || item.username} +
+
+ ))} + + )} + )} - + ); } } diff --git a/awx/ui_next/src/components/Search/Search.jsx b/awx/ui_next/src/components/Search/Search.jsx index 8049a326e7..e92f5c2d16 100644 --- a/awx/ui_next/src/components/Search/Search.jsx +++ b/awx/ui_next/src/components/Search/Search.jsx @@ -40,6 +40,7 @@ function Search({ location, searchableKeys, relatedSearchableKeys, + onShowAdvancedSearch, }) { const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false); const [searchKey, setSearchKey] = useState( @@ -62,7 +63,7 @@ function Search({ const { key: actualSearchKey } = columns.find( ({ name }) => name === target.innerText ); - + onShowAdvancedSearch(actualSearchKey === 'advanced'); setIsFilterDropdownOpen(false); setSearchKey(actualSearchKey); }; @@ -301,6 +302,7 @@ Search.propTypes = { columns: SearchColumns.isRequired, onSearch: PropTypes.func, onRemove: PropTypes.func, + onShowAdvancedSearch: PropTypes.func.isRequired, }; Search.defaultProps = { diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx index a34b6bcc09..e896fcdb37 100644 --- a/awx/ui_next/src/components/Search/Search.test.jsx +++ b/awx/ui_next/src/components/Search/Search.test.jsx @@ -36,7 +36,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -64,7 +69,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -95,7 +105,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -119,7 +134,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -150,7 +170,11 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + , { context: { router: { history } } } @@ -197,6 +221,7 @@ describe('', () => { qsConfig={qsConfigNew} columns={columns} onRemove={onRemove} + onShowAdvancedSearch={jest.fn} /> , @@ -243,6 +268,7 @@ describe('', () => { qsConfig={qsConfigNew} columns={columns} onRemove={onRemove} + onShowAdvancedSearch={jest.fn} /> , @@ -277,7 +303,11 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + , { context: { router: { history } } } diff --git a/awx/ui_next/src/contexts/Kebabified.jsx b/awx/ui_next/src/contexts/Kebabified.jsx new file mode 100644 index 0000000000..34007631b0 --- /dev/null +++ b/awx/ui_next/src/contexts/Kebabified.jsx @@ -0,0 +1,8 @@ +import React, { useContext } from 'react'; + +// eslint-disable-next-line import/prefer-default-export +export const KebabifiedContext = React.createContext({}); + +export const KebabifiedProvider = KebabifiedContext.Provider; +export const Kebabified = KebabifiedContext.Consumer; +export const useKebabified = () => useContext(KebabifiedContext); From a352de3da7834cf67ea75eccad30e3c86b89bfc5 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 11 Aug 2020 14:43:59 -0400 Subject: [PATCH 023/123] change name of hook to be useKebabifiedMenu --- .../src/components/AddDropDownButton/AddDropDownButton.jsx | 4 ++-- .../src/components/PaginatedDataList/ToolbarAddButton.jsx | 4 ++-- awx/ui_next/src/contexts/Kebabified.jsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx index 3bf1963bce..ca4c4a40b6 100644 --- a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx +++ b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx @@ -10,10 +10,10 @@ import { } from '@patternfly/react-core'; import { ToolbarAddButton } from '../PaginatedDataList'; import { toTitleCase } from '../../util/strings'; -import { useKebabified } from '../../contexts/Kebabified'; +import { useKebabifiedMenu } from '../../contexts/Kebabified'; function AddDropDownButton({ dropdownItems, i18n }) { - const { isKebabified } = useKebabified(); + const { isKebabified } = useKebabifiedMenu(); const [isOpen, setIsOpen] = useState(false); const element = useRef(null); diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx index 589a09a64e..4c5c295976 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx @@ -4,10 +4,10 @@ import { Link } from 'react-router-dom'; import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { useKebabified } from '../../contexts/Kebabified'; +import { useKebabifiedMenu } from '../../contexts/Kebabified'; function ToolbarAddButton({ linkTo, onClick, i18n, isDisabled }) { - const { isKebabified } = useKebabified(); + const { isKebabified } = useKebabifiedMenu(); if (!linkTo && !onClick) { throw new Error( diff --git a/awx/ui_next/src/contexts/Kebabified.jsx b/awx/ui_next/src/contexts/Kebabified.jsx index 34007631b0..c50431c73f 100644 --- a/awx/ui_next/src/contexts/Kebabified.jsx +++ b/awx/ui_next/src/contexts/Kebabified.jsx @@ -5,4 +5,4 @@ export const KebabifiedContext = React.createContext({}); export const KebabifiedProvider = KebabifiedContext.Provider; export const Kebabified = KebabifiedContext.Consumer; -export const useKebabified = () => useContext(KebabifiedContext); +export const useKebabifiedMenu = () => useContext(KebabifiedContext); From 1323626d5e7eebe5c712cbc8dad1c947b4eb656a Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 11 Aug 2020 16:10:05 -0400 Subject: [PATCH 024/123] add onShowAdvancedSearch callback test --- .../src/components/Search/Search.test.jsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx index e896fcdb37..32c2548a35 100644 --- a/awx/ui_next/src/components/Search/Search.test.jsx +++ b/awx/ui_next/src/components/Search/Search.test.jsx @@ -93,6 +93,52 @@ describe('', () => { expect(onSearch).toBeCalledWith('description__icontains', 'test-321'); }); + test('changing key select to and from advanced causes onShowAdvancedSearch callback to be invoked', () => { + const searchButton = 'button[aria-label="Search submit button"]'; + const searchTextInput = 'input[aria-label="Search text input"]'; + const columns = [ + { name: 'Name', key: 'name__icontains', isDefault: true }, + { name: 'Description', key: 'description__icontains' }, + { name: 'Advanced', key: 'advanced' }, + ]; + const onSearch = jest.fn(); + const onShowAdvancedSearch = jest.fn(); + const wrapper = mountWithContexts( + {}} + collapseListedFiltersBreakpoint="lg" + > + + + + + ); + + act(() => { + wrapper + .find('Select[aria-label="Simple key select"]') + .invoke('onSelect')({ target: { innerText: 'Advanced' } }); + }); + wrapper.update(); + expect(onShowAdvancedSearch).toHaveBeenCalledTimes(1); + expect(onShowAdvancedSearch).toBeCalledWith(true); + jest.clearAllMocks(); + act(() => { + wrapper + .find('Select[aria-label="Simple key select"]') + .invoke('onSelect')({ target: { innerText: 'Description' } }); + }); + wrapper.update(); + expect(onShowAdvancedSearch).toHaveBeenCalledTimes(1); + expect(onShowAdvancedSearch).toBeCalledWith(false); + }); + test('attempt to search with empty string', () => { const searchButton = 'button[aria-label="Search submit button"]'; const searchTextInput = 'input[aria-label="Search text input"]'; From 54d13b63974b5296ca6ed76de8ea3779dc2be311 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 12 Aug 2020 09:23:14 -0400 Subject: [PATCH 025/123] remove unnecessary selectors from kebabification test --- awx/ui_next/src/components/Search/Search.test.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx index 32c2548a35..6c1badfa56 100644 --- a/awx/ui_next/src/components/Search/Search.test.jsx +++ b/awx/ui_next/src/components/Search/Search.test.jsx @@ -94,8 +94,6 @@ describe('', () => { }); test('changing key select to and from advanced causes onShowAdvancedSearch callback to be invoked', () => { - const searchButton = 'button[aria-label="Search submit button"]'; - const searchTextInput = 'input[aria-label="Search text input"]'; const columns = [ { name: 'Name', key: 'name__icontains', isDefault: true }, { name: 'Description', key: 'description__icontains' }, From dfad5117fac4959741c83dd91dec5ac935fa9554 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Wed, 12 Aug 2020 09:47:10 -0400 Subject: [PATCH 026/123] Add changelog preparing for awx.awx 14.1.0 bug fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f482fe3a05..b316f49d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This is a list of high-level changes for each release of AWX. A full list of com - Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 - Upgraded git-python to fix a bug that caused workflows to sometimes fail - https://github.com/ansible/awx/issues/6119 - Fixed a bug in the AWX CLI that prevented Workflow nodes from importing properly - https://github.com/ansible/awx/issues/7793 +- Fixed a bug in the awx.awx collection release process that templated the wrong version - https://github.com/ansible/awx/issues/7870 ## 14.0.0 (Aug 6, 2020) - As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to: From aa8d3d5ae42c33b8ce7a69248cf6cf20326dc9f0 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 12 Aug 2020 09:49:24 -0400 Subject: [PATCH 027/123] Update websockets.md Add more details about backplane websocket functionality. --- docs/websockets.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/websockets.md b/docs/websockets.md index c9daad83eb..79144ecead 100644 --- a/docs/websockets.md +++ b/docs/websockets.md @@ -16,7 +16,9 @@ Previously, AWX leveraged RabbitMQ to deliver Ansible events that emanated from #### Broadcast Backplane Token -AWX node(s) connect to every other node via the Websocket backplane. Authentication is accomplished via a shared secret that is exchanged via the http header `secret`. The shared secret payload consists of a a `secret`, containing the shared secret, and a `nonce` which is used to mitigate replay attack windows. +AWX node(s) connect to every other node via the Websocket backplane. The backplane websockets initiate from the `wsbroadcast` process and connect to other nodes via the same nginx process that serves webpage websocket connections and marshalls incoming web/API requests. If you have configured AWX to run with an ssl terminated connection in front of nginx then you likely will have nginx configured to handle http traffic and thus the websocket connection will flow unencrypted over http. If you have nginx configured with ssl enabled, then the websocket traffic will flow encrypted. + +Authentication is accomplished via a shared secret that is generated and set at playbook install time. The shared secret is used to derive a payload that is exchanged via the http(s) header `secret`. The shared secret payload consists of a a `secret`, containing the shared secret, and a `nonce` which is used to mitigate replay attack windows. Note that the nonce timestamp is considered valid if it is within `300` second threshold. This is to allow for machine clock skews. ``` @@ -28,6 +30,8 @@ Note that the nonce timestamp is considered valid if it is within `300` second t The payload is encrypted using `HMAC-SHA256` with `settings.BROADCAST_WEBSOCKET_SECRET` as the key. The final payload that is sent, including the http header, is of the form: `secret: nonce_plaintext:HMAC_SHA256({"secret": settings.BROADCAST_WEBSOCKET_SECRET, "nonce": nonce_plaintext})`. +Upon receiving the payload, AWX decrypted the `secret` header using the known shared secret and ensures the `secret` value of the decrypted payload matches the known shared secret, `settings.BROADCAST_WEBSOCKET_SECRET`. If it does not match, the connection is closed. If it does match, the `nonce` is compared to the current time. If the nonce is off by more than `300` seconds, the connection is closed. If both tests pass, the connection is accepted. + ## Protocol You can connect to the AWX channels implementation using any standard websocket library by pointing it to `/websocket`. You must From 5dd2cb10b4fdff6ac1ce833a772073c74edde98e Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 12 Aug 2020 09:52:10 -0400 Subject: [PATCH 028/123] Update websockets.md --- docs/websockets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/websockets.md b/docs/websockets.md index 79144ecead..12b81248b4 100644 --- a/docs/websockets.md +++ b/docs/websockets.md @@ -30,7 +30,7 @@ Note that the nonce timestamp is considered valid if it is within `300` second t The payload is encrypted using `HMAC-SHA256` with `settings.BROADCAST_WEBSOCKET_SECRET` as the key. The final payload that is sent, including the http header, is of the form: `secret: nonce_plaintext:HMAC_SHA256({"secret": settings.BROADCAST_WEBSOCKET_SECRET, "nonce": nonce_plaintext})`. -Upon receiving the payload, AWX decrypted the `secret` header using the known shared secret and ensures the `secret` value of the decrypted payload matches the known shared secret, `settings.BROADCAST_WEBSOCKET_SECRET`. If it does not match, the connection is closed. If it does match, the `nonce` is compared to the current time. If the nonce is off by more than `300` seconds, the connection is closed. If both tests pass, the connection is accepted. +Upon receiving the payload, AWX decrypts the `secret` header using the known shared secret and ensures the `secret` value of the decrypted payload matches the known shared secret, `settings.BROADCAST_WEBSOCKET_SECRET`. If it does not match, the connection is closed. If it does match, the `nonce` is compared to the current time. If the nonce is off by more than `300` seconds, the connection is closed. If both tests pass, the connection is accepted. ## Protocol From 214cb76e1e8f1216c1a0d74697fde574212f3db4 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Tue, 11 Aug 2020 14:22:04 -0400 Subject: [PATCH 029/123] Add custom host toggle tooltip for smart inventory hosts --- .../src/components/HostToggle/HostToggle.jsx | 22 +++++++++++-------- .../SmartInventoryHostListItem.jsx | 16 ++++++++++++-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/awx/ui_next/src/components/HostToggle/HostToggle.jsx b/awx/ui_next/src/components/HostToggle/HostToggle.jsx index 43452ee3f0..638ccb2347 100644 --- a/awx/ui_next/src/components/HostToggle/HostToggle.jsx +++ b/awx/ui_next/src/components/HostToggle/HostToggle.jsx @@ -8,7 +8,18 @@ import ErrorDetail from '../ErrorDetail'; import useRequest from '../../util/useRequest'; import { HostsAPI } from '../../api'; -function HostToggle({ host, isDisabled = false, onToggle, className, i18n }) { +function HostToggle({ + i18n, + className, + host, + isDisabled = false, + onToggle, + tooltip = i18n._( + t`Indicates if a host is available and should be included in running + jobs. For hosts that are part of an external inventory, this may be + reset by the inventory sync process.` + ), +}) { const [isEnabled, setIsEnabled] = useState(host.enabled); const [showError, setShowError] = useState(false); @@ -39,14 +50,7 @@ function HostToggle({ host, isDisabled = false, onToggle, className, i18n }) { return ( - + - + + Smart inventory hosts are read-only. +
+ Toggle indicates if a host is available and should be included + in running jobs. For hosts that are part of an external + inventory, this may be reset by the inventory sync process. + + } + /> From dd68b6ed73451468b401f8e15ff422fcaae695e7 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 12 Aug 2020 12:21:10 -0400 Subject: [PATCH 030/123] update existing relatedSearchKey requests to new convention and fix UJT searchKeys --- .../src/components/JobList/JobList.jsx | 17 ++++----- .../LaunchPrompt/steps/CredentialsStep.jsx | 15 +++----- .../LaunchPrompt/steps/InventoryStep.jsx | 17 ++++----- .../components/Lookup/ApplicationLookup.jsx | 21 +++++----- .../components/Lookup/CredentialLookup.jsx | 17 ++++----- .../Lookup/InstanceGroupsLookup.jsx | 20 +++++----- .../src/components/Lookup/InventoryLookup.jsx | 15 +++----- .../Lookup/MultiCredentialsLookup.jsx | 22 ++++++----- .../src/components/Lookup/ProjectLookup.jsx | 17 ++++----- .../Schedule/ScheduleList/ScheduleList.jsx | 20 ++++++---- .../ApplicationTokenList.jsx | 15 +++----- .../ApplicationsList/ApplicationsList.jsx | 20 ++++++---- .../CredentialsStep.jsx | 15 +++----- .../Host/HostGroups/HostGroupsList.jsx | 20 ++++++---- .../src/screens/Host/HostList/HostList.jsx | 14 +++---- .../InventoryGroupHostList.jsx | 20 ++++++---- .../InventoryHostGroupsList.jsx | 20 ++++++---- .../Inventory/InventoryList/InventoryList.jsx | 20 ++++++---- .../OrganizationList/OrganizationList.jsx | 20 ++++++---- .../Project/ProjectList/ProjectList.jsx | 20 ++++++---- .../src/screens/Team/TeamList/TeamList.jsx | 20 ++++++---- .../screens/Team/TeamRoles/TeamRolesList.jsx | 23 ++++++----- .../Template/TemplateList/TemplateList.jsx | 38 +++++++++---------- .../User/UserAccess/UserAccessList.jsx | 21 ++++++---- .../screens/User/UserTeams/UserTeamList.jsx | 17 ++++----- .../User/UserTokenList/UserTokenList.jsx | 15 ++++---- 26 files changed, 267 insertions(+), 232 deletions(-) diff --git a/awx/ui_next/src/components/JobList/JobList.jsx b/awx/ui_next/src/components/JobList/JobList.jsx index b1108b8cb9..502b4378c7 100644 --- a/awx/ui_next/src/components/JobList/JobList.jsx +++ b/awx/ui_next/src/components/JobList/JobList.jsx @@ -38,7 +38,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { const [selected, setSelected] = useState([]); const location = useLocation(); const { - result: { results, count, actions, relatedSearchFields }, + result: { results, count, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, request: fetchJobs, @@ -53,10 +53,12 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { return { results: response.data.results, count: response.data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location] // eslint-disable-line react-hooks/exhaustive-deps @@ -64,8 +66,8 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { { results: [], count: 0, - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); useEffect(() => { @@ -138,11 +140,6 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { } }; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( <> diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx index 357b9472db..1b736c6ad0 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx @@ -52,7 +52,7 @@ function CredentialsStep({ i18n }) { }, [fetchTypes]); const { - result: { credentials, count, actions, relatedSearchFields }, + result: { credentials, count, relatedSearchableKeys, searchableKeys }, error: credentialsError, isLoading: isCredentialsLoading, request: fetchCredentials, @@ -72,13 +72,15 @@ function CredentialsStep({ i18n }) { return { credentials: data.results, count: data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [selectedType, history.location.search]), - { credentials: [], count: 0, actions: {}, relatedSearchFields: [] } + { credentials: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { @@ -104,11 +106,6 @@ function CredentialsStep({ i18n }) { /> ); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( <> {types && types.length > 0 && ( diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx index 0c23c0c2b2..e7026c66b7 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx @@ -27,7 +27,7 @@ function InventoryStep({ i18n }) { const { isLoading, error, - result: { inventories, count, actions, relatedSearchFields }, + result: { inventories, count, relatedSearchableKeys, searchableKeys }, request: fetchInventories, } = useRequest( useCallback(async () => { @@ -39,17 +39,19 @@ function InventoryStep({ i18n }) { return { inventories: data.results, count: data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location]), { count: 0, inventories: [], - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -57,11 +59,6 @@ function InventoryStep({ i18n }) { fetchInventories(); }, [fetchInventories]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - if (isLoading) { return ; } diff --git a/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx b/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx index 8056114046..01fd154c47 100644 --- a/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx @@ -22,7 +22,7 @@ function ApplicationLookup({ i18n, onChange, value, label }) { const location = useLocation(); const { error, - result: { applications, itemCount, actions, relatedSearchFields }, + result: { applications, itemCount, relatedSearchableKeys, searchableKeys }, request: fetchApplications, } = useRequest( useCallback(async () => { @@ -40,23 +40,24 @@ function ApplicationLookup({ i18n, onChange, value, label }) { return { applications: results, itemCount: count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), - { applications: [], itemCount: 0, actions: {}, relatedSearchFields: [] } + { + applications: [], + itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], + } ); useEffect(() => { fetchApplications(); }, [fetchApplications]); - - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [ credentialTypeId, @@ -78,8 +80,8 @@ function CredentialLookup({ { count: 0, credentials: [], - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -87,11 +89,6 @@ function CredentialLookup({ fetchCredentials(); }, [fetchCredentials]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - // TODO: replace credential type search with REST-based grabbing of cred types return ( diff --git a/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx b/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx index 21fe8cfa8f..49c257cb6a 100644 --- a/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx @@ -30,7 +30,7 @@ function InstanceGroupsLookup(props) { } = props; const { - result: { instanceGroups, count, actions, relatedSearchFields }, + result: { instanceGroups, count, relatedSearchableKeys, searchableKeys }, request: fetchInstanceGroups, error, isLoading, @@ -44,24 +44,26 @@ function InstanceGroupsLookup(props) { return { instanceGroups: data.results, count: data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location]), - { instanceGroups: [], count: 0, actions: {}, relatedSearchFields: [] } + { + instanceGroups: [], + count: 0, + relatedSearchableKeys: [], + searchableKeys: [], + } ); useEffect(() => { fetchInstanceGroups(); }, [fetchInstanceGroups]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location]), - { inventories: [], count: 0, actions: {}, relatedSearchFields: [] } + { inventories: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { fetchInventories(); }, [fetchInventories]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( <> val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [selectedType, history.location]), { credentials: [], credentialsCount: 0, - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -104,11 +111,6 @@ function MultiCredentialsLookup(props) { const isVault = selectedType?.kind === 'vault'; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location.search, autocomplete]), { count: 0, projects: [], - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -67,11 +69,6 @@ function ProjectLookup({ fetchProjects(); }, [fetchProjects]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( val.slice(0, -8)), + searchableKeys: Object.keys( + scheduleActions.data.actions?.GET || {} + ).filter(key => scheduleActions.data.actions?.GET[key].filterable), }; }, [location, loadSchedules, loadScheduleOptions]), { schedules: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -106,10 +116,6 @@ function ScheduleList({ actions && Object.prototype.hasOwnProperty.call(actions, 'POST') && !hideAddButton; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx b/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx index 7d123385ef..2ad56eaa7f 100644 --- a/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx @@ -26,7 +26,7 @@ function ApplicationTokenList({ i18n }) { const { error, isLoading, - result: { tokens, itemCount, actions, relatedSearchFields }, + result: { tokens, itemCount, relatedSearchableKeys, searchableKeys }, request: fetchTokens, } = useRequest( useCallback(async () => { @@ -52,13 +52,15 @@ function ApplicationTokenList({ i18n }) { return { tokens: modifiedResults, itemCount: count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [id, location.search]), - { tokens: [], itemCount: 0, actions: {}, relatedSearchFields: [] } + { tokens: [], itemCount: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { @@ -91,11 +93,6 @@ function ApplicationTokenList({ i18n }) { setSelected([]); }; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( <> { const params = parseQueryString(QS_CONFIG, location.search); @@ -46,16 +52,20 @@ function ApplicationsList({ i18n }) { applications: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { applications: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -89,10 +99,6 @@ function ApplicationsList({ i18n }) { }; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx index 40a305e72f..bc91a98894 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx @@ -25,7 +25,7 @@ function CredentialsStep({ i18n }) { const history = useHistory(); const { - result: { credentials, count, actions, relatedSearchFields }, + result: { credentials, count, relatedSearchableKeys, searchableKeys }, error: credentialsError, isLoading: isCredentialsLoading, request: fetchCredentials, @@ -39,24 +39,21 @@ function CredentialsStep({ i18n }) { return { credentials: data.results, count: data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location.search]), - { credentials: [], count: 0, actions: {}, relatedSearchFields: [] } + { credentials: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { fetchCredentials(); }, [fetchCredentials]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - if (credentialsError) { return ; } diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx index bfcde651a4..6233df73bf 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx @@ -33,7 +33,13 @@ function HostGroupsList({ i18n, host }) { const invId = host.summary_fields.inventory.id; const { - result: { groups, itemCount, actions, relatedSearchFields }, + result: { + groups, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchGroups, @@ -55,16 +61,20 @@ function HostGroupsList({ i18n, host }) { groups: results, itemCount: count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [hostId, search]), { groups: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -128,10 +138,6 @@ function HostGroupsList({ i18n, host }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Host/HostList/HostList.jsx b/awx/ui_next/src/screens/Host/HostList/HostList.jsx index 3e6a2589c6..a11ec3f4d1 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostList.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostList.jsx @@ -29,7 +29,7 @@ function HostList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { hosts, count, actions, relatedSearchFields }, + result: { hosts, count, actions, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, request: fetchHosts, @@ -44,16 +44,20 @@ function HostList({ i18n }) { hosts: results[0].data.results, count: results[0].data.count, actions: results[1].data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( results[1]?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys(results[1].data.actions?.GET || {}).filter( + key => results[1].data.actions?.GET[key].filterable + ), }; }, [location]), { hosts: [], count: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -97,10 +101,6 @@ function HostList({ i18n }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx index 17fc055997..8584c01207 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx @@ -32,7 +32,13 @@ function InventoryGroupHostList({ i18n }) { const history = useHistory(); const { - result: { hosts, hostCount, actions, relatedSearchFields }, + result: { + hosts, + hostCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchHosts, @@ -48,16 +54,20 @@ function InventoryGroupHostList({ i18n }) { hosts: response.data.results, hostCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [groupId, inventoryId, location.search]), { hosts: [], hostCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -127,10 +137,6 @@ function InventoryGroupHostList({ i18n }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); const addFormUrl = `/inventories/inventory/${inventoryId}/groups/${groupId}/nested_hosts/add`; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx index 4b74189602..2f8d8fa1ea 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx @@ -31,7 +31,13 @@ function InventoryHostGroupsList({ i18n }) { const { search } = useLocation(); const { - result: { groups, itemCount, actions, relatedSearchFields }, + result: { + groups, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchGroups, @@ -53,16 +59,20 @@ function InventoryHostGroupsList({ i18n }) { groups: results, itemCount: count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [hostId, search]), // eslint-disable-line react-hooks/exhaustive-deps { groups: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -126,10 +136,6 @@ function InventoryHostGroupsList({ i18n }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx index 94f22e70a3..e5ff1f6dd4 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx @@ -29,7 +29,13 @@ function InventoryList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { results, itemCount, actions, relatedSearchFields }, + result: { + results, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchInventories, @@ -44,16 +50,20 @@ function InventoryList({ i18n }) { results: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { results: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -97,10 +107,6 @@ function InventoryList({ i18n }) { const hasContentLoading = isDeleteLoading || isLoading; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...inventories] : []); diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx index 152004ae5c..7b3c0eeda2 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx @@ -31,7 +31,13 @@ function OrganizationsList({ i18n }) { const addUrl = `${match.url}/add`; const { - result: { organizations, organizationCount, actions, relatedSearchFields }, + result: { + organizations, + organizationCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading: isOrgsLoading, request: fetchOrganizations, @@ -46,16 +52,20 @@ function OrganizationsList({ i18n }) { organizations: orgs.data.results, organizationCount: orgs.data.count, actions: orgActions.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( orgActions?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys(orgActions.data.actions?.GET || {}).filter( + key => orgActions.data.actions?.GET[key].filterable + ), }; }, [location]), { organizations: [], organizationCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -90,10 +100,6 @@ function OrganizationsList({ i18n }) { const hasContentLoading = isDeleteLoading || isOrgsLoading; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...organizations] : []); diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx index 13b3a98c2c..7afbe124b3 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx @@ -30,7 +30,13 @@ function ProjectList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { results, itemCount, actions, relatedSearchFields }, + result: { + results, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchProjects, @@ -45,16 +51,20 @@ function ProjectList({ i18n }) { results: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { results: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -89,10 +99,6 @@ function ProjectList({ i18n }) { const hasContentLoading = isDeleteLoading || isLoading; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...projects] : []); diff --git a/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx b/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx index f660064266..07de516ca4 100644 --- a/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx +++ b/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx @@ -29,7 +29,13 @@ function TeamList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { teams, itemCount, actions, relatedSearchFields }, + result: { + teams, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchTeams, @@ -44,16 +50,20 @@ function TeamList({ i18n }) { teams: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { teams: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -85,10 +95,6 @@ function TeamList({ i18n }) { const hasContentLoading = isDeleteLoading || isLoading; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...teams] : []); diff --git a/awx/ui_next/src/screens/Team/TeamRoles/TeamRolesList.jsx b/awx/ui_next/src/screens/Team/TeamRoles/TeamRolesList.jsx index 05ffce9fe1..31db276045 100644 --- a/awx/ui_next/src/screens/Team/TeamRoles/TeamRolesList.jsx +++ b/awx/ui_next/src/screens/Team/TeamRoles/TeamRolesList.jsx @@ -37,7 +37,13 @@ function TeamRolesList({ i18n, me, team }) { isLoading, request: fetchRoles, contentError, - result: { roleCount, roles, isAdminOfOrg, actions, relatedSearchFields }, + result: { + roleCount, + roles, + isAdminOfOrg, + relatedSearchableKeys, + searchableKeys, + }, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, search); @@ -58,18 +64,20 @@ function TeamRolesList({ i18n, me, team }) { roleCount: count, roles: results, isAdminOfOrg: orgAdminCount > 0, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [me.id, team.id, team.organization, search]), { roles: [], roleCount: 0, isAdminOfOrg: false, - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -98,11 +106,6 @@ function TeamRolesList({ i18n, me, team }) { ); const canAdd = team?.summary_fields?.user_capabilities?.edit || isAdminOfOrg; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - const detailUrl = role => { const { resource_id, resource_type } = role.summary_fields; diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx index 31c04263ad..455bf4ce6c 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx @@ -36,7 +36,14 @@ function TemplateList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { results, count, jtActions, wfjtActions, relatedSearchFields }, + result: { + results, + count, + jtActions, + wfjtActions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchTemplates, @@ -47,20 +54,19 @@ function TemplateList({ i18n }) { 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, - relatedSearchFields: [ - ...(responses[1]?.data?.related_search_fields || []).map(val => - val.slice(0, -8) - ), - ...(responses[2]?.data?.related_search_fields || []).map(val => - val.slice(0, -8) - ), - ], + 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]), { @@ -68,7 +74,8 @@ function TemplateList({ i18n }) { count: 0, jtActions: {}, wfjtActions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -128,17 +135,6 @@ function TemplateList({ i18n }) { const canAddWFJT = wfjtActions && Object.prototype.hasOwnProperty.call(wfjtActions, 'POST'); // spreading Set() returns only unique keys - const relatedSearchableKeys = [...new Set(relatedSearchFields)] || []; - const searchableKeys = [ - ...new Set([ - ...Object.keys(jtActions?.GET || {}).filter( - key => jtActions.GET[key].filterable - ), - ...Object.keys(wfjtActions?.GET || {}).filter( - key => wfjtActions.GET[key].filterable - ), - ]), - ]; const addButtonOptions = []; if (canAddJT) { diff --git a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx index 4d9ff7a671..5e528913c1 100644 --- a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx +++ b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx @@ -38,7 +38,13 @@ function UserAccessList({ i18n, user }) { isLoading, request: fetchRoles, error, - result: { roleCount, roles, actions, relatedSearchFields }, + result: { + roleCount, + roles, + actions, + relatedSearchableKeys, + searchableKeys, + }, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, search); @@ -55,16 +61,20 @@ function UserAccessList({ i18n, user }) { roleCount: count, roles: results, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [user.id, search]), { roles: [], roleCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -92,11 +102,6 @@ function UserAccessList({ i18n, user }) { user?.summary_fields?.user_capabilities?.edit || (actions && Object.prototype.hasOwnProperty.call(actions, 'POST')); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - const saveRoles = () => { setIsWizardOpen(false); fetchRoles(); diff --git a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx index 25d245cffd..868cbb9e55 100644 --- a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx +++ b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx @@ -20,7 +20,7 @@ function UserTeamList({ i18n }) { const { id: userId } = useParams(); const { - result: { teams, count, actions, relatedSearchFields }, + result: { teams, count, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, request: fetchOrgs, @@ -39,17 +39,19 @@ function UserTeamList({ i18n }) { return { teams: results, count: teamCount, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [userId, location.search]), { teams: [], count: 0, - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -57,11 +59,6 @@ function UserTeamList({ i18n }) { fetchOrgs(); }, [fetchOrgs]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( { const params = parseQueryString(QS_CONFIG, location.search); @@ -53,13 +53,15 @@ function UserTokenList({ i18n }) { return { tokens: modifiedResults, itemCount: count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [id, location.search]), - { tokens: [], itemCount: 0, actions: {}, relatedSearchFields: [] } + { tokens: [], itemCount: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { @@ -93,10 +95,7 @@ function UserTokenList({ i18n }) { }; const canAdd = true; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); + return ( <> Date: Mon, 3 Aug 2020 13:06:26 -0400 Subject: [PATCH 031/123] Adds User Token Details page --- awx/ui_next/src/screens/User/User.jsx | 10 +- .../src/screens/User/UserToken/UserToken.jsx | 117 +++++++++++++++++ .../screens/User/UserToken/UserToken.test.jsx | 101 +++++++++++++++ .../src/screens/User/UserToken/index.js | 1 + .../User/UserTokenDetail/UserTokenDetail.jsx | 81 ++++++++++++ .../UserTokenDetail/UserTokenDetail.test.jsx | 120 ++++++++++++++++++ .../src/screens/User/UserTokenDetail/index.js | 1 + .../User/UserTokenList/UserTokenListItem.jsx | 6 +- .../screens/User/UserTokens/UserTokens.jsx | 13 +- awx/ui_next/src/screens/User/Users.jsx | 6 +- 10 files changed, 447 insertions(+), 9 deletions(-) create mode 100644 awx/ui_next/src/screens/User/UserToken/UserToken.jsx create mode 100644 awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx create mode 100644 awx/ui_next/src/screens/User/UserToken/index.js create mode 100644 awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx create mode 100644 awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx create mode 100644 awx/ui_next/src/screens/User/UserTokenDetail/index.js diff --git a/awx/ui_next/src/screens/User/User.jsx b/awx/ui_next/src/screens/User/User.jsx index 95dcb487b4..d071cedd8f 100644 --- a/awx/ui_next/src/screens/User/User.jsx +++ b/awx/ui_next/src/screens/User/User.jsx @@ -80,7 +80,9 @@ function User({ i18n, setBreadcrumb, me }) { } let showCardHeader = true; - if (['edit', 'add'].some(name => location.pathname.includes(name))) { + if ( + ['edit', 'add', 'tokens'].some(name => location.pathname.includes(name)) + ) { showCardHeader = false; } @@ -131,7 +133,11 @@ function User({ i18n, setBreadcrumb, me }) { )} - + diff --git a/awx/ui_next/src/screens/User/UserToken/UserToken.jsx b/awx/ui_next/src/screens/User/UserToken/UserToken.jsx new file mode 100644 index 0000000000..af6f0f0b16 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserToken/UserToken.jsx @@ -0,0 +1,117 @@ +import React, { useEffect, useCallback } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + Link, + Redirect, + Route, + Switch, + useLocation, + useParams, +} from 'react-router-dom'; +import { CaretLeftIcon } from '@patternfly/react-icons'; +import { Card, PageSection } from '@patternfly/react-core'; +import RoutedTabs from '../../../components/RoutedTabs'; +import ContentError from '../../../components/ContentError'; +import { TokensAPI } from '../../../api'; +import useRequest from '../../../util/useRequest'; +import UserTokenDetail from '../UserTokenDetail'; + +function UserToken({ i18n, setBreadcrumb, user }) { + const location = useLocation(); + const { id, tokenId } = useParams(); + const { + isLoading, + error, + request: fetchToken, + result: { token, actions }, + } = useRequest( + useCallback(async () => { + const [response, actionsResponse] = await Promise.all([ + TokensAPI.readDetail(tokenId), + TokensAPI.readOptions(), + ]); + setBreadcrumb(user, response.data); + return { + token: response.data, + actions: actionsResponse.data.actions.POST, + }; + }, [setBreadcrumb, user, tokenId]), + { token: null, actions: null } + ); + useEffect(() => { + fetchToken(); + }, [fetchToken]); + + const tabsArray = [ + { + name: ( + <> + + {i18n._(t`Back to Tokens`)} + + ), + link: `/users/${id}/tokens`, + id: 99, + }, + { + name: i18n._(t`Details`), + link: `/users/${id}/tokens/${tokenId}/details`, + id: 0, + }, + ]; + + let showCardHeader = true; + + if (location.pathname.endsWith('edit')) { + showCardHeader = false; + } + + if (!isLoading && error) { + return ( + + + + {error.response.status === 404 && ( + + {i18n._(t`Token not found.`)}{' '} + + {i18n._(t`View all tokens.`)} + + + )} + + + + ); + } + + return ( + <> + {showCardHeader && } + + + {token && ( + + + + )} + + {!isLoading && ( + + {id && ( + {i18n._(t`View Tokens`)} + )} + + )} + + + + ); +} + +export default withI18n()(UserToken); diff --git a/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx b/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx new file mode 100644 index 0000000000..8e71f1b085 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { TokensAPI } from '../../../api'; +import UserToken from './UserToken'; + +jest.mock('../../../api/models/Tokens'); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + tokenId: 2, + }), +})); +describe('', () => { + let wrapper; + const user = { + id: 1, + type: 'user', + url: '/api/v2/users/1/', + summary_fields: { + user_capabilities: { + edit: true, + delete: false, + }, + }, + created: '2020-06-19T12:55:13.138692Z', + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + email: 'a@g.com', + }; + test('should call api for token details and actions', async () => { + TokensAPI.readDetail.mockResolvedValue({ + id: 2, + type: 'o_auth2_access_token', + url: '/api/v2/tokens/2/', + + summary_fields: { + user: { + id: 1, + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + }, + application: { + id: 3, + name: 'hg', + }, + }, + created: '2020-06-23T19:56:38.422053Z', + modified: '2020-06-23T19:56:38.441353Z', + description: 'cdfsg', + scope: 'read', + }); + TokensAPI.readOptions.mockResolvedValue({ + data: { actions: { POST: true } }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('UserToken').length).toBe(1); + }); + test('should call api for token details and actions', async () => { + TokensAPI.readDetail.mockResolvedValue({ + id: 2, + type: 'o_auth2_access_token', + url: '/api/v2/tokens/2/', + + summary_fields: { + user: { + id: 1, + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + }, + application: { + id: 3, + name: 'hg', + }, + }, + created: '2020-06-23T19:56:38.422053Z', + modified: '2020-06-23T19:56:38.441353Z', + description: 'cdfsg', + scope: 'read', + }); + TokensAPI.readOptions.mockResolvedValue({ + data: { actions: { POST: true } }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(TokensAPI.readDetail).toBeCalledWith(2); + expect(TokensAPI.readOptions).toBeCalled(); + }); +}); diff --git a/awx/ui_next/src/screens/User/UserToken/index.js b/awx/ui_next/src/screens/User/UserToken/index.js new file mode 100644 index 0000000000..f899410e7d --- /dev/null +++ b/awx/ui_next/src/screens/User/UserToken/index.js @@ -0,0 +1 @@ +export { default } from './UserToken'; diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx new file mode 100644 index 0000000000..37eeb63416 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx @@ -0,0 +1,81 @@ +import React, { useCallback } from 'react'; +import { Link, useHistory, useParams } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Button } from '@patternfly/react-core'; + +import AlertModal from '../../../components/AlertModal'; +import { CardBody, CardActionsRow } from '../../../components/Card'; +import DeleteButton from '../../../components/DeleteButton'; +import { DetailList, Detail } from '../../../components/DetailList'; +import ErrorDetail from '../../../components/ErrorDetail'; +import { formatDateString } from '../../../util/dates'; +import { TokensAPI } from '../../../api'; +import useRequest, { useDismissableError } from '../../../util/useRequest'; + +function UserTokenDetail({ token, canEditOrDelete, i18n }) { + const { scope, description, created, modified, summary_fields } = token; + const history = useHistory(); + const { id, tokenId } = useParams(); + const { request: deleteTeam, isLoading, error: deleteError } = useRequest( + useCallback(async () => { + await TokensAPI.destroy(tokenId); + history.push(`/users/${id}/tokens`); + }, [tokenId, id, history]) + ); + + const { error, dismissError } = useDismissableError(deleteError); + + return ( + + + + + + + + + + {canEditOrDelete && ( + <> + + + {i18n._(t`Delete`)} + + + )} + + {error && ( + + {i18n._(t`Failed to user token.`)} + + + )} + + ); +} + +export default withI18n()(UserTokenDetail); diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx new file mode 100644 index 0000000000..6b0d382e0c --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { TokensAPI } from '../../../api'; +import UserTokenDetail from './UserTokenDetail'; + +jest.mock('../../../api/models/Tokens'); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + tokenId: 2, + }), +})); +describe('', () => { + let wrapper; + const token = { + id: 2, + type: 'o_auth2_access_token', + url: '/api/v2/tokens/2/', + + summary_fields: { + user: { + id: 1, + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + }, + application: { + id: 3, + name: 'hg', + }, + }, + created: '2020-06-23T19:56:38.422053Z', + modified: '2020-06-23T19:56:38.441353Z', + description: 'cdfsg', + scope: 'read', + }; + test('should call api for token details and actions', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('UserTokenDetail').length).toBe(1); + }); + test('should call api for token details and actions', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + + expect(wrapper.find('Detail[label="Application"]').prop('value')).toBe( + 'hg' + ); + expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe( + 'cdfsg' + ); + expect(wrapper.find('Detail[label="Scope"]').prop('value')).toBe('read'); + expect(wrapper.find('Detail[label="Created"]').prop('value')).toBe( + '6/23/2020, 7:56:38 PM' + ); + expect(wrapper.find('Detail[label="Last Modified"]').prop('value')).toBe( + '6/23/2020, 7:56:38 PM' + ); + expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(1); + expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(1); + }); + test('should not render edit or delete buttons', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(0); + expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(0); + }); + test('should delete token properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await act(async () => + wrapper.find('Button[aria-label="Delete"]').prop('onClick')() + ); + wrapper.update(); + await act(async () => wrapper.find('DeleteButton').prop('onConfirm')()); + expect(TokensAPI.destroy).toBeCalledWith(2); + }); + test('should throw deletion error', async () => { + TokensAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/tokens', + }, + data: 'An error occurred', + status: 400, + }, + }) + ); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await act(async () => + wrapper.find('Button[aria-label="Delete"]').prop('onClick')() + ); + wrapper.update(); + await act(async () => wrapper.find('DeleteButton').prop('onConfirm')()); + expect(TokensAPI.destroy).toBeCalledWith(2); + wrapper.update(); + expect(wrapper.find('ErrorDetail').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/index.js b/awx/ui_next/src/screens/User/UserTokenDetail/index.js new file mode 100644 index 0000000000..a6a9011996 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTokenDetail/index.js @@ -0,0 +1 @@ +export { default } from './UserTokenDetail'; diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx index 4b1198c5a9..7fabcb878c 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Link, useParams } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { @@ -22,6 +23,7 @@ const NameLabel = styled.b` `; function UserTokenListItem({ i18n, token, isSelected, onSelect }) { + const { id } = useParams(); const labelId = `check-action-${token.id}`; return ( @@ -41,7 +43,9 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) { {token.summary_fields?.application?.name ? ( {i18n._(t`Application`)} - {token.summary_fields.application.name} + + {token.summary_fields.application.name} + ) : ( i18n._(t`Personal access token`) diff --git a/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx b/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx index dc072a6546..c73519d7f9 100644 --- a/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx +++ b/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx @@ -3,17 +3,20 @@ import { withI18n } from '@lingui/react'; import { Switch, Route, useParams } from 'react-router-dom'; import UserTokenAdd from '../UserTokenAdd'; import UserTokenList from '../UserTokenList'; +import UserToken from '../UserToken'; -function UserTokens() { - const { id: userId } = useParams(); - +function UserTokens({ setBreadcrumb, user }) { + const { id } = useParams(); return ( - + + + + - + ); diff --git a/awx/ui_next/src/screens/User/Users.jsx b/awx/ui_next/src/screens/User/Users.jsx index 6f21f8be10..e9fe2d4ef2 100644 --- a/awx/ui_next/src/screens/User/Users.jsx +++ b/awx/ui_next/src/screens/User/Users.jsx @@ -18,7 +18,7 @@ function Users({ i18n }) { const match = useRouteMatch(); const addUserBreadcrumb = useCallback( - user => { + (user, token) => { if (!user) { return; } @@ -34,6 +34,10 @@ function Users({ i18n }) { [`/users/${user.id}/organizations`]: i18n._(t`Organizations`), [`/users/${user.id}/tokens`]: i18n._(t`Tokens`), [`/users/${user.id}/tokens/add`]: i18n._(t`Create user token`), + [`/users/${user.id}/tokens/${token && token.id}`]: `Application Name`, + [`/users/${user.id}/tokens/${token && token.id}/details`]: i18n._( + t`Details` + ), }); }, [i18n] From 15fda43a1007ef9aff006fe3f00fedc2d9c7a52f Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Tue, 4 Aug 2020 14:29:37 -0400 Subject: [PATCH 032/123] Utilizes UserDateDetail, Capitalizes Scope value, fixes spelling errors --- .../components/Lookup/ApplicationLookup.jsx | 2 +- .../User/UserTokenDetail/UserTokenDetail.jsx | 28 ++++++++++++------- .../UserTokenDetail/UserTokenDetail.test.jsx | 12 ++++---- .../User/UserTokenList/UserTokenListItem.jsx | 8 ++++-- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx b/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx index 01fd154c47..ca5871c2cc 100644 --- a/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx @@ -44,7 +44,7 @@ function ApplicationLookup({ i18n, onChange, value, label }) { actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), searchableKeys: Object.keys( - actionsResponse.data.actions?.GET || {} + actionsResponse?.data?.actions?.GET || {} ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx index 37eeb63416..4e6891767d 100644 --- a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx +++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx @@ -7,23 +7,26 @@ import { Button } from '@patternfly/react-core'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; import DeleteButton from '../../../components/DeleteButton'; -import { DetailList, Detail } from '../../../components/DetailList'; +import { + DetailList, + Detail, + UserDateDetail, +} from '../../../components/DetailList'; import ErrorDetail from '../../../components/ErrorDetail'; -import { formatDateString } from '../../../util/dates'; import { TokensAPI } from '../../../api'; import useRequest, { useDismissableError } from '../../../util/useRequest'; +import { toTitleCase } from '../../../util/strings'; function UserTokenDetail({ token, canEditOrDelete, i18n }) { const { scope, description, created, modified, summary_fields } = token; const history = useHistory(); const { id, tokenId } = useParams(); - const { request: deleteTeam, isLoading, error: deleteError } = useRequest( + const { request: deleteToken, isLoading, error: deleteError } = useRequest( useCallback(async () => { await TokensAPI.destroy(tokenId); history.push(`/users/${id}/tokens`); }, [tokenId, id, history]) ); - const { error, dismissError } = useDismissableError(deleteError); return ( @@ -32,14 +35,19 @@ function UserTokenDetail({ token, canEditOrDelete, i18n }) { - - - + + @@ -55,7 +63,7 @@ function UserTokenDetail({ token, canEditOrDelete, i18n }) { {i18n._(t`Delete`)} diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx index 6b0d382e0c..a3462f758f 100644 --- a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx +++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx @@ -58,13 +58,13 @@ describe('', () => { expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe( 'cdfsg' ); - expect(wrapper.find('Detail[label="Scope"]').prop('value')).toBe('read'); - expect(wrapper.find('Detail[label="Created"]').prop('value')).toBe( - '6/23/2020, 7:56:38 PM' - ); - expect(wrapper.find('Detail[label="Last Modified"]').prop('value')).toBe( - '6/23/2020, 7:56:38 PM' + expect(wrapper.find('Detail[label="Scope"]').prop('value')).toBe('Read'); + expect(wrapper.find('UserDateDetail[label="Created"]').prop('date')).toBe( + '2020-06-23T19:56:38.422053Z' ); + expect( + wrapper.find('UserDateDetail[label="Last Modified"]').prop('date') + ).toBe('2020-06-23T19:56:38.441353Z'); expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(1); expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(1); }); diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx index 7fabcb878c..52eb44a7f1 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx @@ -11,7 +11,7 @@ import { import styled from 'styled-components'; import { toTitleCase } from '../../../util/strings'; -import { formatDateStringUTC } from '../../../util/dates'; +import { formatDateString } from '../../../util/dates'; import DataListCell from '../../../components/DataListCell'; const Label = styled.b` @@ -48,7 +48,9 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) { ) : ( - i18n._(t`Personal access token`) + + {i18n._(t`Personal access token`)} + )} , @@ -57,7 +59,7 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) { , - {formatDateStringUTC(token.expires)} + {formatDateString(token.expires)} , ]} /> From 98b7f3b61890d8ee5715f757464343dda6fe8e19 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 6 Aug 2020 13:28:59 -0400 Subject: [PATCH 033/123] Support workflow prompting on launch --- .../src/components/LaunchPrompt/steps/InventoryStep.jsx | 2 -- .../components/LaunchPrompt/steps/useInventoryStep.jsx | 6 +++++- .../screens/Template/TemplateList/TemplateListItem.jsx | 9 +++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx index e7026c66b7..696809f547 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx @@ -9,7 +9,6 @@ import useRequest from '../../../util/useRequest'; import OptionsList from '../../OptionsList'; import ContentLoading from '../../ContentLoading'; import ContentError from '../../ContentError'; -import { required } from '../../../util/validators'; const QS_CONFIG = getQSConfig('inventory', { page: 1, @@ -20,7 +19,6 @@ const QS_CONFIG = getQSConfig('inventory', { function InventoryStep({ i18n }) { const [field, , helpers] = useField({ name: 'inventory', - validate: required(null, i18n), }); const history = useHistory(); diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx index aa8acbd6f6..ebc875d506 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx @@ -9,7 +9,11 @@ export default function useInventoryStep(config, resource, visitedSteps, i18n) { const [stepErrors, setStepErrors] = useState({}); const validate = values => { - if (!config.ask_inventory_on_launch) { + if ( + !config.ask_inventory_on_launch || + (['workflow_job', 'workflow_job_template'].includes(resource.type) && + !resource.inventory) + ) { return {}; } const errors = {}; diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx index 716c83e608..16166ec108 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx @@ -44,9 +44,7 @@ function TemplateListItem({ fetchTemplates, }) { const [isDisabled, setIsDisabled] = useState(false); - const labelId = `check-action-${template.id}`; - const canLaunch = template.summary_fields.user_capabilities.start; const copyTemplate = useCallback(async () => { if (template.type === 'job_template') { @@ -105,8 +103,11 @@ function TemplateListItem({ , ]} /> - - {canLaunch && template.type === 'job_template' && ( + + {template.summary_fields.user_capabilities.start && ( {({ handleLaunch }) => ( From 1e9a71a2e4cd6d3d9ca3ef209ad77c381f47d582 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 13 Aug 2020 11:55:57 -0400 Subject: [PATCH 034/123] Run prettier --- .../src/screens/Template/TemplateList/TemplateListItem.jsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx index 16166ec108..eb0be40346 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx @@ -103,10 +103,7 @@ function TemplateListItem({ , ]} /> - + {template.summary_fields.user_capabilities.start && ( From 54e87378d5127678fd4f38febbce395fc696e6e4 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 17 Aug 2020 20:43:02 -0400 Subject: [PATCH 035/123] Pin pytest-xdist --- requirements/requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 6fd0b2ed2f..3dbcc2f97c 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -12,7 +12,7 @@ pytest-django pytest-pythonpath pytest-mock==1.11.1 pytest-timeout -pytest-xdist +pytest-xdist==1.34.0 # 2.0.0 broke zuul for some reason tox # for awxkit logutils jupyter From 33e2c059eda92453bc15b3465392ddd1bac25a68 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Mon, 17 Aug 2020 14:35:40 -0400 Subject: [PATCH 036/123] make event stdout encoding more resilient to UTF-16 surrogate pairs see: https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates --- CHANGELOG.md | 1 + awx/api/renderers.py | 18 ++++++++++++++++++ awx/settings/defaults.py | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b316f49d55..54573a057f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This is a list of high-level changes for each release of AWX. A full list of com - Upgraded git-python to fix a bug that caused workflows to sometimes fail - https://github.com/ansible/awx/issues/6119 - Fixed a bug in the AWX CLI that prevented Workflow nodes from importing properly - https://github.com/ansible/awx/issues/7793 - Fixed a bug in the awx.awx collection release process that templated the wrong version - https://github.com/ansible/awx/issues/7870 +- Fixed a bug that caused errors rendering stdout that contained UTF-16 surrogate pairs - https://github.com/ansible/awx/pull/7918 ## 14.0.0 (Aug 6, 2020) - As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to: diff --git a/awx/api/renderers.py b/awx/api/renderers.py index bd4136a76a..92d59e2c7b 100644 --- a/awx/api/renderers.py +++ b/awx/api/renderers.py @@ -7,6 +7,24 @@ from prometheus_client.parser import text_string_to_metric_families # Django REST Framework from rest_framework import renderers from rest_framework.request import override_method +from rest_framework.utils import encoders + + +class SurrogateEncoder(encoders.JSONEncoder): + + def encode(self, obj): + ret = super(SurrogateEncoder, self).encode(obj) + try: + ret.encode() + except UnicodeEncodeError as e: + if 'surrogates not allowed' in e.reason: + ret = ret.encode('utf-8', 'replace').decode() + return ret + + +class DefaultJSONRenderer(renderers.JSONRenderer): + + encoder_class = SurrogateEncoder class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index fe7c8c0ba3..355d247f62 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -310,7 +310,7 @@ REST_FRAMEWORK = { 'awx.api.parsers.JSONParser', ), 'DEFAULT_RENDERER_CLASSES': ( - 'rest_framework.renderers.JSONRenderer', + 'awx.api.renderers.DefaultJSONRenderer', 'awx.api.renderers.BrowsableAPIRenderer', ), 'DEFAULT_METADATA_CLASS': 'awx.api.metadata.Metadata', From 806a4686001afe24c700e0b0c6d7693ba3d809ae Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 19 Aug 2020 10:26:53 -0400 Subject: [PATCH 037/123] Use organization api to create users This ensures that the user will be related to the chosen organization when it is created. --- awx/ui_next/src/api/models/Organizations.js | 4 ++++ awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx | 5 +++-- .../src/screens/User/UserAdd/UserAdd.test.jsx | 12 ++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index 3cbe64c284..267c9aba1e 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -15,6 +15,10 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { readTeams(id, params) { return this.http.get(`${this.baseUrl}${id}/teams/`, { params }); } + + createUser(id, data) { + return this.http.post(`${this.baseUrl}${id}/users/`, data); + } } export default Organizations; diff --git a/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx b/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx index d54da84592..78d18340be 100644 --- a/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx +++ b/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx @@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom'; import { Card, PageSection } from '@patternfly/react-core'; import { CardBody } from '../../../components/Card'; import UserForm from '../shared/UserForm'; -import { UsersAPI } from '../../../api'; +import { OrganizationsAPI } from '../../../api'; function UserAdd() { const [formSubmitError, setFormSubmitError] = useState(null); @@ -11,10 +11,11 @@ function UserAdd() { const handleSubmit = async values => { setFormSubmitError(null); + const { organization, ...userValues } = values; try { const { data: { id }, - } = await UsersAPI.create(values); + } = await OrganizationsAPI.createUser(organization, userValues); history.push(`/users/${id}/details`); } catch (error) { setFormSubmitError(error); diff --git a/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx b/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx index ec18de0974..55327cfcbd 100644 --- a/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx +++ b/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx @@ -6,7 +6,7 @@ import { waitForElement, } from '../../../../testUtils/enzymeHelpers'; import UserAdd from './UserAdd'; -import { UsersAPI } from '../../../api'; +import { OrganizationsAPI } from '../../../api'; jest.mock('../../../api'); let wrapper; @@ -16,7 +16,7 @@ describe('', () => { await act(async () => { wrapper = mountWithContexts(); }); - UsersAPI.create.mockResolvedValueOnce({ data: {} }); + OrganizationsAPI.createUser.mockResolvedValueOnce({ data: {} }); const updatedUserData = { username: 'sysadmin', email: 'sysadmin@ansible.com', @@ -30,7 +30,11 @@ describe('', () => { await act(async () => { wrapper.find('UserForm').prop('handleSubmit')(updatedUserData); }); - expect(UsersAPI.create).toHaveBeenCalledWith(updatedUserData); + + const { organization, ...userData } = updatedUserData; + expect(OrganizationsAPI.createUser.mock.calls).toEqual([ + [organization, userData], + ]); }); test('should navigate to users list when cancel is clicked', async () => { @@ -58,7 +62,7 @@ describe('', () => { is_superuser: true, is_system_auditor: false, }; - UsersAPI.create.mockResolvedValueOnce({ + OrganizationsAPI.createUser.mockResolvedValueOnce({ data: { id: 5, ...userData, From dc9f2441dfde3bf1657e4cc8ad43fd06e2db1bab Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 19 Aug 2020 10:52:00 -0400 Subject: [PATCH 038/123] Embolden user organization name --- .../screens/User/UserOrganizations/UserOrganizationListItem.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx b/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx index bc01af942d..d31c1c7670 100644 --- a/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx +++ b/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx @@ -19,7 +19,7 @@ export default function UserOrganizationListItem({ organization }) { to={`/organizations/${organization.id}/details`} id={labelId} > - {organization.name} + {organization.name} , From 612bb81976ba5d590656e534d270d65c697129c2 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 19 Aug 2020 11:02:28 -0400 Subject: [PATCH 039/123] add a deprecation warning for mercurial project syncs see: https://github.com/ansible/awx/issues/7932 --- CHANGELOG.md | 1 + awx/playbooks/action_plugins/hg_deprecation.py | 15 +++++++++++++++ awx/playbooks/project_update_hg_tasks.yml | 3 +++ 3 files changed, 19 insertions(+) create mode 100644 awx/playbooks/action_plugins/hg_deprecation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 54573a057f..15723a0106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/`. ## 14.1.0 (TBD) +- Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 - Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 - Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847 - Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 diff --git a/awx/playbooks/action_plugins/hg_deprecation.py b/awx/playbooks/action_plugins/hg_deprecation.py new file mode 100644 index 0000000000..d4593b2360 --- /dev/null +++ b/awx/playbooks/action_plugins/hg_deprecation.py @@ -0,0 +1,15 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.action import ActionBase + + +class ActionModule(ActionBase): + + def run(self, tmp=None, task_vars=None): + self._supports_check_mode = False + result = super(ActionModule, self).run(tmp, task_vars) + result['changed'] = result['failed'] = False + result['msg'] = '' + self._display.deprecated("Mercurial support is deprecated") + return result diff --git a/awx/playbooks/project_update_hg_tasks.yml b/awx/playbooks/project_update_hg_tasks.yml index 251013698f..3553d60984 100644 --- a/awx/playbooks/project_update_hg_tasks.yml +++ b/awx/playbooks/project_update_hg_tasks.yml @@ -1,4 +1,7 @@ --- +- name: Mercurial support is deprecated. + hg_deprecation: + - name: update project using hg hg: dest: "{{project_path|quote}}" From 8a2bf8c1fc993f0d712d281f1cc2bed7867736e5 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Wed, 19 Aug 2020 12:18:04 -0400 Subject: [PATCH 040/123] adds fix to allow look up to fetch data --- awx/ui_next/src/components/Lookup/CredentialLookup.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx index da62a053ce..22f7dc54a6 100644 --- a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx +++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx @@ -68,8 +68,8 @@ function CredentialLookup({ 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), + actionsResponse.data?.actions?.GET || {} + ).filter(key => actionsResponse.data?.actions?.GET[key].filterable), }; }, [ credentialTypeId, From 815d691622c612153b62f6c23d35b2d682f62a0f Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 19 Aug 2020 12:21:32 -0400 Subject: [PATCH 041/123] clean up old authtoken support just use Bearer tokens - those are the only type of tokens we support --- awxkit/awxkit/api/client.py | 7 +++---- awxkit/awxkit/awx/utils.py | 2 +- awxkit/awxkit/cli/client.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/awxkit/awxkit/api/client.py b/awxkit/awxkit/api/client.py index 7844a7fa11..77c71a569b 100644 --- a/awxkit/awxkit/api/client.py +++ b/awxkit/awxkit/api/client.py @@ -15,12 +15,11 @@ class ConnectionException(exc.Common): class Token_Auth(requests.auth.AuthBase): - def __init__(self, token, auth_type='Token'): + def __init__(self, token): self.token = token - self.auth_type = auth_type def __call__(self, request): - request.headers['Authorization'] = '{0.auth_type} {0.token}'.format(self) + request.headers['Authorization'] = 'Bearer {0.token}'.format(self) return request @@ -57,7 +56,7 @@ class Connection(object): else: self.session.auth = (username, password) elif token: - self.session.auth = Token_Auth(token, auth_type=kwargs.get('auth_type', 'Token')) + self.session.auth = Token_Auth(token) else: self.session.auth = None diff --git a/awxkit/awxkit/awx/utils.py b/awxkit/awxkit/awx/utils.py index 2238297b67..d25e555ad6 100644 --- a/awxkit/awxkit/awx/utils.py +++ b/awxkit/awxkit/awx/utils.py @@ -90,7 +90,7 @@ def as_user(v, username, password=None): if session_id: del connection.session.cookies['sessionid'] if access_token: - kwargs = dict(token=access_token, auth_type='Bearer') + kwargs = dict(token=access_token) else: kwargs = connection.get_session_requirements() else: diff --git a/awxkit/awxkit/cli/client.py b/awxkit/awxkit/cli/client.py index 3feded89dc..f14d6df135 100755 --- a/awxkit/awxkit/cli/client.py +++ b/awxkit/awxkit/cli/client.py @@ -88,7 +88,7 @@ class CLI(object): token = self.get_config('token') if token: self.root.connection.login( - None, None, token=token, auth_type='Bearer' + None, None, token=token, ) else: config.use_sessions = True From b14515b28792253df17efbe398131ce7003ee02b Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 19 Aug 2020 13:18:44 -0400 Subject: [PATCH 042/123] fix a bug that prevents the explicit removal of instances from groups --- awx/main/management/commands/remove_from_queue.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/main/management/commands/remove_from_queue.py b/awx/main/management/commands/remove_from_queue.py index df7530992c..b249749219 100644 --- a/awx/main/management/commands/remove_from_queue.py +++ b/awx/main/management/commands/remove_from_queue.py @@ -32,4 +32,7 @@ class Command(BaseCommand): sys.exit(1) i = i.first() ig.instances.remove(i) + if i.hostname in ig.policy_instance_list: + ig.policy_instance_list.remove(i.hostname) + ig.save() print("Instance removed from instance group") From 7bff11379c55e0d027772a95b7fb7a64a877df4c Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Wed, 19 Aug 2020 13:32:48 -0400 Subject: [PATCH 043/123] Update awx/ui_next/src/components/Lookup/CredentialLookup.jsx Co-authored-by: Jake McDermott --- awx/ui_next/src/components/Lookup/CredentialLookup.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx index 22f7dc54a6..c67e0087c6 100644 --- a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx +++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx @@ -69,7 +69,7 @@ function CredentialLookup({ ).map(val => val.slice(0, -8)), searchableKeys: Object.keys( actionsResponse.data?.actions?.GET || {} - ).filter(key => actionsResponse.data?.actions?.GET[key].filterable), + ).filter(key => actionsResponse.data?.actions?.GET[key]?.filterable), }; }, [ credentialTypeId, From c0cb1dee91bae4a45580841eb3682cfad93b0104 Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 19 Aug 2020 13:55:42 -0400 Subject: [PATCH 044/123] Adds workflow detail tab to workflow results --- awx/ui_next/src/screens/Job/Job.jsx | 4 +--- .../src/screens/Job/JobDetail/JobDetail.jsx | 15 ++++++++++++++- .../src/screens/Job/JobDetail/JobDetail.test.jsx | 2 +- .../screens/Job/WorkflowDetail/WorkflowDetail.jsx | 7 ------- .../src/screens/Job/WorkflowDetail/index.js | 1 - 5 files changed, 16 insertions(+), 13 deletions(-) delete mode 100644 awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx delete mode 100644 awx/ui_next/src/screens/Job/WorkflowDetail/index.js diff --git a/awx/ui_next/src/screens/Job/Job.jsx b/awx/ui_next/src/screens/Job/Job.jsx index bc95cfaf39..9530c4eeb3 100644 --- a/awx/ui_next/src/screens/Job/Job.jsx +++ b/awx/ui_next/src/screens/Job/Job.jsx @@ -7,10 +7,8 @@ import { Card, PageSection } from '@patternfly/react-core'; import { JobsAPI } from '../../api'; import ContentError from '../../components/ContentError'; import RoutedTabs from '../../components/RoutedTabs'; - import JobDetail from './JobDetail'; import JobOutput from './JobOutput'; -import WorkflowDetail from './WorkflowDetail'; import { WorkflowOutput } from './WorkflowOutput'; import { JOB_TYPE_URL_SEGMENTS } from '../../constants'; @@ -129,7 +127,7 @@ class Job extends Component { {job && job.type === 'workflow_job' && [ - + , diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx index dc7a3e0e6b..cd7b2c4dd3 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx @@ -84,6 +84,7 @@ function JobDetail({ job, i18n }) { instance_group: instanceGroup, inventory, job_template: jobTemplate, + workflow_job_template: workflowJobTemplate, labels, project, } = job.summary_fields; @@ -143,7 +144,7 @@ function JobDetail({ job, i18n }) { /> {jobTemplate && ( {jobTemplate.name} @@ -151,6 +152,18 @@ function JobDetail({ job, i18n }) { } /> )} + {workflowJobTemplate && ( + + {workflowJobTemplate.name} + + } + /> + )} ', () => { assertDetail('Status', 'Successful'); assertDetail('Started', '8/8/2019, 7:24:18 PM'); assertDetail('Finished', '8/8/2019, 7:24:50 PM'); - assertDetail('Template', mockJobData.summary_fields.job_template.name); + assertDetail('Job Template', mockJobData.summary_fields.job_template.name); assertDetail('Job Type', 'Run'); assertDetail('Launched By', mockJobData.summary_fields.created_by.username); assertDetail('Inventory', mockJobData.summary_fields.inventory.name); diff --git a/awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx b/awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx deleted file mode 100644 index 26d0384ab3..0000000000 --- a/awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -function WorkflowDetail() { - return
Workflow Detail!
; -} - -export default WorkflowDetail; diff --git a/awx/ui_next/src/screens/Job/WorkflowDetail/index.js b/awx/ui_next/src/screens/Job/WorkflowDetail/index.js deleted file mode 100644 index 3ced22dd95..0000000000 --- a/awx/ui_next/src/screens/Job/WorkflowDetail/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './WorkflowDetail'; From 40f67414741b44a8c76c104825362aded869898b Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 3 Aug 2020 12:09:00 -0400 Subject: [PATCH 045/123] Adding import/export awx kit features Changed library structure Origional TowerModule becomes TowerLegacyModule TowerModule from tower_api becomes TowerAPIModule A real base TowerModule is created in tower_module.py A new TowerAWXKitModule is created in tower_awxkit TowerAWXKitModule and TowerAPIModule are child classes of TowerModule --- awx_collection/plugins/inventory/tower.py | 6 +- awx_collection/plugins/lookup/tower_api.py | 6 +- .../plugins/module_utils/tower_api.py | 242 +----------------- .../plugins/module_utils/tower_awxkit.py | 50 ++++ .../{ansible_tower.py => tower_legacy.py} | 4 +- .../plugins/module_utils/tower_module.py | 240 +++++++++++++++++ .../plugins/modules/tower_credential.py | 4 +- .../modules/tower_credential_input_source.py | 4 +- .../plugins/modules/tower_credential_type.py | 4 +- .../plugins/modules/tower_export.py | 155 +++++++++++ awx_collection/plugins/modules/tower_group.py | 4 +- awx_collection/plugins/modules/tower_host.py | 4 +- .../plugins/modules/tower_import.py | 95 +++++++ .../plugins/modules/tower_inventory.py | 4 +- .../plugins/modules/tower_inventory_source.py | 4 +- .../plugins/modules/tower_job_cancel.py | 4 +- .../plugins/modules/tower_job_launch.py | 4 +- .../plugins/modules/tower_job_list.py | 4 +- .../plugins/modules/tower_job_template.py | 4 +- .../plugins/modules/tower_job_wait.py | 4 +- awx_collection/plugins/modules/tower_label.py | 4 +- .../plugins/modules/tower_license.py | 4 +- awx_collection/plugins/modules/tower_meta.py | 4 +- .../plugins/modules/tower_notification.py | 4 +- .../plugins/modules/tower_organization.py | 4 +- .../plugins/modules/tower_project.py | 4 +- .../plugins/modules/tower_receive.py | 4 +- awx_collection/plugins/modules/tower_role.py | 4 +- .../plugins/modules/tower_schedule.py | 4 +- awx_collection/plugins/modules/tower_send.py | 4 +- .../plugins/modules/tower_settings.py | 4 +- awx_collection/plugins/modules/tower_team.py | 4 +- awx_collection/plugins/modules/tower_token.py | 4 +- awx_collection/plugins/modules/tower_user.py | 4 +- .../modules/tower_workflow_job_template.py | 4 +- .../tower_workflow_job_template_node.py | 4 +- .../plugins/modules/tower_workflow_launch.py | 4 +- .../modules/tower_workflow_template.py | 6 +- awx_collection/test/awx/conftest.py | 25 +- awx_collection/test/awx/test_module_utils.py | 18 +- .../roles/generate/templates/tower_module.j2 | 4 +- 41 files changed, 652 insertions(+), 315 deletions(-) create mode 100644 awx_collection/plugins/module_utils/tower_awxkit.py rename awx_collection/plugins/module_utils/{ansible_tower.py => tower_legacy.py} (97%) create mode 100644 awx_collection/plugins/module_utils/tower_module.py create mode 100644 awx_collection/plugins/modules/tower_export.py create mode 100644 awx_collection/plugins/modules/tower_import.py diff --git a/awx_collection/plugins/inventory/tower.py b/awx_collection/plugins/inventory/tower.py index 872e2a3328..7dc4aaa1a3 100644 --- a/awx_collection/plugins/inventory/tower.py +++ b/awx_collection/plugins/inventory/tower.py @@ -72,7 +72,7 @@ from ansible.errors import AnsibleParserError, AnsibleOptionsError from ansible.plugins.inventory import BaseInventoryPlugin from ansible.config.manager import ensure_type -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def handle_error(**kwargs): @@ -104,12 +104,12 @@ class InventoryModule(BaseInventoryPlugin): # Defer processing of params to logic shared with the modules module_params = {} - for plugin_param, module_param in TowerModule.short_params.items(): + for plugin_param, module_param in TowerAPIModule.short_params.items(): opt_val = self.get_option(plugin_param) if opt_val is not None: module_params[module_param] = opt_val - module = TowerModule( + module = TowerAPIModule( argument_spec={}, direct_params=module_params, error_callback=handle_error, warn_callback=self.warn_callback ) diff --git a/awx_collection/plugins/lookup/tower_api.py b/awx_collection/plugins/lookup/tower_api.py index 9829507125..76b32be60a 100644 --- a/awx_collection/plugins/lookup/tower_api.py +++ b/awx_collection/plugins/lookup/tower_api.py @@ -115,7 +115,7 @@ from ansible.plugins.lookup import LookupBase from ansible.errors import AnsibleError from ansible.module_utils._text import to_native from ansible.utils.display import Display -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule class LookupModule(LookupBase): @@ -133,13 +133,13 @@ class LookupModule(LookupBase): # Defer processing of params to logic shared with the modules module_params = {} - for plugin_param, module_param in TowerModule.short_params.items(): + for plugin_param, module_param in TowerAPIModule.short_params.items(): opt_val = self.get_option(plugin_param) if opt_val is not None: module_params[module_param] = opt_val # Create our module - module = TowerModule( + module = TowerAPIModule( argument_spec={}, direct_params=module_params, error_callback=self.handle_error, warn_callback=self.warn_callback ) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index d0120ec003..089273bff1 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -1,37 +1,17 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils.basic import AnsibleModule, env_fallback +from . tower_module import TowerModule from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError -from ansible.module_utils.six import PY2, string_types -from ansible.module_utils.six.moves import StringIO -from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode +from ansible.module_utils.six import PY2 +from ansible.module_utils.six.moves.urllib.parse import urlencode from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.http_cookiejar import CookieJar -from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError -from socket import gethostbyname import re from json import loads, dumps -from os.path import isfile, expanduser, split, join, exists, isdir -from os import access, R_OK, getcwd -from distutils.util import strtobool -try: - import yaml - HAS_YAML = True -except ImportError: - HAS_YAML = False - - -class ConfigFileException(Exception): - pass - - -class ItemNotDefined(Exception): - pass - - -class TowerModule(AnsibleModule): +class TowerAPIModule(TowerModule): + # TODO: Move the collection version check into tower_module.py # This gets set by the make process so whatever is in here is irrelevant _COLLECTION_VERSION = "0.0.1-devel" _COLLECTION_TYPE = "awx" @@ -41,197 +21,15 @@ class TowerModule(AnsibleModule): 'awx': 'AWX', 'tower': 'Red Hat Ansible Tower', } - url = None - AUTH_ARGSPEC = dict( - tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), - tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), - tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), - validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), - tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), - tower_config_file=dict(type='path', required=False, default=None), - ) - short_params = { - 'host': 'tower_host', - 'username': 'tower_username', - 'password': 'tower_password', - 'verify_ssl': 'validate_certs', - 'oauth_token': 'tower_oauthtoken', - } - host = '127.0.0.1' - username = None - password = None - verify_ssl = True - oauth_token = None - oauth_token_id = None session = None cookie_jar = CookieJar() - authenticated = False - config_name = 'tower_cli.cfg' - ENCRYPTED_STRING = "$encrypted$" - version_checked = False - error_callback = None - warn_callback = None def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): - full_argspec = {} - full_argspec.update(TowerModule.AUTH_ARGSPEC) - full_argspec.update(argument_spec) kwargs['supports_check_mode'] = True - self.error_callback = error_callback - self.warn_callback = warn_callback - - self.json_output = {'changed': False} - - if direct_params is not None: - self.params = direct_params - else: - super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs) - - self.load_config_files() - - # Parameters specified on command line will override settings in any config - for short_param, long_param in self.short_params.items(): - direct_value = self.params.get(long_param) - if direct_value is not None: - setattr(self, short_param, direct_value) - - # Perform magic depending on whether tower_oauthtoken is a string or a dict - if self.params.get('tower_oauthtoken'): - token_param = self.params.get('tower_oauthtoken') - if type(token_param) is dict: - if 'token' in token_param: - self.oauth_token = self.params.get('tower_oauthtoken')['token'] - else: - self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") - elif isinstance(token_param, string_types): - self.oauth_token = self.params.get('tower_oauthtoken') - else: - error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) - self.fail_json(msg=error_msg) - - # Perform some basic validation - if not re.match('^https{0,1}://', self.host): - self.host = "https://{0}".format(self.host) - - # Try to parse the hostname as a url - try: - self.url = urlparse(self.host) - except Exception as e: - self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e)) - - # Try to resolve the hostname - hostname = self.url.netloc.split(':')[0] - try: - gethostbyname(hostname) - except Exception as e: - self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e)) - + super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, error_callback=error_callback, warn_callback=warn_callback, **kwargs) self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl) - def load_config_files(self): - # Load configs like TowerCLI would have from least import to most - config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))] - local_dir = getcwd() - config_files.append(join(local_dir, self.config_name)) - while split(local_dir)[1]: - local_dir = split(local_dir)[0] - config_files.insert(2, join(local_dir, ".{0}".format(self.config_name))) - - # If we have a specified tower config, load it - if self.params.get('tower_config_file'): - duplicated_params = [ - fn for fn in self.AUTH_ARGSPEC - if fn != 'tower_config_file' and self.params.get(fn) is not None - ] - if duplicated_params: - self.warn(( - 'The parameter(s) {0} were provided at the same time as tower_config_file. ' - 'Precedence may be unstable, we suggest either using config file or params.' - ).format(', '.join(duplicated_params))) - try: - # TODO: warn if there are conflicts with other params - self.load_config(self.params.get('tower_config_file')) - except ConfigFileException as cfe: - # Since we were told specifically to load this we want it to fail if we have an error - self.fail_json(msg=cfe) - else: - for config_file in config_files: - if exists(config_file) and not isdir(config_file): - # Only throw a formatting error if the file exists and is not a directory - try: - self.load_config(config_file) - except ConfigFileException: - self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file)) - - def load_config(self, config_path): - # Validate the config file is an actual file - if not isfile(config_path): - raise ConfigFileException('The specified config file does not exist') - - if not access(config_path, R_OK): - raise ConfigFileException("The specified config file cannot be read") - - # Read in the file contents: - with open(config_path, 'r') as f: - config_string = f.read() - - # First try to yaml load the content (which will also load json) - try: - try_config_parsing = True - if HAS_YAML: - try: - config_data = yaml.load(config_string, Loader=yaml.SafeLoader) - # If this is an actual ini file, yaml will return the whole thing as a string instead of a dict - if type(config_data) is not dict: - raise AssertionError("The yaml config file is not properly formatted as a dict.") - try_config_parsing = False - - except(AttributeError, yaml.YAMLError, AssertionError): - try_config_parsing = True - - if try_config_parsing: - # TowerCLI used to support a config file with a missing [general] section by prepending it if missing - if '[general]' not in config_string: - config_string = '[general]\n{0}'.format(config_string) - - config = ConfigParser() - - try: - placeholder_file = StringIO(config_string) - # py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3 - # This "if" removes the deprecation warning - if hasattr(config, 'read_file'): - config.read_file(placeholder_file) - else: - config.readfp(placeholder_file) - - # If we made it here then we have values from reading the ini file, so let's pull them out into a dict - config_data = {} - for honorred_setting in self.short_params: - try: - config_data[honorred_setting] = config.get('general', honorred_setting) - except NoOptionError: - pass - - except Exception as e: - raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e)) - - except Exception as e: - raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) - - # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here - for honorred_setting in self.short_params: - if honorred_setting in config_data: - # Veriffy SSL must be a boolean - if honorred_setting == 'verify_ssl': - if type(config_data[honorred_setting]) is str: - setattr(self, honorred_setting, strtobool(config_data[honorred_setting])) - else: - setattr(self, honorred_setting, bool(config_data[honorred_setting])) - else: - setattr(self, honorred_setting, config_data[honorred_setting]) - @staticmethod def param_to_endpoint(name): exceptions = { @@ -650,13 +448,13 @@ class TowerModule(AnsibleModule): """ if isinstance(obj, dict): for val in obj.values(): - if TowerModule.has_encrypted_values(val): + if TowerAPIModule.has_encrypted_values(val): return True elif isinstance(obj, list): for val in obj: - if TowerModule.has_encrypted_values(val): + if TowerAPIModule.has_encrypted_values(val): return True - elif obj == TowerModule.ENCRYPTED_STRING: + elif obj == TowerAPIModule.ENCRYPTED_STRING: return True return False @@ -678,10 +476,9 @@ class TowerModule(AnsibleModule): # This will exit from the module on its own # If the method successfully updates an item and on_update param is defined, # the on_update parameter will be called as a method pasing in this object and the json from the response - # This will return one of three things: + # This will return one of two things: # 1. None if the existing_item does not need to be updated # 2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module. - # 3. An ItemNotDefined exception, if the existing_item does not exist # Note: common error codes from the Tower API can cause the module to fail response = None if existing_item: @@ -777,25 +574,6 @@ class TowerModule(AnsibleModule): # Sanity check: Did the server send back some kind of internal error? self.warn('Failed to release tower token {0}: {1}'.format(self.oauth_token_id, e)) - def fail_json(self, **kwargs): - # Try to log out if we are authenticated - self.logout() - if self.error_callback: - self.error_callback(**kwargs) - else: - super(TowerModule, self).fail_json(**kwargs) - - def exit_json(self, **kwargs): - # Try to log out if we are authenticated - self.logout() - super(TowerModule, self).exit_json(**kwargs) - - def warn(self, warning): - if self.warn_callback is not None: - self.warn_callback(warning) - else: - super(TowerModule, self).warn(warning) - def is_job_done(self, job_status): if job_status in ['new', 'pending', 'waiting', 'running']: return False diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py new file mode 100644 index 0000000000..eedf3a7387 --- /dev/null +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from . tower_module import TowerModule +from ansible.module_utils.basic import missing_required_lib + +try: + from awxkit.api.client import Connection + from awxkit.api.pages.api import ApiV2 + from awxkit.api import get_registered_page + HAS_AWX_KIT = True +except ImportError: + HAS_AWX_KIT = False + + +class TowerAWXKitModule(TowerModule): + connection = None + apiV2Ref = None + + def __init__(self, argument_spec, **kwargs): + kwargs['supports_check_mode'] = False + + super(TowerAWXKitModule, self).__init__(argument_spec=argument_spec, **kwargs) + + # Die if we don't have AWX_KIT installed + if not HAS_AWX_KIT: + self.exit_module(msg=missing_required_lib('awxkit')) + + # Establish our conneciton object + self.connection = Connection(self.host, verify=self.verify_ssl) + + def authenticate(self): + try: + self.connection.login(username=self.username, password=self.password, token=self.oauth_token) + # If we have neither of these, then we can try un-authenticated access + self.authenticated = True + except Exception: + self.exit_module("Failed to authenticate") + + def get_api_v2_object(self): + if not self.apiV2Ref: + if not self.authenticated: + self.authenticate() + v2_index = get_registered_page('/api/v2/')(self.connection).get() + self.api_ref = ApiV2(connection=self.connection, **{'json': v2_index}) + return self.api_ref + + def logout(self): + if self.authenticated: + self.connection.logout() diff --git a/awx_collection/plugins/module_utils/ansible_tower.py b/awx_collection/plugins/module_utils/tower_legacy.py similarity index 97% rename from awx_collection/plugins/module_utils/ansible_tower.py rename to awx_collection/plugins/module_utils/tower_legacy.py index 17d6a38680..3c8408610d 100644 --- a/awx_collection/plugins/module_utils/ansible_tower.py +++ b/awx_collection/plugins/module_utils/tower_legacy.py @@ -91,7 +91,7 @@ def tower_check_mode(module): module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo)) -class TowerModule(AnsibleModule): +class TowerLegacyModule(AnsibleModule): def __init__(self, argument_spec, **kwargs): args = dict( tower_host=dict(), @@ -110,7 +110,7 @@ class TowerModule(AnsibleModule): ('tower_config_file', 'validate_certs'), )) - super(TowerModule, self).__init__(argument_spec=args, **kwargs) + super(TowerLegacyModule, self).__init__(argument_spec=args, **kwargs) if not HAS_TOWER_CLI: self.fail_json(msg=missing_required_lib('ansible-tower-cli'), diff --git a/awx_collection/plugins/module_utils/tower_module.py b/awx_collection/plugins/module_utils/tower_module.py new file mode 100644 index 0000000000..5d5b4d4239 --- /dev/null +++ b/awx_collection/plugins/module_utils/tower_module.py @@ -0,0 +1,240 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.six import PY2, string_types +from ansible.module_utils.six.moves import StringIO +from ansible.module_utils.six.moves.urllib.parse import urlparse +from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError +from socket import gethostbyname +import re +from os.path import isfile, expanduser, split, join, exists, isdir +from os import access, R_OK, getcwd +from distutils.util import strtobool + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +class ConfigFileException(Exception): + pass + + +class ItemNotDefined(Exception): + pass + + +class TowerModule(AnsibleModule): + url = None + AUTH_ARGSPEC = dict( + tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), + tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), + tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), + validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), + tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), + tower_config_file=dict(type='path', required=False, default=None), + ) + short_params = { + 'host': 'tower_host', + 'username': 'tower_username', + 'password': 'tower_password', + 'verify_ssl': 'validate_certs', + 'oauth_token': 'tower_oauthtoken', + } + host = '127.0.0.1' + username = None + password = None + verify_ssl = True + oauth_token = None + oauth_token_id = None + authenticated = False + config_name = 'tower_cli.cfg' + ENCRYPTED_STRING = "$encrypted$" + version_checked = False + error_callback = None + warn_callback = None + + def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs): + full_argspec = {} + full_argspec.update(TowerModule.AUTH_ARGSPEC) + full_argspec.update(argument_spec) + kwargs['supports_check_mode'] = True + + self.error_callback = error_callback + self.warn_callback = warn_callback + + self.json_output = {'changed': False} + + if direct_params is not None: + self.params = direct_params + else: + super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs) + + self.load_config_files() + + # Parameters specified on command line will override settings in any config + for short_param, long_param in self.short_params.items(): + direct_value = self.params.get(long_param) + if direct_value is not None: + setattr(self, short_param, direct_value) + + # Perform magic depending on whether tower_oauthtoken is a string or a dict + if self.params.get('tower_oauthtoken'): + token_param = self.params.get('tower_oauthtoken') + if type(token_param) is dict: + if 'token' in token_param: + self.oauth_token = self.params.get('tower_oauthtoken')['token'] + else: + self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") + elif isinstance(token_param, string_types): + self.oauth_token = self.params.get('tower_oauthtoken') + else: + error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) + self.fail_json(msg=error_msg) + + # Perform some basic validation + if not re.match('^https{0,1}://', self.host): + self.host = "https://{0}".format(self.host) + + # Try to parse the hostname as a url + try: + self.url = urlparse(self.host) + except Exception as e: + self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e)) + + # Try to resolve the hostname + hostname = self.url.netloc.split(':')[0] + try: + gethostbyname(hostname) + except Exception as e: + self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e)) + + def load_config_files(self): + # Load configs like TowerCLI would have from least import to most + config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))] + local_dir = getcwd() + config_files.append(join(local_dir, self.config_name)) + while split(local_dir)[1]: + local_dir = split(local_dir)[0] + config_files.insert(2, join(local_dir, ".{0}".format(self.config_name))) + + # If we have a specified tower config, load it + if self.params.get('tower_config_file'): + duplicated_params = [ + fn for fn in self.AUTH_ARGSPEC + if fn != 'tower_config_file' and self.params.get(fn) is not None + ] + if duplicated_params: + self.warn(( + 'The parameter(s) {0} were provided at the same time as tower_config_file. ' + 'Precedence may be unstable, we suggest either using config file or params.' + ).format(', '.join(duplicated_params))) + try: + # TODO: warn if there are conflicts with other params + self.load_config(self.params.get('tower_config_file')) + except ConfigFileException as cfe: + # Since we were told specifically to load this we want it to fail if we have an error + self.fail_json(msg=cfe) + else: + for config_file in config_files: + if exists(config_file) and not isdir(config_file): + # Only throw a formatting error if the file exists and is not a directory + try: + self.load_config(config_file) + except ConfigFileException: + self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file)) + + def load_config(self, config_path): + # Validate the config file is an actual file + if not isfile(config_path): + raise ConfigFileException('The specified config file does not exist') + + if not access(config_path, R_OK): + raise ConfigFileException("The specified config file cannot be read") + + # Read in the file contents: + with open(config_path, 'r') as f: + config_string = f.read() + + # First try to yaml load the content (which will also load json) + try: + try_config_parsing = True + if HAS_YAML: + try: + config_data = yaml.load(config_string, Loader=yaml.SafeLoader) + # If this is an actual ini file, yaml will return the whole thing as a string instead of a dict + if type(config_data) is not dict: + raise AssertionError("The yaml config file is not properly formatted as a dict.") + try_config_parsing = False + + except(AttributeError, yaml.YAMLError, AssertionError): + try_config_parsing = True + + if try_config_parsing: + # TowerCLI used to support a config file with a missing [general] section by prepending it if missing + if '[general]' not in config_string: + config_string = '[general]\n{0}'.format(config_string) + + config = ConfigParser() + + try: + placeholder_file = StringIO(config_string) + # py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3 + # This "if" removes the deprecation warning + if hasattr(config, 'read_file'): + config.read_file(placeholder_file) + else: + config.readfp(placeholder_file) + + # If we made it here then we have values from reading the ini file, so let's pull them out into a dict + config_data = {} + for honorred_setting in self.short_params: + try: + config_data[honorred_setting] = config.get('general', honorred_setting) + except NoOptionError: + pass + + except Exception as e: + raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e)) + + except Exception as e: + raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) + + # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here + for honorred_setting in self.short_params: + if honorred_setting in config_data: + # Veriffy SSL must be a boolean + if honorred_setting == 'verify_ssl': + if type(config_data[honorred_setting]) is str: + setattr(self, honorred_setting, strtobool(config_data[honorred_setting])) + else: + setattr(self, honorred_setting, bool(config_data[honorred_setting])) + else: + setattr(self, honorred_setting, config_data[honorred_setting]) + + + def logout(self): + # This method is intended to be overridden + pass + + def fail_json(self, **kwargs): + # Try to log out if we are authenticated + self.logout() + if self.error_callback: + self.error_callback(**kwargs) + else: + super(TowerModule, self).fail_json(**kwargs) + + def exit_json(self, **kwargs): + # Try to log out if we are authenticated + self.logout() + super(TowerModule, self).exit_json(**kwargs) + + def warn(self, warning): + if self.warn_callback is not None: + self.warn_callback(warning) + else: + super(TowerModule, self).warn(warning) diff --git a/awx_collection/plugins/modules/tower_credential.py b/awx_collection/plugins/modules/tower_credential.py index 97a801aa8f..2d4cbd3331 100644 --- a/awx_collection/plugins/modules/tower_credential.py +++ b/awx_collection/plugins/modules/tower_credential.py @@ -269,7 +269,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule KIND_CHOICES = { 'ssh': 'Machine', @@ -336,7 +336,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']]) + module = TowerAPIModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']]) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_credential_input_source.py b/awx_collection/plugins/modules/tower_credential_input_source.py index bc2cb85579..cdc55cb1f0 100644 --- a/awx_collection/plugins/modules/tower_credential_input_source.py +++ b/awx_collection/plugins/modules/tower_credential_input_source.py @@ -70,7 +70,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -85,7 +85,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters description = module.params.get('description') diff --git a/awx_collection/plugins/modules/tower_credential_type.py b/awx_collection/plugins/modules/tower_credential_type.py index 561ae78f5a..53f0cc45c9 100644 --- a/awx_collection/plugins/modules/tower_credential_type.py +++ b/awx_collection/plugins/modules/tower_credential_type.py @@ -81,7 +81,7 @@ EXAMPLES = ''' RETURN = ''' # ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule KIND_CHOICES = { 'ssh': 'Machine', @@ -105,7 +105,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py new file mode 100644 index 0000000000..84c08fe454 --- /dev/null +++ b/awx_collection/plugins/modules/tower_export.py @@ -0,0 +1,155 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_export +author: "John Westcott IV (@john-westcott-iv)" +version_added: "3.7" +short_description: export resources from Ansible Tower. +description: + - Export assets from Ansible Tower. +options: + all: + description: + - Export all assets + type: bool + default: 'False' + organizations: + description: + - organization name to export + default: '' + type: str + user: + description: + - user name to export + default: '' + type: str + team: + description: + - team name to export + default: '' + type: str + credential_type: + description: + - credential type name to export + default: '' + type: str + credential: + description: + - credential name to export + default: '' + type: str + notification_template: + description: + - notification template name to export + default: '' + type: str + inventory_script: + description: + - inventory script name to export + default: '' + type: str + inventory: + description: + - inventory name to export + default: '' + type: str + project: + description: + - project name to export + default: '' + type: str + job_template: + description: + - job template name to export + default: '' + type: str + workflow: + description: + - workflow name to export + default: '' + type: str +requirements: + - "awxkit >= 9.3.0" +notes: + - Specifying a name of "all" for any asset type will export all items of that asset type. +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Export all tower assets + tower_export: + all: True +- name: Export all inventories + tower_export: + inventory: 'all' +- name: Export a job template named "My Template" and all Credentials + tower_export: + job_template: "My Template" + credential: 'all' +''' + +from os import environ + +from ..module_utils.tower_awxkit import TowerAWXKitModule + +try: + from awxkit.api.pages.api import EXPORTABLE_RESOURCES + HAS_EXPORTABLE_RESOURCES=True +except ImportError: + HAS_EXPORTABLE_RESOURCES=False + + +def main(): + argument_spec = dict( + all=dict(type='bool', default=False), + ) + + # We are not going to raise an error here because the __init__ method of TowerAWXKitModule will do that for us + if HAS_EXPORTABLE_RESOURCES: + for resource in EXPORTABLE_RESOURCES: + argument_spec[resource] = dict() + + module = TowerAWXKitModule(argument_spec=argument_spec) + + if not HAS_EXPORTABLE_RESOURCES: + module.fail_json(msg="Your version of awxkit does not have import/export") + + # The export process will never change a Tower system + module.json_output['changed'] = False + + # The exporter code currently works like the following: + # Empty list == all assets of that type + # string = just one asset of that type (by name) + # None = skip asset type + # Here we are going to setup a dict of values to export + export_args = {} + for resource in EXPORTABLE_RESOURCES: + if module.params.get('all') or module.params.get(resource) == 'all': + # If we are exporting everything or we got the keyword "all" we pass in an empty list for this asset type + export_args[resource] = [] + else: + # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items + export_args[resource] = module.params.get(resource) + + # Run the export process + module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_group.py b/awx_collection/plugins/modules/tower_group.py index a64826eb88..9e2eaf4e7e 100644 --- a/awx_collection/plugins/modules/tower_group.py +++ b/awx_collection/plugins/modules/tower_group.py @@ -76,7 +76,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -94,7 +94,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_host.py b/awx_collection/plugins/modules/tower_host.py index cb4712a27c..f6bfe55404 100644 --- a/awx_collection/plugins/modules/tower_host.py +++ b/awx_collection/plugins/modules/tower_host.py @@ -72,7 +72,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -89,7 +89,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py new file mode 100644 index 0000000000..0bb5dec75a --- /dev/null +++ b/awx_collection/plugins/modules/tower_import.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_import +author: "John Westcott (@john-westcott-iv)" +version_added: "3.7" +short_description: import resources into Ansible Tower. +description: + - Import assets into Ansible Tower. See + U(https://www.ansible.com/tower) for an overview. +options: + assets: + description: + - The assets to import. + - This can be the output of tower_export or loaded from a file + required: True + type: dict +requirements: + - "awxkit >= 9.3.0" +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Import all tower assets + tower_import: + assets: "{{ export_output.assets }}" +''' + +from ..module_utils.tower_awxkit import TowerAWXKitModule + +# These two lines are not needed if awxkit changes to do progamatic notifications on issues +from ansible.module_utils.six.moves import StringIO +import logging + +# In this module we don't use EXPORTABLE_RESOURCES, we just want to validate that our installed awxkit has import/export +try: + from awxkit.api.pages.api import EXPORTABLE_RESOURCES + HAS_EXPORTABLE_RESOURCES=True +except ImportError: + HAS_EXPORTABLE_RESOURCES=False + +def main(): + argument_spec = dict( + assets=dict(type='dict', required=True) + ) + + module = TowerAWXKitModule(argument_spec=argument_spec, supports_check_mode=False) + + assets = module.params.get('assets') + + if not HAS_EXPORTABLE_RESOURCES: + module.fail_json(msg="Your version of awxkit does not appear to have import/export") + + # Currently the import process does not return anything on error + # It simply just logs to pythons logger + # Setup a log gobbler to get error messages from import_assets + logger = logging.getLogger('awxkit.api.pages.api') + logger.setLevel(logging.WARNING) + log_capture_string = StringIO() + ch = logging.StreamHandler(log_capture_string) + ch.setLevel(logging.WARNING) + logger.addHandler(ch) + log_contents = '' + + # Run the import process + try: + module.json_output['changed'] = module.get_api_v2_object().import_assets(assets) + except Exception as e: + module.fail_json(msg="Failed to import assets {0}".format(e)) + finally: + # Finally consume the logs incase there were any errors and die if there were + log_contents = log_capture_string.getvalue() + log_capture_string.close() + if log_contents != '': + module.fail_json(msg=log_contents) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_inventory.py b/awx_collection/plugins/modules/tower_inventory.py index d0ced63048..7f03645e01 100644 --- a/awx_collection/plugins/modules/tower_inventory.py +++ b/awx_collection/plugins/modules/tower_inventory.py @@ -71,7 +71,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -88,7 +88,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index afa6c229e2..5b0e2961df 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -144,7 +144,7 @@ EXAMPLES = ''' private: false ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule from json import dumps @@ -184,7 +184,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_job_cancel.py b/awx_collection/plugins/modules/tower_job_cancel.py index 5e82834f6c..7404d452a4 100644 --- a/awx_collection/plugins/modules/tower_job_cancel.py +++ b/awx_collection/plugins/modules/tower_job_cancel.py @@ -50,7 +50,7 @@ id: ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -61,7 +61,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters job_id = module.params.get('job_id') diff --git a/awx_collection/plugins/modules/tower_job_launch.py b/awx_collection/plugins/modules/tower_job_launch.py index f3447bf24c..25a1c52fdf 100644 --- a/awx_collection/plugins/modules/tower_job_launch.py +++ b/awx_collection/plugins/modules/tower_job_launch.py @@ -124,7 +124,7 @@ status: sample: pending ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -146,7 +146,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) optional_args = {} # Extract our parameters diff --git a/awx_collection/plugins/modules/tower_job_list.py b/awx_collection/plugins/modules/tower_job_list.py index 2ecfd9d98a..642a48b03b 100644 --- a/awx_collection/plugins/modules/tower_job_list.py +++ b/awx_collection/plugins/modules/tower_job_list.py @@ -80,7 +80,7 @@ results: ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -93,7 +93,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, mutually_exclusive=[ ('page', 'all_pages'), diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index 1f1d776f28..1f12d5aadd 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -317,7 +317,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -388,7 +388,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py index 77a6977c5c..5e5801b25f 100644 --- a/awx_collection/plugins/modules/tower_job_wait.py +++ b/awx_collection/plugins/modules/tower_job_wait.py @@ -92,7 +92,7 @@ status: ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import time @@ -120,7 +120,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters job_id = module.params.get('job_id') diff --git a/awx_collection/plugins/modules/tower_label.py b/awx_collection/plugins/modules/tower_label.py index d0820d93a8..6a3a8288a2 100644 --- a/awx_collection/plugins/modules/tower_label.py +++ b/awx_collection/plugins/modules/tower_label.py @@ -54,7 +54,7 @@ EXAMPLES = ''' organization: My Organization ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -67,7 +67,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_license.py b/awx_collection/plugins/modules/tower_license.py index a1d9840d50..25d337cc24 100644 --- a/awx_collection/plugins/modules/tower_license.py +++ b/awx_collection/plugins/modules/tower_license.py @@ -43,12 +43,12 @@ EXAMPLES = ''' eula_accepted: True ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): - module = TowerModule( + module = TowerAPIModule( argument_spec=dict( data=dict(type='dict', required=True), eula_accepted=dict(type='bool', required=True), diff --git a/awx_collection/plugins/modules/tower_meta.py b/awx_collection/plugins/modules/tower_meta.py index 6d5c801ade..9455bdf0f4 100644 --- a/awx_collection/plugins/modules/tower_meta.py +++ b/awx_collection/plugins/modules/tower_meta.py @@ -62,11 +62,11 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): - module = TowerModule(argument_spec={}) + module = TowerAPIModule(argument_spec={}) namespace = { 'awx': 'awx', 'tower': 'ansible' diff --git a/awx_collection/plugins/modules/tower_notification.py b/awx_collection/plugins/modules/tower_notification.py index bfe672a50e..12dd28ef31 100644 --- a/awx_collection/plugins/modules/tower_notification.py +++ b/awx_collection/plugins/modules/tower_notification.py @@ -300,7 +300,7 @@ EXAMPLES = ''' RETURN = ''' # ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule OLD_INPUT_NAMES = ( 'username', 'sender', 'recipients', 'use_tls', @@ -355,7 +355,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index fbbbf2885c..1637828149 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -88,7 +88,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -106,7 +106,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 12f9e2809c..36a4f8666a 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -157,7 +157,7 @@ EXAMPLES = ''' import time -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def wait_for_project_update(module, last_request): @@ -205,7 +205,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_receive.py b/awx_collection/plugins/modules/tower_receive.py index b673e9b81d..bd08682503 100644 --- a/awx_collection/plugins/modules/tower_receive.py +++ b/awx_collection/plugins/modules/tower_receive.py @@ -134,7 +134,7 @@ assets: sample: [ {}, {} ] ''' -from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI +from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI try: from tower_cli.cli.transfer.receive import Receiver @@ -163,7 +163,7 @@ def main(): workflow=dict(type='list', default=[], elements='str'), ) - module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) + module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False) module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="awx.awx:14.0.0") diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index 4cba215b0a..d0d010a0a7 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -89,7 +89,7 @@ EXAMPLES = ''' state: present ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -109,7 +109,7 @@ def main(): state=dict(choices=['present', 'absent'], default='present'), ) - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) role_type = module.params.pop('role') role_field = role_type + '_role' diff --git a/awx_collection/plugins/modules/tower_schedule.py b/awx_collection/plugins/modules/tower_schedule.py index 24f8468e4a..4922aaa688 100644 --- a/awx_collection/plugins/modules/tower_schedule.py +++ b/awx_collection/plugins/modules/tower_schedule.py @@ -136,7 +136,7 @@ EXAMPLES = ''' register: result ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -161,7 +161,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters rrule = module.params.get('rrule') diff --git a/awx_collection/plugins/modules/tower_send.py b/awx_collection/plugins/modules/tower_send.py index 7ac60ece59..772b2b67ec 100644 --- a/awx_collection/plugins/modules/tower_send.py +++ b/awx_collection/plugins/modules/tower_send.py @@ -81,7 +81,7 @@ import os import sys from ansible.module_utils.six.moves import StringIO -from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI +from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI from tempfile import mkstemp @@ -103,7 +103,7 @@ def main(): password_management=dict(default='default', choices=['default', 'random']), ) - module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) + module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False) module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="awx.awx:14.0.0") diff --git a/awx_collection/plugins/modules/tower_settings.py b/awx_collection/plugins/modules/tower_settings.py index 9db41d9975..c2e8ed1ae5 100644 --- a/awx_collection/plugins/modules/tower_settings.py +++ b/awx_collection/plugins/modules/tower_settings.py @@ -70,7 +70,7 @@ EXAMPLES = ''' last_name: "surname" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule try: import yaml @@ -111,7 +111,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, required_one_of=[['name', 'settings']], mutually_exclusive=[['name', 'settings']], diff --git a/awx_collection/plugins/modules/tower_team.py b/awx_collection/plugins/modules/tower_team.py index e1506b2425..8ed56e48dc 100644 --- a/awx_collection/plugins/modules/tower_team.py +++ b/awx_collection/plugins/modules/tower_team.py @@ -60,7 +60,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -74,7 +74,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 165590520d..ee6fd5c200 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -117,7 +117,7 @@ tower_token: returned: on successful create ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def return_token(module, last_response): @@ -143,7 +143,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, mutually_exclusive=[ ('existing_token', 'existing_token_id'), diff --git a/awx_collection/plugins/modules/tower_user.py b/awx_collection/plugins/modules/tower_user.py index 7d049de016..15c41cb081 100644 --- a/awx_collection/plugins/modules/tower_user.py +++ b/awx_collection/plugins/modules/tower_user.py @@ -102,7 +102,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -119,7 +119,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters username = module.params.get('username') diff --git a/awx_collection/plugins/modules/tower_workflow_job_template.py b/awx_collection/plugins/modules/tower_workflow_job_template.py index c3ad692af4..8fb350b919 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template.py @@ -137,7 +137,7 @@ EXAMPLES = ''' organization: Default ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -176,7 +176,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 16902e9421..7ef9e14619 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -157,7 +157,7 @@ EXAMPLES = ''' - my-first-node ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -185,7 +185,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters identifier = module.params.get('identifier') diff --git a/awx_collection/plugins/modules/tower_workflow_launch.py b/awx_collection/plugins/modules/tower_workflow_launch.py index 8ef73d82fc..249feeed35 100644 --- a/awx_collection/plugins/modules/tower_workflow_launch.py +++ b/awx_collection/plugins/modules/tower_workflow_launch.py @@ -91,7 +91,7 @@ EXAMPLES = ''' wait: False ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json import time @@ -111,7 +111,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) optional_args = {} # Extract our parameters diff --git a/awx_collection/plugins/modules/tower_workflow_template.py b/awx_collection/plugins/modules/tower_workflow_template.py index 9a652a4373..a8557b2ad2 100644 --- a/awx_collection/plugins/modules/tower_workflow_template.py +++ b/awx_collection/plugins/modules/tower_workflow_template.py @@ -108,8 +108,8 @@ EXAMPLES = ''' RETURN = ''' # ''' -from ..module_utils.ansible_tower import ( - TowerModule, +from ..module_utils.tower_legacy import ( + TowerLegacyModule, tower_auth_config, tower_check_mode ) @@ -140,7 +140,7 @@ def main(): state=dict(choices=['present', 'absent'], default='present'), ) - module = TowerModule( + module = TowerLegacyModule( argument_spec=argument_spec, supports_check_mode=False ) diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py index 025b8d033d..53270d0e7f 100644 --- a/awx_collection/test/awx/conftest.py +++ b/awx_collection/test/awx/conftest.py @@ -10,7 +10,7 @@ from contextlib import redirect_stdout, suppress from unittest import mock import logging -from requests.models import Response +from requests.models import Response, PreparedRequest import pytest @@ -23,6 +23,11 @@ try: except ImportError: HAS_TOWER_CLI = False +try: + import awxkit + HAS_AWX_KIT = True +except ImportError: + HAS_AWX_KIT = False logger = logging.getLogger('awx.main.tests') @@ -90,7 +95,8 @@ def run_module(request, collection_import): if 'params' in kwargs and method == 'GET': # query params for GET are handled a bit differently by # tower-cli and python requests as opposed to REST framework APIRequestFactory - kwargs_copy.setdefault('data', {}) + if not kwargs_copy.get('data'): + kwargs_copy['data'] = {} if isinstance(kwargs['params'], dict): kwargs_copy['data'].update(kwargs['params']) elif isinstance(kwargs['params'], list): @@ -117,6 +123,8 @@ def run_module(request, collection_import): request_user.username, resp.status_code ) + resp.request = PreparedRequest() + resp.request.prepare(method=method, url=url) return resp def new_open(self, method, url, **kwargs): @@ -142,11 +150,22 @@ def run_module(request, collection_import): def mock_load_params(self): self.params = module_params - with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params): + if getattr(resource_module, 'TowerAWXKitModule', None): + resource_class = resource_module.TowerAWXKitModule + elif getattr(resource_module, 'TowerAPIModule', None): + resource_class = resource_module.TowerAPIModule + elif getattr(resource_module, 'TowerLegacyModule', None): + resource_class = resource_module.TowerLegacyModule + else: + raise("The module has neither a TowerLegacyModule, TowerAWXKitModule or a TowerAPIModule") + + with mock.patch.object(resource_class, '_load_params', new=mock_load_params): # Call the test utility (like a mock server) instead of issuing HTTP requests with mock.patch('ansible.module_utils.urls.Request.open', new=new_open): if HAS_TOWER_CLI: tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request) + elif HAS_AWX_KIT: + tower_cli_mgr = mock.patch('awxkit.api.client.requests.Session.request', new=new_request) else: tower_cli_mgr = suppress() with tower_cli_mgr: diff --git a/awx_collection/test/awx/test_module_utils.py b/awx_collection/test/awx/test_module_utils.py index 0f443890e8..e93b6ee939 100644 --- a/awx_collection/test/awx/test_module_utils.py +++ b/awx_collection/test/awx/test_module_utils.py @@ -30,12 +30,12 @@ def mock_ping_response(self, method, url, **kwargs): def test_version_warning(collection_import, silence_warning): - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule cli_data = {'ANSIBLE_MODULE_ARGS': {}} testargs = ['module_file2.py', json.dumps(cli_data)] with mock.patch.object(sys, 'argv', testargs): with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): - my_module = TowerModule(argument_spec=dict()) + my_module = TowerAPIModule(argument_spec=dict()) my_module._COLLECTION_VERSION = "1.0.0" my_module._COLLECTION_TYPE = "not-junk" my_module.collection_to_version['not-junk'] = 'not-junk' @@ -46,12 +46,12 @@ def test_version_warning(collection_import, silence_warning): def test_type_warning(collection_import, silence_warning): - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule cli_data = {'ANSIBLE_MODULE_ARGS': {}} testargs = ['module_file2.py', json.dumps(cli_data)] with mock.patch.object(sys, 'argv', testargs): with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): - my_module = TowerModule(argument_spec={}) + my_module = TowerAPIModule(argument_spec={}) my_module._COLLECTION_VERSION = "1.2.3" my_module._COLLECTION_TYPE = "junk" my_module.collection_to_version['junk'] = 'junk' @@ -63,7 +63,7 @@ def test_type_warning(collection_import, silence_warning): def test_duplicate_config(collection_import, silence_warning): # imports done here because of PATH issues unique to this test suite - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule data = { 'name': 'zigzoom', 'zig': 'zoom', @@ -71,12 +71,12 @@ def test_duplicate_config(collection_import, silence_warning): 'tower_config_file': 'my_config' } - with mock.patch.object(TowerModule, 'load_config') as mock_load: + with mock.patch.object(TowerAPIModule, 'load_config') as mock_load: argument_spec = dict( name=dict(required=True), zig=dict(type='str'), ) - TowerModule(argument_spec=argument_spec, direct_params=data) + TowerAPIModule(argument_spec=argument_spec, direct_params=data) assert mock_load.mock_calls[-1] == mock.call('my_config') silence_warning.assert_called_once_with( @@ -92,8 +92,8 @@ def test_no_templated_values(collection_import): Those replacements should happen at build time, so they should not be checked into source. """ - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule - assert TowerModule._COLLECTION_VERSION == "0.0.1-devel", ( + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule + assert TowerAPIModule._COLLECTION_VERSION == "0.0.1-devel", ( 'The collection version is templated when the collection is built ' 'and the code should retain the placeholder of "0.0.1-devel".' ) diff --git a/awx_collection/tools/roles/generate/templates/tower_module.j2 b/awx_collection/tools/roles/generate/templates/tower_module.j2 index a9834db28d..3606cff547 100644 --- a/awx_collection/tools/roles/generate/templates/tower_module.j2 +++ b/awx_collection/tools/roles/generate/templates/tower_module.j2 @@ -96,7 +96,7 @@ EXAMPLES = ''' {% endif %} ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -142,7 +142,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters {% for option in item['json']['actions']['POST'] %} From 08e5dd87e6c0771162fb0ab246c53117b94fa29e Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 3 Aug 2020 14:14:40 -0400 Subject: [PATCH 046/123] Adding integration tests and example in import --- .../plugins/modules/tower_import.py | 4 + .../targets/tower_export/tasks/main.yml | 77 +++++++++++++ .../targets/tower_import/tasks/main.yml | 108 ++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 awx_collection/tests/integration/targets/tower_export/tasks/main.yml create mode 100644 awx_collection/tests/integration/targets/tower_import/tasks/main.yml diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index 0bb5dec75a..eeea7b35a3 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -38,6 +38,10 @@ EXAMPLES = ''' - name: Import all tower assets tower_import: assets: "{{ export_output.assets }}" + +- name: Import orgs from a json file + tower_import: + assets: "{{ lookup('file', 'org.json') | from_json() }}" ''' from ..module_utils.tower_awxkit import TowerAWXKitModule diff --git a/awx_collection/tests/integration/targets/tower_export/tasks/main.yml b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml new file mode 100644 index 0000000000..ce33f50019 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + org_name1: "AWX-Collection-tests-tower_export-organization-{{ test_id }}" + org_name2: "AWX-Collection-tests-tower_export-organization2-{{ test_id }}" + inventory_name1: "AWX-Collection-tests-tower_export-inv1-{{ test_id }}" + +- block: + - name: Create some organizations + tower_organization: + name: "{{ item }}" + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" + + - name: Create an inventory + tower_inventory: + name: "{{ inventory_name1 }}" + organization: "{{ org_name1 }}" + + - name: Export all tower assets + tower_export: + all: True + register: all_assets + + - assert: + that: + - all_assets is not changed + - all_assets is successful + - all_assets['assets']['organizations'] | length() >= 2 + + - name: Export all inventories + tower_export: + inventory: 'all' + register: inventory_export + + - assert: + that: + - inventory_export is successful + - inventory_export is not changed + - inventory_export['assets']['inventory'] | length() >= 1 + - "'organizations' not in inventory_export['assets']" + + # This mimics the example in the module + - name: Export an all and a specific + tower_export: + inventory: 'all' + organizations: "{{ org_name1 }}" + register: mixed_export + + - assert: + that: + - mixed_export is successful + - mixed_export is not changed + - mixed_export['assets']['inventory'] | length() >= 1 + - mixed_export['assets']['organizations'] | length() == 1 + - "'workflow_job_templates' not in mixed_export['assets']" + + always: + - name: Remove our inventory + tower_inventory: + name: "{{ inventory_name1 }}" + organization: "{{ org_name1 }}" + state: absent + + - name: Remove test organizations + tower_organization: + name: "{{ item }}" + state: absent + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml new file mode 100644 index 0000000000..09a91c85b0 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -0,0 +1,108 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + org_name1: "AWX-Collection-tests-tower_import-organization-{{ test_id }}" + org_name2: "AWX-Collection-tests-tower_import-organization2-{{ test_id }}" + +- block: + - name: "Import something" + tower_import: + assets: + organizations: + - name: "{{ org_name1 }}" + description: "" + max_hosts: 0 + custom_virtualenv: null + related: + notification_templates: [] + notification_templates_started: [] + notification_templates_success: [] + notification_templates_error: [] + notification_templates_approvals: [] + natural_key: + name: "Default" + type: "organization" + register: import_output + + - assert: + that: + - import_output is changed + + - name: "Import something again (awxkit is not idempotent, this tests a filure)" + tower_import: + assets: + organizations: + - name: "{{ org_name1 }}" + description: "" + max_hosts: 0 + custom_virtualenv: null + related: + notification_templates: [] + notification_templates_started: [] + notification_templates_success: [] + notification_templates_error: [] + notification_templates_approvals: [] + natural_key: + name: "Default" + type: "organization" + register: import_output + ignore_errors: True + + - assert: + that: + - import_output is failed + - "'Organization with this Name already exists' in import_output.msg" + + - name: "Write out a json file" + copy: + content: | + { + "organizations": [ + { + "name": "{{ org_name2 }}", + "description": "", + "max_hosts": 0, + "custom_virtualenv": null, + "related": { + "notification_templates": [], + "notification_templates_started": [], + "notification_templates_success": [], + "notification_templates_error": [], + "notification_templates_approvals": [] + }, + "natural_key": { + "name": "Default", + "type": "organization" + } + } + ] + } + dest: ./org.json + + - name: "Load assets from a file" + tower_import: + assets: "{{ lookup('file', 'org.json') | from_json() }}" + register: import_output + + - assert: + that: + - import_output is changed + + always: + - name: Remove organizations + tower_organization: + name: "{{ item }}" + state: absent + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" + + - name: Delete org.json + file: + path: ./org.json + state: absent From 748bdbd2dd7e31cf858dcd8093c715bee196b3d7 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 11:21:18 -0400 Subject: [PATCH 047/123] Fix python3 Zuul error with awxkit --- awx_collection/test/awx/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py index 53270d0e7f..5db00d6325 100644 --- a/awx_collection/test/awx/conftest.py +++ b/awx_collection/test/awx/conftest.py @@ -24,7 +24,10 @@ except ImportError: HAS_TOWER_CLI = False try: - import awxkit + # Because awxkit will be a directory at the root of this makefile and we are using python3, import awxkit will work even if its not installed. + # However, awxkit will not contain api whih causes a stack failure down on line 170 when we try to mock it. + # So here we are importing awxkit.api to prevent that. Then you only get an error on tests for awxkit functionality. + import awxkit.api HAS_AWX_KIT = True except ImportError: HAS_AWX_KIT = False From 8a0cd747e11bc72aa2c7a2bd508e95dde078b00e Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 11:33:29 -0400 Subject: [PATCH 048/123] Fixing truthy linting issues --- .../tests/integration/targets/tower_export/tasks/main.yml | 2 +- .../tests/integration/targets/tower_import/tasks/main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awx_collection/tests/integration/targets/tower_export/tasks/main.yml b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml index ce33f50019..7ffbc15820 100644 --- a/awx_collection/tests/integration/targets/tower_export/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml @@ -25,7 +25,7 @@ - name: Export all tower assets tower_export: - all: True + all: true register: all_assets - assert: diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml index 09a91c85b0..ea18bb584f 100644 --- a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -33,7 +33,7 @@ that: - import_output is changed - - name: "Import something again (awxkit is not idempotent, this tests a filure)" + - name: "Import something again (awxkit is not idempotent, this tests a failure)" tower_import: assets: organizations: @@ -51,7 +51,7 @@ name: "Default" type: "organization" register: import_output - ignore_errors: True + ignore_errors: true - assert: that: From f2b9bdd5529256c8154c04408b1bcd0af622286c Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 13:06:13 -0400 Subject: [PATCH 049/123] Removed default: '' and updated [] to '' per specification --- .../plugins/modules/tower_export.py | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index 84c08fe454..6dabf17f39 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -30,57 +30,46 @@ options: organizations: description: - organization name to export - default: '' type: str user: description: - user name to export - default: '' type: str team: description: - team name to export - default: '' type: str credential_type: description: - credential type name to export - default: '' type: str credential: description: - credential name to export - default: '' type: str notification_template: description: - notification template name to export - default: '' type: str inventory_script: description: - inventory script name to export - default: '' type: str inventory: description: - inventory name to export - default: '' type: str project: description: - project name to export - default: '' type: str job_template: description: - job template name to export - default: '' type: str workflow: description: - workflow name to export - default: '' type: str requirements: - "awxkit >= 9.3.0" @@ -132,15 +121,15 @@ def main(): module.json_output['changed'] = False # The exporter code currently works like the following: - # Empty list == all assets of that type - # string = just one asset of that type (by name) - # None = skip asset type + # Empty string == all assets of that type + # Non-Empty string = just one asset of that type (by name or ID) + # Asset type not present or None = skip asset type (unless everything is None, then export all) # Here we are going to setup a dict of values to export export_args = {} for resource in EXPORTABLE_RESOURCES: if module.params.get('all') or module.params.get(resource) == 'all': - # If we are exporting everything or we got the keyword "all" we pass in an empty list for this asset type - export_args[resource] = [] + # If we are exporting everything or we got the keyword "all" we pass in an empty string for this asset type + export_args[resource] = '' else: # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items export_args[resource] = module.params.get(resource) From 9bf19daa5e544487ddc715c9e7791325836ad4b2 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 13:25:05 -0400 Subject: [PATCH 050/123] Another linting issue --- .../tests/integration/targets/tower_import/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml index ea18bb584f..9835ff89a5 100644 --- a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -52,7 +52,7 @@ type: "organization" register: import_output ignore_errors: true - + - assert: that: - import_output is failed From 5107f164a23dcb88836aba646a8b50308cc154fa Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 10:32:46 -0400 Subject: [PATCH 051/123] Expanding examples --- awx_collection/plugins/modules/tower_import.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index eeea7b35a3..0dddeb64a0 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -35,11 +35,16 @@ extends_documentation_fragment: awx.awx.auth ''' EXAMPLES = ''' -- name: Import all tower assets +- name: Export all assets + tower_export: + all: True + registeR: export_output + +- name: Import all tower assets from our export tower_import: assets: "{{ export_output.assets }}" -- name: Import orgs from a json file +- name: Load data from a json file created by a command like awx export --organization Default tower_import: assets: "{{ lookup('file', 'org.json') | from_json() }}" ''' From 3fe61cfa4fa7ed38cb4bd3275d81ee110c2b4241 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 14:48:01 -0400 Subject: [PATCH 052/123] Fixing linting issues --- awx_collection/plugins/modules/tower_export.py | 4 ++-- awx_collection/plugins/modules/tower_import.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index 6dabf17f39..ad8da8f3ce 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -97,9 +97,9 @@ from ..module_utils.tower_awxkit import TowerAWXKitModule try: from awxkit.api.pages.api import EXPORTABLE_RESOURCES - HAS_EXPORTABLE_RESOURCES=True + HAS_EXPORTABLE_RESOURCES = True except ImportError: - HAS_EXPORTABLE_RESOURCES=False + HAS_EXPORTABLE_RESOURCES = False def main(): diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index 0dddeb64a0..37de035123 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -58,9 +58,9 @@ import logging # In this module we don't use EXPORTABLE_RESOURCES, we just want to validate that our installed awxkit has import/export try: from awxkit.api.pages.api import EXPORTABLE_RESOURCES - HAS_EXPORTABLE_RESOURCES=True + HAS_EXPORTABLE_RESOURCES = True except ImportError: - HAS_EXPORTABLE_RESOURCES=False + HAS_EXPORTABLE_RESOURCES = False def main(): argument_spec = dict( From 8688740e933e5875d6e9298636f406db9caf2f42 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 14:57:30 -0400 Subject: [PATCH 053/123] Fixing ansible pep8 issues --- awx_collection/plugins/module_utils/tower_api.py | 4 +++- awx_collection/plugins/module_utils/tower_module.py | 1 - awx_collection/plugins/modules/tower_import.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 089273bff1..6b5ed87531 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -10,6 +10,7 @@ from ansible.module_utils.six.moves.http_cookiejar import CookieJar import re from json import loads, dumps + class TowerAPIModule(TowerModule): # TODO: Move the collection version check into tower_module.py # This gets set by the make process so whatever is in here is irrelevant @@ -27,7 +28,8 @@ class TowerAPIModule(TowerModule): def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): kwargs['supports_check_mode'] = True - super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, error_callback=error_callback, warn_callback=warn_callback, **kwargs) + super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, + error_callback=error_callback, warn_callback=warn_callback, **kwargs) self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl) @staticmethod diff --git a/awx_collection/plugins/module_utils/tower_module.py b/awx_collection/plugins/module_utils/tower_module.py index 5d5b4d4239..553a35248c 100644 --- a/awx_collection/plugins/module_utils/tower_module.py +++ b/awx_collection/plugins/module_utils/tower_module.py @@ -215,7 +215,6 @@ class TowerModule(AnsibleModule): else: setattr(self, honorred_setting, config_data[honorred_setting]) - def logout(self): # This method is intended to be overridden pass diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index 37de035123..a39a98a5e3 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -62,6 +62,7 @@ try: except ImportError: HAS_EXPORTABLE_RESOURCES = False + def main(): argument_spec = dict( assets=dict(type='dict', required=True) From c2e0c0655ba2b2ec63bcabfd03e026e1991827fa Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 15:04:09 -0400 Subject: [PATCH 054/123] Fixing validate-module errors --- .../plugins/modules/tower_export.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index ad8da8f3ce..9f8f479b71 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -31,43 +31,43 @@ options: description: - organization name to export type: str - user: + users: description: - user name to export type: str - team: + teams: description: - team name to export type: str - credential_type: + credential_types: description: - credential type name to export type: str - credential: + credentials: description: - credential name to export type: str - notification_template: + notification_templates: description: - notification template name to export type: str - inventory_script: + inventory_sources: description: - - inventory script name to export + - inventory soruce to export type: str inventory: description: - inventory name to export type: str - project: + projects: description: - project name to export type: str - job_template: + job_templates: description: - job template name to export type: str - workflow: + workflow_job_templates: description: - workflow name to export type: str @@ -110,7 +110,7 @@ def main(): # We are not going to raise an error here because the __init__ method of TowerAWXKitModule will do that for us if HAS_EXPORTABLE_RESOURCES: for resource in EXPORTABLE_RESOURCES: - argument_spec[resource] = dict() + argument_spec[resource] = dict(type='str') module = TowerAWXKitModule(argument_spec=argument_spec) From 01e08ba0e14eaf3444396610ac96cf57e3592257 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 15:07:23 -0400 Subject: [PATCH 055/123] Fixing exit_module -> exit_json --- awx_collection/plugins/module_utils/tower_awxkit.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index eedf3a7387..24a83b3c0d 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -24,7 +24,7 @@ class TowerAWXKitModule(TowerModule): # Die if we don't have AWX_KIT installed if not HAS_AWX_KIT: - self.exit_module(msg=missing_required_lib('awxkit')) + self.exit_json(msg=missing_required_lib('awxkit')) # Establish our conneciton object self.connection = Connection(self.host, verify=self.verify_ssl) @@ -35,7 +35,7 @@ class TowerAWXKitModule(TowerModule): # If we have neither of these, then we can try un-authenticated access self.authenticated = True except Exception: - self.exit_module("Failed to authenticate") + self.exit_json("Failed to authenticate") def get_api_v2_object(self): if not self.apiV2Ref: @@ -48,3 +48,5 @@ class TowerAWXKitModule(TowerModule): def logout(self): if self.authenticated: self.connection.logout() + + From 76f08744f6a89bd97ad0105969b43f8e88915695 Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 18 Aug 2020 15:02:17 -0400 Subject: [PATCH 056/123] Fix linter whitespace error --- awx_collection/plugins/module_utils/tower_awxkit.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index 24a83b3c0d..ddd2d190c7 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -48,5 +48,3 @@ class TowerAWXKitModule(TowerModule): def logout(self): if self.authenticated: self.connection.logout() - - From a2eab45d61fd7f097c11f2e9200cef0a46f07dd2 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 12:12:30 -0400 Subject: [PATCH 057/123] Trying to gobble up logs incase there are errors --- .../plugins/modules/tower_export.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index 9f8f479b71..e7a788559c 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -92,7 +92,8 @@ EXAMPLES = ''' ''' from os import environ - +import logging +from ansible.module_utils.six.moves import StringIO from ..module_utils.tower_awxkit import TowerAWXKitModule try: @@ -134,11 +135,31 @@ def main(): # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items export_args[resource] = module.params.get(resource) - # Run the export process - module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) + # Currently the import process does not return anything on error + # It simply just logs to pythons logger + # Setup a log gobbler to get error messages from import_assets + log_capture_string = StringIO() + ch = logging.StreamHandler(log_capture_string) + for logger_name in ['awxkit.api.pages.api', 'awxkit.api.pages.page']: + logger = logging.getLogger(logger_name) + logger.setLevel(logging.WARNING) + ch.setLevel(logging.WARNING) - module.exit_json(**module.json_output) + logger.addHandler(ch) + log_contents = '' + # Run the import process + try: + module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) + module.exit_json(**module.json_output) + except Exception as e: + module.fail_json(msg="Failed to export assets {0}".format(e)) + finally: + # Finally consume the logs incase there were any errors and die if there were + log_contents = log_capture_string.getvalue() + log_capture_string.close() + if log_contents != '': + module.fail_json(msg=log_contents) if __name__ == '__main__': main() From 3abd77c4c02a29f68d2148a317bb0c8cf6c99a23 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 12:18:07 -0400 Subject: [PATCH 058/123] Fixing oauth token login and making module respect token over username/password --- awx_collection/plugins/module_utils/tower_awxkit.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index ddd2d190c7..506b64fb20 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -31,9 +31,12 @@ class TowerAWXKitModule(TowerModule): def authenticate(self): try: - self.connection.login(username=self.username, password=self.password, token=self.oauth_token) - # If we have neither of these, then we can try un-authenticated access - self.authenticated = True + if self.oauth_token: + self.connection.login(None, None, token=self.oauth_token, auth_type='Bearer') + self.authenticated = True + elif self.username: + self.connection.login(username=self.username, password=self.password) + self.authenticated = True except Exception: self.exit_json("Failed to authenticate") From 2c8c1ff595b89c63c55c160f218cd186d94f4255 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 14:14:49 -0400 Subject: [PATCH 059/123] Fixing sanity error --- awx_collection/plugins/modules/tower_export.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index e7a788559c..bd951d1744 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -161,5 +161,6 @@ def main(): if log_contents != '': module.fail_json(msg=log_contents) + if __name__ == '__main__': main() From b93319e3591ae925dc95110581afc0713e3105cf Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 14:27:02 -0400 Subject: [PATCH 060/123] Updating to remove auth_type since its not longer required --- awx_collection/plugins/module_utils/tower_awxkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index 506b64fb20..fc4e232f1b 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -32,7 +32,7 @@ class TowerAWXKitModule(TowerModule): def authenticate(self): try: if self.oauth_token: - self.connection.login(None, None, token=self.oauth_token, auth_type='Bearer') + self.connection.login(None, None, token=self.oauth_token) self.authenticated = True elif self.username: self.connection.login(username=self.username, password=self.password) From a5afe0214a50f96bc5b074aded25a40a433a9001 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 14:29:42 -0400 Subject: [PATCH 061/123] Trying to make AWXKIT tests not run on python2 --- awx_collection/tests/integration/targets/tower_export/aliases | 1 + awx_collection/tests/integration/targets/tower_import/aliases | 1 + 2 files changed, 2 insertions(+) create mode 100644 awx_collection/tests/integration/targets/tower_export/aliases create mode 100644 awx_collection/tests/integration/targets/tower_import/aliases diff --git a/awx_collection/tests/integration/targets/tower_export/aliases b/awx_collection/tests/integration/targets/tower_export/aliases new file mode 100644 index 0000000000..527d07c3cb --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_export/aliases @@ -0,0 +1 @@ +skip/python2 diff --git a/awx_collection/tests/integration/targets/tower_import/aliases b/awx_collection/tests/integration/targets/tower_import/aliases new file mode 100644 index 0000000000..527d07c3cb --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_import/aliases @@ -0,0 +1 @@ +skip/python2 From efa12b12ec434ffad9259e9f9c423ea6f13cb872 Mon Sep 17 00:00:00 2001 From: nixocio Date: Wed, 19 Aug 2020 17:17:32 -0400 Subject: [PATCH 062/123] Use a patternfly CSS variable instead of red Use a patternfly CSS variable instead of red. See: https://pf4.patternfly.org/documentation/overview/global-css-variables --- .../InstanceGroupDetails/InstanceGroupDetails.jsx | 8 ++++++-- .../InstanceGroupList/InstanceGroupListItem.jsx | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx index 8df5e5b863..c6d3313ffe 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx @@ -3,7 +3,7 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link, useHistory } from 'react-router-dom'; import { Button } from '@patternfly/react-core'; -import 'styled-components/macro'; +import styled from 'styled-components'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; @@ -17,6 +17,10 @@ import { import useRequest, { useDismissableError } from '../../../util/useRequest'; import { InstanceGroupsAPI } from '../../../api'; +const Unavailable = styled.span` + color: var(--pf-global--danger-color--200); +`; + function InstanceGroupDetails({ instanceGroup, i18n }) { const { id, name } = instanceGroup; @@ -78,7 +82,7 @@ function InstanceGroupDetails({ instanceGroup, i18n }) { ) : ( {i18n._(t`Unavailable`)}} + value={{i18n._(t`Unavailable`)}} dataCy="instance-group-used-capacity" /> )} diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx index 93e334d367..915aa44d44 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx @@ -46,6 +46,10 @@ const DataListAction = styled(_DataListAction)` grid-template-columns: 40px; `; +const Unavailable = styled.span` + color: var(--pf-global--danger-color--200); +`; + function InstanceGroupListItem({ instanceGroup, detailUrl, @@ -78,7 +82,7 @@ function InstanceGroupListItem({ /> ); } - return {i18n._(t`Unavailable`)}; + return {i18n._(t`Unavailable`)}; } return null; } From 821cfba88a8db85c0fab420585f1df62f89c06ef Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 20 Aug 2020 10:02:24 -0400 Subject: [PATCH 063/123] changelog for arm64 builds --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15723a0106..79d1ff8cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/`. ## 14.1.0 (TBD) +- AWX images can now be built on ARM64 - https://github.com/ansible/awx/pull/7607 - Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 - Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 - Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847 From 0266ed383659c703efa03b2f3d87b5ff964e6992 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 20 Aug 2020 10:43:34 -0400 Subject: [PATCH 064/123] more changelog updates --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d1ff8cc3..60f79d4f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This is a list of high-level changes for each release of AWX. A full list of com ## 14.1.0 (TBD) - AWX images can now be built on ARM64 - https://github.com/ansible/awx/pull/7607 - Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 +- Added resource import/export support to the official AWX collection - https://github.com/ansible/awx/issues/7329 - Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 - Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847 - Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 From 70295c3e7505ebd2834daacc5ac335186e609bfe Mon Sep 17 00:00:00 2001 From: nixocio Date: Thu, 20 Aug 2020 11:36:41 -0400 Subject: [PATCH 065/123] Add list of jobs for instance groups Add list of jobs for instance groups. See: https://github.com/ansible/awx/issues/7930 --- awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx index a42048f503..4597bbd61c 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx @@ -17,10 +17,10 @@ import { InstanceGroupsAPI } from '../../api'; import RoutedTabs from '../../components/RoutedTabs'; import ContentError from '../../components/ContentError'; import ContentLoading from '../../components/ContentLoading'; +import JobList from '../../components/JobList'; import InstanceGroupDetails from './InstanceGroupDetails'; import InstanceGroupEdit from './InstanceGroupEdit'; -import Jobs from './Jobs'; import Instances from './Instances'; function InstanceGroup({ i18n, setBreadcrumb }) { @@ -126,7 +126,10 @@ function InstanceGroup({ i18n, setBreadcrumb }) {
- + )} From 2cfa4eb60a190565770f14d7749381aa8a5d8520 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Fri, 6 Dec 2019 13:18:02 -0500 Subject: [PATCH 066/123] Add archive option to SCM_TYPE_CHOICES for Remote Archives Signed-off-by: Philip Douglass --- awx/main/models/projects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 5d8cfd5290..bc52d4269c 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -55,6 +55,7 @@ class ProjectOptions(models.Model): ('hg', _('Mercurial')), ('svn', _('Subversion')), ('insights', _('Red Hat Insights')), + ('archive', _('Remote Archive')), ] class Meta: From 47cabc42290f5df77eb280bf4ef74e4d8c8f1fed Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 13:32:25 -0400 Subject: [PATCH 067/123] Add archive SCM url handling to update_scm_url() Signed-off-by: Philip Douglass --- awx/main/utils/common.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 75fcd306ef..f34cf4e4d8 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -257,7 +257,7 @@ def update_scm_url(scm_type, url, username=True, password=True, # git: https://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS # hg: http://www.selenic.com/mercurial/hg.1.html#url-paths # svn: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.advanced.reposurls - if scm_type not in ('git', 'hg', 'svn', 'insights'): + if scm_type not in ('git', 'hg', 'svn', 'insights', 'archive'): raise ValueError(_('Unsupported SCM type "%s"') % str(scm_type)) if not url.strip(): return '' @@ -303,7 +303,8 @@ def update_scm_url(scm_type, url, username=True, password=True, 'git': ('ssh', 'git', 'git+ssh', 'http', 'https', 'ftp', 'ftps', 'file'), 'hg': ('http', 'https', 'ssh', 'file'), 'svn': ('http', 'https', 'svn', 'svn+ssh', 'file'), - 'insights': ('http', 'https') + 'insights': ('http', 'https'), + 'archive': ('http', 'https'), } if parts.scheme not in scm_type_schemes.get(scm_type, ()): raise ValueError(_('Unsupported %s URL') % scm_type) @@ -339,7 +340,7 @@ def update_scm_url(scm_type, url, username=True, password=True, #raise ValueError('Password not supported for SSH with Mercurial.') netloc_password = '' - if netloc_username and parts.scheme != 'file' and scm_type != "insights": + if netloc_username and parts.scheme != 'file' and scm_type not in ("insights", "archive"): netloc = u':'.join([urllib.parse.quote(x,safe='') for x in (netloc_username, netloc_password) if x]) else: netloc = u'' From 997351eee3d831f394310aa5712bc090cf840846 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 13:43:47 -0400 Subject: [PATCH 068/123] Add archive project data to Dashboard view Signed-off-by: Philip Douglass --- awx/api/views/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index f6378f5282..c5b22d105a 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -242,6 +242,8 @@ class DashboardView(APIView): svn_failed_projects = svn_projects.filter(last_job_failed=True) hg_projects = user_projects.filter(scm_type='hg') hg_failed_projects = hg_projects.filter(last_job_failed=True) + archive_projects = user_projects.filter(scm_type='archive') + archive_failed_projects = archive_projects.filter(last_job_failed=True) data['scm_types'] = {} data['scm_types']['git'] = {'url': reverse('api:project_list', request=request) + "?scm_type=git", 'label': 'Git', @@ -258,6 +260,11 @@ class DashboardView(APIView): 'failures_url': reverse('api:project_list', request=request) + "?scm_type=hg&last_job_failed=True", 'total': hg_projects.count(), 'failed': hg_failed_projects.count()} + data['scm_types']['archive'] = {'url': reverse('api:project_list', request=request) + "?scm_type=archive", + 'label': 'Remote Archive', + 'failures_url': reverse('api:project_list', request=request) + "?scm_type=archive&last_job_failed=True", + 'total': archive_projects.count(), + 'failed': archive_failed_projects.count()} user_list = get_user_queryset(request.user, models.User) team_list = get_user_queryset(request.user, models.Team) From d224aa09f0bd908944e9be778867f018149b3974 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 13:49:27 -0400 Subject: [PATCH 069/123] Add archive to TestProjectUpdateCredentials test parametrize Signed-off-by: Philip Douglass --- awx/main/tests/unit/test_tasks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 71bcd8d03c..7dcea33ccd 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -1792,16 +1792,19 @@ class TestProjectUpdateCredentials(TestJobExecution): dict(scm_type='git'), dict(scm_type='hg'), dict(scm_type='svn'), + dict(scm_type='archive'), ], 'test_ssh_key_auth': [ dict(scm_type='git'), dict(scm_type='hg'), dict(scm_type='svn'), + dict(scm_type='archive'), ], 'test_awx_task_env': [ dict(scm_type='git'), dict(scm_type='hg'), dict(scm_type='svn'), + dict(scm_type='archive'), ] } From 2f3f6e60d1f3f084748f1c13580e6cc138eb8649 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 13:52:08 -0400 Subject: [PATCH 070/123] Add archive to scm_type expected test results Signed-off-by: Philip Douglass --- .../tests/functional/analytics/test_projects_by_scm_type.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/main/tests/functional/analytics/test_projects_by_scm_type.py b/awx/main/tests/functional/analytics/test_projects_by_scm_type.py index 29ffbd6283..1590b5d3bb 100644 --- a/awx/main/tests/functional/analytics/test_projects_by_scm_type.py +++ b/awx/main/tests/functional/analytics/test_projects_by_scm_type.py @@ -12,7 +12,8 @@ def test_empty(): 'git': 0, 'svn': 0, 'hg': 0, - 'insights': 0 + 'insights': 0, + 'archive': 0, } @@ -24,7 +25,8 @@ def test_multiple(scm_type): 'git': 0, 'svn': 0, 'hg': 0, - 'insights': 0 + 'insights': 0, + 'archive': 0, } for i in range(random.randint(0, 10)): Project(scm_type=scm_type).save() From 8157ab2fa94923fd149a8ab45aa1c8f6e2d9fc4f Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 16:29:57 -0400 Subject: [PATCH 071/123] Hide scm_branch field when scm_type is archive Signed-off-by: Philip Douglass --- awx/ui/client/src/projects/projects.form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/projects/projects.form.js b/awx/ui/client/src/projects/projects.form.js index 13ef01d2f0..dac0118c03 100644 --- a/awx/ui/client/src/projects/projects.form.js +++ b/awx/ui/client/src/projects/projects.form.js @@ -124,7 +124,7 @@ export default ['i18n', 'NotificationsList', 'TemplateList', scm_branch: { labelBind: "scmBranchLabel", type: 'text', - ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights'", + ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights' && scm_type.value !== 'archive'", ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)', awPopOver: '

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

', dataTitle: i18n._('SCM Branch'), From 8402cf97de0888f11976b9b7c17e43574d1fcc76 Mon Sep 17 00:00:00 2001 From: nixocio Date: Sun, 16 Aug 2020 14:57:00 -0400 Subject: [PATCH 072/123] Add type column to users list Add type column to users list. Also, update `UserListItem` to be a functional component. See: https://github.com/ansible/awx/issues/5684 --- .../screens/User/UserList/UserListItem.jsx | 147 ++++++++++-------- .../User/UserList/UserListItem.test.jsx | 8 +- 2 files changed, 89 insertions(+), 66 deletions(-) diff --git a/awx/ui_next/src/screens/User/UserList/UserListItem.jsx b/awx/ui_next/src/screens/User/UserList/UserListItem.jsx index 19b8b5b476..b772a47566 100644 --- a/awx/ui_next/src/screens/User/UserList/UserListItem.jsx +++ b/awx/ui_next/src/screens/User/UserList/UserListItem.jsx @@ -19,72 +19,89 @@ import DataListCell from '../../../components/DataListCell'; import { User } from '../../../types'; -class UserListItem extends React.Component { - static propTypes = { - user: User.isRequired, - detailUrl: string.isRequired, - isSelected: bool.isRequired, - onSelect: func.isRequired, - }; +function UserListItem({ user, isSelected, onSelect, detailUrl, i18n }) { + const labelId = `check-action-${user.id}`; - render() { - const { user, isSelected, onSelect, detailUrl, i18n } = this.props; - const labelId = `check-action-${user.id}`; - return ( - - - - - - {user.username} - -
, - - {user.first_name && ( - - {i18n._(t`First Name`)} - {user.first_name} - - )} - , - - {user.last_name && ( - - {i18n._(t`Last Name`)} - {user.last_name} - - )} - , - ]} - /> - - {user.summary_fields.user_capabilities.edit && ( - - - - )} - - -
- ); + let user_type; + if (user.is_superuser) { + user_type = i18n._(t`System Administrator`); + } else if (user.is_system_auditor) { + user_type = i18n._(t`System Auditor`); + } else { + user_type = i18n._(t`Normal User`); } + + return ( + + + + + + {user.username} + + , + + {user.first_name && ( + + {i18n._(t`First Name`)} + {user.first_name} + + )} + , + + {user.last_name && ( + + {i18n._(t`Last Name`)} + {user.last_name} + + )} + , + + {user_type} + , + ]} + /> + + {user.summary_fields.user_capabilities.edit && ( + + + + )} + + + + ); } + +UserListItem.prototype = { + user: User.isRequired, + detailUrl: string.isRequired, + isSelected: bool.isRequired, + onSelect: func.isRequired, +}; + export default withI18n()(UserListItem); diff --git a/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx b/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx index acdc7eecc0..e11b6da00c 100644 --- a/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx +++ b/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx @@ -28,12 +28,18 @@ describe('UserListItem with full permissions', () => { ); }); - test('initially renders succesfully', () => { + test('initially renders successfully', () => { expect(wrapper.length).toBe(1); }); test('edit button shown to users with edit capabilities', () => { expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); }); + + test('should display user type', () => { + expect( + wrapper.find('DataListCell[aria-label="user type"]').prop('children') + ).toEqual('System Administrator'); + }); }); describe('UserListItem without full permissions', () => { From aee2a81b271d8b85475cb6990bf01e7e65afd6db Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 13 Aug 2020 15:30:00 -0400 Subject: [PATCH 073/123] update newly useRequested lists to get advanced searchableKeys --- awx/ui_next/src/api/models/Credentials.js | 5 +++ awx/ui_next/src/api/models/Inventories.js | 5 +++ awx/ui_next/src/api/models/JobTemplates.js | 5 +++ awx/ui_next/src/api/models/Organizations.js | 8 +++++ awx/ui_next/src/api/models/Projects.js | 5 +++ awx/ui_next/src/api/models/Teams.js | 4 +++ .../src/api/models/WorkflowJobTemplates.js | 4 +++ .../components/Lookup/OrganizationLookup.jsx | 21 ++++++++--- .../NotificationList/NotificationList.jsx | 28 ++++++++++----- .../ResourceAccessList/ResourceAccessList.jsx | 17 +++++++-- .../ResourceAccessList.test.jsx | 9 +++++ .../InventoryGroups/InventoryGroupsList.jsx | 18 +++++++++- .../OrganizationTeamList.jsx | 21 ++++++++--- .../OrganizationTeamList.test.jsx | 9 +++++ .../ProjectJobTemplatesList.jsx | 18 +++++++++- .../Modals/NodeModals/NodeModal.test.jsx | 36 +++++++++++++++++++ .../NodeTypeStep/InventorySourcesList.jsx | 21 ++++++++--- .../InventorySourcesList.test.jsx | 9 +++++ .../NodeTypeStep/JobTemplatesList.jsx | 25 +++++++++---- .../NodeTypeStep/JobTemplatesList.test.jsx | 18 ++++++++++ .../NodeTypeStep/NodeTypeStep.test.jsx | 36 +++++++++++++++++++ .../NodeModals/NodeTypeStep/ProjectsList.jsx | 21 ++++++++--- .../NodeTypeStep/ProjectsList.test.jsx | 9 +++++ .../NodeTypeStep/WorkflowJobTemplatesList.jsx | 30 ++++++++++++---- .../WorkflowJobTemplatesList.test.jsx | 9 +++++ .../src/screens/User/UserList/UserList.jsx | 18 +++++++++- 26 files changed, 367 insertions(+), 42 deletions(-) diff --git a/awx/ui_next/src/api/models/Credentials.js b/awx/ui_next/src/api/models/Credentials.js index ec7f97812d..95e954fc0c 100644 --- a/awx/ui_next/src/api/models/Credentials.js +++ b/awx/ui_next/src/api/models/Credentials.js @@ -6,6 +6,7 @@ class Credentials extends Base { this.baseUrl = '/api/v2/credentials/'; this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readInputSources = this.readInputSources.bind(this); } @@ -15,6 +16,10 @@ class Credentials extends Base { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readInputSources(id, params) { return this.http.get(`${this.baseUrl}${id}/input_sources/`, { params, diff --git a/awx/ui_next/src/api/models/Inventories.js b/awx/ui_next/src/api/models/Inventories.js index ab828e32d6..077a534d27 100644 --- a/awx/ui_next/src/api/models/Inventories.js +++ b/awx/ui_next/src/api/models/Inventories.js @@ -7,6 +7,7 @@ class Inventories extends InstanceGroupsMixin(Base) { this.baseUrl = '/api/v2/inventories/'; this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readHosts = this.readHosts.bind(this); this.readHostDetail = this.readHostDetail.bind(this); this.readGroups = this.readGroups.bind(this); @@ -20,6 +21,10 @@ class Inventories extends InstanceGroupsMixin(Base) { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + createHost(id, data) { return this.http.post(`${this.baseUrl}${id}/hosts/`, data); } diff --git a/awx/ui_next/src/api/models/JobTemplates.js b/awx/ui_next/src/api/models/JobTemplates.js index 0e2eba8079..4f631cec2a 100644 --- a/awx/ui_next/src/api/models/JobTemplates.js +++ b/awx/ui_next/src/api/models/JobTemplates.js @@ -16,6 +16,7 @@ class JobTemplates extends SchedulesMixin( this.disassociateLabel = this.disassociateLabel.bind(this); this.readCredentials = this.readCredentials.bind(this); this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readWebhookKey = this.readWebhookKey.bind(this); } @@ -66,6 +67,10 @@ class JobTemplates extends SchedulesMixin( }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readScheduleList(id, params) { return this.http.get(`${this.baseUrl}${id}/schedules/`, { params, diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index 267c9aba1e..e6f12a26a3 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -12,10 +12,18 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { return this.http.get(`${this.baseUrl}${id}/access_list/`, { params }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readTeams(id, params) { return this.http.get(`${this.baseUrl}${id}/teams/`, { params }); } + readTeamsOptions(id) { + return this.http.options(`${this.baseUrl}${id}/teams/`); + } + createUser(id, data) { return this.http.post(`${this.baseUrl}${id}/users/`, data); } diff --git a/awx/ui_next/src/api/models/Projects.js b/awx/ui_next/src/api/models/Projects.js index 269ef18f8a..38879a2bc2 100644 --- a/awx/ui_next/src/api/models/Projects.js +++ b/awx/ui_next/src/api/models/Projects.js @@ -11,6 +11,7 @@ class Projects extends SchedulesMixin( this.baseUrl = '/api/v2/projects/'; this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readInventories = this.readInventories.bind(this); this.readPlaybooks = this.readPlaybooks.bind(this); this.readSync = this.readSync.bind(this); @@ -21,6 +22,10 @@ class Projects extends SchedulesMixin( return this.http.get(`${this.baseUrl}${id}/access_list/`, { params }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readInventories(id) { return this.http.get(`${this.baseUrl}${id}/inventories/`); } diff --git a/awx/ui_next/src/api/models/Teams.js b/awx/ui_next/src/api/models/Teams.js index 1a205993d4..180c59032c 100644 --- a/awx/ui_next/src/api/models/Teams.js +++ b/awx/ui_next/src/api/models/Teams.js @@ -35,6 +35,10 @@ class Teams extends Base { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readUsersAccessOptions(teamId) { return this.http.options(`${this.baseUrl}${teamId}/users/`); } diff --git a/awx/ui_next/src/api/models/WorkflowJobTemplates.js b/awx/ui_next/src/api/models/WorkflowJobTemplates.js index 3074608796..6326ebecae 100644 --- a/awx/ui_next/src/api/models/WorkflowJobTemplates.js +++ b/awx/ui_next/src/api/models/WorkflowJobTemplates.js @@ -54,6 +54,10 @@ class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readSurvey(id) { return this.http.get(`${this.baseUrl}${id}/survey_spec/`); } diff --git a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx index ec60c553cd..dfc4e1391f 100644 --- a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx @@ -29,21 +29,32 @@ function OrganizationLookup({ history, }) { const { - result: { itemCount, organizations }, + result: { itemCount, organizations, relatedSearchableKeys, searchableKeys }, error: contentError, request: fetchOrganizations, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, history.location.search); - const { data } = await OrganizationsAPI.read(params); + const [response, actionsResponse] = await Promise.all([ + OrganizationsAPI.read(params), + OrganizationsAPI.readOptions(), + ]); return { - organizations: data.results, - itemCount: data.count, + organizations: response.data.results, + itemCount: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location.search]), { organizations: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -98,6 +109,8 @@ function OrganizationLookup({ key: 'name', }, ]} + searchableKeys={searchableKeys} + relatedSearchableKeys={relatedSearchableKeys} readOnly={!canDelete} selectItem={item => dispatch({ type: 'SELECT_ITEM', item })} deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })} diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.jsx index b1bca91d37..882a304654 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationList.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationList.jsx @@ -23,7 +23,7 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { const [toggleError, setToggleError] = useState(null); const { - result: fetchNotificationsResult, + result: fetchNotificationsResults, result: { notifications, itemCount, @@ -31,6 +31,8 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { successTemplateIds, errorTemplateIds, typeLabels, + relatedSearchableKeys, + searchableKeys, }, error: contentError, isLoading, @@ -43,15 +45,13 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { { data: { results: notificationsResults, count: notificationsCount }, }, - { - data: { actions }, - }, + actionsResponse, ] = await Promise.all([ NotificationTemplatesAPI.read(params), NotificationTemplatesAPI.readOptions(), ]); - const labels = actions.GET.notification_type.choices.reduce( + const labels = actionsResponse.data.actions.GET.notification_type.choices.reduce( (map, notifType) => ({ ...map, [notifType[0]]: notifType[1] }), {} ); @@ -78,6 +78,12 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { successTemplateIds: successTemplates.results.map(su => su.id), errorTemplateIds: errorTemplates.results.map(e => e.id), typeLabels: labels, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [apiModel, id, location]), { @@ -87,6 +93,8 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { successTemplateIds: [], errorTemplateIds: [], typeLabels: {}, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -108,8 +116,8 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { status ); setValue({ - ...fetchNotificationsResult, - [`${status}TemplateIds`]: fetchNotificationsResult[ + ...fetchNotificationsResults, + [`${status}TemplateIds`]: fetchNotificationsResults[ `${status}TemplateIds` ].filter(i => i !== notificationId), }); @@ -120,8 +128,8 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { status ); setValue({ - ...fetchNotificationsResult, - [`${status}TemplateIds`]: fetchNotificationsResult[ + ...fetchNotificationsResults, + [`${status}TemplateIds`]: fetchNotificationsResults[ `${status}TemplateIds` ].concat(notificationId), }); @@ -179,6 +187,8 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={notification => ( { const params = parseQueryString(QS_CONFIG, location.search); - const response = await apiModel.readAccessList(resource.id, params); + const [response, actionsResponse] = await Promise.all([ + apiModel.readAccessList(resource.id, params), + apiModel.readAccessOptions(resource.id), + ]); return { accessRecords: response.data.results, itemCount: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [apiModel, location, resource.id]), { accessRecords: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -106,6 +117,8 @@ function ResourceAccessList({ i18n, apiModel, resource }) { key: 'last_name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( ', () => { beforeEach(async () => { OrganizationsAPI.readAccessList.mockResolvedValue({ data }); + OrganizationsAPI.readAccessOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); TeamsAPI.disassociateRole.mockResolvedValue({}); UsersAPI.disassociateRole.mockResolvedValue({}); await act(async () => { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx index 3f3dcbdb1c..3ba0ec1fff 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx @@ -47,7 +47,13 @@ function InventoryGroupsList({ i18n }) { const { id: inventoryId } = useParams(); const { - result: { groups, groupCount, actions }, + result: { + groups, + groupCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchGroups, @@ -62,12 +68,20 @@ function InventoryGroupsList({ i18n }) { groups: response.data.results, groupCount: response.data.count, actions: actionsResponse.data.actions, + 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), }; }, [inventoryId, location]), { groups: [], groupCount: 0, actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -168,6 +182,8 @@ function InventoryGroupsList({ i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={item => ( { const params = parseQueryString(QS_CONFIG, location.search); - const results = await OrganizationsAPI.readTeams(id, params); + const [response, actionsResponse] = await Promise.all([ + OrganizationsAPI.readTeams(id, params), + OrganizationsAPI.readTeamsOptions(id), + ]); return { - teams: results.data.results, - count: results.data.count, + teams: 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), }; }, [id, location]), { teams: [], count: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -71,6 +82,8 @@ function OrganizationTeamList({ id, i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={item => ( ', () => { beforeEach(() => { OrganizationsAPI.readTeams.mockResolvedValue(listData); + OrganizationsAPI.readTeamsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); }); afterEach(() => { diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx index d804a6ea83..f22c393866 100644 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx @@ -27,7 +27,13 @@ function ProjectJobTemplatesList({ i18n }) { const location = useLocation(); const { - result: { jobTemplates, itemCount, actions }, + result: { + jobTemplates, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchTemplates, @@ -43,12 +49,20 @@ function ProjectJobTemplatesList({ i18n }) { jobTemplates: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, + 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, projectId]), { jobTemplates: [], itemCount: 0, actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -142,6 +156,8 @@ function ProjectJobTemplatesList({ i18n }) { key: 'type', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( { ], }, }); + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); ProjectsAPI.read.mockResolvedValue({ data: { count: 1, @@ -53,6 +62,15 @@ describe('NodeModal', () => { ], }, }); + ProjectsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); InventorySourcesAPI.read.mockResolvedValue({ data: { count: 1, @@ -66,6 +84,15 @@ describe('NodeModal', () => { ], }, }); + InventorySourcesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); WorkflowJobTemplatesAPI.read.mockResolvedValue({ data: { count: 1, @@ -79,6 +106,15 @@ describe('NodeModal', () => { ], }, }); + WorkflowJobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); }); afterAll(() => { jest.clearAllMocks(); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.jsx index 2ccdbef448..47e400f716 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.jsx @@ -20,22 +20,33 @@ function InventorySourcesList({ i18n, nodeResource, onUpdateNodeResource }) { const location = useLocation(); const { - result: { inventorySources, count }, + result: { inventorySources, count, relatedSearchableKeys, searchableKeys }, error, isLoading, request: fetchInventorySources, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const results = await InventorySourcesAPI.read(params); + const [response, actionsResponse] = await Promise.all([ + InventorySourcesAPI.read(params), + InventorySourcesAPI.readOptions(), + ]); return { - inventorySources: results.data.results, - count: results.data.count, + inventorySources: 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]), { inventorySources: [], count: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -94,6 +105,8 @@ function InventorySourcesList({ i18n, nodeResource, onUpdateNodeResource }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} /> ); } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx index 8725beff57..ad6fd57211 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx @@ -38,6 +38,15 @@ describe('InventorySourcesList', () => { ], }, }); + InventorySourcesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( { const params = parseQueryString(QS_CONFIG, location.search); - const results = await JobTemplatesAPI.read(params, { - role_level: 'execute_role', - }); + const [response, actionsResponse] = await Promise.all([ + JobTemplatesAPI.read(params, { + role_level: 'execute_role', + }), + JobTemplatesAPI.readOptions(), + ]); return { - jobTemplates: results.data.results, - count: results.data.count, + jobTemplates: 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]), { jobTemplates: [], count: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -92,6 +103,8 @@ function JobTemplatesList({ i18n, nodeResource, onUpdateNodeResource }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} /> ); } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx index 580e96d465..3b9fdec0e9 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx @@ -38,6 +38,15 @@ describe('JobTemplatesList', () => { ], }, }); + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( { }); test('Error shown when read() request errors', async () => { JobTemplatesAPI.read.mockRejectedValue(new Error()); + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( { ], }, }); + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); ProjectsAPI.read.mockResolvedValue({ data: { count: 1, @@ -48,6 +57,15 @@ describe('NodeTypeStep', () => { ], }, }); + ProjectsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); InventorySourcesAPI.read.mockResolvedValue({ data: { count: 1, @@ -61,6 +79,15 @@ describe('NodeTypeStep', () => { ], }, }); + InventorySourcesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); WorkflowJobTemplatesAPI.read.mockResolvedValue({ data: { count: 1, @@ -74,6 +101,15 @@ describe('NodeTypeStep', () => { ], }, }); + WorkflowJobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); }); afterAll(() => { jest.clearAllMocks(); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx index e7dfa098c9..3a9b747e35 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx @@ -20,22 +20,33 @@ function ProjectsList({ i18n, nodeResource, onUpdateNodeResource }) { const location = useLocation(); const { - result: { projects, count }, + result: { projects, count, relatedSearchableKeys, searchableKeys }, error, isLoading, request: fetchProjects, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const results = await ProjectsAPI.read(params); + const [response, actionsResponse] = await Promise.all([ + ProjectsAPI.read(params), + ProjectsAPI.readOptions(), + ]); return { - projects: results.data.results, - count: results.data.count, + projects: 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]), { projects: [], count: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -101,6 +112,8 @@ function ProjectsList({ i18n, nodeResource, onUpdateNodeResource }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} /> ); } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx index eff4ccf517..765a329d3d 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx @@ -38,6 +38,15 @@ describe('ProjectsList', () => { ], }, }); + ProjectsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( { const params = parseQueryString(QS_CONFIG, location.search); - const results = await WorkflowJobTemplatesAPI.read(params, { - role_level: 'execute_role', - }); + const [response, actionsResponse] = await Promise.all([ + WorkflowJobTemplatesAPI.read(params, { + role_level: 'execute_role', + }), + WorkflowJobTemplatesAPI.readOptions(), + ]); return { - workflowJobTemplates: results.data.results, - count: results.data.count, + workflowJobTemplates: 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]), { workflowJobTemplates: [], count: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -100,6 +116,8 @@ function WorkflowJobTemplatesList({ key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} /> ); } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx index f3bf00a1d9..121c62cce6 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx @@ -38,6 +38,15 @@ describe('WorkflowJobTemplatesList', () => { ], }, }); + WorkflowJobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { users: [], itemCount: 0, actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -124,6 +138,8 @@ function UserList({ i18n }) { key: 'last_name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( Date: Thu, 20 Aug 2020 12:53:26 -0400 Subject: [PATCH 074/123] add searchable keys support for AssociateModal and SelectResourceStep lists --- .../components/AddRole/AddResourceRole.jsx | 6 ++++ .../components/AddRole/SelectResourceStep.jsx | 29 +++++++++++++++---- .../AddRole/SelectResourceStep.test.jsx | 21 ++++++++++++++ .../AssociateModal/AssociateModal.jsx | 24 +++++++++++---- .../AssociateModal/AssociateModal.test.jsx | 10 +++++++ .../UserAndTeamAccessAdd.jsx | 1 + .../UserAndTeamAccessAdd.test.jsx | 13 +++++++++ .../getResourceAccessConfig.js | 6 ++++ .../Host/HostGroups/HostGroupsList.jsx | 6 ++++ .../Host/HostGroups/HostGroupsList.test.jsx | 9 ++++++ .../InventoryGroupHostList.jsx | 6 ++++ .../InventoryHostGroupsList.jsx | 6 ++++ .../InventoryHostGroupsList.test.jsx | 9 ++++++ 13 files changed, 135 insertions(+), 11 deletions(-) diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index 2da3b02e9d..95cb910295 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx @@ -11,8 +11,12 @@ import { TeamsAPI, UsersAPI } from '../../api'; const readUsers = async queryParams => UsersAPI.read(Object.assign(queryParams, { is_superuser: false })); +const readUsersOptions = async () => UsersAPI.readOptions(); + const readTeams = async queryParams => TeamsAPI.read(queryParams); +const readTeamsOptions = async () => TeamsAPI.readOptions(); + class AddResourceRole extends React.Component { constructor(props) { super(props); @@ -259,6 +263,7 @@ class AddResourceRole extends React.Component { displayKey="username" onRowClick={this.handleResourceCheckboxClick} fetchItems={readUsers} + fetchOptions={readUsersOptions} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={selectedResourceRows} sortedColumnKey="username" @@ -270,6 +275,7 @@ class AddResourceRole extends React.Component { sortColumns={teamSortColumns} onRowClick={this.handleResourceCheckboxClick} fetchItems={readTeams} + fetchOptions={readTeamsOptions} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={selectedResourceRows} /> diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx index 461327587e..f9a73d24ce 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx @@ -29,6 +29,7 @@ function SelectResourceStep({ selectedLabel, selectedResourceRows, fetchItems, + fetchOptions, i18n, }) { const location = useLocation(); @@ -37,7 +38,7 @@ function SelectResourceStep({ isLoading, error, request: readResourceList, - result: { resources, itemCount }, + result: { resources, itemCount, relatedSearchableKeys, searchableKeys }, } = useRequest( useCallback(async () => { const queryParams = parseQueryString( @@ -45,14 +46,28 @@ function SelectResourceStep({ location.search ); - const { - data: { count, results }, - } = await fetchItems(queryParams); - return { resources: results, itemCount: count }; - }, [location, fetchItems, sortColumns]), + const [ + { + data: { count, results }, + }, + actionsResponse, + ] = await Promise.all([fetchItems(queryParams), fetchOptions()]); + return { + resources: results, + itemCount: count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), + }; + }, [location, fetchItems, fetchOptions, sortColumns]), { resources: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -84,6 +99,8 @@ function SelectResourceStep({ onRowClick={onRowClick} toolbarSearchColumns={searchColumns} toolbarSortColumns={sortColumns} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={item => ( i.id === item.id)} diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx index c4c83f9d3b..4e307b7595 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx @@ -35,6 +35,7 @@ describe('', () => { displayKey="username" onRowClick={() => {}} fetchItems={() => {}} + fetchOptions={() => {}} /> ); }); @@ -49,6 +50,15 @@ describe('', () => { ], }, }); + const options = jest.fn().mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); let wrapper; await act(async () => { wrapper = mountWithContexts( @@ -58,6 +68,7 @@ describe('', () => { displayKey="username" onRowClick={() => {}} fetchItems={handleSearch} + fetchOptions={options} /> ); }); @@ -78,6 +89,15 @@ describe('', () => { { id: 2, username: 'bar', url: 'item/2' }, ], }; + const options = jest.fn().mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); let wrapper; await act(async () => { wrapper = mountWithContexts( @@ -87,6 +107,7 @@ describe('', () => { displayKey="username" onRowClick={handleRowClick} fetchItems={() => ({ data })} + fetchOptions={options} selectedResourceRows={[]} /> ); diff --git a/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx b/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx index 339b5ed744..f7a1b2da67 100644 --- a/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx +++ b/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx @@ -21,6 +21,7 @@ function AssociateModal({ onClose, onAssociate, fetchRequest, + optionsRequest, isModalOpen = false, }) { const history = useHistory(); @@ -28,24 +29,35 @@ function AssociateModal({ const { request: fetchItems, - result: { items, itemCount }, + result: { items, itemCount, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, history.location.search); - const { - data: { count, results }, - } = await fetchRequest(params); + const [ + { + data: { count, results }, + }, + actionsResponse, + ] = await Promise.all([fetchRequest(params), optionsRequest()]); return { items: results, itemCount: count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; - }, [fetchRequest, history.location.search]), + }, [fetchRequest, optionsRequest, history.location.search]), { items: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -132,6 +144,8 @@ function AssociateModal({ key: 'name', }, ]} + searchableKeys={searchableKeys} + relatedSearchableKeys={relatedSearchableKeys} />
diff --git a/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx b/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx index 2b2280b38b..4b58e900e3 100644 --- a/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx +++ b/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx @@ -15,6 +15,15 @@ describe('', () => { const onClose = jest.fn(); const onAssociate = jest.fn().mockResolvedValue(); const fetchRequest = jest.fn().mockReturnValue({ data: { ...mockHosts } }); + const optionsRequest = jest.fn().mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); beforeEach(async () => { await act(async () => { @@ -23,6 +32,7 @@ describe('', () => { onClose={onClose} onAssociate={onAssociate} fetchRequest={fetchRequest} + optionsRequest={optionsRequest} isModalOpen /> ); diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx index ceac1a32f5..ad83ce61e9 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx @@ -96,6 +96,7 @@ function UserAndTeamAccessAdd({ displayKey="name" onRowClick={handleResourceSelect} fetchItems={selectedResourceType.fetchItems} + fetchOptions={selectedResourceType.fetchOptions} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={resourcesSelected} sortedColumnKey="username" diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx index a46d5d87a3..7ad19c9057 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx @@ -43,6 +43,15 @@ describe('', () => { count: 1, }, }; + const options = { + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }; let wrapper; beforeEach(async () => { await act(async () => { @@ -111,11 +120,13 @@ describe('', () => { test('should call api to associate role', async () => { JobTemplatesAPI.read.mockResolvedValue(resources); + JobTemplatesAPI.readOptions.mockResolvedValue(options); UsersAPI.associateRole.mockResolvedValue({}); await act(async () => wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({ fetchItems: JobTemplatesAPI.read, + fetchOptions: JobTemplatesAPI.readOptions, label: 'Job template', selectedResource: 'jobTemplate', searchColumns: [ @@ -169,6 +180,7 @@ describe('', () => { test('should throw error', async () => { JobTemplatesAPI.read.mockResolvedValue(resources); + JobTemplatesAPI.readOptions.mockResolvedValue(options); UsersAPI.associateRole.mockRejectedValue( new Error({ response: { @@ -192,6 +204,7 @@ describe('', () => { await act(async () => wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({ fetchItems: JobTemplatesAPI.read, + fetchOptions: JobTemplatesAPI.readOptions, label: 'Job template', selectedResource: 'jobTemplate', searchColumns: [ diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js index cd922c23aa..8df258973b 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js @@ -39,6 +39,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => JobTemplatesAPI.read(queryParams), + fetchOptions: () => JobTemplatesAPI.readOptions(), }, { selectedResource: 'workflowJobTemplate', @@ -69,6 +70,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => WorkflowJobTemplatesAPI.read(queryParams), + fetchOptions: () => WorkflowJobTemplatesAPI.readOptions(), }, { selectedResource: 'credential', @@ -110,6 +112,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => CredentialsAPI.read(queryParams), + fetchOptions: () => CredentialsAPI.readOptions(), }, { selectedResource: 'inventory', @@ -136,6 +139,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => InventoriesAPI.read(queryParams), + fetchOptions: () => InventoriesAPI.readOptions(), }, { selectedResource: 'project', @@ -177,6 +181,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => ProjectsAPI.read(queryParams), + fetchOptions: () => ProjectsAPI.readOptions(), }, { selectedResource: 'organization', @@ -203,6 +208,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => OrganizationsAPI.read(queryParams), + fetchOptions: () => OrganizationsAPI.readOptions(), }, ]; } diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx index 6233df73bf..f902b5e0a6 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx @@ -118,6 +118,11 @@ function HostGroupsList({ i18n, host }) { [invId, hostId] ); + const fetchGroupsOptions = useCallback( + () => InventoriesAPI.readGroupsOptions(invId), + [invId] + ); + const { request: handleAssociate, error: associateError } = useRequest( useCallback( async groupsToAssociate => { @@ -224,6 +229,7 @@ function HostGroupsList({ i18n, host }) { setIsModalOpen(false)} diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx index 1b2984ed81..4720de35ed 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx @@ -207,6 +207,15 @@ describe('', () => { results: [{ id: 123, name: 'foo', url: '/api/v2/groups/123/' }], }, }); + InventoriesAPI.readGroupsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper.find('ToolbarAddButton').simulate('click'); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx index 8584c01207..84203a700e 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx @@ -111,6 +111,11 @@ function InventoryGroupHostList({ i18n }) { [groupId, inventoryId] ); + const fetchHostsOptions = useCallback( + () => InventoriesAPI.readHostsOptions(inventoryId), + [inventoryId] + ); + const { request: handleAssociate, error: associateErr } = useRequest( useCallback( async hostsToAssociate => { @@ -227,6 +232,7 @@ function InventoryGroupHostList({ i18n }) { setIsModalOpen(false)} diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx index 2f8d8fa1ea..609de04f5b 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx @@ -116,6 +116,11 @@ function InventoryHostGroupsList({ i18n }) { [invId, hostId] ); + const fetchGroupsOptions = useCallback( + () => InventoriesAPI.readGroupsOptions(invId), + [invId] + ); + const { request: handleAssociate, error: associateError } = useRequest( useCallback( async groupsToAssociate => { @@ -221,6 +226,7 @@ function InventoryHostGroupsList({ i18n }) { setIsModalOpen(false)} diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx index 1b72f73bd1..493e9dc65a 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx @@ -199,6 +199,15 @@ describe('', () => { results: [{ id: 123, name: 'foo', url: '/api/v2/groups/123/' }], }, }); + InventoriesAPI.readGroupsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper.find('ToolbarAddButton').simulate('click'); }); From 681b765b9afe62418c8860a47bce920eb7e46a60 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 3 Aug 2020 15:20:17 -0400 Subject: [PATCH 075/123] Adds support for toggling approval notifications on orgs and wfjts --- .../src/api/mixins/Notifications.mixin.js | 14 ++++ awx/ui_next/src/api/models/Organizations.js | 21 ++++++ .../src/api/models/WorkflowJobTemplates.js | 21 ++++++ .../NotificationList/NotificationList.jsx | 37 ++++++++++- .../NotificationList/NotificationListItem.jsx | 50 ++++++++++---- .../NotificationListItem.test.jsx | 65 +++++++++++++++++-- .../NotificationListItem.test.jsx.snap | 10 ++- .../CredentialEdit/CredentialEdit.test.jsx | 5 +- .../src/screens/Organization/Organization.jsx | 1 + .../screens/Template/WorkflowJobTemplate.jsx | 1 + 10 files changed, 200 insertions(+), 25 deletions(-) diff --git a/awx/ui_next/src/api/mixins/Notifications.mixin.js b/awx/ui_next/src/api/mixins/Notifications.mixin.js index 0198f0054f..87a7002ec5 100644 --- a/awx/ui_next/src/api/mixins/Notifications.mixin.js +++ b/awx/ui_next/src/api/mixins/Notifications.mixin.js @@ -87,6 +87,13 @@ const NotificationsMixin = parent => notificationId, notificationType ) { + if (notificationType === 'approvals') { + return this.associateNotificationTemplatesApprovals( + resourceId, + notificationId + ); + } + if (notificationType === 'started') { return this.associateNotificationTemplatesStarted( resourceId, @@ -126,6 +133,13 @@ const NotificationsMixin = parent => notificationId, notificationType ) { + if (notificationType === 'approvals') { + return this.disassociateNotificationTemplatesApprovals( + resourceId, + notificationId + ); + } + if (notificationType === 'started') { return this.disassociateNotificationTemplatesStarted( resourceId, diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index 267c9aba1e..a76b1e9ab1 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -19,6 +19,27 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { createUser(id, data) { return this.http.post(`${this.baseUrl}${id}/users/`, data); } + + readNotificationTemplatesApprovals(id, params) { + return this.http.get( + `${this.baseUrl}${id}/notification_templates_approvals/`, + { params } + ); + } + + associateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId } + ); + } + + disassociateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId, disassociate: true } + ); + } } export default Organizations; diff --git a/awx/ui_next/src/api/models/WorkflowJobTemplates.js b/awx/ui_next/src/api/models/WorkflowJobTemplates.js index 3074608796..91739d1082 100644 --- a/awx/ui_next/src/api/models/WorkflowJobTemplates.js +++ b/awx/ui_next/src/api/models/WorkflowJobTemplates.js @@ -65,6 +65,27 @@ class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) { destroySurvey(id) { return this.http.delete(`${this.baseUrl}${id}/survey_spec/`); } + + readNotificationTemplatesApprovals(id, params) { + return this.http.get( + `${this.baseUrl}${id}/notification_templates_approvals/`, + { params } + ); + } + + associateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId } + ); + } + + disassociateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId, disassociate: true } + ); + } } export default WorkflowJobTemplates; diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.jsx index b1bca91d37..f274e4cca3 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationList.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationList.jsx @@ -17,7 +17,13 @@ const QS_CONFIG = getQSConfig('notification', { order_by: 'name', }); -function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { +function NotificationList({ + apiModel, + canToggleNotifications, + id, + i18n, + showApprovalsToggle, +}) { const location = useLocation(); const [isToggleLoading, setIsToggleLoading] = useState(false); const [toggleError, setToggleError] = useState(null); @@ -27,6 +33,7 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { result: { notifications, itemCount, + approvalsTemplateIds, startedTemplateIds, successTemplateIds, errorTemplateIds, @@ -71,7 +78,7 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { apiModel.readNotificationTemplatesError(id, idMatchParams), ]); - return { + const rtnObj = { notifications: notificationsResults, itemCount: notificationsCount, startedTemplateIds: startedTemplates.results.map(st => st.id), @@ -79,10 +86,27 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { errorTemplateIds: errorTemplates.results.map(e => e.id), typeLabels: labels, }; - }, [apiModel, id, location]), + + if (showApprovalsToggle) { + const { + data: approvalsTemplates, + } = await apiModel.readNotificationTemplatesApprovals( + id, + idMatchParams + ); + rtnObj.approvalsTemplateIds = approvalsTemplates.results.map( + st => st.id + ); + } else { + rtnObj.approvalsTemplateIds = []; + } + + return rtnObj; + }, [apiModel, id, location, showApprovalsToggle]), { notifications: [], itemCount: 0, + approvalsTemplateIds: [], startedTemplateIds: [], successTemplateIds: [], errorTemplateIds: [], @@ -186,10 +210,12 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { detailUrl={`/notifications/${notification.id}`} canToggleNotifications={canToggleNotifications && !isToggleLoading} toggleNotification={handleNotificationToggle} + approvalsTurnedOn={approvalsTemplateIds.includes(notification.id)} errorTurnedOn={errorTemplateIds.includes(notification.id)} startedTurnedOn={startedTemplateIds.includes(notification.id)} successTurnedOn={successTemplateIds.includes(notification.id)} typeLabels={typeLabels} + showApprovalsToggle={showApprovalsToggle} /> )} /> @@ -212,6 +238,11 @@ NotificationList.propTypes = { apiModel: shape({}).isRequired, id: number.isRequired, canToggleNotifications: bool.isRequired, + showApprovalsToggle: bool, +}; + +NotificationList.defaultProps = { + showApprovalsToggle: false, }; export default withI18n()(NotificationList); diff --git a/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx b/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx index 6857114479..cbf048bc7c 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx @@ -17,25 +17,25 @@ const DataListAction = styled(_DataListAction)` align-items: center; display: grid; grid-gap: 16px; - grid-template-columns: repeat(3, max-content); + grid-template-columns: ${props => `repeat(${props.columns}, max-content)`}; `; const Label = styled.b` margin-right: 20px; `; -function NotificationListItem(props) { - const { - canToggleNotifications, - notification, - detailUrl, - startedTurnedOn, - successTurnedOn, - errorTurnedOn, - toggleNotification, - i18n, - typeLabels, - } = props; - +function NotificationListItem({ + canToggleNotifications, + notification, + detailUrl, + approvalsTurnedOn, + startedTurnedOn, + successTurnedOn, + errorTurnedOn, + toggleNotification, + i18n, + typeLabels, + showApprovalsToggle, +}) { return ( + {showApprovalsToggle && ( + + toggleNotification( + notification.id, + approvalsTurnedOn, + 'approvals' + ) + } + aria-label={i18n._(t`Toggle notification approvals`)} + /> + )} ', () => { /> ); expect(wrapper.find('NotificationListItem')).toMatchSnapshot(); + expect(wrapper.find('Switch').length).toBe(3); + }); + + test('shows approvals toggle when configured', () => { + wrapper = mountWithContexts( + + ); + expect(wrapper.find('Switch').length).toBe(4); }); test('displays correct label in correct column', () => { @@ -58,7 +73,46 @@ describe('', () => { expect(typeCell.text()).toContain('Slack'); }); - test('handles start click when toggle is on', () => { + test('handles approvals click when toggle is on', () => { + wrapper = mountWithContexts( + + ); + wrapper + .find('Switch[aria-label="Toggle notification approvals"]') + .first() + .find('input') + .simulate('change'); + expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'approvals'); + }); + + test('handles approvals click when toggle is off', () => { + wrapper = mountWithContexts( + + ); + wrapper + .find('Switch[aria-label="Toggle notification approvals"]') + .find('input') + .simulate('change'); + expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'approvals'); + }); + + test('handles started click when toggle is on', () => { wrapper = mountWithContexts( ', () => { /> ); wrapper - .find('Switch') - .first() + .find('Switch[aria-label="Toggle notification start"]') .find('input') .simulate('change'); expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'started'); }); - test('handles start click when toggle is off', () => { + test('handles started click when toggle is off', () => { wrapper = mountWithContexts( ', () => { expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'started'); }); - test('handles error click when toggle is on', () => { + test('handles success click when toggle is on', () => { wrapper = mountWithContexts( ', () => { expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'success'); }); - test('handles error click when toggle is off', () => { + test('handles success click when toggle is off', () => { wrapper = mountWithContexts( initially renders succesfully and displays correct label 1`] = ` initially renders succe "notification_type": "slack", } } + showApprovalsToggle={false} startedTurnedOn={false} successTurnedOn={false} toggleNotification={[MockFunction]} @@ -215,6 +217,7 @@ exports[` initially renders succe initially renders succe initially renders succe align-items: center; display: grid; grid-gap: 16px; - grid-template-columns: repeat(3, max-content); + grid-template-columns: ", + [Function], + "; ", ], }, @@ -257,11 +263,13 @@ exports[` initially renders succe aria-label="actions" aria-labelledby="items-list-item-9000" className="sc-bwzfXH llKtln" + columns={3} id="items-list-item-9000" rowid="items-list-item-9000" >
', () => { test('handleCancel returns the user to credential detail', async () => { await waitForElement(wrapper, 'isLoading', el => el.length === 0); - wrapper.find('Button[aria-label="Cancel"]').simulate('click'); + await act(async () => { + wrapper.find('Button[aria-label="Cancel"]').simulate('click'); + }); + wrapper.update(); expect(history.location.pathname).toEqual('/credentials/3/details'); }); diff --git a/awx/ui_next/src/screens/Organization/Organization.jsx b/awx/ui_next/src/screens/Organization/Organization.jsx index 6e8cf87f1d..d16113d8dc 100644 --- a/awx/ui_next/src/screens/Organization/Organization.jsx +++ b/awx/ui_next/src/screens/Organization/Organization.jsx @@ -200,6 +200,7 @@ class Organization extends Component { id={Number(match.params.id)} canToggleNotifications={canToggleNotifications} apiModel={OrganizationsAPI} + showApprovalsToggle /> )} diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx index 8f19ebb6d3..51abaab73a 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx @@ -233,6 +233,7 @@ class WorkflowJobTemplate extends Component { id={Number(match.params.id)} canToggleNotifications={canToggleNotifications} apiModel={WorkflowJobTemplatesAPI} + showApprovalsToggle /> )} From b011e34faeb65d88ef49b1b7400562a48a6f92bc Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 16:34:49 -0400 Subject: [PATCH 076/123] Show fields for archive when selected as scm_type Signed-off-by: Philip Douglass --- .../client/src/projects/add/projects-add.controller.js | 10 +++++++++- .../src/projects/edit/projects-edit.controller.js | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/projects/add/projects-add.controller.js b/awx/ui/client/src/projects/add/projects-add.controller.js index c34a6ecdf2..952cb07974 100644 --- a/awx/ui/client/src/projects/add/projects-add.controller.js +++ b/awx/ui/client/src/projects/add/projects-add.controller.js @@ -23,7 +23,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', $scope.canEditOrg = true; const virtualEnvs = ConfigData.custom_virtualenvs || []; $scope.custom_virtualenvs_options = virtualEnvs; - + const [ProjectModel] = resolvedModels; $scope.canAdd = ProjectModel.options('actions.POST'); @@ -170,6 +170,14 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', $scope.lookupType = 'scm_credential'; $scope.scmBranchLabel = i18n._('SCM Branch/Tag/Revision'); break; + case 'archive': + $scope.credentialLabel = "SCM " + i18n._("Credential"); + $scope.urlPopover = '

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

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

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

' + + '
  • https://github.com/username/project/archive/v0.0.1.tar.gz
  • ' + + '
  • http://github.com/username/project/archive/v0.0.2.zip
'; + $scope.credRequired = false; + $scope.lookupType = 'scm_credential'; + break; case 'insights': $scope.pathRequired = false; $scope.scmRequired = false; From 6720cd9bda2bd11da2e2661cf777483f767225d0 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 22:34:14 -0400 Subject: [PATCH 077/123] Add block to download and unpack a remote archive Signed-off-by: Philip Douglass --- awx/playbooks/project_update.yml | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index e572496497..22478f9f0c 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -101,6 +101,65 @@ tags: - update_insights + - block: + - name: Ensure the project directory is present + file: + dest: "{{ project_path|quote }}" + state: directory + + - name: Get archive from url + get_url: + url: "{{ scm_url|quote }}" + dest: "{{ project_path|quote }}" + url_username: "{{ scm_username|default(omit) }}" + url_password: "{{ scm_password|default(omit) }}" + force_basic_auth: yes + force: "{{ scm_clean }}" + register: get_url_result + + - name: Unpack archive + unarchive: + src: "{{ get_url_result.dest }}" + dest: "{{ project_path|quote }}" + keep_newer: "{{ not scm_clean }}" + list_files: yes + register: unarchived + + - set_fact: + archive_root_dirs: "{{ archive_root_dirs |default([]) |union(item.split('/')[0:1]) }}" + loop: "{{ unarchived.files }}" + + - block: + - name: Delete unarchived single root directory + file: + dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" + state: absent + + - name: Link single root directory to project directory + file: + src: "{{ project_path|quote }}" + dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" + state: link + + - name: Unpack archive + unarchive: + src: "{{ get_url_result.dest }}" + dest: "{{ project_path|quote }}" + keep_newer: "{{ not scm_clean }}" + + - name: Delete link + file: + dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" + state: absent + when: archive_root_dirs |length == 1 + + - name: Set scm_version to archive sha1 checksum + set_fact: + scm_version: "{{ get_url_result.checksum_src }}" + + tags: + - update_archive + - name: Repository Version debug: msg: "Repository Version {{ scm_version }}" @@ -109,6 +168,7 @@ - update_hg - update_svn - update_insights + - update_archive - hosts: localhost gather_facts: false From f72b777b079dbe060b2f2b8cceb1b3336974106c Mon Sep 17 00:00:00 2001 From: Philip DOUGLASS Date: Sun, 16 Aug 2020 14:47:23 -0400 Subject: [PATCH 078/123] Add plugin to cleanly manage possibly versioned project archives Signed-off-by: Philip Douglass --- .../action_plugins/project_archive.py | 82 ++++++++++++++++++ awx/playbooks/library/project_archive.py | 40 +++++++++ awx/playbooks/project_update.yml | 85 ++++++++----------- 3 files changed, 157 insertions(+), 50 deletions(-) create mode 100644 awx/playbooks/action_plugins/project_archive.py create mode 100644 awx/playbooks/library/project_archive.py diff --git a/awx/playbooks/action_plugins/project_archive.py b/awx/playbooks/action_plugins/project_archive.py new file mode 100644 index 0000000000..753650d12b --- /dev/null +++ b/awx/playbooks/action_plugins/project_archive.py @@ -0,0 +1,82 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import zipfile +import tarfile +import os + +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display + +display = Display() + + +class ActionModule(ActionBase): + def run(self, tmp=None, task_vars=None): + self._supports_check_mode = False + + result = super(ActionModule, self).run(tmp, task_vars) + + src = self._task.args.get("src") + proj_path = self._task.args.get("project_path") + force = self._task.args.get("force", False) + + try: + archive = zipfile.ZipFile(src) + get_filenames = archive.namelist + get_members = archive.infolist + except zipfile.BadZipFile: + archive = tarfile.open(src) + get_filenames = archive.getnames + get_members = archive.getmembers + except tarfile.ReadError: + result["failed"] = True + result["msg"] = "{0} is not a valid archive".format(src) + return result + + # Most well formed archives contain a single root directory, typically named + # project-name-1.0.0. The project contents should be inside that directory. + start_index = 0 + root_contents = set( + [filename.split(os.path.sep)[0] for filename in get_filenames()] + ) + if len(root_contents) == 1: + start_index = len(list(root_contents)[0]) + 1 + + for member in get_members(): + try: + filename = member.filename + except AttributeError: + filename = member.name + + # Skip the archive base directory + if not filename[start_index:]: + continue + + dest = os.path.join(proj_path, filename[start_index:]) + + if not force and os.path.exists(dest): + continue + + try: + is_dir = member.is_dir() + except AttributeError: + is_dir = member.isdir() + + if is_dir: + os.makedirs(dest, exist_ok=True) + else: + try: + member_f = archive.open(member) + except TypeError: + member_f = tarfile.ExFileObject(archive, member) + + with open(dest, "wb") as f: + f.write(member_f.read()) + member_f.close() + + archive.close() + + result["changed"] = True + return result diff --git a/awx/playbooks/library/project_archive.py b/awx/playbooks/library/project_archive.py new file mode 100644 index 0000000000..4a046e354d --- /dev/null +++ b/awx/playbooks/library/project_archive.py @@ -0,0 +1,40 @@ +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "status": ["stableinterface"], + "supported_by": "community", +} + + +DOCUMENTATION = """ +--- +module: project_archive +short_description: unpack a project archive +description: + - Unpacks an archive that contains a project, in order to support handling versioned + artifacts from (for example) GitHub Releases or Artifactory builds. + - Handles projects in the archive root, or in a single base directory of the archive. +version_added: "2.9" +options: + src: + description: + - The source archive of the project artifact + required: true + project_path: + description: + - Directory to write the project archive contents + required: true + force: + description: + - Files in the project_path will be overwritten by matching files in the archive + default: False + +author: + - "Philip Douglass" @philipsd6 +""" + +EXAMPLES = """ +- project_archive: + src: "{{ project_path }}/.archive/project.tar.gz" + project_path: "{{ project_path }}" + force: "{{ scm_clean }}" +""" diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index 22478f9f0c..169273d628 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -102,61 +102,46 @@ - update_insights - block: - - name: Ensure the project directory is present - file: - dest: "{{ project_path|quote }}" - state: directory + - name: Ensure the project archive directory is present + file: + dest: "{{ project_path|quote }}/.archive" + state: directory - - name: Get archive from url - get_url: - url: "{{ scm_url|quote }}" - dest: "{{ project_path|quote }}" - url_username: "{{ scm_username|default(omit) }}" - url_password: "{{ scm_password|default(omit) }}" - force_basic_auth: yes - force: "{{ scm_clean }}" - register: get_url_result + - name: Get archive from url + get_url: + url: "{{ scm_url|quote }}" + dest: "{{ project_path|quote }}/.archive/" + url_username: "{{ scm_username|default(omit) }}" + url_password: "{{ scm_password|default(omit) }}" + force_basic_auth: true + register: get_archive - - name: Unpack archive - unarchive: - src: "{{ get_url_result.dest }}" - dest: "{{ project_path|quote }}" - keep_newer: "{{ not scm_clean }}" - list_files: yes - register: unarchived + - name: Unpack archive + project_archive: + src: "{{ get_archive.dest }}" + project_path: "{{ project_path|quote }}" + force: "{{ scm_clean }}" + when: get_archive.changed or scm_clean + register: unarchived - - set_fact: - archive_root_dirs: "{{ archive_root_dirs |default([]) |union(item.split('/')[0:1]) }}" - loop: "{{ unarchived.files }}" + - name: Find previous archives + find: + paths: "{{ project_path|quote }}/.archive/" + excludes: + - "{{ get_archive.dest|basename }}" + when: unarchived.changed + register: previous_archive - - block: - - name: Delete unarchived single root directory - file: - dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" - state: absent - - - name: Link single root directory to project directory - file: - src: "{{ project_path|quote }}" - dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" - state: link - - - name: Unpack archive - unarchive: - src: "{{ get_url_result.dest }}" - dest: "{{ project_path|quote }}" - keep_newer: "{{ not scm_clean }}" - - - name: Delete link - file: - dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" - state: absent - when: archive_root_dirs |length == 1 - - - name: Set scm_version to archive sha1 checksum - set_fact: - scm_version: "{{ get_url_result.checksum_src }}" + - name: Remove previous archives + file: + path: "{{ item.path }}" + state: absent + loop: "{{ previous_archive.files }}" + when: previous_archive.files|default([]) + - name: Set scm_version to archive sha1 checksum + set_fact: + scm_version: "{{ get_archive.checksum_src }}" tags: - update_archive From 70cbccd2ef281b954f1c971adbd938ed98282fe8 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 18 Aug 2020 18:42:11 -0400 Subject: [PATCH 079/123] Add migration for new Remote Archive SCM Type --- .../0118_add_remote_archive_scm_type.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 awx/main/migrations/0118_add_remote_archive_scm_type.py diff --git a/awx/main/migrations/0118_add_remote_archive_scm_type.py b/awx/main/migrations/0118_add_remote_archive_scm_type.py new file mode 100644 index 0000000000..246ca4c823 --- /dev/null +++ b/awx/main/migrations/0118_add_remote_archive_scm_type.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.11 on 2020-08-18 22:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0117_v400_remove_cloudforms_inventory'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='scm_type', + field=models.CharField(blank=True, choices=[('', 'Manual'), ('git', 'Git'), ('hg', 'Mercurial'), ('svn', 'Subversion'), ('insights', 'Red Hat Insights'), ('archive', 'Remote Archive')], default='', help_text='Specifies the source control system used to store the project.', max_length=8, verbose_name='SCM Type'), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_type', + field=models.CharField(blank=True, choices=[('', 'Manual'), ('git', 'Git'), ('hg', 'Mercurial'), ('svn', 'Subversion'), ('insights', 'Red Hat Insights'), ('archive', 'Remote Archive')], default='', help_text='Specifies the source control system used to store the project.', max_length=8, verbose_name='SCM Type'), + ), + ] From d2595003329dbc79faaddb7e331471ec27cb3a3c Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Wed, 19 Aug 2020 18:55:13 -0400 Subject: [PATCH 080/123] Implement Remote Archive SCM Type for ui_next Signed-off-by: Philip Douglass --- .../src/components/Lookup/ProjectLookup.jsx | 1 + .../getResourceAccessConfig.js | 2 + .../Project/ProjectAdd/ProjectAdd.test.jsx | 1 + .../Project/ProjectEdit/ProjectEdit.test.jsx | 1 + .../Project/ProjectList/ProjectList.jsx | 1 + .../Project/ProjectList/ProjectList.test.jsx | 22 ++++++++++- .../screens/Project/shared/ProjectForm.jsx | 8 ++++ .../Project/shared/ProjectForm.test.jsx | 1 + .../shared/ProjectSubForms/ArchiveSubForm.jsx | 38 +++++++++++++++++++ .../Project/shared/ProjectSubForms/index.js | 1 + .../NodeModals/NodeTypeStep/ProjectsList.jsx | 1 + awx/ui_next/src/types.js | 2 +- 12 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx diff --git a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx index dcc16669b8..5c8ec16dee 100644 --- a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx @@ -105,6 +105,7 @@ function ProjectLookup({ [`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/UserAndTeamAccessAdd/getResourceAccessConfig.js b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js index cd922c23aa..fd19a58299 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js @@ -87,6 +87,7 @@ export default function getResourceAccessConfig(i18n) { [`git`, i18n._(t`Git`)], [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], + [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], ], }, @@ -154,6 +155,7 @@ export default function getResourceAccessConfig(i18n) { [`git`, i18n._(t`Git`)], [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], + [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], ], }, 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 a47bb424bf..cb2c16a23f 100644 --- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx @@ -37,6 +37,7 @@ describe('', () => { ['git', 'Git'], ['hg', 'Mercurial'], ['svn', 'Subversion'], + ['archive', 'Remote Archive'], ['insights', 'Red Hat Insights'], ], }, 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 5b75396d65..5171e0f532 100644 --- a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx @@ -49,6 +49,7 @@ describe('', () => { ['git', 'Git'], ['hg', 'Mercurial'], ['svn', 'Subversion'], + ['archive', 'Remote Archive'], ['insights', 'Red Hat Insights'], ], }, diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx index 7afbe124b3..b7b8661c1b 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx @@ -138,6 +138,7 @@ function ProjectList({ i18n }) { [`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/ProjectList.test.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx index b50e569f18..5a6945d892 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx @@ -61,6 +61,24 @@ const mockProjects = [ }, }, }, + { + id: 4, + name: 'Project 4', + url: '/api/v2/projects/4', + type: 'project', + scm_type: 'archive', + scm_revision: 'odsd9ajf8aagjisooajfij34ikdj3fs994s4daiaos7', + summary_fields: { + last_job: { + id: 9004, + status: 'successful', + }, + user_capabilities: { + delete: false, + update: false, + }, + }, + }, ]; describe('', () => { @@ -94,7 +112,7 @@ describe('', () => { }); wrapper.update(); - expect(wrapper.find('ProjectListItem')).toHaveLength(3); + expect(wrapper.find('ProjectListItem')).toHaveLength(4); }); test('should select project when checked', async () => { @@ -133,7 +151,7 @@ describe('', () => { wrapper.update(); const items = wrapper.find('ProjectListItem'); - expect(items).toHaveLength(3); + expect(items).toHaveLength(4); items.forEach(item => { expect(item.prop('isSelected')).toEqual(true); }); diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx index 56c740d73d..5a9f7b1ea7 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx @@ -25,6 +25,7 @@ import { GitSubForm, HgSubForm, SvnSubForm, + ArchiveSubForm, InsightsSubForm, ManualSubForm, } from './ProjectSubForms'; @@ -240,6 +241,13 @@ function ProjectFormFields({ scmUpdateOnLaunch={formik.values.scm_update_on_launch} /> ), + archive: ( + + ), insights: ( ', () => { ['git', 'Git'], ['hg', 'Mercurial'], ['svn', 'Subversion'], + ['archive', 'Remote Archive'], ['insights', 'Red Hat Insights'], ], }, diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx new file mode 100644 index 0000000000..ba65b0b6ff --- /dev/null +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx @@ -0,0 +1,38 @@ +import 'styled-components/macro'; +import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + UrlFormField, + ScmCredentialFormField, + ScmTypeOptions, +} from './SharedFields'; + +const ArchiveSubForm = ({ + i18n, + credential, + onCredentialSelection, + scmUpdateOnLaunch, +}) => ( + <> + + {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
  • +
+ + } + /> + + + +); + +export default withI18n()(ArchiveSubForm); 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 022673187f..8cf3e5594b 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js @@ -3,3 +3,4 @@ export { default as HgSubForm } from './HgSubForm'; export { default as InsightsSubForm } from './InsightsSubForm'; export { default as ManualSubForm } from './ManualSubForm'; export { default as SvnSubForm } from './SvnSubForm'; +export { default as ArchiveSubForm } from './ArchiveSubForm'; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx index e7dfa098c9..4e73c5b6e0 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx @@ -79,6 +79,7 @@ function ProjectsList({ i18n, nodeResource, onUpdateNodeResource }) { [`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/types.js b/awx/ui_next/src/types.js index 7a66ae3c68..5ee55ef5d9 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -148,7 +148,7 @@ export const Project = shape({ created: string, name: string.isRequired, description: string, - scm_type: oneOf(['', 'git', 'hg', 'svn', 'insights']), + scm_type: oneOf(['', 'git', 'hg', 'svn', 'archive', 'insights']), scm_url: string, scm_branch: string, scm_refspec: string, From 6ed65a9c81f01696be0722534802fddc6819d707 Mon Sep 17 00:00:00 2001 From: nixocio Date: Fri, 7 Aug 2020 13:35:10 -0400 Subject: [PATCH 081/123] Add label to show isolated group Add label to show isolated group. See: https://tower-mockups.testing.ansible.com/patternfly/instance-groups/instance-groups/ --- .../InstanceGroupDetails.jsx | 20 +++++++++++++++-- .../InstanceGroupDetails.test.jsx | 22 ++++++++++++++++++- .../InstanceGroupListItem.jsx | 16 ++++++++++---- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx index c6d3313ffe..0d6964559a 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx @@ -2,8 +2,8 @@ import React, { useCallback } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link, useHistory } from 'react-router-dom'; -import { Button } from '@patternfly/react-core'; import styled from 'styled-components'; +import { Button, Label } from '@patternfly/react-core'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; @@ -46,12 +46,28 @@ function InstanceGroupDetails({ instanceGroup, i18n }) { ); }; + const verifyIsIsolated = item => { + if (item.is_isolated) { + return ( + <> + {item.name} + + + + + ); + } + return <>{item.name}; + }; + return ( ', () => { }); wrapper.update(); - expectDetailToMatch(wrapper, 'Name', instanceGroups[0].name); + + expect(wrapper.find('Detail[label="Name"]').text()).toEqual( + expect.stringContaining(instanceGroups[0].name) + ); + expect(wrapper.find('Detail[label="Name"]')).toHaveLength(1); expectDetailToMatch(wrapper, 'Type', `Instance group`); const dates = wrapper.find('UserDateDetail'); expect(dates).toHaveLength(2); @@ -144,4 +150,18 @@ describe('', () => { expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(0); }); + + test('should display isolated label', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + expect( + wrapper.find('Label[aria-label="isolated instance"]').prop('children') + ).toEqual('Isolated'); + }); }); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx index 915aa44d44..f531f8b182 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx @@ -6,15 +6,16 @@ import { Link } from 'react-router-dom'; import 'styled-components/macro'; import { Badge as PFBadge, - Progress, - ProgressMeasureLocation, - ProgressSize, Button, DataListAction as _DataListAction, DataListCheck, DataListItem, - DataListItemRow, DataListItemCells, + DataListItemRow, + Label, + Progress, + ProgressMeasureLocation, + ProgressSize, Tooltip, } from '@patternfly/react-core'; import { PencilAltIcon } from '@patternfly/react-icons'; @@ -112,6 +113,13 @@ function InstanceGroupListItem({ {instanceGroup.name} + {instanceGroup.is_isolated ? ( + + + + ) : null} , Date: Thu, 20 Aug 2020 14:24:05 -0400 Subject: [PATCH 082/123] Only disable single notification row when toggling, not all rows --- .../NotificationList/NotificationList.jsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.jsx index f274e4cca3..b11e27f9e5 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationList.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationList.jsx @@ -25,7 +25,7 @@ function NotificationList({ showApprovalsToggle, }) { const location = useLocation(); - const [isToggleLoading, setIsToggleLoading] = useState(false); + const [loadingToggleIds, setLoadingToggleIds] = useState([]); const [toggleError, setToggleError] = useState(null); const { @@ -123,7 +123,7 @@ function NotificationList({ isCurrentlyOn, status ) => { - setIsToggleLoading(true); + setLoadingToggleIds(loadingToggleIds.concat([notificationId])); try { if (isCurrentlyOn) { await apiModel.disassociateNotificationTemplate( @@ -153,7 +153,9 @@ function NotificationList({ } catch (err) { setToggleError(err); } finally { - setIsToggleLoading(false); + setLoadingToggleIds( + loadingToggleIds.filter(item => item !== notificationId) + ); } }; @@ -208,7 +210,10 @@ function NotificationList({ key={notification.id} notification={notification} detailUrl={`/notifications/${notification.id}`} - canToggleNotifications={canToggleNotifications && !isToggleLoading} + canToggleNotifications={ + canToggleNotifications && + !loadingToggleIds.includes(notification.id) + } toggleNotification={handleNotificationToggle} approvalsTurnedOn={approvalsTemplateIds.includes(notification.id)} errorTurnedOn={errorTemplateIds.includes(notification.id)} @@ -223,7 +228,7 @@ function NotificationList({ setToggleError(null)} > {i18n._(t`Failed to toggle notification.`)} From 222a65c8753ce6b2552d6c0e4449545c6f2adf1c Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 17 Aug 2020 16:49:15 -0400 Subject: [PATCH 083/123] Adds extra variables to schedule details. Updates parameters by which we display prompt fields on schedule details. Extend VariableDetails component to be able to handle values that come in raw JSON form. --- .../CodeMirrorInput/VariablesDetail.jsx | 28 +++-- .../CodeMirrorInput/VariablesField.jsx | 6 +- .../CodeMirrorInput/VariablesInput.jsx | 6 +- .../src/components/Schedule/Schedule.test.jsx | 20 ++- .../ScheduleDetail/ScheduleDetail.jsx | 115 ++++++++++++++---- .../ScheduleDetail/ScheduleDetail.test.jsx | 46 ++++++- awx/ui_next/src/util/yaml.js | 8 +- 7 files changed, 186 insertions(+), 43 deletions(-) diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx index 7fbcd63cfa..3d02eb43bd 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx @@ -1,10 +1,15 @@ import 'styled-components/macro'; import React, { useState, useEffect } from 'react'; -import { string, node, number } from 'prop-types'; +import { node, number, oneOfType, shape, string } from 'prop-types'; import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core'; import { DetailName, DetailValue } from '../DetailList'; import MultiButtonToggle from '../MultiButtonToggle'; -import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml'; +import { + yamlToJson, + jsonToYaml, + isJsonObject, + isJsonString, +} from '../../util/yaml'; import CodeMirrorInput from './CodeMirrorInput'; import { JSON_MODE, YAML_MODE } from './constants'; @@ -15,7 +20,7 @@ function getValueAsMode(value, mode) { } return '---'; } - const modeMatches = isJson(value) === (mode === JSON_MODE); + const modeMatches = isJsonString(value) === (mode === JSON_MODE); if (modeMatches) { return value; } @@ -23,12 +28,21 @@ function getValueAsMode(value, mode) { } function VariablesDetail({ value, label, rows, fullHeight }) { - const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE); - const [currentValue, setCurrentValue] = useState(value || '---'); + const [mode, setMode] = useState( + isJsonObject(value) || isJsonString(value) ? JSON_MODE : YAML_MODE + ); + const [currentValue, setCurrentValue] = useState( + isJsonObject(value) ? JSON.stringify(value, null, 2) : value || '---' + ); const [error, setError] = useState(null); useEffect(() => { - setCurrentValue(getValueAsMode(value, mode)); + setCurrentValue( + getValueAsMode( + isJsonObject(value) ? JSON.stringify(value, null, 2) : value, + mode + ) + ); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [value]); @@ -95,7 +109,7 @@ function VariablesDetail({ value, label, rows, fullHeight }) { ); } VariablesDetail.propTypes = { - value: string.isRequired, + value: oneOfType([shape({}), string]).isRequired, label: node.isRequired, rows: number, }; diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx index f39d413fac..4d63fcc663 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx @@ -7,7 +7,7 @@ import styled from 'styled-components'; import { Split, SplitItem } from '@patternfly/react-core'; import { CheckboxField, FieldTooltip } from '../FormField'; import MultiButtonToggle from '../MultiButtonToggle'; -import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml'; +import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml'; import CodeMirrorInput from './CodeMirrorInput'; import { JSON_MODE, YAML_MODE } from './constants'; @@ -30,7 +30,9 @@ function VariablesField({ tooltip, }) { const [field, meta, helpers] = useField(name); - const [mode, setMode] = useState(isJson(field.value) ? JSON_MODE : YAML_MODE); + const [mode, setMode] = useState( + isJsonString(field.value) ? JSON_MODE : YAML_MODE + ); return (
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx index 5f3886e20b..a43962bd76 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { string, func, bool, number } from 'prop-types'; import { Split, SplitItem } from '@patternfly/react-core'; import styled from 'styled-components'; -import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml'; +import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml'; import MultiButtonToggle from '../MultiButtonToggle'; import CodeMirrorInput from './CodeMirrorInput'; import { JSON_MODE, YAML_MODE } from './constants'; @@ -18,11 +18,11 @@ const SplitItemRight = styled(SplitItem)` function VariablesInput(props) { const { id, label, readOnly, rows, error, onError, className } = props; /* eslint-disable react/destructuring-assignment */ - const defaultValue = isJson(props.value) + const defaultValue = isJsonString(props.value) ? formatJson(props.value) : props.value; const [value, setValue] = useState(defaultValue); - const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE); + const [mode, setMode] = useState(isJsonString(value) ? JSON_MODE : YAML_MODE); const isControlled = !!props.onChange; /* eslint-enable react/destructuring-assignment */ diff --git a/awx/ui_next/src/components/Schedule/Schedule.test.jsx b/awx/ui_next/src/components/Schedule/Schedule.test.jsx index f0f58c0710..e3c394cc95 100644 --- a/awx/ui_next/src/components/Schedule/Schedule.test.jsx +++ b/awx/ui_next/src/components/Schedule/Schedule.test.jsx @@ -6,10 +6,12 @@ import { mountWithContexts, waitForElement, } from '../../../testUtils/enzymeHelpers'; -import { SchedulesAPI } from '../../api'; +import { JobTemplatesAPI, SchedulesAPI } from '../../api'; import Schedule from './Schedule'; +jest.mock('../../api/models/JobTemplates'); jest.mock('../../api/models/Schedules'); +jest.mock('../../api/models/WorkflowJobTemplates'); SchedulesAPI.readDetail.mockResolvedValue({ data: { @@ -62,6 +64,22 @@ SchedulesAPI.readCredentials.mockResolvedValue({ }, }); +JobTemplatesAPI.readLaunch.mockResolvedValue({ + data: { + ask_credential_on_launch: false, + ask_diff_mode_on_launch: false, + ask_inventory_on_launch: false, + ask_job_type_on_launch: false, + ask_limit_on_launch: false, + ask_scm_branch_on_launch: false, + ask_skip_tags_on_launch: false, + ask_tags_on_launch: false, + ask_variables_on_launch: false, + ask_verbosity_on_launch: false, + survey_enabled: false, + }, +}); + describe('', () => { let wrapper; let history; diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx index 64b8863fe6..4c63669590 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx @@ -17,10 +17,15 @@ import ScheduleOccurrences from '../ScheduleOccurrences'; import ScheduleToggle from '../ScheduleToggle'; import { formatDateString } from '../../../util/dates'; import useRequest, { useDismissableError } from '../../../util/useRequest'; -import { SchedulesAPI } from '../../../api'; +import { + JobTemplatesAPI, + SchedulesAPI, + WorkflowJobTemplatesAPI, +} from '../../../api'; import DeleteButton from '../../DeleteButton'; import ErrorDetail from '../../ErrorDetail'; import ChipGroup from '../../ChipGroup'; +import { VariablesDetail } from '../../CodeMirrorInput'; const PromptTitle = styled(Title)` --pf-c-title--m-md--FontWeight: 700; @@ -35,9 +40,9 @@ function ScheduleDetail({ schedule, i18n }) { diff_mode, dtend, dtstart, + extra_data, job_tags, job_type, - inventory, limit, modified, name, @@ -67,20 +72,47 @@ function ScheduleDetail({ schedule, i18n }) { const { error, dismissError } = useDismissableError(deleteError); const { - result: [credentials, preview], + result: [credentials, preview, launchData], isLoading, error: readContentError, request: fetchCredentialsAndPreview, } = useRequest( useCallback(async () => { - const [{ data }, { data: schedulePreview }] = await Promise.all([ + const promises = [ SchedulesAPI.readCredentials(id), SchedulesAPI.createPreview({ rrule, }), - ]); - return [data.results, schedulePreview]; - }, [id, rrule]), + ]; + + if ( + schedule?.summary_fields?.unified_job_template?.unified_job_type === + 'job' + ) { + promises.push( + JobTemplatesAPI.readLaunch( + schedule.summary_fields.unified_job_template.id + ) + ); + } else if ( + schedule?.summary_fields?.unified_job_template?.unified_job_type === + 'workflow_job' + ) { + promises.push( + WorkflowJobTemplatesAPI.readLaunch( + schedule.summary_fields.unified_job_template.id + ) + ); + } else { + promises.push(Promise.resolve()); + } + + const [{ data }, { data: schedulePreview }, launch] = await Promise.all( + promises + ); + + return [data.results, schedulePreview, launch?.data]; + }, [id, schedule, rrule]), [] ); @@ -93,15 +125,33 @@ function ScheduleDetail({ schedule, i18n }) { rule.options.freq === RRule.MINUTELY && dtstart === dtend ? i18n._(t`None (Run Once)`) : rule.toText().replace(/^\w/, c => c.toUpperCase()); + + const { + ask_credential_on_launch, + ask_diff_mode_on_launch, + ask_inventory_on_launch, + ask_job_type_on_launch, + ask_limit_on_launch, + ask_scm_branch_on_launch, + ask_skip_tags_on_launch, + ask_tags_on_launch, + ask_variables_on_launch, + ask_verbosity_on_launch, + survey_enabled, + } = launchData || {}; + const showPromptedFields = - (credentials && credentials.length > 0) || - job_type || - (inventory && summary_fields.inventory) || - scm_branch || - limit || - typeof diff_mode === 'boolean' || - (job_tags && job_tags.length > 0) || - (skip_tags && skip_tags.length > 0); + ask_credential_on_launch || + ask_diff_mode_on_launch || + ask_inventory_on_launch || + ask_job_type_on_launch || + ask_limit_on_launch || + ask_scm_branch_on_launch || + ask_skip_tags_on_launch || + ask_tags_on_launch || + ask_variables_on_launch || + ask_verbosity_on_launch || + survey_enabled; if (isLoading) { return ; @@ -144,8 +194,10 @@ function ScheduleDetail({ schedule, i18n }) { {i18n._(t`Prompted Fields`)} - - {inventory && summary_fields.inventory && ( + {ask_job_type_on_launch && ( + + )} + {ask_inventory_on_launch && ( )} - - - {typeof diff_mode === 'boolean' && ( + {ask_scm_branch_on_launch && ( + + )} + {ask_limit_on_launch && ( + + )} + {ask_diff_mode_on_launch && typeof diff_mode === 'boolean' && ( )} - {credentials && credentials.length > 0 && ( + {ask_credential_on_launch && ( )} - {job_tags && job_tags.length > 0 && ( + {ask_tags_on_launch && job_tags && job_tags.length > 0 && ( )} - {skip_tags && skip_tags.length > 0 && ( + {ask_skip_tags_on_launch && skip_tags && skip_tags.length > 0 && ( )} + {(ask_variables_on_launch || survey_enabled) && ( + + )} )} diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx index fe9175c6de..da325174d5 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx @@ -2,14 +2,48 @@ import React from 'react'; import { Route } from 'react-router-dom'; import { createMemoryHistory } from 'history'; import { act } from 'react-dom/test-utils'; -import { SchedulesAPI } from '../../../api'; +import { SchedulesAPI, JobTemplatesAPI } from '../../../api'; import { mountWithContexts, waitForElement, } from '../../../../testUtils/enzymeHelpers'; import ScheduleDetail from './ScheduleDetail'; +jest.mock('../../../api/models/JobTemplates'); jest.mock('../../../api/models/Schedules'); +jest.mock('../../../api/models/WorkflowJobTemplates'); + +const allPrompts = { + data: { + ask_credential_on_launch: true, + ask_diff_mode_on_launch: true, + ask_inventory_on_launch: true, + ask_job_type_on_launch: true, + ask_limit_on_launch: true, + ask_scm_branch_on_launch: true, + ask_skip_tags_on_launch: true, + ask_tags_on_launch: true, + ask_variables_on_launch: true, + ask_verbosity_on_launch: true, + survey_enabled: true, + }, +}; + +const noPrompts = { + data: { + ask_credential_on_launch: false, + ask_diff_mode_on_launch: false, + ask_inventory_on_launch: false, + ask_job_type_on_launch: false, + ask_limit_on_launch: false, + ask_scm_branch_on_launch: false, + ask_skip_tags_on_launch: false, + ask_tags_on_launch: false, + ask_variables_on_launch: false, + ask_verbosity_on_launch: false, + survey_enabled: false, + }, +}; const schedule = { url: '/api/v2/schedules/1', @@ -53,6 +87,7 @@ const schedule = { dtstart: '2020-03-16T04:00:00Z', dtend: '2020-07-06T04:00:00Z', next_run: '2020-03-16T04:00:00Z', + extra_data: {}, }; SchedulesAPI.createPreview.mockResolvedValue({ @@ -79,6 +114,7 @@ describe('', () => { results: [], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( ', () => { expect(wrapper.find('Detail[label="Credentials"]').length).toBe(0); expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(0); expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(0); + expect(wrapper.find('VariablesDetail').length).toBe(0); }); test('details should render with the proper values with prompts', async () => { SchedulesAPI.readCredentials.mockResolvedValue({ @@ -151,6 +188,7 @@ describe('', () => { ], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(allPrompts); const scheduleWithPrompts = { ...schedule, job_type: 'run', @@ -161,6 +199,7 @@ describe('', () => { limit: 'localhost', diff_mode: true, verbosity: 1, + extra_data: { foo: 'fii' }, }; await act(async () => { wrapper = mountWithContexts( @@ -182,7 +221,6 @@ describe('', () => { ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - // await waitForElement(wrapper, 'Title', el => el.length > 0); expect( wrapper .find('Detail[label="Name"]') @@ -231,6 +269,7 @@ describe('', () => { expect(wrapper.find('Detail[label="Credentials"]').length).toBe(1); expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(1); expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(1); + expect(wrapper.find('VariablesDetail').length).toBe(1); }); test('error shown when error encountered fetching credentials', async () => { SchedulesAPI.readCredentials.mockRejectedValueOnce( @@ -245,6 +284,7 @@ describe('', () => { }, }) ); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( ', () => { results: [], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( ', () => { results: [], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( Date: Thu, 20 Aug 2020 21:42:07 -0400 Subject: [PATCH 084/123] Switch back to built-in kubectl connection plugin There's a bug in the upstream community.kubernetes plugin. We can open up a follow-up PR once that has been patched. --- awx/main/isolated/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/isolated/manager.py b/awx/main/isolated/manager.py index 551867986f..623bb5700e 100644 --- a/awx/main/isolated/manager.py +++ b/awx/main/isolated/manager.py @@ -58,7 +58,7 @@ class IsolatedManager(object): os.chmod(temp.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) for host in hosts: inventory['all']['hosts'][host] = { - "ansible_connection": "community.kubernetes.kubectl", + "ansible_connection": "kubectl", "ansible_kubectl_config": path, } else: From 80fe98b8d6fbe076729e1bc8b55b7b5de3c61774 Mon Sep 17 00:00:00 2001 From: mabashian Date: Fri, 21 Aug 2020 09:20:29 -0400 Subject: [PATCH 085/123] Fix npm audit warnings --- awx/ui_next/package-lock.json | 2875 +++++++++++++++------------------ awx/ui_next/package.json | 2 +- 2 files changed, 1267 insertions(+), 1610 deletions(-) diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json index 6e5590c6db..04fc14aa73 100644 --- a/awx/ui_next/package-lock.json +++ b/awx/ui_next/package-lock.json @@ -13,9 +13,9 @@ } }, "@babel/compat-data": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.10.3.tgz", - "integrity": "sha512-BDIfJ9uNZuI0LajPfoYV28lX8kyCPMHY6uY4WH1lJdcicmAfxCK5ASzaeV0D/wsUaRH/cLk+amuxtC37sZ8TUg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz", + "integrity": "sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==", "dev": true, "requires": { "browserslist": "^4.12.0", @@ -105,127 +105,127 @@ } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.3.tgz", - "integrity": "sha512-lo4XXRnBlU6eRM92FkiZxpo1xFLmv3VsPFk61zJKMm7XYJfwqXHsYJTY6agoc4a3L8QPw1HqWehO18coZgbT6A==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", + "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", "dev": true, "requires": { - "@babel/helper-explode-assignable-expression": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-explode-assignable-expression": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-builder-react-jsx": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.3.tgz", - "integrity": "sha512-vkxmuFvmovtqTZknyMGj9+uQAZzz5Z9mrbnkJnPkaYGfKTaSsYcjQdXP0lgrWLVh8wU6bCjOmXOpx+kqUi+S5Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz", + "integrity": "sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/types": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-builder-react-jsx-experimental": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.1.tgz", - "integrity": "sha512-irQJ8kpQUV3JasXPSFQ+LCCtJSc5ceZrPFVj6TElR6XCHssi3jV8ch3odIrNtjJFRZZVbrOEfJMI79TPU/h1pQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz", + "integrity": "sha512-Buewnx6M4ttG+NLkKyt7baQn7ScC/Td+e99G914fRU8fGIUivDDgVIQeDHFa5e4CRSJQt58WpNHhsAZgtzVhsg==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-module-imports": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-module-imports": "^7.10.4", + "@babel/types": "^7.10.5" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-compilation-targets": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.2.tgz", - "integrity": "sha512-hYgOhF4To2UTB4LTaZepN/4Pl9LD4gfbJx8A34mqoluT8TLbof1mhUlYuNWTEebONa8+UlCC4X0TEXu7AOUyGA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz", + "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.10.1", + "@babel/compat-data": "^7.10.4", "browserslist": "^4.12.0", "invariant": "^2.2.4", "levenary": "^1.1.1", @@ -241,366 +241,255 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.3.tgz", - "integrity": "sha512-iRT9VwqtdFmv7UheJWthGc/h2s7MqoweBF9RUj77NFZsg9VfISvBTum3k6coAhJ8RWv2tj3yUjA03HxPd0vfpQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz", + "integrity": "sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-member-expression-to-functions": "^7.10.3", - "@babel/helper-optimise-call-expression": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/helper-replace-supers": "^7.10.1", - "@babel/helper-split-export-declaration": "^7.10.1" + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.10.5", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.1.tgz", - "integrity": "sha512-Rx4rHS0pVuJn5pJOqaqcZR4XSgeF9G/pO/79t+4r7380tXFJdzImFnxMU19f83wjSrmKHq6myrM10pFHTGzkUA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz", + "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-regex": "^7.10.1", + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-regex": "^7.10.4", "regexpu-core": "^4.7.0" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-define-map": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.3.tgz", - "integrity": "sha512-bxRzDi4Sin/k0drWCczppOhov1sBSdBvXJObM1NLHQzjhXhwRtn7aRWGvLJWCYbuu2qUk3EKs6Ci9C9ps8XokQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.3", - "@babel/types": "^7.10.3", - "lodash": "^4.17.13" + "@babel/helper-function-name": "^7.10.4", + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-explode-assignable-expression": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.3.tgz", - "integrity": "sha512-0nKcR64XrOC3lsl+uhD15cwxPvaB6QKUDlD84OT9C3myRbhJqTMYir69/RWItUvHpharv0eJ/wk7fl34ONSwZw==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz", + "integrity": "sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==", "dev": true, "requires": { - "@babel/traverse": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" }, "dependencies": { - "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.3" - } - }, - "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", - "dev": true, - "requires": { - "@babel/types": "^7.10.1" - } - }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, - "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true } } }, @@ -623,56 +512,56 @@ } }, "@babel/helper-hoist-variables": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.3.tgz", - "integrity": "sha512-9JyafKoBt5h20Yv1+BXQMdcXXavozI1vt401KBiRc2qzUepbVnd7ogVNymY1xkQN9fekGwfxtotH2Yf5xsGzgg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", + "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-member-expression-to-functions": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.3.tgz", - "integrity": "sha512-q7+37c4EPLSjNb2NmWOjNwj0+BOyYlssuQ58kHEWk1Z78K5i8vTUsteq78HMieRPQSl/NtpQyJfdjt3qZ5V2vw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", + "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.11.0" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } @@ -687,409 +576,334 @@ } }, "@babel/helper-module-transforms": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz", - "integrity": "sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", + "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.10.1", - "@babel/helper-replace-supers": "^7.10.1", - "@babel/helper-simple-access": "^7.10.1", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/template": "^7.10.1", - "@babel/types": "^7.10.1", - "lodash": "^4.17.13" + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/template": "^7.10.4", + "@babel/types": "^7.11.0", + "lodash": "^4.17.19" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-optimise-call-expression": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.3.tgz", - "integrity": "sha512-kT2R3VBH/cnSz+yChKpaKRJQJWxdGoc6SjioRId2wkeV3bK0wLLioFpJROrX0U4xr/NmxSSAWT/9Ih5snwIIzg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-plugin-utils": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.3.tgz", - "integrity": "sha512-j/+j8NAWUTxOtx4LKHybpSClxHoq6I91DQ/mKgAXn5oNUPIUiGppjPIX3TDtJWPrdfP9Kfl7e4fgVMiQR9VE/g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true }, "@babel/helper-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.1.tgz", - "integrity": "sha512-7isHr19RsIJWWLLFn21ubFt223PjQyg1HY7CZEMRr820HttHPpVvrsIN3bUOo44DEfFV4kBXO7Abbn9KTUZV7g==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", + "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", "dev": true, "requires": { - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/helper-remap-async-to-generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.3.tgz", - "integrity": "sha512-sLB7666ARbJUGDO60ZormmhQOyqMX/shKBXZ7fy937s+3ID8gSrneMvKSSb+8xIM5V7Vn6uNVtOY1vIm26XLtA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz", + "integrity": "sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-wrap-function": "^7.10.1", - "@babel/template": "^7.10.3", - "@babel/traverse": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-wrap-function": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" - } - }, - "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" - } - }, - "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", - "dev": true, - "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true } } }, "@babel/helper-replace-supers": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz", - "integrity": "sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.1", - "@babel/helper-optimise-call-expression": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", + "integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "dev": true, "requires": { - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", + "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.0", + "@babel/types": "^7.11.0", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -1111,66 +925,94 @@ } }, "@babel/helper-simple-access": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz", - "integrity": "sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", "dev": true, "requires": { - "@babel/template": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz", + "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } @@ -1190,126 +1032,125 @@ "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" }, "@babel/helper-wrap-function": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.1.tgz", - "integrity": "sha512-C0MzRGteVDn+H32/ZgbAv5r56f2o1fZSA/rj/TYo8JEJNHg+9BdSmKBUND0shxWRztWhjlT2cvHYuynpPsVJwQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", + "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.1", - "@babel/template": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/helper-function-name": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", + "integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "dev": true, "requires": { - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", + "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.0", + "@babel/types": "^7.11.0", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -1331,125 +1172,124 @@ } }, "@babel/helpers": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.1.tgz", - "integrity": "sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", "dev": true, "requires": { - "@babel/template": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", + "integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "dev": true, "requires": { - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", + "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.0", + "@babel/types": "^7.11.0", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -1486,24 +1326,24 @@ "integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==" }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.3.tgz", - "integrity": "sha512-WUUWM7YTOudF4jZBAJIW9D7aViYC/Fn0Pln4RIHlQALyno3sXSjqmTA4Zy1TKC2D49RCR8Y/Pn4OIUtEypK3CA==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", + "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/helper-remap-async-to-generator": "^7.10.3", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.10.4", "@babel/plugin-syntax-async-generators": "^7.8.0" } }, "@babel/plugin-proposal-class-properties": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz", - "integrity": "sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", + "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-proposal-decorators": { @@ -1518,94 +1358,115 @@ } }, "@babel/plugin-proposal-dynamic-import": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz", - "integrity": "sha512-Cpc2yUVHTEGPlmiQzXj026kqwjEQAD9I4ZC16uzdbgWgitg/UHKHLffKNCQZ5+y8jpIZPJcKcwsr2HwPh+w3XA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", + "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0" } }, - "@babel/plugin-proposal-json-strings": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.1.tgz", - "integrity": "sha512-m8r5BmV+ZLpWPtMY2mOKN7wre6HIO4gfIiV+eOmsnZABNenrt/kzYBwrh+KOfgumSWpnlGs5F70J8afYMSJMBg==", + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz", + "integrity": "sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz", + "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.0" } }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.1.tgz", - "integrity": "sha512-56cI/uHYgL2C8HVuHOuvVowihhX0sxb3nnfVRzUeVHTWmRHTZrKuAh/OBIMggGU/S1g/1D2CRCXqP+3u7vX7iA==", + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz", + "integrity": "sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", + "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" } }, "@babel/plugin-proposal-numeric-separator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.1.tgz", - "integrity": "sha512-jjfym4N9HtCiNfyyLAVD8WqPYeHUrw4ihxuAynWj6zzp2gf9Ey2f7ImhFm6ikB3CLf5Z/zmcJDri6B4+9j9RsA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz", + "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-numeric-separator": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.3.tgz", - "integrity": "sha512-ZZh5leCIlH9lni5bU/wB/UcjtcVLgR8gc+FAgW2OOY+m9h1II3ItTO1/cewNUcsIDZSYcSaz/rYVls+Fb0ExVQ==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz", + "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-transform-parameters": "^7.10.1" + "@babel/plugin-transform-parameters": "^7.10.4" } }, "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.1.tgz", - "integrity": "sha512-VqExgeE62YBqI3ogkGoOJp1R6u12DFZjqwJhqtKc2o5m1YTUuUWnos7bZQFBhwkxIFpWYJ7uB75U7VAPPiKETA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz", + "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.3.tgz", - "integrity": "sha512-yyG3n9dJ1vZ6v5sfmIlMMZ8azQoqx/5/nZTSWX1td6L1H1bsjzA8TInDChpafCZiJkeOFzp/PtrfigAQXxI1Ng==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz", + "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0" } }, "@babel/plugin-proposal-private-methods": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.1.tgz", - "integrity": "sha512-RZecFFJjDiQ2z6maFprLgrdnm0OzoC23Mx89xf1CcEsxmHuzuXOdniEuI+S3v7vjQG4F5sa6YtUp+19sZuSxHg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz", + "integrity": "sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.1.tgz", - "integrity": "sha512-JjfngYRvwmPwmnbRZyNiPFI8zxCZb8euzbCG/LxyKdeTb59tVciKo9GK9bi6JYKInk1H11Dq9j/zRqIH4KigfQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz", + "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-async-generators": { @@ -1618,21 +1479,21 @@ } }, "@babel/plugin-syntax-class-properties": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.1.tgz", - "integrity": "sha512-Gf2Yx/iRs1JREDtVZ56OrjjgFHCaldpTnuy9BHla10qyVT3YkIIGEtoDWhyop0ksu1GvNjHIoYRBqm3zoR1jyQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", + "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-decorators": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.1.tgz", - "integrity": "sha512-a9OAbQhKOwSle1Vr0NJu/ISg1sPfdEkfRKWpgPuzhnWWzForou2gIeUIIwjAMHRekhhpJ7eulZlYs0H14Cbi+g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.4.tgz", + "integrity": "sha512-2NaoC6fAk2VMdhY1eerkfHV+lVYC1u8b+jmRJISqANCJlTxYy19HGdIkkQtix2UtkcPuPu+IlDgrVseZnU03bw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-dynamic-import": { @@ -1644,13 +1505,22 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, - "@babel/plugin-syntax-flow": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.10.1.tgz", - "integrity": "sha512-b3pWVncLBYoPP60UOTc7NMlbtsHQ6ITim78KQejNHK6WJ2mzV5kCcg4mIWpasAfJEgwVTibwo2e+FU7UEIKQUg==", + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-flow": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.10.4.tgz", + "integrity": "sha512-yxQsX1dJixF4qEEdzVbst3SZQ58Nrooz8NV9Z9GL4byTE25BvJgl5lf0RECUf0fh28rZBb/RYTWn/eeKwCMrZQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-json-strings": { @@ -1663,12 +1533,21 @@ } }, "@babel/plugin-syntax-jsx": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.1.tgz", - "integrity": "sha512-+OxyOArpVFXQeXKLO9o+r2I4dIoVoy6+Uu0vKELrlweDM3QJADZj+Z+5ERansZqIZBcLj42vHnDI8Rz9BnRIuQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz", + "integrity": "sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-nullish-coalescing-operator": { @@ -1681,12 +1560,12 @@ } }, "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.1.tgz", - "integrity": "sha512-uTd0OsHrpe3tH5gRPTxG8Voh99/WCU78vIm5NMRYPAqC8lR4vajt6KkCAknCHrx24vkPdd/05yfdGSB4EIY2mg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-object-rest-spread": { @@ -1717,245 +1596,244 @@ } }, "@babel/plugin-syntax-top-level-await": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.1.tgz", - "integrity": "sha512-hgA5RYkmZm8FTFT3yu2N9Bx7yVVOKYT6yEdXXo6j2JTm0wNxgqaGeQVaSHRjhfnQbX91DtjFB6McRFSlcJH3xQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz", + "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-typescript": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.1.tgz", - "integrity": "sha512-X/d8glkrAtra7CaQGMiGs/OGa6XgUzqPcBXCIGFCpCqnfGlT0Wfbzo/B89xHhnInTaItPK8LALblVXcUOEh95Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz", + "integrity": "sha512-oSAEz1YkBCAKr5Yiq8/BNtvSAPwkp/IyUnwZogd8p+F0RuYQQrLeRUzIQhueQTTBy/F+a40uS7OFKxnkRvmvFQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.1.tgz", - "integrity": "sha512-6AZHgFJKP3DJX0eCNJj01RpytUa3SOGawIxweHkNX2L6PYikOZmoh5B0d7hIHaIgveMjX990IAa/xK7jRTN8OA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz", + "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.1.tgz", - "integrity": "sha512-XCgYjJ8TY2slj6SReBUyamJn3k2JLUIiiR5b6t1mNCMSvv7yx+jJpaewakikp0uWFQSF7ChPPoe3dHmXLpISkg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz", + "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-remap-async-to-generator": "^7.10.1" + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.10.4" }, "dependencies": { "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.1.tgz", - "integrity": "sha512-B7K15Xp8lv0sOJrdVAoukKlxP9N59HS48V1J3U/JGj+Ad+MHq+am6xJVs85AgXrQn4LV8vaYFOB+pr/yIuzW8Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz", + "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.1.tgz", - "integrity": "sha512-8bpWG6TtF5akdhIm/uWTyjHqENpy13Fx8chg7pFH875aNLwX8JxIxqm08gmAT+Whe6AOmaTeLPe7dpLbXt+xUw==", + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz", + "integrity": "sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "lodash": "^4.17.13" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-classes": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.3.tgz", - "integrity": "sha512-irEX0ChJLaZVC7FvvRoSIxJlmk0IczFLcwaRXUArBKYHCHbOhe57aG8q3uw/fJsoSXvZhjRX960hyeAGlVBXZw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz", + "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-define-map": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-optimise-call-expression": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/helper-replace-supers": "^7.10.1", - "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-define-map": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", "globals": "^11.1.0" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-computed-properties": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.3.tgz", - "integrity": "sha512-GWzhaBOsdbjVFav96drOz7FzrcEW6AP5nax0gLIpstiFaI3LOb2tAg06TimaWU6YKOfUACK3FVrxPJ4GSc5TgA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz", + "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-destructuring": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.1.tgz", - "integrity": "sha512-V/nUc4yGWG71OhaTH705pU8ZSdM6c1KmmLP8ys59oOYbT7RpMYAR3MsVOt6OHL0WzG7BlTU076va9fjJyYzJMA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz", + "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.1.tgz", - "integrity": "sha512-19VIMsD1dp02RvduFUmfzj8uknaO3uiHHF0s3E1OHnVsNj8oge8EQ5RzHRbJjGSetRnkEuBYO7TG1M5kKjGLOA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz", + "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.1.tgz", - "integrity": "sha512-wIEpkX4QvX8Mo9W6XF3EdGttrIPZWozHfEaDTU0WJD/TDnXMvdDh30mzUl/9qWhnf7naicYartcEfUghTCSNpA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz", + "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.1.tgz", - "integrity": "sha512-lr/przdAbpEA2BUzRvjXdEDLrArGRRPwbaF9rvayuHRvdQ7lUTTkZnhZrJ4LE2jvgMRFF4f0YuPQ20vhiPYxtA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz", + "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==", "dev": true, "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-flow-strip-types": { @@ -1969,353 +1847,353 @@ } }, "@babel/plugin-transform-for-of": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.1.tgz", - "integrity": "sha512-US8KCuxfQcn0LwSCMWMma8M2R5mAjJGsmoCBVwlMygvmDUMkTCykc84IqN1M7t+agSfOmLYTInLCHJM+RUoz+w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz", + "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-function-name": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.1.tgz", - "integrity": "sha512-//bsKsKFBJfGd65qSNNh1exBy5Y9gD9ZN+DvrJ8f7HXr4avE5POW6zB7Rj6VnqHV33+0vXWUwJT0wSHubiAQkw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz", + "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-literals": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.1.tgz", - "integrity": "sha512-qi0+5qgevz1NHLZroObRm5A+8JJtibb7vdcPQF1KQE12+Y/xxl8coJ+TpPW9iRq+Mhw/NKLjm+5SHtAHCC7lAw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", + "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.1.tgz", - "integrity": "sha512-UmaWhDokOFT2GcgU6MkHC11i0NQcL63iqeufXWfRy6pUOGYeCGEKhvfFO6Vz70UfYJYHwveg62GS83Rvpxn+NA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", + "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.1.tgz", - "integrity": "sha512-31+hnWSFRI4/ACFr1qkboBbrTxoBIzj7qA69qlq8HY8p7+YCzkCT6/TvQ1a4B0z27VeWtAeJd6pr5G04dc1iHw==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz", + "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.1.tgz", - "integrity": "sha512-AQG4fc3KOah0vdITwt7Gi6hD9BtQP/8bhem7OjbaMoRNCH5Djx42O2vYMfau7QnAzQCa+RJnhJBmFFMGpQEzrg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", + "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-simple-access": "^7.10.1", + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.3.tgz", - "integrity": "sha512-GWXWQMmE1GH4ALc7YXW56BTh/AlzvDWhUNn9ArFF0+Cz5G8esYlVbXfdyHa1xaD1j+GnBoCeoQNlwtZTVdiG/A==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz", + "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==", "dev": true, "requires": { - "@babel/helper-hoist-variables": "^7.10.3", - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3", + "@babel/helper-hoist-variables": "^7.10.4", + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-umd": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.1.tgz", - "integrity": "sha512-EIuiRNMd6GB6ulcYlETnYYfgv4AxqrswghmBRQbWLHZxN4s7mupxzglnHqk9ZiUpDI4eRWewedJJNj67PWOXKA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", + "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.3.tgz", - "integrity": "sha512-I3EH+RMFyVi8Iy/LekQm948Z4Lz4yKT7rK+vuCAeRm0kTa6Z5W7xuhRxDNJv0FPya/her6AUgrDITb70YHtTvA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz", + "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.10.4" } }, "@babel/plugin-transform-new-target": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.1.tgz", - "integrity": "sha512-MBlzPc1nJvbmO9rPr1fQwXOM2iGut+JC92ku6PbiJMMK7SnQc1rytgpopveE3Evn47gzvGYeCdgfCDbZo0ecUw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz", + "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-object-super": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.1.tgz", - "integrity": "sha512-WnnStUDN5GL+wGQrJylrnnVlFhFmeArINIR9gjhSeYyvroGhBrSAXYg/RHsnfzmsa+onJrTJrEClPzgNmmQ4Gw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz", + "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-replace-supers": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4" } }, "@babel/plugin-transform-parameters": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.1.tgz", - "integrity": "sha512-tJ1T0n6g4dXMsL45YsSzzSDZCxiHXAQp/qHrucOq5gEHncTA3xDxnd5+sZcoQp+N1ZbieAaB8r/VUCG0gqseOg==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz", + "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-property-literals": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.1.tgz", - "integrity": "sha512-Kr6+mgag8auNrgEpbfIWzdXYOvqDHZOF0+Bx2xh4H2EDNwcbRb9lY6nkZg8oSjsX+DH9Ebxm9hOqtKW+gRDeNA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz", + "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-react-constant-elements": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.1.tgz", - "integrity": "sha512-V4os6bkWt/jbrzfyVcZn2ZpuHZkvj3vyBU0U/dtS8SZuMS7Rfx5oknTrtfyXJ2/QZk8gX7Yls5Z921ItNpE30Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.4.tgz", + "integrity": "sha512-cYmQBW1pXrqBte1raMkAulXmi7rjg3VI6ZLg9QIic8Hq7BtYXaWuZSxsr2siOMI6SWwpxjWfnwhTUrd7JlAV7g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-react-display-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.3.tgz", - "integrity": "sha512-dOV44bnSW5KZ6kYF6xSHBth7TFiHHZReYXH/JH3XnFNV+soEL1F5d8JT7AJ3ZBncd19Qul7SN4YpBnyWOnQ8KA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.4.tgz", + "integrity": "sha512-Zd4X54Mu9SBfPGnEcaGcOrVAYOtjT2on8QZkLKEq1S/tHexG39d9XXGZv19VfRrDjPJzFmPfTAqOQS1pfFOujw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-react-jsx": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.3.tgz", - "integrity": "sha512-Y21E3rZmWICRJnvbGVmDLDZ8HfNDIwjGF3DXYHx1le0v0mIHCs0Gv5SavyW5Z/jgAHLaAoJPiwt+Dr7/zZKcOQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz", + "integrity": "sha512-L+MfRhWjX0eI7Js093MM6MacKU4M6dnCRa/QPDwYMxjljzSCzzlzKzj9Pk4P3OtrPcxr2N3znR419nr3Xw+65A==", "dev": true, "requires": { - "@babel/helper-builder-react-jsx": "^7.10.3", - "@babel/helper-builder-react-jsx-experimental": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-builder-react-jsx": "^7.10.4", + "@babel/helper-builder-react-jsx-experimental": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-jsx-development": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.1.tgz", - "integrity": "sha512-XwDy/FFoCfw9wGFtdn5Z+dHh6HXKHkC6DwKNWpN74VWinUagZfDcEJc3Y8Dn5B3WMVnAllX8Kviaw7MtC5Epwg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.4.tgz", + "integrity": "sha512-RM3ZAd1sU1iQ7rI2dhrZRZGv0aqzNQMbkIUCS1txYpi9wHQ2ZHNjo5TwX+UD6pvFW4AbWqLVYvKy5qJSAyRGjQ==", "dev": true, "requires": { - "@babel/helper-builder-react-jsx-experimental": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-builder-react-jsx-experimental": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-jsx-self": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.1.tgz", - "integrity": "sha512-4p+RBw9d1qV4S749J42ZooeQaBomFPrSxa9JONLHJ1TxCBo3TzJ79vtmG2S2erUT8PDDrPdw4ZbXGr2/1+dILA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz", + "integrity": "sha512-yOvxY2pDiVJi0axdTWHSMi5T0DILN+H+SaeJeACHKjQLezEzhLx9nEF9xgpBLPtkZsks9cnb5P9iBEi21En3gg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-jsx-source": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.1.tgz", - "integrity": "sha512-neAbaKkoiL+LXYbGDvh6PjPG+YeA67OsZlE78u50xbWh2L1/C81uHiNP5d1fw+uqUIoiNdCC8ZB+G4Zh3hShJA==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz", + "integrity": "sha512-wTeqHVkN1lfPLubRiZH3o73f4rfon42HpgxUSs86Nc+8QIcm/B9s8NNVXu/gwGcOyd7yDib9ikxoDLxJP0UiDA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-pure-annotations": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.3.tgz", - "integrity": "sha512-n/fWYGqvTl7OLZs/QcWaKMFdADPvC3V6jYuEOpPyvz97onsW9TXn196fHnHW1ZgkO20/rxLOgKnEtN1q9jkgqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz", + "integrity": "sha512-+njZkqcOuS8RaPakrnR9KvxjoG1ASJWpoIv/doyWngId88JoFlPlISenGXjrVacZUIALGUr6eodRs1vmPnF23A==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-regenerator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.3.tgz", - "integrity": "sha512-H5kNeW0u8mbk0qa1jVIVTeJJL6/TJ81ltD4oyPx0P499DhMJrTmmIFCmJ3QloGpQG8K9symccB7S7SJpCKLwtw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz", + "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==", "dev": true, "requires": { "regenerator-transform": "^0.14.2" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.1.tgz", - "integrity": "sha512-qN1OMoE2nuqSPmpTqEM7OvJ1FkMEV+BjVeZZm9V9mq/x1JLKQ4pcv8riZJMNN3u2AUGl0ouOMjRr2siecvHqUQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz", + "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-runtime": { @@ -2339,108 +2217,109 @@ } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz", - "integrity": "sha512-AR0E/lZMfLstScFwztApGeyTHJ5u3JUKMjneqRItWeEqDdHWZwAOKycvQNCasCK/3r5YXsuNG25funcJDu7Y2g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz", + "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-spread": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.1.tgz", - "integrity": "sha512-8wTPym6edIrClW8FI2IoaePB91ETOtg36dOkj3bYcNe7aDMN2FXEoUa+WrmPc4xa1u2PQK46fUX2aCb+zo9rfw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz", + "integrity": "sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.1.tgz", - "integrity": "sha512-j17ojftKjrL7ufX8ajKvwRilwqTok4q+BjkknmQw9VNHnItTyMP5anPFzxFJdCQs7clLcWpCV3ma+6qZWLnGMA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz", + "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-regex": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-regex": "^7.10.4" } }, "@babel/plugin-transform-template-literals": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.3.tgz", - "integrity": "sha512-yaBn9OpxQra/bk0/CaA4wr41O0/Whkg6nqjqApcinxM7pro51ojhX6fv1pimAnVjVfDy14K0ULoRL70CA9jWWA==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz", + "integrity": "sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.1.tgz", - "integrity": "sha512-qX8KZcmbvA23zDi+lk9s6hC1FM7jgLHYIjuLgULgc8QtYnmB3tAVIYkNoKRQ75qWBeyzcoMoK8ZQmogGtC/w0g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz", + "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-typescript": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.3.tgz", - "integrity": "sha512-qU9Lu7oQyh3PGMQncNjQm8RWkzw6LqsWZQlZPQMgrGt6s3YiBIaQ+3CQV/FA/icGS5XlSWZGwo/l8ErTyelS0Q==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.11.0.tgz", + "integrity": "sha512-edJsNzTtvb3MaXQwj8403B7mZoGu9ElDJQZOKjGUnvilquxBA3IQoEIOvkX/1O8xfAsnHS/oQhe2w/IXrr+w0w==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/plugin-syntax-typescript": "^7.10.1" + "@babel/helper-create-class-features-plugin": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-typescript": "^7.10.4" } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.1.tgz", - "integrity": "sha512-zZ0Poh/yy1d4jeDWpx/mNwbKJVwUYJX73q+gyh4bwtG0/iUlzdEu0sLMda8yuDFS6LBQlT/ST1SJAR6zYwXWgw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz", + "integrity": "sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.1.tgz", - "integrity": "sha512-Y/2a2W299k0VIUdbqYm9X2qS6fE0CUBhhiPpimK6byy7OJ/kORLlIX+J6UrjgNu5awvs62k+6RSslxhcvVw2Tw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz", + "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/polyfill": { @@ -2462,70 +2341,74 @@ } }, "@babel/preset-env": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.10.3.tgz", - "integrity": "sha512-jHaSUgiewTmly88bJtMHbOd1bJf2ocYxb5BWKSDQIP5tmgFuS/n0gl+nhSrYDhT33m0vPxp+rP8oYYgPgMNQlg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.0.tgz", + "integrity": "sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg==", "dev": true, "requires": { - "@babel/compat-data": "^7.10.3", - "@babel/helper-compilation-targets": "^7.10.2", - "@babel/helper-module-imports": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/plugin-proposal-async-generator-functions": "^7.10.3", - "@babel/plugin-proposal-class-properties": "^7.10.1", - "@babel/plugin-proposal-dynamic-import": "^7.10.1", - "@babel/plugin-proposal-json-strings": "^7.10.1", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1", - "@babel/plugin-proposal-numeric-separator": "^7.10.1", - "@babel/plugin-proposal-object-rest-spread": "^7.10.3", - "@babel/plugin-proposal-optional-catch-binding": "^7.10.1", - "@babel/plugin-proposal-optional-chaining": "^7.10.3", - "@babel/plugin-proposal-private-methods": "^7.10.1", - "@babel/plugin-proposal-unicode-property-regex": "^7.10.1", + "@babel/compat-data": "^7.11.0", + "@babel/helper-compilation-targets": "^7.10.4", + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-proposal-async-generator-functions": "^7.10.4", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-dynamic-import": "^7.10.4", + "@babel/plugin-proposal-export-namespace-from": "^7.10.4", + "@babel/plugin-proposal-json-strings": "^7.10.4", + "@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", + "@babel/plugin-proposal-numeric-separator": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.11.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.10.4", + "@babel/plugin-proposal-optional-chaining": "^7.11.0", + "@babel/plugin-proposal-private-methods": "^7.10.4", + "@babel/plugin-proposal-unicode-property-regex": "^7.10.4", "@babel/plugin-syntax-async-generators": "^7.8.0", - "@babel/plugin-syntax-class-properties": "^7.10.1", + "@babel/plugin-syntax-class-properties": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", - "@babel/plugin-syntax-numeric-separator": "^7.10.1", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0", - "@babel/plugin-syntax-top-level-await": "^7.10.1", - "@babel/plugin-transform-arrow-functions": "^7.10.1", - "@babel/plugin-transform-async-to-generator": "^7.10.1", - "@babel/plugin-transform-block-scoped-functions": "^7.10.1", - "@babel/plugin-transform-block-scoping": "^7.10.1", - "@babel/plugin-transform-classes": "^7.10.3", - "@babel/plugin-transform-computed-properties": "^7.10.3", - "@babel/plugin-transform-destructuring": "^7.10.1", - "@babel/plugin-transform-dotall-regex": "^7.10.1", - "@babel/plugin-transform-duplicate-keys": "^7.10.1", - "@babel/plugin-transform-exponentiation-operator": "^7.10.1", - "@babel/plugin-transform-for-of": "^7.10.1", - "@babel/plugin-transform-function-name": "^7.10.1", - "@babel/plugin-transform-literals": "^7.10.1", - "@babel/plugin-transform-member-expression-literals": "^7.10.1", - "@babel/plugin-transform-modules-amd": "^7.10.1", - "@babel/plugin-transform-modules-commonjs": "^7.10.1", - "@babel/plugin-transform-modules-systemjs": "^7.10.3", - "@babel/plugin-transform-modules-umd": "^7.10.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.3", - "@babel/plugin-transform-new-target": "^7.10.1", - "@babel/plugin-transform-object-super": "^7.10.1", - "@babel/plugin-transform-parameters": "^7.10.1", - "@babel/plugin-transform-property-literals": "^7.10.1", - "@babel/plugin-transform-regenerator": "^7.10.3", - "@babel/plugin-transform-reserved-words": "^7.10.1", - "@babel/plugin-transform-shorthand-properties": "^7.10.1", - "@babel/plugin-transform-spread": "^7.10.1", - "@babel/plugin-transform-sticky-regex": "^7.10.1", - "@babel/plugin-transform-template-literals": "^7.10.3", - "@babel/plugin-transform-typeof-symbol": "^7.10.1", - "@babel/plugin-transform-unicode-escapes": "^7.10.1", - "@babel/plugin-transform-unicode-regex": "^7.10.1", + "@babel/plugin-syntax-top-level-await": "^7.10.4", + "@babel/plugin-transform-arrow-functions": "^7.10.4", + "@babel/plugin-transform-async-to-generator": "^7.10.4", + "@babel/plugin-transform-block-scoped-functions": "^7.10.4", + "@babel/plugin-transform-block-scoping": "^7.10.4", + "@babel/plugin-transform-classes": "^7.10.4", + "@babel/plugin-transform-computed-properties": "^7.10.4", + "@babel/plugin-transform-destructuring": "^7.10.4", + "@babel/plugin-transform-dotall-regex": "^7.10.4", + "@babel/plugin-transform-duplicate-keys": "^7.10.4", + "@babel/plugin-transform-exponentiation-operator": "^7.10.4", + "@babel/plugin-transform-for-of": "^7.10.4", + "@babel/plugin-transform-function-name": "^7.10.4", + "@babel/plugin-transform-literals": "^7.10.4", + "@babel/plugin-transform-member-expression-literals": "^7.10.4", + "@babel/plugin-transform-modules-amd": "^7.10.4", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/plugin-transform-modules-systemjs": "^7.10.4", + "@babel/plugin-transform-modules-umd": "^7.10.4", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.4", + "@babel/plugin-transform-new-target": "^7.10.4", + "@babel/plugin-transform-object-super": "^7.10.4", + "@babel/plugin-transform-parameters": "^7.10.4", + "@babel/plugin-transform-property-literals": "^7.10.4", + "@babel/plugin-transform-regenerator": "^7.10.4", + "@babel/plugin-transform-reserved-words": "^7.10.4", + "@babel/plugin-transform-shorthand-properties": "^7.10.4", + "@babel/plugin-transform-spread": "^7.11.0", + "@babel/plugin-transform-sticky-regex": "^7.10.4", + "@babel/plugin-transform-template-literals": "^7.10.4", + "@babel/plugin-transform-typeof-symbol": "^7.10.4", + "@babel/plugin-transform-unicode-escapes": "^7.10.4", + "@babel/plugin-transform-unicode-regex": "^7.10.4", "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "browserslist": "^4.12.0", "core-js-compat": "^3.6.2", "invariant": "^2.2.2", @@ -2534,28 +2417,28 @@ }, "dependencies": { "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -2581,18 +2464,18 @@ } }, "@babel/preset-react": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.10.1.tgz", - "integrity": "sha512-Rw0SxQ7VKhObmFjD/cUcKhPTtzpeviEFX1E6PgP+cYOhQ98icNqtINNFANlsdbQHrmeWnqdxA4Tmnl1jy5tp3Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.10.4.tgz", + "integrity": "sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-transform-react-display-name": "^7.10.1", - "@babel/plugin-transform-react-jsx": "^7.10.1", - "@babel/plugin-transform-react-jsx-development": "^7.10.1", - "@babel/plugin-transform-react-jsx-self": "^7.10.1", - "@babel/plugin-transform-react-jsx-source": "^7.10.1", - "@babel/plugin-transform-react-pure-annotations": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-transform-react-display-name": "^7.10.4", + "@babel/plugin-transform-react-jsx": "^7.10.4", + "@babel/plugin-transform-react-jsx-development": "^7.10.4", + "@babel/plugin-transform-react-jsx-self": "^7.10.4", + "@babel/plugin-transform-react-jsx-source": "^7.10.4", + "@babel/plugin-transform-react-pure-annotations": "^7.10.4" } }, "@babel/preset-typescript": { @@ -3427,9 +3310,9 @@ } }, "@types/babel__traverse": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.12.tgz", - "integrity": "sha512-t4CoEokHTfcyfb4hUaF9oOHu9RmmNWnm1CP0YmMqOOfClKascOmvlEM736vlqeScuGvBDsHkf8R2INd4DWreQA==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.13.tgz", + "integrity": "sha512-i+zS7t6/s9cdQvbqKDARrcbrPvtJGlbYsMkazo03nTAK3RX9FNrLllXys22uiTGJapPOTZTQ35nHh4ISph4SLQ==", "dev": true, "requires": { "@babel/types": "^7.3.0" @@ -3448,9 +3331,9 @@ "dev": true }, "@types/glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "requires": { "@types/minimatch": "*", @@ -3805,9 +3688,9 @@ "dev": true }, "abab": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", - "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", + "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==", "dev": true }, "accepts": { @@ -3956,9 +3839,9 @@ "dev": true }, "ajv-keywords": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.0.tgz", - "integrity": "sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true }, "alphanum-sort": { @@ -4257,14 +4140,14 @@ } }, "autoprefixer": { - "version": "9.8.4", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.4.tgz", - "integrity": "sha512-84aYfXlpUe45lvmS+HoAWKCkirI/sw4JK0/bTeeqgHYco3dcsOn0NqdejISjptsYwNji/21dnkDri9PsYKk89A==", + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", "dev": true, "requires": { "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001087", - "colorette": "^1.2.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", "postcss": "^7.0.32", @@ -4278,9 +4161,9 @@ "dev": true }, "aws4": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", - "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", + "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", "dev": true }, "axios": { @@ -4570,9 +4453,9 @@ } }, "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", + "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -4826,9 +4709,9 @@ } }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "dev": true }, "semver": { @@ -4985,6 +4868,16 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -5100,8 +4993,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browser-process-hrtime": { "version": "1.0.0", @@ -5235,15 +5127,15 @@ } }, "browserslist": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.2.tgz", - "integrity": "sha512-MfZaeYqR8StRZdstAK9hCKDd2StvePCYp5rHzQCPicUjfFliDgmuaBNPHYUTpAywBN8+Wc/d7NYVFkO0aqaBUw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.0.tgz", + "integrity": "sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001088", - "electron-to-chromium": "^1.3.483", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" + "caniuse-lite": "^1.0.30001111", + "electron-to-chromium": "^1.3.523", + "escalade": "^3.0.2", + "node-releases": "^1.1.60" } }, "bser": { @@ -5430,9 +5322,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001090", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001090.tgz", - "integrity": "sha512-QzPRKDCyp7RhjczTPZaqK3CjPA5Ht2UnXhZhCI4f7QiB5JK6KEuZBxIzyWnB3wO4hgAj4GMRxAhuiacfw0Psjg==", + "version": "1.0.30001117", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001117.tgz", + "integrity": "sha512-4tY0Fatzdx59kYjQs+bNxUwZB03ZEBgVmJ1UkFPz/Q8OLiUUbjct2EdpnXj0fvFTPej2EkbPIG0w8BWsjAyk1Q==", "dev": true }, "capture-exit": { @@ -5536,9 +5428,9 @@ } }, "chokidar": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", - "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", + "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", "dev": true, "requires": { "anymatch": "~3.1.1", @@ -5786,12 +5678,6 @@ "q": "^1.1.2" } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "codemirror": { "version": "5.53.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.53.2.tgz", @@ -5841,9 +5727,9 @@ } }, "colorette": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.0.tgz", - "integrity": "sha512-soRSroY+OF/8OdA3PTQXwaDJeMc7TfknKKrxeSCencL2a4+Tx5zhxmmv7hdpCjhKBjehzp8+bwe/T68K0hpIjw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "dev": true }, "colors": { @@ -7270,9 +7156,9 @@ "dev": true }, "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, "duplexify": { @@ -7304,16 +7190,15 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.483", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.483.tgz", - "integrity": "sha512-+05RF8S9rk8S0G8eBCqBRBaRq7+UN3lDs2DAvnG8SBSgQO3hjy0+qt4CmRk5eiuGbTcaicgXfPmBi31a+BD3lg==", + "version": "1.3.540", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.540.tgz", + "integrity": "sha512-IoGiZb8SMqTtkDYJtP8EtCdvv3VMtd1QoTlypO2RUBxRq/Wk0rU5IzhzhMckPaC9XxDqUvWsL0XKOBhTiYVN3w==", "dev": true }, "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", - "dev": true, + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -7325,10 +7210,9 @@ }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" } } }, @@ -7369,9 +7253,9 @@ } }, "enhanced-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz", - "integrity": "sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -7582,9 +7466,9 @@ } }, "escalade": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.1.tgz", - "integrity": "sha512-DR6NO3h9niOT+MZs7bjxlj2a1k+POu5RN8CLTPX2+i78bRi9eLe7+0zXgUHMnGXWybYcL61E9hGhPKqedy8tQA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", + "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==", "dev": true }, "escape-html": { @@ -8568,6 +8452,13 @@ "tslib": "^1.9.0" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "filesize": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.0.1.tgz", @@ -9076,13 +8967,27 @@ "dev": true }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "dev": true, "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, "harmony-reflect": { @@ -9212,7 +9117,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -9247,7 +9151,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -9445,12 +9348,6 @@ } } }, - "http-parser-js": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", - "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==", - "dev": true - }, "http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -9670,8 +9567,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", @@ -9803,12 +9699,6 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -9966,9 +9856,9 @@ "dev": true }, "is-docker": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", - "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", "dev": true }, "is-extendable": { @@ -10510,7 +10400,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } } } }, @@ -11034,15 +10928,6 @@ "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", "dev": true }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -11354,15 +11239,6 @@ "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", "dev": true }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -11401,17 +11277,6 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memoize-one": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", @@ -11593,14 +11458,12 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", @@ -11644,9 +11507,9 @@ } }, "minipass-pipeline": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz", - "integrity": "sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, "requires": { "minipass": "^3.0.0" @@ -11774,6 +11637,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "dev": true, + "optional": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -11835,9 +11705,9 @@ "dev": true }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, "next-tick": { @@ -12017,9 +11887,9 @@ } }, "node-releases": { - "version": "1.1.58", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.58.tgz", - "integrity": "sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==", + "version": "1.1.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz", + "integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==", "dev": true }, "normalize-package-data": { @@ -12093,12 +11963,6 @@ "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", "dev": true }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -12296,9 +12160,9 @@ } }, "open": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz", - "integrity": "sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.1.0.tgz", + "integrity": "sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA==", "dev": true, "requires": { "is-docker": "^2.0.0", @@ -12621,29 +12485,12 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, "p-each-series": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", @@ -12659,12 +12506,6 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -13140,14 +12981,14 @@ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" }, "portfinder": { - "version": "1.0.26", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz", - "integrity": "sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", "dev": true, "requires": { "async": "^2.6.2", "debug": "^3.1.1", - "mkdirp": "^0.5.1" + "mkdirp": "^0.5.5" }, "dependencies": { "debug": { @@ -13221,9 +13062,9 @@ } }, "postcss-calc": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.2.tgz", - "integrity": "sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.3.tgz", + "integrity": "sha512-IB/EAEmZhIMEIhG7Ov4x+l47UaXOS1n2f4FBUk/aKllQhtSCxWhTzn0nJgkqN7fo/jcWySvWTSB6Syk9L+31bA==", "dev": true, "requires": { "postcss": "^7.0.27", @@ -13726,15 +13567,15 @@ } }, "postcss-modules-local-by-default": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", - "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", "dev": true, "requires": { "icss-utils": "^4.1.1", - "postcss": "^7.0.16", + "postcss": "^7.0.32", "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.0" + "postcss-value-parser": "^4.1.0" } }, "postcss-modules-scope": { @@ -14244,12 +14085,6 @@ "react-is": "^16.8.4" } }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -14562,9 +14397,9 @@ "dev": true }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "dev": true } } @@ -14877,9 +14712,9 @@ } }, "react-scripts": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.1.tgz", - "integrity": "sha512-JpTdi/0Sfd31mZA6Ukx+lq5j1JoKItX7qqEK4OiACjVQletM1P38g49d9/D0yTxp9FrSF+xpJFStkGgKEIRjlQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.3.tgz", + "integrity": "sha512-oSnoWmii/iKdeQiwaO6map1lUaZLmG0xIUyb/HwCVFLT7gNbj8JZ9RmpvMCZ4fB98ZUMRfNmp/ft8uy/xD1RLA==", "dev": true, "requires": { "@babel/core": "7.9.0", @@ -14928,11 +14763,11 @@ "sass-loader": "8.0.2", "semver": "6.3.0", "style-loader": "0.23.1", - "terser-webpack-plugin": "2.3.5", + "terser-webpack-plugin": "2.3.8", "ts-pnp": "1.1.6", "url-loader": "2.3.0", "webpack": "4.42.0", - "webpack-dev-server": "3.10.3", + "webpack-dev-server": "3.11.0", "webpack-manifest-plugin": "2.2.0", "workbox-webpack-plugin": "4.3.1" }, @@ -15134,13 +14969,12 @@ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regenerator-transform": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.4.tgz", - "integrity": "sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", "dev": true, "requires": { - "@babel/runtime": "^7.8.4", - "private": "^0.1.8" + "@babel/runtime": "^7.8.4" } }, "regex-not": { @@ -15332,21 +15166,21 @@ } }, "request-promise-core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", - "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", "dev": true, "requires": { - "lodash": "^4.17.15" + "lodash": "^4.17.19" } }, "request-promise-native": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", - "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", "dev": true, "requires": { - "request-promise-core": "1.1.3", + "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" } @@ -15814,10 +15648,13 @@ } }, "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } }, "serve-index": { "version": "1.9.1", @@ -16170,13 +16007,14 @@ } }, "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", "dev": true, "requires": { "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" } }, "sockjs-client": { @@ -16962,19 +16800,19 @@ } }, "terser-webpack-plugin": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.5.tgz", - "integrity": "sha512-WlWksUoq+E4+JlJ+h+U+QUzXpcsMSSNXkDy9lBVkSqDn1w23Gg29L/ary9GeJVYCGiNJJX7LnVc4bwL1N3/g1w==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz", + "integrity": "sha512-/fKw3R+hWyHfYx7Bv6oPqmk4HGQcrWLtV3X6ggvPuwPNHSnzvVV51z6OaaCOus4YLjutYGOz3pEpbhe6Up2s1w==", "dev": true, "requires": { "cacache": "^13.0.1", - "find-cache-dir": "^3.2.0", - "jest-worker": "^25.1.0", - "p-limit": "^2.2.2", - "schema-utils": "^2.6.4", - "serialize-javascript": "^2.1.2", + "find-cache-dir": "^3.3.1", + "jest-worker": "^25.4.0", + "p-limit": "^2.3.0", + "schema-utils": "^2.6.6", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", - "terser": "^4.4.3", + "terser": "^4.6.12", "webpack-sources": "^1.4.3" }, "dependencies": { @@ -17675,12 +17513,12 @@ } }, "watchpack": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz", - "integrity": "sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", + "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", "dev": true, "requires": { - "chokidar": "^3.4.0", + "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", "neo-async": "^2.5.0", "watchpack-chokidar2": "^2.0.0" @@ -17729,7 +17567,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -17890,15 +17732,6 @@ "ajv-keywords": "^3.1.0" } }, - "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -17915,16 +17748,16 @@ } }, "terser-webpack-plugin": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz", - "integrity": "sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", "dev": true, "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^3.1.0", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", @@ -17947,9 +17780,9 @@ } }, "webpack-dev-server": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz", - "integrity": "sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -17960,31 +17793,31 @@ "debug": "^4.1.1", "del": "^4.1.1", "express": "^4.17.1", - "html-entities": "^1.2.1", + "html-entities": "^1.3.1", "http-proxy-middleware": "0.19.1", "import-local": "^2.0.0", "internal-ip": "^4.3.0", "ip": "^1.1.5", "is-absolute-url": "^3.0.3", "killable": "^1.0.1", - "loglevel": "^1.6.6", + "loglevel": "^1.6.8", "opn": "^5.5.0", "p-retry": "^3.0.1", - "portfinder": "^1.0.25", + "portfinder": "^1.0.26", "schema-utils": "^1.0.0", "selfsigned": "^1.10.7", "semver": "^6.3.0", "serve-index": "^1.9.1", - "sockjs": "0.3.19", + "sockjs": "0.3.20", "sockjs-client": "1.4.0", - "spdy": "^4.0.1", + "spdy": "^4.0.2", "strip-ansi": "^3.0.1", "supports-color": "^6.1.0", "url": "^0.11.0", "webpack-dev-middleware": "^3.7.2", "webpack-log": "^2.0.0", "ws": "^6.2.1", - "yargs": "12.0.5" + "yargs": "^13.3.2" }, "dependencies": { "ansi-regex": { @@ -18019,34 +17852,6 @@ "upath": "^1.1.1" } }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -18056,27 +17861,16 @@ "ms": "^2.1.1" } }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, "fsevents": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -18126,22 +17920,6 @@ "binary-extensions": "^1.0.0" } }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -18154,30 +17932,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, "readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", @@ -18189,12 +17943,6 @@ "readable-stream": "^2.0.2" } }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -18206,33 +17954,6 @@ "ajv-keywords": "^3.1.0" } }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -18251,38 +17972,6 @@ "has-flag": "^3.0.0" } }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, "ws": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", @@ -18291,36 +17980,6 @@ "requires": { "async-limiter": "~1.0.0" } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -18378,13 +18037,11 @@ } }, "websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", "dev": true, "requires": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, @@ -18404,9 +18061,9 @@ } }, "whatwg-fetch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.1.0.tgz", - "integrity": "sha512-pgmbsVWKpH9GxLXZmtdowDIqtb/rvPyjjQv3z9wLcmgWKFHilKnZD3ldgrOlwJoPGOUluQsRPWd52yVkPfmI1A==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz", + "integrity": "sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==", "dev": true }, "whatwg-mimetype": { diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json index 83818e3a86..2fc0e84108 100644 --- a/awx/ui_next/package.json +++ b/awx/ui_next/package.json @@ -49,7 +49,7 @@ "jest-websocket-mock": "^2.0.2", "mock-socket": "^9.0.3", "prettier": "^1.18.2", - "react-scripts": "^3.4.1" + "react-scripts": "^3.4.3" }, "scripts": { "start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start", From e4095a0c27b846b5870fc40816ca76f6c9783147 Mon Sep 17 00:00:00 2001 From: Taras Dyshkant Date: Fri, 21 Aug 2020 15:35:39 +0300 Subject: [PATCH 086/123] Fix broadcast_websocket_secret length Password lookup parameters must be within the same set of quotes. Otherwise a default value of length is used (20). --- installer/roles/kubernetes/tasks/main.yml | 2 +- installer/roles/local_docker/tasks/main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/installer/roles/kubernetes/tasks/main.yml b/installer/roles/kubernetes/tasks/main.yml index 6a9d9a2deb..9e0d6f4f7d 100644 --- a/installer/roles/kubernetes/tasks/main.yml +++ b/installer/roles/kubernetes/tasks/main.yml @@ -1,7 +1,7 @@ --- - name: Generate broadcast websocket secret set_fact: - broadcast_websocket_secret: "{{ lookup('password', '/dev/null', length=128) }}" + broadcast_websocket_secret: "{{ lookup('password', '/dev/null length=128') }}" run_once: true no_log: true when: broadcast_websocket_secret is not defined diff --git a/installer/roles/local_docker/tasks/main.yml b/installer/roles/local_docker/tasks/main.yml index aab1260a36..e2b793e50e 100644 --- a/installer/roles/local_docker/tasks/main.yml +++ b/installer/roles/local_docker/tasks/main.yml @@ -1,7 +1,7 @@ --- - name: Generate broadcast websocket secret set_fact: - broadcast_websocket_secret: "{{ lookup('password', '/dev/null', length=128) }}" + broadcast_websocket_secret: "{{ lookup('password', '/dev/null length=128') }}" run_once: true no_log: true when: broadcast_websocket_secret is not defined From 2d23748971c155707f19e84b18e9ef4ff2a3c2a4 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Fri, 21 Aug 2020 12:39:28 -0400 Subject: [PATCH 087/123] Add Remote Archive SCM Type feature to CHANGELOG.md Signed-off-by: Philip Douglass --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f79d4f57..3d6a657365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ This is a list of high-level changes for each release of AWX. A full list of com ## 14.1.0 (TBD) - AWX images can now be built on ARM64 - https://github.com/ansible/awx/pull/7607 +- Added the Remote Archive SCM Type to support using immutable artifacts and releases (such as tarballs and zip files) as projects - https://github.com/ansible/awx/issues/7954 - Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 - Added resource import/export support to the official AWX collection - https://github.com/ansible/awx/issues/7329 - Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 From 30ae0f53ec083d05bc82e536688122909ea4adf6 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Fri, 21 Aug 2020 13:09:09 -0400 Subject: [PATCH 088/123] Reject setting scm_branch for Remote Archive projects --- awx/api/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index be6a9d640b..429bf512be 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1336,6 +1336,8 @@ class ProjectOptionsSerializer(BaseSerializer): attrs.pop('local_path', None) if 'local_path' in attrs and attrs['local_path'] not in valid_local_paths: errors['local_path'] = _('This path is already being used by another manual project.') + if attrs.get('scm_branch') and scm_type == 'archive': + errors['scm_branch'] = _('SCM branch cannot be used with archive projects.') if attrs.get('scm_refspec') and scm_type != 'git': errors['scm_refspec'] = _('SCM refspec can only be used with git projects.') From 17a40808b4461220fd176a99279e51dc12b3b4d0 Mon Sep 17 00:00:00 2001 From: nixocio Date: Fri, 21 Aug 2020 14:25:30 -0400 Subject: [PATCH 089/123] Update disassociate button variant Update disassociate button variant. See: https://github.com/ansible/awx/issues/7041 Also: https://tower-mockups.testing.ansible.com/patternfly/instance-groups/instance-groups-instances/ --- .../src/components/DisassociateButton/DisassociateButton.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx b/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx index cafde49f73..fab8186f67 100644 --- a/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx +++ b/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx @@ -61,7 +61,7 @@ function DisassociateButton({