diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index 4bd3e991f..f8b26d558 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -45,15 +45,15 @@ jobs:
--health-retries 5
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Cache pip
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
# This path is specific to Ubuntu
path: ~/.cache/pip
@@ -73,7 +73,9 @@ jobs:
pytest
- name: Upload Coverage Report
- uses: codecov/codecov-action@v1
+ uses: codecov/codecov-action@v3
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
file: "./backend/coverage.xml"
flags: backend
diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml
new file mode 100644
index 000000000..1e3dd4648
--- /dev/null
+++ b/.github/workflows/containers.yml
@@ -0,0 +1,72 @@
+name: Container release
+
+on:
+ push:
+ branches:
+ - 'main'
+ tags:
+ - 'v*'
+
+ pull_request:
+
+ workflow_dispatch:
+
+permissions: {}
+
+jobs:
+ frontend:
+ name: Build/release frontend container
+ runs-on: ubuntu-latest
+ permissions:
+ packages: write
+ steps:
+ - uses: actions/checkout@v3
+ - uses: docker/setup-buildx-action@v2
+ - uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - uses: docker/metadata-action@v5
+ id: metadata
+ with:
+ images: ghcr.io/esgf2-us/metagrid-frontend
+ tags: |
+ type=semver,pattern={{version}}
+ - uses: docker/build-push-action@v4
+ with:
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ context: frontend/
+ file: frontend/docker/production/react/Dockerfile
+ push: ${{ github.event_name != 'pull_request' }}
+ tags: ${{ steps.metadata.outputs.tags }}
+ labels: ${{ steps.metadata.outputs.labels }}
+ backend:
+ name: Build/release backend container
+ runs-on: ubuntu-latest
+ permissions:
+ packages: write
+ steps:
+ - uses: actions/checkout@v3
+ - uses: docker/setup-buildx-action@v2
+ - uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - uses: docker/metadata-action@v5
+ id: metadata
+ with:
+ images: ghcr.io/esgf2-us/metagrid-backend
+ tags: |
+ type=semver,pattern={{version}}
+ - uses: docker/build-push-action@v4
+ with:
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ context: backend/
+ file: backend/docker/production/django/Dockerfile
+ push: ${{ github.event_name != 'pull_request' }}
+ tags: ${{ steps.metadata.outputs.tags }}
+ labels: ${{ steps.metadata.outputs.labels }}
diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml
index 7510412b7..ab4c0df9b 100644
--- a/.github/workflows/frontend.yml
+++ b/.github/workflows/frontend.yml
@@ -21,15 +21,15 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Use Node.js 21.x
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v4
with:
node-version: "21.x"
- name: Cache node modules
- uses: actions/cache@v2
+ uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -44,11 +44,20 @@ jobs:
run: yarn install --frozen-lockfile
- name: Run Tests
+ env:
+ RELEASE: dev
+ ENV_FILE: .envs/.react
+ HTML_PATH: public
run: |
+ # Replaces react-scripts substitution during build for index.html and generates runtime_env.js
+ docker/production/react/entrypoint
+
yarn test:coverage
- name: Upload Coverage Report
- uses: codecov/codecov-action@v1
+ uses: codecov/codecov-action@v3
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
file: "./frontend/coverage/coverage-final.json"
flags: frontend
diff --git a/.gitignore b/.gitignore
index 7ac10759a..4b7a1a572 100644
--- a/.gitignore
+++ b/.gitignore
@@ -189,3 +189,4 @@ tags
.env
.envs/*
!.envs/.local/
+frontend/public/runtime_env.js
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f08b006a8..2ad2432c3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,7 +6,7 @@
"editor.rulers": [72, 79, 120],
"editor.wordWrap": "wordWrapColumn",
"editor.wordWrapColumn": 120,
- "editor.defaultFormatter": "ms-python.python"
+ "editor.defaultFormatter": "ms-python.black-formatter"
},
// Update as needed
"python.pythonPath": "backend/venv/bin/python",
diff --git a/backend/README.md b/backend/README.md
index 86d6ac34a..451dbd083 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -1 +1,31 @@
# MetaGrid API Back-end
+
+# Create a new python virtual environment for testing
+python3 -m venv backend/venv
+
+# Activate virtual env
+source backend/venv/bin/activate
+
+# Deactivate virtual env
+deactivate
+
+# Use Black formatting, cd to backend
+black .
+
+# Run Flake8 to linter in backend, cd to backend
+Flake8 .
+
+# Update virtual environment with current requirements (after activating it)
+pip install -r requirements/local.txt
+
+# Rebuild container for testing backend
+docker compose -p metagrid_backend_dev build --no-cache
+
+# Run pyTest (May need to rebuild container before tests)
+docker compose -p metagrid_backend_dev run --rm django pytest
+
+# Run manage.py function
+docker compose -p metagrid_backend_dev run --rm django python manage.py
+
+# View backend output in browser (esgf-dev1 example)
+Enter this browser url: https://aims2.llnl.gov/metagrid-backend/api/v1/projects/
\ No newline at end of file
diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py
index a75d00a99..55c177466 100755
--- a/backend/config/settings/base.py
+++ b/backend/config/settings/base.py
@@ -334,11 +334,11 @@
# django-cors-headers
# -------------------------------------------------------------------------------
# https://github.com/adamchainz/django-cors-headers#setup
-CORS_ALLOWED_ORIGINS = [
- "http://localhost:3000",
-]
+CORS_ALLOWED_ORIGINS = env.list(
+ "CORS_ALLOWED_ORIGINS", default=["http://localhost:5000"]
+)
CORS_ALLOW_CREDENTIALS = True
-CORS_ORIGIN_WHITELIST = env.list("CORS_ORIGIN_WHITELIST")
+CORS_ORIGIN_WHITELIST = env.list("CORS_ORIGIN_WHITELIST", default=[])
SEARCH_URL = env("REACT_APP_SEARCH_URL")
WGET_URL = env("REACT_APP_WGET_API_URL")
diff --git a/backend/metagrid/api_globus/tests/test_views.py b/backend/metagrid/api_globus/tests/test_views.py
index fa59aac27..2ed594b44 100644
--- a/backend/metagrid/api_globus/tests/test_views.py
+++ b/backend/metagrid/api_globus/tests/test_views.py
@@ -7,11 +7,16 @@
class TestGlobusViewSet(APITestCase):
def test_truncate(self):
- lst = [{"url": ["test_url:globus_value|Globus"]}]
+ lst = [
+ {
+ "url": ["test_url:globus_value|Globus"],
+ "data_node": "aims3.llnl.gov",
+ }
+ ]
results = []
for value in truncate_urls(lst):
results.append(value)
- assert results == ["globus_value"]
+ assert results == [("globus_value", "aims3.llnl.gov")]
def test_split_value(self):
result = split_value(1)
@@ -36,15 +41,13 @@ def test_split_value(self):
def test_get_access_token(self):
url = reverse("globus_auth")
+
getdata = {}
response = self.client.post(url, getdata)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_globus_transfer(self):
url = reverse("globus_transfer")
- getdata = {}
- response = self.client.get(url, getdata)
- assert response.status_code == status.HTTP_400_BAD_REQUEST
postdata = {
"access_token": "",
@@ -52,5 +55,5 @@ def test_globus_transfer(self):
"endpointId": "test",
"path": "bad/path",
}
- response = self.client.post(url, postdata)
+ response = self.client.post(url, postdata, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST
diff --git a/backend/metagrid/api_globus/views.py b/backend/metagrid/api_globus/views.py
index 2a27ff605..4da3afcbe 100644
--- a/backend/metagrid/api_globus/views.py
+++ b/backend/metagrid/api_globus/views.py
@@ -3,17 +3,29 @@
import re
import urllib.parse
import urllib.request
+import uuid
from datetime import datetime, timedelta
from django.conf import settings
-from django.http import HttpResponse, HttpResponseBadRequest
+from django.http import (
+ HttpResponse,
+ HttpResponseBadRequest,
+ HttpResponseServerError,
+)
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from globus_sdk import AccessTokenAuthorizer, TransferClient, TransferData
from metagrid.api_proxy.views import do_request
-TRANSFER_TEMP_ENDPOINT = "1889ea03-25ad-4f9f-8110-1ce8833a9d7e"
+ENDPOINT_MAP = {
+ "415a6320-e49c-11e5-9798-22000b9da45e": "1889ea03-25ad-4f9f-8110-1ce8833a9d7e"
+}
+
+DATANODE_MAP = {"esgf-node.ornl.gov": "dea29ae8-bb92-4c63-bdbc-260522c92fe8"}
+
+TEST_SHARDS_MAP = {"esgf-fedtest.llnl.gov": "esgf-node.llnl.gov"}
+
# reserved query keywords
OFFSET = "offset"
@@ -47,10 +59,11 @@
def truncate_urls(lst):
for x in lst:
+ z = x["data_node"]
for y in x["url"]:
parts = y.split("|")
if parts[1] == "Globus":
- yield (parts[0].split(":")[1])
+ yield (parts[0].split(":")[1], z)
def split_value(value):
@@ -113,6 +126,7 @@ def split_value(value):
return _values
+# flake8: noqa
def get_files(url_params): # pragma: no cover
solr_url = getattr(
settings,
@@ -124,8 +138,20 @@ def get_files(url_params): # pragma: no cover
file_offset = 0
use_distrib = True
- # xml_shards = get_solr_shards_from_xml()
- xml_shards = ["esgf-node.llnl.gov:80/solr"]
+ port = "80"
+
+ try:
+ res = urllib.parse.urlparse(query_url)
+ hostname = (
+ res.hostname
+ ) # TODO need to populate the shards based on the Solr URL
+ if res.port:
+ port = res.port
+ except RuntimeError as e:
+ return HttpResponseServerError(f"Malformed URL in search results {e}")
+ if hostname in TEST_SHARDS_MAP:
+ hostname = TEST_SHARDS_MAP[hostname]
+ xml_shards = [f"{hostname}:{port}/solr"]
querys = []
file_query = ["type:File"]
@@ -139,21 +165,11 @@ def get_files(url_params): # pragma: no cover
if param[-1] == "!":
param = param[:-1]
- # Create list of parameters to be saved in the script
- url_params_list = []
- for param, value_list in url_params.lists():
- for v in value_list:
- url_params_list.append("{}={}".format(param, v))
-
# Set a Solr query string
if url_params.get(QUERY):
_query = url_params.pop(QUERY)[0]
querys.append(_query)
- # Set range for timestamps to query
-
- # Set datetime start and stop
-
if len(querys) == 0:
querys.append("*:*")
query_string = " AND ".join(querys)
@@ -167,11 +183,9 @@ def get_files(url_params): # pragma: no cover
# Get directory structure for downloaded files
# Collect remaining constraints
- for param, value_list in url_params.lists():
- # Check for negative constraints
- if param[-1] == "!":
- param = "-" + param[:-1]
+ for param in url_params:
+ value_list = url_params[param]
# Split values separated by commas
# but don't split at commas inside parentheses
# (i.e. cases such as "CESM1(CAM5.1,FV2)")
@@ -209,7 +223,7 @@ def get_files(url_params): # pragma: no cover
# then use the allowed projects as the project query
# Get facets for the file name, URL, checksum
- file_attributes = ["url"]
+ file_attributes = ["url", "data_node"]
# Solr query parameters
query_params = dict(
@@ -277,39 +291,47 @@ def submit_transfer(
transfer_task.add_item(source_file, target_file)
# submit the transfer request
+ response = {}
try:
data = transfer_client.submit_transfer(transfer_task)
- task_id = data["task_id"]
- print("Submitted transfer task with id: %s" % task_id)
+ response["success"] = True
+ response["task_id"] = data["task_id"]
+ print("Submitted transfer task with id: %s" % response["task_id"])
except Exception as e:
- print("Could not submit the transfer. Error: %s" % str(e))
- task_id = "Error"
- return task_id
+ response["success"] = False
+ error_uuid = uuid.uuid4()
+ print(f"Could not submit the transfer. Error: {e} - ID {error_uuid}")
+ response["error_uuid"] = error_uuid
+ return response
@require_http_methods(["GET", "POST"])
@csrf_exempt
def do_globus_transfer(request): # pragma: no cover
+ print(request.body)
+
if request.method == "POST":
- url_params = request.POST.copy()
+ url_params = json.loads(request.body)
elif request.method == "GET":
url_params = request.GET.copy()
else: # pragma: no cover
return HttpResponseBadRequest("Request method must be POST or GET.")
+ print(url_params)
+
# check for bearer token and set if present
access_token = None
refresh_token = None
target_endpoint = None
target_folder = None
if A_TOKEN in url_params:
- access_token = url_params.pop(A_TOKEN)[0]
+ access_token = url_params.pop(A_TOKEN)
if R_TOKEN in url_params:
- refresh_token = url_params.pop(R_TOKEN)[0]
+ refresh_token = url_params.pop(R_TOKEN)
if "endpointId" in url_params:
- target_endpoint = url_params.pop("endpointId")[0]
+ target_endpoint = url_params.pop("endpointId")
if "path" in url_params:
- target_folder = url_params.pop("path")[0]
+ target_folder = url_params.pop("path")
if (
(not target_endpoint)
@@ -324,34 +346,44 @@ def do_globus_transfer(request): # pragma: no cover
task_ids = [] # list of submitted task ids
- urls = []
endpoint_id = ""
download_map = {}
- for file in files_list:
+ for file, data_node in files_list:
parts = file.split("/")
- if endpoint_id == "":
+ if data_node in DATANODE_MAP:
+ endpoint_id = DATANODE_MAP[data_node]
+ print("Data node mapping.....")
+ else:
endpoint_id = parts[0]
- urls.append("/" + "/".join(parts[1:]))
- download_map[endpoint_id] = urls
+ if endpoint_id in ENDPOINT_MAP:
+ endpoint_id = ENDPOINT_MAP[endpoint_id]
+ if endpoint_id not in download_map:
+ download_map[endpoint_id] = []
+
+ download_map[endpoint_id].append("/" + "/".join(parts[1:]))
token_authorizer = AccessTokenAuthorizer(access_token)
transfer_client = TransferClient(authorizer=token_authorizer)
+ print()
+ print(" --- DEBUG ---")
+ print(download_map)
+ print()
for source_endpoint, source_files in list(download_map.items()):
# submit transfer request
- task_id = submit_transfer(
+ task_response = submit_transfer(
transfer_client,
- TRANSFER_TEMP_ENDPOINT,
+ source_endpoint,
source_files,
target_endpoint,
target_folder,
)
- if task_id == "Error":
- return HttpResponseBadRequest("Error")
+ if not task_response["success"]:
+ return HttpResponseBadRequest(task_response["error_uuid"])
- task_ids.append(task_id)
+ task_ids.append(task_response["task_id"])
- return HttpResponse(json.dumps({"status": "OK", "taskid": task_id}))
+ return HttpResponse(json.dumps({"status": "OK", "taskid": task_ids}))
@require_http_methods(["POST"])
diff --git a/backend/metagrid/projects/models.py b/backend/metagrid/projects/models.py
index 6948c52e3..f294d2e4a 100644
--- a/backend/metagrid/projects/models.py
+++ b/backend/metagrid/projects/models.py
@@ -81,6 +81,7 @@ def project_param(self) -> Dict[str, str]:
"All (except CMIP6)": {"project!": "CMIP6"},
"input4MIPs": {"activity_id": self.name},
"obs4MIPs": {"activity_id": self.name},
+ "CMIP5": {"project": "CMIP5,TAMIP,EUCLIPSE,LUCID,GeoMIP,PMIP3"},
}
return project_params.get(self.name, {"project": self.name})
diff --git a/frontend/.envs/.react b/frontend/.envs/.react
index c2d185b66..bd3319525 100644
--- a/frontend/.envs/.react
+++ b/frontend/.envs/.react
@@ -1,5 +1,7 @@
# =====================FRONTEND CONFIG====================
+PUBLIC_URL=
+
# Redirect the frontend to home page when old subdirectory is used (optional)
REACT_APP_PREVIOUS_URL=metagrid
diff --git a/frontend/Makefile b/frontend/Makefile
new file mode 100644
index 000000000..14bed30fc
--- /dev/null
+++ b/frontend/Makefile
@@ -0,0 +1,20 @@
+TAG ?= v1.1.0-beta
+IMAGE ?= ghcr.io/esgf2-us/metagrid-frontend:$(TAG)
+
+.PHONY: build
+build:
+ docker build $(ARGS) -t $(IMAGE) -f docker/production/react/Dockerfile .
+
+.PHONY: build-local
+build-local:
+ docker build $(ARGS) -t $(IMAGE) -f docker/local/Dockerfile .
+
+.PHONY: run
+run: ARGS ?= -e RELEASE=production
+run:
+ docker run $(ARGS) -it --rm -p 3000:3000 $(IMAGE)
+
+.PHONY: shell
+run: ARGS ?= -e RELEASE=production
+shell:
+ docker run $(ARGS) -it --rm -p 3000:3000 --entrypoint /bin/sh $(IMAGE)
diff --git a/frontend/docker-compose.yml b/frontend/docker-compose.yml
index 106f17e0d..264c3bf48 100644
--- a/frontend/docker-compose.yml
+++ b/frontend/docker-compose.yml
@@ -7,6 +7,11 @@ services:
dockerfile: ./docker/local/Dockerfile
image: metagrid_local_react
container_name: react
+ environment:
+ - DEBUG=true
+ - RELEASE=dev
+ - HTML_PATH=/app/public
+ - ENV_FILE=/app/.envs/.react
env_file:
- .envs/.react
volumes:
diff --git a/frontend/docker/local/Dockerfile b/frontend/docker/local/Dockerfile
index d1e7bab34..736bc8522 100644
--- a/frontend/docker/local/Dockerfile
+++ b/frontend/docker/local/Dockerfile
@@ -1,5 +1,5 @@
# Pull official base image
-FROM node:slim
+FROM node:latest
# Set working directory
WORKDIR /app
@@ -15,5 +15,16 @@ RUN yarn install
# Add app
COPY . ./
+# Determines which conf to use if app is/is not being served through subdirectory
+COPY ./docker/production/react/entrypoint /entrypoint
+# gettext-base is required for envsubst
+RUN sed -i 's/\r$//g' /entrypoint && \
+ chmod +x /entrypoint && \
+ apt-get update && \
+ apt-get install -y gettext-base && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+ENTRYPOINT ["/entrypoint"]
+
# Start app
CMD ["yarn", "start:local"]
diff --git a/frontend/docker/production/react/Dockerfile b/frontend/docker/production/react/Dockerfile
index 87a3d484f..a3d341f94 100644
--- a/frontend/docker/production/react/Dockerfile
+++ b/frontend/docker/production/react/Dockerfile
@@ -1,5 +1,5 @@
# Pull official base image
-FROM node:slim as build
+FROM node:latest as build
# Set working directory
WORKDIR /app
@@ -10,10 +10,18 @@ ENV PATH /app/node_modules/.bin:$PATH
# Install app dependencies
COPY package.json ./
COPY yarn.lock ./
-RUN yarn install --frozen-lock-file --network-timeout=1000000
+RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
+ yarn install --frozen-lock-file --network-timeout=1000000
# Add app
-COPY . ./
+COPY src ./src
+COPY tsconfig.json ./
+COPY public ./public
+COPY .eslintrc.js ./
+COPY .prettierignore ./
+COPY .prettierrc ./
+# required as a placeholder
+COPY .envs/.react /app/.envs/.prod.env
RUN yarn build:production
# Build production environment
@@ -21,6 +29,7 @@ FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY docker/production/nginx/nginx.conf /nginx.conf
COPY docker/production/nginx/nginx.subdir.conf /nginx.subdir.conf
+COPY .envs/.react /env
# Determines which conf to use if app is/is not being served through subdirectory
COPY ./docker/production/react/entrypoint /entrypoint
diff --git a/frontend/docker/production/react/entrypoint b/frontend/docker/production/react/entrypoint
old mode 100644
new mode 100755
index 4aed86b7f..78cadd496
--- a/frontend/docker/production/react/entrypoint
+++ b/frontend/docker/production/react/entrypoint
@@ -1,13 +1,73 @@
#!/bin/sh
-export PUBLIC_URL
-export PREVIOUS_URL
-
-if [[ -z "${PUBLIC_URL}" ]]
-then
- envsubst '${PREVIOUS_URL}' < /nginx.conf > /etc/nginx/conf.d/default.conf
-else
- envsubst '${PREVIOUS_URL},${PUBLIC_URL}' < /nginx.subdir.conf > /etc/nginx/conf.d/default.conf
+DEBUG="${DEBUG:-false}"
+
+[ "${DEBUG}" == "true" ] && set -x
+
+TMP_PATH=/tmp
+
+export RELEASE="${RELEASE:-production}"
+export ENV_FILE="${ENV_FILE:-/env}"
+export HTML_PATH="${HTML_PATH:-/usr/share/nginx/html}"
+export STATIC_PATH=""
+
+if [ "${RELEASE}" = "production" ]; then
+ export STATIC_PATH="/static/js"
+fi
+
+export STATIC_URL="${PUBLIC_URL}${STATIC_PATH}"
+export RUNTIME_ENV_FILE="${HTML_PATH}${STATIC_PATH}/runtime_env.js"
+
+if [ ! -e "$(dirname ${RUNTIME_ENV_FILE})" ]; then
+ mkdir -p "$(dirname ${RUNTIME_ENV_FILE})"
+fi
+
+# Create $RUNTIME_ENV_FILE from contents of $ENV_FILE
+if [ -e "${ENV_FILE}" ]; then
+ echo "window.ENV = {" > "${RUNTIME_ENV_FILE}"
+
+ while read -r line; do
+ [ -z "$(echo ${line} | grep -vE '^# |^$')" ] && continue
+
+ varname=$(printf '%s\n' "${line}" | cut -d"=" -f1)
+ varvalue=$(printenv "${varname}")
+
+ if [ -z "${varvalue}" ]; then
+ varvalue=$(printf '%s\n' "${line}" | cut -d"=" -f2)
+
+ export "${varname}"="${varvalue}"
+ fi
+
+ echo " ${varname}: \"${varvalue}\"," >> "${RUNTIME_ENV_FILE}"
+ done < ${ENV_FILE}
+
+ echo "};" >> "${RUNTIME_ENV_FILE}"
+fi
+
+if [ ! -e "/index.html" ]; then
+ cp "${HTML_PATH}/index.html" "${TMP_PATH}/index.html"
+fi
+
+# Template nginx configs
+if [ "${RELEASE}" = "production" ] && [ "${SKIP_NGINX_CONF:-false}" != "true" ]; then
+ echo "Writing nginx config"
+
+ if [ -z "${PUBLIC_URL}" ]; then
+ envsubst '${PREVIOUS_URL}' < /nginx.conf > /etc/nginx/conf.d/default.conf
+ export PUBLIC_URL=""
+ else
+ envsubst '${PREVIOUS_URL},${PUBLIC_URL}' < /nginx.subdir.conf > /etc/nginx/conf.d/default.conf
+ fi
+fi
+
+# Fixes react-scripts static path e.g. /static/ -> $PUBLIC_URL/static/
+sed -i"" "s/\"\/static\//\"\$PUBLIC_URL\/static\//g" "${TMP_PATH}/index.html"
+
+envsubst '$STATIC_URL,$PUBLIC_URL,$REACT_APP_GOOGLE_ANALYTICS_TRACKING_ID' < "${TMP_PATH}/index.html" > "${HTML_PATH}/index.html"
+
+if [ "${DEBUG}" == "true" ]; then
+ cat "${HTML_PATH}/index.html"
+ cat "/etc/nginx/conf.d/default.conf"
fi
exec "$@"
diff --git a/frontend/package.json b/frontend/package.json
index da1980f36..9c80bcc69 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "frontend",
- "version": "1.0.10-beta",
+ "version": "1.1.0",
"private": true,
"scripts": {
"build:local": "env-cmd -f .envs/.react react-scripts build",
@@ -90,8 +90,10 @@
"uuid": "8.3.2"
},
"devDependencies": {
- "@babel/core": "7.17.9",
+ "@babel/core": "7.22.5",
+ "@babel/eslint-parser": "7.22.5",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
+ "@babel/preset-env": "7.22.5",
"@testing-library/dom": "9.3.1",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "14.0.0",
@@ -116,7 +118,7 @@
"eslint-plugin-react": "7.33.0",
"eslint-plugin-react-hooks": "4.6.0",
"msw": "0.28.1",
- "postcss": "8.4.21",
+ "postcss": "8.4.31",
"prettier": "2.2.1",
"setimmediate": "1.0.5"
}
diff --git a/frontend/public/changelog/v1.1.0.md b/frontend/public/changelog/v1.1.0.md
new file mode 100644
index 000000000..770135d97
--- /dev/null
+++ b/frontend/public/changelog/v1.1.0.md
@@ -0,0 +1,9 @@
+## Summary
+
+This update includes several improvements, additional features, bug fixes and enhancements.
+
+**Changes**
+
+1. Added ability to select a managed endpoint and obtain required scopes and permissions to successfully perform a transfer.
+2. Added feature to allow users to copy a list of the available facet options when selecting facet filters. The list is copied as text to the user's clipboard and include the result count to match the frontend display.
+3. Updated the search page to no longer use a button to confirm selected project. CMIP6 project will be selected by default.
diff --git a/frontend/public/index.html b/frontend/public/index.html
index f8678e370..14f2c2aef 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -1,21 +1,21 @@
-
+
-
+
-
+
-
+
ESGF MetaGrid
+
-
+
diff --git a/frontend/public/messages/metagrid_messages.md b/frontend/public/messages/metagrid_messages.md
index adc1dc70c..5c2c4bdf5 100644
--- a/frontend/public/messages/metagrid_messages.md
+++ b/frontend/public/messages/metagrid_messages.md
@@ -1,15 +1,11 @@
-# Welcome to the Metagrid Beta test v1.0.10
+# Welcome to the Metagrid Major Release v1.1.0
To view the latest documentation and FAQ, please visit this page:
[https://esgf.github.io/esgf-user-support/metagrid.html](https://esgf.github.io/esgf-user-support/metagrid.html)
-# NOTICE
-
-All ESGF services and data hosted at LLNL will go **offline on Friday 23 February starting ~8am PST** for a weekend site maintenance period. We expect to restore services on Monday 27 February. In the meantime please visit our partner sites listed here, or use the Federated Nodes link: https://esgf.github.io/nodes.html
-
## Globus Auth
-We now support logins via Globus Auth at LLNL.
+We now support logins via Globus Auth at LLNL!
## Globus Transfers enabled
diff --git a/frontend/src/api/index.test.ts b/frontend/src/api/index.test.ts
index 759f23369..89e6ffcbb 100644
--- a/frontend/src/api/index.test.ts
+++ b/frontend/src/api/index.test.ts
@@ -679,7 +679,7 @@ describe('test startGlobusTransfer function', () => {
it('catches and throws an error based on HTTP status code', async () => {
server.use(
- rest.get(apiRoutes.globusTransfer.path, (_req, res, ctx) =>
+ rest.post(apiRoutes.globusTransfer.path, (_req, res, ctx) =>
res(ctx.status(404))
)
);
@@ -688,7 +688,7 @@ describe('test startGlobusTransfer function', () => {
startGlobusTransfer('asdfs', 'asdfs', 'endpointTest', 'path', 'id', [
'clt',
])
- ).rejects.toThrow(apiRoutes.globusTransfer.handleErrorMsg(404));
+ ).rejects.toThrow(apiRoutes.globusTransfer.handleErrorMsg(408));
});
});
diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts
index 8e36b1906..9af7ae9b9 100644
--- a/frontend/src/api/index.ts
+++ b/frontend/src/api/index.ts
@@ -8,7 +8,7 @@
import 'setimmediate'; // Added because in Jest 27, setImmediate is not defined, causing test errors
import humps from 'humps';
import queryString from 'query-string';
-import { AxiosResponse } from 'axios';
+import { AxiosResponse, AxiosError } from 'axios';
import axios from '../lib/axios';
import {
RawUserCart,
@@ -37,7 +37,7 @@ export interface ResponseError extends Error {
const getCookie = (name: string): null | string => {
let cookieValue = null;
- if (document.cookie && document.cookie !== '') {
+ if (document && document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i += 1) {
const cookie = cookies[i].trim();
@@ -660,42 +660,35 @@ export const saveSessionValue = async (
* If the API returns a 200, it returns the axios response.
*/
export const startGlobusTransfer = async (
+ transferAccessToken: string,
accessToken: string,
- refreshToken: string,
endpointId: string,
path: string,
ids: string[] | string,
filenameVars?: string[]
): Promise => {
- let url = queryString.stringifyUrl({
- url: apiRoutes.globusTransfer.path,
- query: {
- access_token: accessToken,
- refresh_token: refreshToken,
- endpointId,
- path,
- dataset_id: ids,
- },
- });
- if (filenameVars && filenameVars.length > 0) {
- const filenameVarsParam = queryString.stringify(
- { query: filenameVars },
- {
- arrayFormat: 'comma',
- }
- );
- url += `&${filenameVarsParam}`;
- }
-
return axios
- .get(url)
+ .post(
+ apiRoutes.globusTransfer.path,
+ JSON.stringify({
+ access_token: transferAccessToken,
+ refresh_token: accessToken,
+ endpointId,
+ path,
+ dataset_id: ids,
+ filenameVars,
+ })
+ )
.then((resp) => {
return resp;
})
- .catch((error: ResponseError) => {
- throw new Error(
- errorMsgBasedOnHTTPStatusCode(error, apiRoutes.globusTransfer)
- );
+ .catch((error: AxiosError) => {
+ let message = '';
+ /* istanbul ignore else */
+ if (error.response) {
+ message = error.response.data;
+ }
+ throw new Error(message);
});
};
diff --git a/frontend/src/api/mock/fixtures.ts b/frontend/src/api/mock/fixtures.ts
index 79376adcb..477ef4dac 100644
--- a/frontend/src/api/mock/fixtures.ts
+++ b/frontend/src/api/mock/fixtures.ts
@@ -52,8 +52,8 @@ export const rawProjectFixture = (
export const projectsFixture = (): RawProjects => [
rawProjectFixture(),
- rawProjectFixture({ name: 'test2' }),
- rawProjectFixture({ name: 'test3' }),
+ rawProjectFixture({ name: 'test2', fullName: 'test2' }),
+ rawProjectFixture({ name: 'test3', fullName: 'test3' }),
];
/**
diff --git a/frontend/src/api/mock/server-handlers.ts b/frontend/src/api/mock/server-handlers.ts
index 97aa5d7f1..ebbc25a1e 100644
--- a/frontend/src/api/mock/server-handlers.ts
+++ b/frontend/src/api/mock/server-handlers.ts
@@ -30,7 +30,7 @@ const handlers = [
rest.get(apiRoutes.globusAuth.path, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(userAuthFixture()))
),
- rest.get(apiRoutes.globusTransfer.path, (_req, res, ctx) =>
+ rest.post(apiRoutes.globusTransfer.path, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(globusTransferResponseFixture()))
),
rest.get(apiRoutes.userInfo.path, (_req, res, ctx) =>
diff --git a/frontend/src/api/routes.ts b/frontend/src/api/routes.ts
index 14ba271e4..1278d43bd 100644
--- a/frontend/src/api/routes.ts
+++ b/frontend/src/api/routes.ts
@@ -1,6 +1,6 @@
import { esgfSearchURL, metagridApiURL } from '../env';
-export type HTTPCodeType = 400 | 401 | 403 | 404 | 405 | 'generic';
+export type HTTPCodeType = 400 | 401 | 403 | 404 | 405 | 408 | 'generic';
/**
* Update this function if more API HTTP codes need to be handled.
@@ -16,6 +16,7 @@ export const mapHTTPErrorCodes = (
403: `Your request to the ${service} service was forbidden. Please contact support.`,
404: `The requested resource at the ${service} service was invalid. Please contact support.`,
405: `Could not perform operation at the ${service} service. Please contact support`,
+ 408: '',
// Adds verbosity to network errors that have generic messages.
// For example, the axios default network error message is "Error: Network Error".
// This typically occurs when an API/service is down and unable to be reached.
diff --git a/frontend/src/common/reactJoyrideSteps.test.ts b/frontend/src/common/reactJoyrideSteps.test.ts
index ee91e9528..358123c8e 100644
--- a/frontend/src/common/reactJoyrideSteps.test.ts
+++ b/frontend/src/common/reactJoyrideSteps.test.ts
@@ -1,3 +1,4 @@
+import { mockConfig } from '../test/jestTestFunctions';
import {
getCurrentAppPage,
delay,
@@ -91,8 +92,17 @@ describe('Test reactJoyrideStep util functions', () => {
cartEmpty.className = 'ant-tabs-tabpane-active ant-empty-description';
document.body.appendChild(root);
- const mainTour = createMainPageTour();
- expect(mainTour).toBeTruthy();
+ // Test main tour that has no globus nodes
+ mockConfig.globusEnabledNodes = [];
+
+ const mainTourNoGlobus = createMainPageTour();
+ expect(mainTourNoGlobus).toBeTruthy();
+
+ // Test main tour that includes globus options
+ mockConfig.globusEnabledNodes = ['node1', 'node2', 'node3'];
+
+ const mainTourWithGlobus = createMainPageTour();
+ expect(mainTourWithGlobus).toBeTruthy();
const cartTour = createCartItemsTour(() => {});
expect(cartTour).toBeTruthy();
const searchTour = createSearchCardTour(() => {});
diff --git a/frontend/src/common/reactJoyrideSteps.ts b/frontend/src/common/reactJoyrideSteps.ts
index 7aeb9946d..070094c34 100644
--- a/frontend/src/common/reactJoyrideSteps.ts
+++ b/frontend/src/common/reactJoyrideSteps.ts
@@ -408,37 +408,6 @@ export const createMainPageTour = (): JoyrideTour => {
'right'
);
- /* istanbul ignore if */
- if (mainTableEmpty()) {
- tour
- .addNextStep(
- leftSidebarTargets.projectSelectLeftSideBtn.selector(),
- 'Then you click this button to load the results for the project you selected...',
- 'right',
- () => {
- clickFirstElement(
- leftSidebarTargets.projectSelectLeftSideBtn.selector()
- );
- }
- )
- .addNextStep(
- leftSidebarTargets.projectSelectLeftSideBtn.selector(),
- "NOTE: The search results may take a few seconds to load... Click 'next' to continue.",
- 'right',
- async () => {
- if (mainTableEmpty()) {
- await delay(1000);
- }
- }
- );
- } else {
- tour.addNextStep(
- leftSidebarTargets.projectSelectLeftSideBtn.selector(),
- 'Then you click this button to load results for the project you selected.',
- 'right'
- );
- }
-
tour.addNextStep(
leftSidebarTargets.projectWebsiteBtn.selector(),
'Once a project is selected, if you wish, you can go view the project website by clicking this button.',
diff --git a/frontend/src/common/utils.test.ts b/frontend/src/common/utils.test.tsx
similarity index 85%
rename from frontend/src/common/utils.test.ts
rename to frontend/src/common/utils.test.tsx
index a1a33aafc..735c722f3 100644
--- a/frontend/src/common/utils.test.ts
+++ b/frontend/src/common/utils.test.tsx
@@ -1,3 +1,5 @@
+import { render } from '@testing-library/react';
+import React from 'react';
import { rawProjectFixture } from '../api/mock/fixtures';
import { UserSearchQueries, UserSearchQuery } from '../components/Cart/types';
import {
@@ -346,34 +348,80 @@ describe('Test unsavedLocal searches', () => {
});
describe('Test show notices function', () => {
+ // Creating a test component to render the messages and verify they're rendered
+ type Props = { testFunc: () => void };
+ const TestComponent: React.FC> = ({
+ testFunc,
+ }) => {
+ React.useEffect(() => {
+ testFunc();
+ }, []);
+ return
;
+ };
+
it('Shows a success message', () => {
- showNotice('Test notification successful', {
- duration: 5,
- type: 'success',
- });
+ const notice = (): void => {
+ showNotice('Test notification successful', {
+ duration: 5,
+ type: 'success',
+ });
+ };
+
+ const { getByText } = render( );
+ expect(getByText('Test notification successful')).toBeTruthy();
});
+
it('Shows a warning message', () => {
- showNotice('Test warning notification', {
- duration: 5,
- type: 'warning',
- });
+ const notice = (): void => {
+ showNotice('Test warning notification', {
+ duration: 5,
+ type: 'warning',
+ });
+ };
+
+ const { getByText } = render( );
+ expect(getByText('Test warning notification')).toBeTruthy();
});
+
it('Shows a error message', () => {
- showNotice('Test error notification', {
- duration: 5,
- type: 'error',
- });
+ const notice = (): void => {
+ showNotice('Test error notification', {
+ duration: 5,
+ type: 'error',
+ });
+ };
+
+ const { getByText } = render( );
+ expect(getByText('Test error notification')).toBeTruthy();
});
- it('Shows a success message', () => {
- showNotice('Test info notification', {
- duration: 5,
- type: 'info',
- });
+
+ it('Shows an info message', () => {
+ const notice = (): void => {
+ showNotice('Test info notification', {
+ duration: 5,
+ type: 'info',
+ });
+ };
+
+ const { getByText } = render( );
+ expect(getByText('Test info notification')).toBeTruthy();
});
+
it('Shows a default message', () => {
- showNotice('Test info notification');
+ const notice = (): void => {
+ showNotice('Test default notification');
+ };
+
+ const { getByText } = render( );
+ expect(getByText('Test default notification')).toBeTruthy();
});
+
it('Shows a error notification', () => {
- showError('');
+ const notice = (): void => {
+ showError('');
+ };
+
+ const { getByText } = render( );
+ expect(getByText('An unknown error has occurred.')).toBeTruthy();
});
});
diff --git a/frontend/src/common/utils.ts b/frontend/src/common/utils.ts
index f674d02d3..c788fab04 100644
--- a/frontend/src/common/utils.ts
+++ b/frontend/src/common/utils.ts
@@ -35,21 +35,23 @@ export async function showNotice(
case 'success':
await message.success(msgConfig);
/* istanbul ignore next */
- break;
+ return;
case 'warning':
await message.warning(msgConfig);
/* istanbul ignore next */
- break;
+ return;
case 'error':
await message.error(msgConfig);
/* istanbul ignore next */
- break;
+ return;
case 'info':
await message.info(msgConfig);
/* istanbul ignore next */
- break;
+ return;
default:
- await message.success(msgConfig);
+ await message.info(msgConfig);
+ /* istanbul ignore next */
+ break;
}
}
diff --git a/frontend/src/components/App/App.test.tsx b/frontend/src/components/App/App.test.tsx
index 3914b9af3..7c0c651cb 100644
--- a/frontend/src/components/App/App.test.tsx
+++ b/frontend/src/components/App/App.test.tsx
@@ -114,32 +114,10 @@ it('handles setting filename searches and duplicates', async () => {
const renderedApp = customRenderKeycloak( );
const { getByTestId, getByText } = renderedApp;
- // Select a project for the test
-
// Check applicable components render
const leftSearchColumn = await waitFor(() => getByTestId('search-facets'));
expect(leftSearchColumn).toBeTruthy();
- // Wait for project form to render
- const projectForm = await waitFor(() => getByTestId('project-form'));
- expect(projectForm).toBeTruthy();
-
- // Check project select form exists and mouseDown to expand list of options to expand options
- const projectFormSelect = within(projectForm).getByRole('combobox');
- expect(projectFormSelect).toBeTruthy();
- fireEvent.mouseDown(projectFormSelect);
-
- // Select a project option
- const projectOption = getByTestId('project_1');
- expect(projectOption).toBeTruthy();
- await user.click(projectOption);
-
- // Submit the form
- const submitBtn = within(projectForm).getByRole('img', {
- name: 'select',
- });
- fireEvent.submit(submitBtn);
-
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
await waitFor(() => getByTestId('facets-form'));
@@ -183,26 +161,12 @@ it('handles setting filename searches and duplicates', async () => {
});
it('handles setting and removing text input filters and clearing all search filters', async () => {
- const { getByPlaceholderText, getByTestId, getByText } = customRenderKeycloak(
-
- );
+ const renderedApp = customRenderKeycloak( );
- // Check applicable components render
- const leftMenuComponent = await waitFor(() => getByTestId('left-menu'));
- expect(leftMenuComponent).toBeTruthy();
+ const { getByTestId, getByText } = renderedApp;
// Change value for free-text input
- const freeTextInput = await waitFor(() =>
- getByPlaceholderText('Search for a keyword')
- );
- expect(freeTextInput).toBeTruthy();
- fireEvent.change(freeTextInput, { target: { value: 'foo' } });
-
- // Submit the form
- const submitBtn = within(leftMenuComponent).getByRole('img', {
- name: 'search',
- });
- fireEvent.submit(submitBtn);
+ await submitKeywordSearch(renderedApp, 'foo', user);
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
@@ -217,8 +181,7 @@ it('handles setting and removing text input filters and clearing all search filt
await user.click(clearAllTag);
// Change value for free-text input and submit again
- fireEvent.change(freeTextInput, { target: { value: 'baz' } });
- fireEvent.submit(submitBtn);
+ await submitKeywordSearch(renderedApp, 'baz', user);
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
@@ -239,28 +202,6 @@ it('handles applying general facets', async () => {
);
- // Check applicable components render
-
- // Wait for project form to render
- const projectForm = await waitFor(() => getByTestId('project-form'));
- expect(projectForm).toBeTruthy();
-
- // Check project select form exists and mouseDown to expand list of options to expand options
- const projectFormSelect = within(projectForm).getByRole('combobox');
- expect(projectFormSelect).toBeTruthy();
- fireEvent.mouseDown(projectFormSelect);
-
- // Select the second project option
- const projectOption = getByTestId('project_1');
- expect(projectOption).toBeTruthy();
- await user.click(projectOption);
-
- // Submit the form
- const submitBtn = within(projectForm).getByRole('img', {
- name: 'select',
- });
- fireEvent.submit(submitBtn);
-
// Wait for facets forms to rerender
const facetsForm = await waitFor(() => getByTestId('facets-form'));
expect(facetsForm).toBeTruthy();
@@ -271,7 +212,6 @@ it('handles applying general facets', async () => {
});
await user.click(additionalPropertiesPanel);
- // Change result type
// Check facet select form exists and mouseDown to expand list of options
const resultTypeSelect = getByTestId('result-type-form-select');
expect(resultTypeSelect).toBeTruthy();
@@ -295,28 +235,6 @@ it('handles applying and removing project facets', async () => {
const facetsComponent = await waitFor(() => getByTestId('search-facets'));
expect(facetsComponent).toBeTruthy();
- // Wait for project form to render
- const projectForm = await waitFor(() => getByTestId('project-form'));
- expect(projectForm).toBeTruthy();
-
- // Check project select form exists and mouseDown to expand list of options to
- // expand options
- const projectFormSelect = within(projectForm).getByRole('combobox');
-
- expect(projectFormSelect).toBeTruthy();
- fireEvent.mouseDown(projectFormSelect);
-
- // Select the second project option
- const projectOption = getByTestId('project_1');
- expect(projectOption).toBeTruthy();
- await user.click(projectOption);
-
- // Submit the form
- const submitBtn = within(facetsComponent).getByRole('img', {
- name: 'select',
- });
- fireEvent.submit(submitBtn);
-
// Wait for project form to render
const facetsForm = await waitFor(() => getByTestId('facets-form'));
expect(facetsForm).toBeTruthy();
@@ -387,99 +305,35 @@ it('handles applying and removing project facets', async () => {
await waitFor(() => getByTestId('search'));
});
-it('handles project changes and clearing filters when the active project !== selected project', async () => {
- const { getByTestId, getByText } = customRenderKeycloak(
+it('fetches the data node status every defined interval', async () => {
+ jest.useFakeTimers();
+
+ const { getByTestId } = customRenderKeycloak(
);
+ act(() => {
+ jest.advanceTimersByTime(295000);
+ jest.useRealTimers();
+ });
+
// Check applicable components render
const facetsComponent = await waitFor(() => getByTestId('search-facets'));
expect(facetsComponent).toBeTruthy();
-
- // Wait for project form to render
- const projectForm = await waitFor(() => getByTestId('project-form'));
- expect(projectForm).toBeTruthy();
-
- // Check project select form exists and mouseDown to expand list of options
- const projectFormSelect = within(projectForm).getByRole('combobox');
-
- expect(projectFormSelect).toBeTruthy();
- fireEvent.mouseDown(projectFormSelect);
-
- // Select the second project option
- const projectOption = await waitFor(() => getByTestId('project_1'));
- expect(projectOption).toBeInTheDocument();
- await user.click(projectOption);
-
- // Check facets component re-renders
- const facetsComponent2 = await waitFor(() => getByTestId('search-facets'));
- expect(facetsComponent).toBeTruthy();
-
- // Submit the form
- const submitBtn = within(facetsComponent2).getByRole('img', {
- name: 'select',
- });
-
- fireEvent.submit(submitBtn);
-
- // Wait for components to rerender
- await waitFor(() => getByTestId('search-facets'));
-
- // Check project select form exists again and mouseDown to expand list of options
- const projectFormSelect2 = within(projectForm).getByRole('combobox');
-
- fireEvent.mouseDown(projectFormSelect2);
-
- // Select the first project option
- const firstOption = await waitFor(() => getByText('test1'));
- expect(firstOption).toBeInTheDocument();
- await user.click(firstOption);
-
- // Submit the form
- fireEvent.submit(submitBtn);
-
- // Wait for components to rerender
- await waitFor(() => getByTestId('search-facets'));
});
-it('fetches the data node status every defined interval', () => {
- jest.useFakeTimers();
-
- customRenderKeycloak( );
-
- act(() => {
- jest.advanceTimersByTime(295000);
- });
- jest.useRealTimers();
-});
describe('User cart', () => {
it('handles authenticated user adding and removing items from cart', async () => {
- const {
- getByRole,
- getByTestId,
- getByText,
- getByPlaceholderText,
- } = customRenderKeycloak( , {
- token: 'token',
- });
-
- // Check applicable components render
- const leftMenuComponent = await waitFor(() => getByTestId('left-menu'));
- expect(leftMenuComponent).toBeTruthy();
-
- // Change value for free-text input
- const input = 'foo';
- const freeTextInput = await waitFor(() =>
- getByPlaceholderText('Search for a keyword')
+ const renderedApp = customRenderKeycloak(
+ ,
+ {
+ token: 'token',
+ }
);
- expect(freeTextInput).toBeTruthy();
- fireEvent.change(freeTextInput, { target: { value: input } });
+ const { getByRole, getByTestId, getByText } = renderedApp;
- // Submit the form
- const submitBtn = within(leftMenuComponent).getByRole('img', {
- name: 'search',
- });
- fireEvent.submit(submitBtn);
+ // Change value for free-text input
+ await submitKeywordSearch(renderedApp, 'foo', user);
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
@@ -516,32 +370,16 @@ describe('User cart', () => {
});
it("displays authenticated user's number of files in the cart summary and handles clearing the cart", async () => {
- const {
- getByRole,
- getByTestId,
- getByText,
- getByPlaceholderText,
- } = customRenderKeycloak( , {
- token: 'token',
- });
-
- // Check applicable components render
- const leftMenuComponent = await waitFor(() => getByTestId('left-menu'));
- expect(leftMenuComponent).toBeTruthy();
-
- // Change value for free-text input
- const input = 'foo';
- const freeTextInput = await waitFor(() =>
- getByPlaceholderText('Search for a keyword')
+ const renderedApp = customRenderKeycloak(
+ ,
+ {
+ token: 'token',
+ }
);
- expect(freeTextInput).toBeTruthy();
- await user.type(freeTextInput, input);
+ const { getByRole, getByTestId, getByText, findByText } = renderedApp;
- // Submit the form
- const submitBtn = within(leftMenuComponent).getByRole('img', {
- name: 'search',
- });
- await user.click(submitBtn);
+ // Change value for free-text input
+ await submitKeywordSearch(renderedApp, 'foo', user);
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
@@ -574,7 +412,10 @@ describe('User cart', () => {
name: 'shopping-cart',
});
expect(cartLink).toBeTruthy();
- await user.click(cartLink);
+
+ await act(async () => {
+ await user.click(cartLink);
+ });
// Check number of files and datasets are correctly displayed
const cart = await waitFor(() => getByTestId('cart'));
@@ -615,35 +456,22 @@ describe('User cart', () => {
expect(numFilesText.textContent).toEqual('Number of Files: 0');
// Check empty alert renders
- const emptyAlert = getByText('Your cart is empty');
+ const emptyAlert = await findByText('Your cart is empty');
expect(emptyAlert).toBeTruthy();
});
it('handles anonymous user adding and removing items from cart', async () => {
// Render component as anonymous
- const {
- getByRole,
- getByTestId,
- getByPlaceholderText,
- } = customRenderKeycloak( , {}, true);
+ const renderedApp = customRenderKeycloak(
+ ,
+ {},
+ true
+ );
- // Check applicable components render
- const leftMenuComponent = await waitFor(() => getByTestId('left-menu'));
- expect(leftMenuComponent).toBeTruthy();
+ const { getByRole, getByTestId } = renderedApp;
// Change value for free-text input
- const input = 'foo';
- const freeTextInput = await waitFor(() =>
- getByPlaceholderText('Search for a keyword')
- );
- expect(freeTextInput).toBeTruthy();
- fireEvent.change(freeTextInput, { target: { value: input } });
-
- // Submit the form
- const submitBtn = within(leftMenuComponent).getByRole('img', {
- name: 'search',
- });
- fireEvent.submit(submitBtn);
+ await submitKeywordSearch(renderedApp, 'foo', user);
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
@@ -669,12 +497,11 @@ describe('User cart', () => {
it('displays anonymous user"s number of files in the cart summary and handles clearing the cart', async () => {
// Render component as anonymous
- const {
- getByRole,
- getByTestId,
- getByText,
- getByPlaceholderText,
- } = customRenderKeycloak( , {}, true);
+ const { getByRole, getByTestId, getByText } = customRenderKeycloak(
+ ,
+ {},
+ true
+ );
// Check applicable components render
const leftMenuComponent = await waitFor(() => getByTestId('left-menu'));
@@ -682,20 +509,6 @@ describe('User cart', () => {
const rightMenuComponent = await waitFor(() => getByTestId('right-menu'));
expect(leftMenuComponent).toBeTruthy();
- // Change value for free-text input
- const input = 'foo';
- const freeTextInput = await waitFor(() =>
- getByPlaceholderText('Search for a keyword')
- );
- expect(freeTextInput).toBeTruthy();
- fireEvent.change(freeTextInput, { target: { value: input } });
-
- // Submit the form
- const submitBtn = within(leftMenuComponent).getByRole('img', {
- name: 'search',
- });
- fireEvent.submit(submitBtn);
-
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
@@ -795,34 +608,20 @@ describe('User cart', () => {
describe('User search library', () => {
it('handles authenticated user saving and applying searches', async () => {
- const {
- getByTestId,
- getByPlaceholderText,
- getByRole,
- } = customRenderKeycloak( , {
- token: 'token',
- });
+ const renderedApp = customRenderKeycloak(
+ ,
+ {
+ token: 'token',
+ }
+ );
+ // Change value for free-text input
+ await submitKeywordSearch(renderedApp, 'foo', user);
+ const { getByTestId, getByRole } = renderedApp;
// Check applicable components render
- const leftMenuComponent = await waitFor(() => getByTestId('left-menu'));
- expect(leftMenuComponent).toBeTruthy();
const rightMenuComponent = await waitFor(() => getByTestId('right-menu'));
expect(rightMenuComponent).toBeTruthy();
- // Change value for free-text input
- const input = 'foo';
- const freeTextInput = await waitFor(() =>
- getByPlaceholderText('Search for a keyword')
- );
- expect(freeTextInput).toBeTruthy();
- fireEvent.change(freeTextInput, { target: { value: input } });
-
- // Submit the form
- const submitBtn = within(leftMenuComponent).getByRole('img', {
- name: 'search',
- });
- await user.click(submitBtn);
-
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
@@ -902,11 +701,11 @@ describe('User search library', () => {
it('handles anonymous user saving and applying searches', async () => {
// Render component as anonymous
- const {
- getByTestId,
- getByPlaceholderText,
- getByRole,
- } = customRenderKeycloak( , {}, true);
+ const { getByTestId, getByRole } = customRenderKeycloak(
+ ,
+ {},
+ true
+ );
// Check applicable components render
const leftMenuComponent = await waitFor(() => getByTestId('left-menu'));
@@ -914,20 +713,6 @@ describe('User search library', () => {
const rightMenuComponent = await waitFor(() => getByTestId('right-menu'));
expect(rightMenuComponent).toBeTruthy();
- // Change value for free-text input
- const input = 'foo';
- const freeTextInput = await waitFor(() =>
- getByPlaceholderText('Search for a keyword')
- );
- expect(freeTextInput).toBeTruthy();
- fireEvent.change(freeTextInput, { target: { value: input } });
-
- // Submit the form
- const submitBtn = within(leftMenuComponent).getAllByRole('img', {
- name: 'search',
- })[0];
- await user.click(submitBtn);
-
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
@@ -962,32 +747,20 @@ describe('User search library', () => {
it('handles anonymous user removing searches from the search library', async () => {
// Render component as anonymous
- const {
- getByPlaceholderText,
- getByRole,
- getByTestId,
- } = customRenderKeycloak( , {}, true);
+ const renderedApp = customRenderKeycloak(
+ ,
+ {},
+ true
+ );
+ const { getByRole, getByTestId } = renderedApp;
+
+ // Change value for free-text input
+ await submitKeywordSearch(renderedApp, 'foo', user);
// Check applicable components render
- const leftMenuComponent = await waitFor(() => getByTestId('left-menu'));
- expect(leftMenuComponent).toBeTruthy();
const rightMenuComponent = await waitFor(() => getByTestId('right-menu'));
expect(rightMenuComponent).toBeTruthy();
- // Change value for free-text input
- const input = 'foo';
- const freeTextInput = await waitFor(() =>
- getByPlaceholderText('Search for a keyword')
- );
- expect(freeTextInput).toBeTruthy();
- fireEvent.change(freeTextInput, { target: { value: input } });
-
- // Submit the form
- const submitBtn = within(leftMenuComponent).getByRole('img', {
- name: 'search',
- });
- await user.click(submitBtn);
-
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
@@ -1020,31 +793,19 @@ describe('User search library', () => {
});
it('handles anonymous user copying search to clipboard', async () => {
- const {
- getByTestId,
- getByPlaceholderText,
- getByRole,
- } = customRenderKeycloak( , {}, true);
+ const renderedApp = customRenderKeycloak(
+ ,
+ {},
+ true
+ );
+ const { getByTestId, getByRole } = renderedApp;
// Check applicable components render
- const leftMenuComponent = await waitFor(() => getByTestId('left-menu'));
- expect(leftMenuComponent).toBeTruthy();
const rightMenuComponent = await waitFor(() => getByTestId('right-menu'));
expect(rightMenuComponent).toBeTruthy();
// Change value for free-text input
- const input = 'foo';
- const freeTextInput = await waitFor(() =>
- getByPlaceholderText('Search for a keyword')
- );
- expect(freeTextInput).toBeTruthy();
- fireEvent.change(freeTextInput, { target: { value: input } });
-
- // Submit the form
- const submitBtn = within(leftMenuComponent).getByRole('img', {
- name: 'search',
- });
- fireEvent.submit(submitBtn);
+ await submitKeywordSearch(renderedApp, 'foo', user);
// Wait for components to rerender
await waitFor(() => getByTestId('search'));
@@ -1085,13 +846,13 @@ describe('User search library', () => {
});
it('shows a disabled save search button due to failed search results', async () => {
- const {
- getByTestId,
- getByPlaceholderText,
- getByRole,
- } = customRenderKeycloak( , {
- token: 'token',
- });
+ const renderedApp = customRenderKeycloak(
+ ,
+ {
+ token: 'token',
+ }
+ );
+ const { getByRole } = renderedApp;
server.use(
rest.post(apiRoutes.userSearches.path, (_req, res, ctx) =>
@@ -1099,23 +860,8 @@ describe('User search library', () => {
)
);
- // Check applicable components render
- const leftMenuComponent = await waitFor(() => getByTestId('left-menu'));
- expect(leftMenuComponent).toBeTruthy();
-
// Change value for free-text input
- const input = 'foo';
- const freeTextInput = await waitFor(() =>
- getByPlaceholderText('Search for a keyword')
- );
- expect(freeTextInput).toBeTruthy();
- await user.type(freeTextInput, input);
-
- // Submit the form
- const submitBtn = within(leftMenuComponent).getByRole('img', {
- name: 'search',
- });
- await user.click(submitBtn);
+ await submitKeywordSearch(renderedApp, 'foo', user);
// Check Save Search button exists and click it
const saveSearch = await waitFor(() =>
@@ -1141,38 +887,6 @@ describe('User search library', () => {
);
const { getByTestId, getAllByText, getByText } = renderedApp;
- // Select a project for the test
-
- // Check applicable components render
- const leftSearchColumn = await waitFor(() =>
- getByTestId('search-facets')
- );
- expect(leftSearchColumn).toBeTruthy();
-
- // Wait for project form to render
- const projectForm = await waitFor(() => getByTestId('project-form'));
- expect(projectForm).toBeTruthy();
-
- // Check project select form exists and mouseDown to expand list of options to expand options
- const projectFormSelect = within(projectForm).getByRole('combobox');
- expect(projectFormSelect).toBeTruthy();
- fireEvent.mouseDown(projectFormSelect);
-
- // Select a project option
- const projectOption = getByTestId('project_1');
- expect(projectOption).toBeTruthy();
- await user.click(projectOption);
-
- // Submit the form
- const submitBtn = within(projectForm).getByRole('img', {
- name: 'select',
- });
- fireEvent.submit(submitBtn);
-
- // Wait for components to rerender
- await waitFor(() => getByTestId('search'));
- await waitFor(() => getByTestId('facets-form'));
-
// Check delete button renders for the saved search and click it
const saveBtn = await waitFor(() => getByText('Save Search'));
expect(saveBtn).toBeTruthy();
diff --git a/frontend/src/components/App/App.tsx b/frontend/src/components/App/App.tsx
index 45a4b2baf..13541a6e1 100644
--- a/frontend/src/components/App/App.tsx
+++ b/frontend/src/components/App/App.tsx
@@ -1,4 +1,5 @@
/* eslint-disable no-void */
+
import {
BookOutlined,
DeleteOutlined,
@@ -278,7 +279,11 @@ const App: React.FC> = ({ searchQuery }) => {
};
const handleProjectChange = (selectedProject: RawProject): void => {
- setActiveSearchQuery(projectBaseQuery(selectedProject));
+ if (selectedProject.pk !== activeSearchQuery.project.pk) {
+ setActiveSearchQuery(projectBaseQuery(selectedProject));
+ } else {
+ setActiveSearchQuery({ ...activeSearchQuery, project: selectedProject });
+ }
};
const handleRemoveFilter = (removedTag: TagValue, type: TagType): void => {
@@ -401,26 +406,25 @@ const App: React.FC> = ({ searchQuery }) => {
.then(() => {
saveSuccess();
})
- .catch((error: ResponseError) => {
- showError(error.message);
- });
+ .catch(
+ /* istanbul ignore next */
+ (error: ResponseError) => {
+ showError(error.message);
+ }
+ );
} else {
saveSuccess();
}
};
const handleShareSearchQuery = (): void => {
- const shareSuccess = (): void => {
- // copy link to clipboard
- /* istanbul ignore next */
- if (navigator && navigator.clipboard) {
- navigator.clipboard.writeText(getUrlFromSearch(activeSearchQuery));
- showNotice('Search copied to clipboard!', {
- icon: ,
- });
- }
- };
- shareSuccess();
+ /* istanbul ignore else */
+ if (navigator && navigator.clipboard) {
+ navigator.clipboard.writeText(getUrlFromSearch(activeSearchQuery));
+ showNotice('Search copied to clipboard!', {
+ icon: ,
+ });
+ }
};
const handleRemoveSearchQuery = (searchUUID: string): void => {
diff --git a/frontend/src/components/Facets/FacetsForm.test.tsx b/frontend/src/components/Facets/FacetsForm.test.tsx
index e11161f46..cbe3afa85 100644
--- a/frontend/src/components/Facets/FacetsForm.test.tsx
+++ b/frontend/src/components/Facets/FacetsForm.test.tsx
@@ -101,6 +101,31 @@ describe('test FacetsForm component', () => {
await user.click(collapseAllBtn);
});
+ it('handles copying facet items to clip board', async () => {
+ const { getByText, getByRole } = customRenderKeycloak(
+
+ );
+
+ // Expand the group1 panel
+ const group1Btn = getByText('Group1');
+ expect(group1Btn).toBeTruthy();
+ await user.click(group1Btn);
+
+ // Click the copy facets button
+ const copyBtn = getByRole('img', { name: 'copy' });
+ expect(copyBtn).toBeTruthy();
+ await user.click(copyBtn);
+
+ // Check the clipboard has items
+ const items = await navigator.clipboard.readText();
+ expect(items).toEqual('aims3.llnl.gov (3)\nesgf1.dkrz.de (5)');
+
+ // Expect result message to show
+ const resultNotification = getByText('Data Nodes copied to clipboard!');
+ expect(resultNotification).toBeTruthy();
+ await user.click(resultNotification);
+ });
+
it('handles changing expand to collapse and vice-versa base on user actions', async () => {
const { getByText } = customRenderKeycloak(
diff --git a/frontend/src/components/Facets/FacetsForm.tsx b/frontend/src/components/Facets/FacetsForm.tsx
index b76abcc33..a7159cc05 100644
--- a/frontend/src/components/Facets/FacetsForm.tsx
+++ b/frontend/src/components/Facets/FacetsForm.tsx
@@ -1,4 +1,5 @@
import {
+ CopyOutlined,
InfoCircleOutlined,
RightCircleOutlined,
SearchOutlined,
@@ -30,6 +31,7 @@ import {
} from '../Search/types';
import { ActiveFacets, ParsedFacets } from './types';
import { globusEnabledNodes } from '../../env';
+import { showNotice } from '../../common/utils';
const styles: CSSinJS = {
container: {
@@ -346,15 +348,53 @@ const FacetsForm: React.FC> = ({
const isOptionalforDatasets =
facetOptions.length > 0 &&
facetOptions[0].includes('none');
+ const facetNameHumanized = humanizeFacetNames(facet);
return (
+ {humanizeFacetNames(facet)}
+
+
+
+ }
+ onClick={() => {
+ // copy link to clipboard
+ /* istanbul ignore else */
+ if (navigator && navigator.clipboard) {
+ navigator.clipboard.writeText(
+ facetOptions
+ .map((item) => {
+ return `${item[0]} (${item[1]})`;
+ })
+ .join('\n')
+ );
+ showNotice(
+ `${facetNameHumanized}s copied to clipboard!`,
+ {
+ icon: (
+
+ ),
+ }
+ );
+ }
+ }}
+ >
+
}
- style={{ marginBottom: '0px' }}
+ style={{ marginBottom: 0 }}
tooltip={
isOptionalforDatasets
? {
diff --git a/frontend/src/components/Facets/ProjectForm.test.tsx b/frontend/src/components/Facets/ProjectForm.test.tsx
index 5450614f6..5f8b6d1e3 100644
--- a/frontend/src/components/Facets/ProjectForm.test.tsx
+++ b/frontend/src/components/Facets/ProjectForm.test.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ResponseError } from '../../api';
import {
@@ -8,8 +9,7 @@ import {
import { mapHTTPErrorCodes } from '../../api/routes';
import ProjectsForm, { Props } from './ProjectForm';
import { customRenderKeycloak } from '../../test/custom-render';
-
-const user = userEvent.setup();
+import { showNotice } from '../../common/utils';
const defaultProps: Props = {
activeSearchQuery: activeSearchQueryFixture(),
@@ -19,23 +19,7 @@ const defaultProps: Props = {
onFinish: jest.fn(),
};
-it('renders Popconfirm component when there is an active project and active facets', async () => {
- const { getByRole, getByText } = customRenderKeycloak(
-
- );
-
- // Click the submit button
- const submitBtn = getByRole('img', { name: 'select' });
- await user.click(submitBtn);
-
- // Check popover exists
- const popOver = getByRole('img', { name: 'question-circle' });
- expect(popOver).toBeTruthy();
-
- // Submit popover
- const popOverSubmitBtn = getByText('OK');
- await user.click(popOverSubmitBtn);
-});
+const user = userEvent.setup();
it('renders empty form', () => {
const { queryByRole } = customRenderKeycloak(
@@ -43,8 +27,44 @@ it('renders empty form', () => {
);
// Check submit button does not exist
- const submitBtn = queryByRole('img', { name: 'select' });
- expect(submitBtn).toBeNull();
+ const selectDropdown = queryByRole('form');
+ expect(selectDropdown).toBeNull();
+});
+
+it('Runs project form submit when changing projects', async () => {
+ const { getByRole, getByText } = customRenderKeycloak(
+ {
+ showNotice(`${projName} was selected!`, {
+ duration: 0,
+ type: 'success',
+ });
+ }}
+ />
+ );
+
+ // First project should be selected by default, calling 'onFinish'
+ const option1Selected = await waitFor(() => {
+ return getByText('test1 was selected!');
+ });
+ expect(option1Selected).toBeTruthy();
+
+ // Open the project dropdown
+ const projectDropDown = getByRole('combobox');
+ expect(projectDropDown).toBeTruthy();
+ fireEvent.mouseDown(projectDropDown);
+
+ // Select the 3rd project in the drop-down
+ const option3 = await waitFor(() => {
+ return getByText('test3');
+ });
+ await user.click(option3);
+
+ // The 3rd project should now be selected
+ const option3Selected = getByText('test3 was selected!');
+ expect(option3Selected).toBeTruthy();
});
it('renders error message when projects can"t be fetched', () => {
diff --git a/frontend/src/components/Facets/ProjectForm.tsx b/frontend/src/components/Facets/ProjectForm.tsx
index 3669ba740..9189bb116 100644
--- a/frontend/src/components/Facets/ProjectForm.tsx
+++ b/frontend/src/components/Facets/ProjectForm.tsx
@@ -1,15 +1,12 @@
-import { QuestionCircleOutlined, SelectOutlined } from '@ant-design/icons';
-import { Alert, Form, Popconfirm, Select, Spin } from 'antd';
+import { Alert, Form, Select, Spin } from 'antd';
import React from 'react';
import { ResponseError } from '../../api';
import { leftSidebarTargets } from '../../common/reactJoyrideSteps';
-import { objectIsEmpty } from '../../common/utils';
-import Button from '../General/Button';
import { ActiveSearchQuery } from '../Search/types';
import { RawProject, RawProjects } from './types';
const styles = {
- form: { width: '280px' },
+ form: { width: '360px' },
};
export type Props = {
@@ -19,7 +16,7 @@ export type Props = {
};
apiIsLoading: boolean;
apiError?: ResponseError;
- onFinish: (allValues: { [key: string]: string }) => void;
+ onFinish: (selection: string) => void;
};
const ProjectsForm: React.FC> = ({
@@ -35,6 +32,7 @@ const ProjectsForm: React.FC> = ({
*/
React.useEffect(() => {
projectForm.resetFields();
+ projectForm.submit();
}, [projectForm, activeSearchQuery.project]);
// Note, have to wrap Alert and Spin with Form to suppress warning about
@@ -61,6 +59,9 @@ const ProjectsForm: React.FC> = ({
project:
(activeSearchQuery.project as RawProject).name || results[0].name,
};
+ const projectOptions = results.map((project) => {
+ return { value: project.name, label: project.name };
+ });
return (
@@ -69,53 +70,24 @@ const ProjectsForm: React.FC
> = ({
layout="inline"
size="small"
initialValues={initialValues}
- onFinish={onFinish}
+ onFinish={() => {
+ onFinish(projectForm.getFieldValue('projectDropdown') as string);
+ }}
>
{
+ projectForm.submit();
+ }}
showArrow
- >
- {results.map((projectObj: RawProject, index: number) => (
-
-
- {projectObj.name}
-
-
- ))}
-
-
-
- {!objectIsEmpty(activeSearchQuery.project) &&
- !objectIsEmpty(activeSearchQuery.activeFacets) ? (
- projectForm.submit()}
- icon={ }
- placement="right"
- >
-
- }
- >
-
-
- ) : (
- }
- >
- )}
+ options={projectOptions}
+ />
diff --git a/frontend/src/components/Facets/index.test.tsx b/frontend/src/components/Facets/index.test.tsx
index 0d942780d..0fc6ca30e 100644
--- a/frontend/src/components/Facets/index.test.tsx
+++ b/frontend/src/components/Facets/index.test.tsx
@@ -33,45 +33,6 @@ it('renders component', async () => {
expect(projectForm).toBeTruthy();
});
-it('handles when the project form is submitted', async () => {
- const { getByTestId } = customRenderKeycloak(
-
- );
-
- // Check FacetsForm component renders
- const facetsForm = await waitFor(() => getByTestId('facets-form'));
- await waitFor(() => expect(facetsForm).toBeTruthy());
-
- // Check ProjectForm component renders
- const projectForm = await waitFor(() => getByTestId('project-form'));
- expect(projectForm).toBeTruthy();
-
- // Check facet select form exists and mouseDown to expand list of options
- const projectFormSelect = document.querySelector(
- '[data-testid=project-form-select] > .ant-select-selector'
- ) as HTMLInputElement;
- expect(projectFormSelect).toBeTruthy();
- fireEvent.mouseDown(projectFormSelect);
-
- // Select the second project option
- const projectOption = getByTestId('project_1');
- expect(projectOption).toBeTruthy();
- await user.click(projectOption);
-
- // Wait for facet form component to re-render
- await waitFor(() => getByTestId('facets-form'));
-
- // Submit the form
- // NOTE: Submit button is inside the form, so use submit
- const projectFormBtn = within(projectForm).getByRole('img', {
- name: 'select',
- });
- fireEvent.submit(projectFormBtn);
-});
-
it('handles facets form auto-filtering', async () => {
const { getByTestId, getByText, getByRole } = customRenderKeycloak(
diff --git a/frontend/src/components/Facets/index.tsx b/frontend/src/components/Facets/index.tsx
index 90ddd993c..b4dc8d0ac 100644
--- a/frontend/src/components/Facets/index.tsx
+++ b/frontend/src/components/Facets/index.tsx
@@ -50,16 +50,14 @@ const Facets: React.FC> = ({
const [curProject, setCurProject] = React.useState();
- const handleSubmitProjectForm = (selectedProject: {
- [key: string]: string;
- }): void => {
+ const handleSubmitProjectForm = (selectedProject: string): void => {
/* istanbul ignore else */
if (data) {
const selectedProj: RawProject | undefined = data.results.find(
- (obj: RawProject) => obj.name === selectedProject.project
+ (obj: RawProject) => obj.name === selectedProject
);
/* istanbul ignore else */
- if (selectedProj) {
+ if (selectedProj && activeSearchQuery.textInputs) {
onProjectChange(selectedProj);
setCurProject(selectedProj);
}
@@ -67,11 +65,11 @@ const Facets: React.FC> = ({
};
useEffect(() => {
- /* istanbul ignore else */
- if (activeSearchQuery.project) {
- setCurProject(activeSearchQuery.project as RawProject);
+ if (!isLoading && data && data.results.length > 0) {
+ setCurProject(data.results[0]);
+ onProjectChange(data.results[0]);
}
- }, [activeSearchQuery]);
+ }, [isLoading]);
return (
{
});
describe('DatasetDownload form tests', () => {
- it('Download form renders.', () => {
+ it('Download form renders.', async () => {
const downloadForm = customRenderKeycloak(
);
expect(downloadForm).toBeTruthy();
+
+ await downloadForm.findByTestId('downloadTypeSelector');
});
it('Start the wget transfer after adding an item to cart', async () => {
@@ -1224,7 +1226,7 @@ describe('DatasetDownload form tests', () => {
});
// TODO: Figure out why this test passes locally, but fails when run in the github CI
- xit('Perform Transfer process when sign in tokens and endpoint are BOTH ready', async () => {
+ it('Perform Transfer process when sign in tokens and endpoint are BOTH ready', async () => {
// Setting the tokens so that the sign-in step should be completed
mockSaveValue(CartStateKeys.cartItemSelections, userCartFixture());
mockSaveValue(GlobusStateKeys.refreshToken, 'refreshToken');
@@ -1452,13 +1454,15 @@ describe('DatasetDownload form tests', () => {
describe('Testing globus transfer related failures', () => {
beforeAll(() => {
+ jest.spyOn(console, 'error').mockImplementation(jest.fn());
tempStorageSetMock('pkce-pass', false);
jest.resetModules();
});
+ // TODO: Figure out why this test passes locally, but fails when run in the github CI
it('Shows an error message if transfer task fails', async () => {
server.use(
- rest.get(apiRoutes.globusTransfer.path, (_req, res, ctx) =>
+ rest.post(apiRoutes.globusTransfer.path, (_req, res, ctx) =>
res(ctx.status(404))
)
);
@@ -1475,8 +1479,7 @@ describe('Testing globus transfer related failures', () => {
access_token: '',
refresh_expires_in: 0,
refresh_token: 'something',
- scope:
- 'openid profile email offline_access urn:globus:auth:scope:transfer.api.globus.org:all',
+ scope: 'openid profile email offline_access ',
token_type: '',
} as GlobusTokenResponse);
mockSaveValue(
@@ -1530,7 +1533,7 @@ describe('Testing globus transfer related failures', () => {
/** Until that is done, this test will fail and will need to use istanbul ignore statements
* for the mean time.
*/
- xit('Shows error message if url tokens are not valid for transfer', async () => {
+ it('Shows error message if url tokens are not valid for transfer', async () => {
// Setting the tokens so that the sign-in step should be skipped
mockSaveValue(CartStateKeys.cartItemSelections, userCartFixture());
mockSaveValue(GlobusStateKeys.continueGlobusPrepSteps, true);
diff --git a/frontend/src/components/Globus/DatasetDownload.tsx b/frontend/src/components/Globus/DatasetDownload.tsx
index 900949514..3e735de29 100644
--- a/frontend/src/components/Globus/DatasetDownload.tsx
+++ b/frontend/src/components/Globus/DatasetDownload.tsx
@@ -268,7 +268,7 @@ const DatasetDownloadForm: React.FC
> = () => {
if (globusTransferToken && refreshToken) {
let messageContent: React.ReactNode | string = null;
let messageType: NotificationType = 'success';
-
+ let durationVal = 5;
startGlobusTransfer(
globusTransferToken.access_token,
refreshToken,
@@ -317,7 +317,8 @@ const DatasetDownloadForm: React.FC> = () => {
})
.catch(async (error: ResponseError) => {
if (error.message !== '') {
- messageContent = `Globus transfer task failed: ${error.message}`;
+ messageContent = `Globus transfer task failed. ${error.message} is your error code. Please contact ESGF support.`;
+ durationVal = 60;
} else {
messageContent = `Globus transfer task failed. Resetting tokens.`;
// eslint-disable-next-line no-console
@@ -329,7 +330,7 @@ const DatasetDownloadForm: React.FC> = () => {
.finally(async () => {
setDownloadActive(false);
await showNotice(messageContent, {
- duration: 3,
+ duration: durationVal,
type: messageType,
});
setDownloadActive(true);
diff --git a/frontend/src/components/Messaging/Templates/ChangeLog.tsx b/frontend/src/components/Messaging/Templates/ChangeLog.tsx
index 2fea74062..1df7f0498 100644
--- a/frontend/src/components/Messaging/Templates/ChangeLog.tsx
+++ b/frontend/src/components/Messaging/Templates/ChangeLog.tsx
@@ -11,11 +11,11 @@ const ChangeLogTemplate: React.FC> = ({
New with Metagrid v{props.version}
-
+
{props.changesFile && (
)}
-
+
>
);
};
diff --git a/frontend/src/components/Messaging/messageDisplayData.ts b/frontend/src/components/Messaging/messageDisplayData.ts
index a5d44cc2a..6dde64d43 100644
--- a/frontend/src/components/Messaging/messageDisplayData.ts
+++ b/frontend/src/components/Messaging/messageDisplayData.ts
@@ -5,6 +5,7 @@ export const rightDrawerMessages: MarkdownMessage[] = [
];
export const rightDrawerChanges: MarkdownMessage[] = [
+ { title: 'V1.1.0', fileName: 'changelog/v1.1.0.md' },
{ title: 'V1.0.10', fileName: 'changelog/v1.0.10-beta.md' },
{ title: 'V1.0.9', fileName: 'changelog/v1.0.9-beta.md' },
{ title: 'V1.0.8', fileName: 'changelog/v1.0.8-beta.md' },
@@ -12,9 +13,18 @@ export const rightDrawerChanges: MarkdownMessage[] = [
];
const startupMessages: StartPopupData = {
- messageToShow: 'v1.0.10-beta',
+ messageToShow: 'v1.1.0',
defaultMessageId: 'welcome',
messageData: [
+ {
+ messageId: 'v1.1.0',
+ template: MessageTemplates.ChangeLog,
+ style: { minWidth: '700px' },
+ data: {
+ changesFile: 'changelog/v1.1.0.md',
+ version: '1.1.0',
+ },
+ },
{
messageId: 'v1.0.10-beta',
template: MessageTemplates.ChangeLog,
diff --git a/frontend/src/components/Search/Table.test.tsx b/frontend/src/components/Search/Table.test.tsx
index c8ec36f13..a254c7d7c 100644
--- a/frontend/src/components/Search/Table.test.tsx
+++ b/frontend/src/components/Search/Table.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, waitFor, within, screen } from '@testing-library/react';
+import { fireEvent, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import {
@@ -33,13 +33,10 @@ it('renders component', () => {
expect(table).toBeTruthy();
});
-xit('renders component without results', () => {
- const { getByText } = customRenderKeycloak(
-
- );
-
- const noDataText = getByText('No Data');
- expect(noDataText).toBeTruthy();
+it('renders component without results', () => {
+ const table = customRenderKeycloak();
+ const row = table.getAllByRole('row')[1];
+ expect(row).toHaveClass('ant-table-placeholder');
});
it('renders not available for total size and number of files columns when dataset doesn"t have those attributes', () => {
@@ -127,7 +124,6 @@ xit('renders record metadata in an expandable panel', async () => {
const expandableIcon = within(expandableCell).getByRole('img', {
name: 'right-circle',
});
- screen.debug(expandableCell, Infinity);
expect(expandableIcon).toBeTruthy();
// await act(async () => {
// await user.click(expandableIcon);
diff --git a/frontend/src/components/Search/Tabs.tsx b/frontend/src/components/Search/Tabs.tsx
index 01f81498b..2f39aef3e 100644
--- a/frontend/src/components/Search/Tabs.tsx
+++ b/frontend/src/components/Search/Tabs.tsx
@@ -104,10 +104,132 @@ const Tabs: React.FC> = ({
const showAdditionalTab =
showESDOC || showQualityFlags || showAdditionalLinks;
+ const tabList = [
+ {
+ key: '1',
+ disabled: record.retracted === true,
+ label: Files
,
+ children: (
+
+ ),
+ },
+ {
+ key: '2',
+ disabled: record.retracted === true,
+ label: (
+ Metadata
+ ),
+ children: (
+ <>
+ Displaying {Object.keys(record).length} keys
+
+ (option as Record<'value', string>).value
+ .toUpperCase()
+ .indexOf(inputValue.toUpperCase()) !== -1
+ }
+ />
+
+ {Object.keys(record).map((key) => (
+
+ {key} :{' '}
+ {String(record[key])}
+
+ ))}
+ >
+ ),
+ },
+ ];
+
+ if (showCitation) {
+ tabList.push({
+ key: '3',
+ disabled: record.retracted === true,
+ label: (
+ Citation
+ ),
+ children: (
+
+ ),
+ });
+ }
+
+ if (showAdditionalTab) {
+ tabList.push({
+ key: '4',
+ disabled: record.retracted === true,
+ label: (
+
+ Additional
+
+ ),
+ children: (
+ <>
+ {showAdditionalLinks && additionalLinks}
+ {showESDOC &&
+ ((record.further_info_url as unknown) as string)[0] !== '' && (
+
+ ES-DOC
+
+ )}
+ {showQualityFlags && (
+
+
+ }
+ >
+
+ {Object.keys(qualityFlags).map((key) => (
+
+ ))}
+
+
+
+ )}
+ >
+ ),
+ });
+ }
+
return (
- // Disable all tabs excep metadata if the record is retracted
-
-
+ );
+ // Disable all tabs excep metadata if the record is retracted
+ /* ;
+ Files }
key="1"
@@ -118,7 +240,7 @@ const Tabs: React.FC> = ({
filenameVars={filenameVars}
/>
-
Metadata
@@ -133,7 +255,7 @@ const Tabs: React.FC> = ({
options={metaData}
placeholder="Lookup a key..."
filterOption={
- /* istanbul ignore next */ (inputValue, option) =>
+ /* istanbul ignore next (inputValue, option) =>
(option as Record<'value', string>).value
.toUpperCase()
.indexOf(inputValue.toUpperCase()) !== -1
@@ -147,7 +269,7 @@ const Tabs: React.FC> = ({
))}
- {showCitation && (
+ {showCitation && (
> = ({
/>
)}
- {showAdditionalTab && (
+ {showAdditionalTab && (
> = ({
)}
- )}
-
- );
+ )}*/
};
export default Tabs;
diff --git a/frontend/src/env.test.ts b/frontend/src/env.test.ts
new file mode 100644
index 000000000..ce3ee1c0c
--- /dev/null
+++ b/frontend/src/env.test.ts
@@ -0,0 +1,62 @@
+import { getConfig } from './env';
+
+const PENV = process.env;
+
+beforeEach(() => {
+ jest.resetModules();
+
+ const { env } = process;
+
+ try {
+ delete env.REACT_APP_METAGRID_API_URL;
+ } catch {
+ // eslint-disable-next-line no-console
+ console.log('REACT_APP_METAGRID_API_URL not defined');
+ }
+
+ process.env = env;
+
+ window.ENV = {};
+});
+
+afterEach(() => {
+ process.env = PENV;
+});
+
+describe('Test getConfig', () => {
+ it('should be empty', () => {
+ expect(getConfig('REACT_APP_METAGRID_API_URL')).toBe('');
+ });
+
+ it('should use window.ENV', () => {
+ window.ENV = {
+ REACT_APP_METAGRID_API_URL: 'https://dummy.io/metagrid',
+ };
+
+ expect(getConfig('REACT_APP_METAGRID_API_URL')).toBe(
+ 'https://dummy.io/metagrid'
+ );
+ });
+
+ it('should use process.env', () => {
+ process.env.REACT_APP_METAGRID_API_URL =
+ 'https://anotherdummy.io/metagrid2';
+
+ expect(getConfig('REACT_APP_METAGRID_API_URL')).toBe(
+ 'https://anotherdummy.io/metagrid2'
+ );
+ });
+
+ it('should ignore process.env when window.ENV is set', () => {
+ window.ENV = {
+ REACT_APP_METAGRID_API_URL: 'https://dummy.io/metagrid',
+ };
+
+ process.env.REACT_APP_METAGRID_API_URL =
+ 'https://anotherdummy.io/metagrid2';
+
+ expect(getConfig('REACT_APP_METAGRID_API_URL')).toBe(
+ 'https://dummy.io/metagrid'
+ );
+ });
+});
diff --git a/frontend/src/env.ts b/frontend/src/env.ts
index 8fea1a1db..1f288101c 100644
--- a/frontend/src/env.ts
+++ b/frontend/src/env.ts
@@ -4,22 +4,41 @@
* Make sure it is consistent with .envs/frontend, .react!
*/
+declare global {
+ interface Window {
+ ENV: {
+ [key: string]: string | undefined;
+ };
+ }
+}
+
+export function getConfig(name: string): string {
+ let value = '';
+
+ if (window && window.ENV) {
+ value = window.ENV[name] || '';
+ }
+
+ if (value === '') {
+ value = process.env[name] || '';
+ }
+
+ return value;
+}
+
// MetaGrid API
// ------------------------------------------------------------------------------
// https://github.com/aims-group/metagrid/tree/master/backend
-export const metagridApiURL = `${
- process.env.REACT_APP_METAGRID_API_URL as string
-}`;
+export const metagridApiURL = getConfig('REACT_APP_METAGRID_API_URL');
// Redirect frontend
-export const publicUrl = process.env.PUBLIC_URL;
-export const previousPublicUrl = process.env.REACT_APP_PREVIOUS_URL as string;
+export const publicUrl = getConfig('PUBLIC_URL');
+export const previousPublicUrl = getConfig('REACT_APP_PREVIOUS_URL');
// Globus variables
-export const globusRedirectUrl = process.env
- .REACT_APP_GLOBUS_REDIRECT as string;
-export const globusClientID = process.env.REACT_APP_CLIENT_ID as string;
-const globusNodesString = process.env.REACT_APP_GLOBUS_NODES as string;
+export const globusRedirectUrl = getConfig('REACT_APP_GLOBUS_REDIRECT');
+export const globusClientID = getConfig('REACT_APP_CLIENT_ID');
+const globusNodesString = getConfig('REACT_APP_GLOBUS_NODES');
/* istanbul ignore next */
export const globusEnabledNodes = globusNodesString
? globusNodesString.split(',')
@@ -28,39 +47,36 @@ export const globusEnabledNodes = globusNodesString
// ESGF wget API
// ------------------------------------------------------------------------------
// https://github.com/ESGF/esgf-wget
-export const wgetApiURL = process.env.REACT_APP_WGET_API_URL as string;
+export const wgetApiURL = getConfig('REACT_APP_WGET_API_URL');
// ESGF Search API
// ------------------------------------------------------------------------------
// https://esgf.github.io/esg-search/ESGF_Search_RESTful_API.html
-export const esgfSearchURL = `${process.env.REACT_APP_SEARCH_URL as string}`;
+export const esgfSearchURL = getConfig('REACT_APP_SEARCH_URL');
// ESGF Node Status API
// ------------------------------------------------------------------------------
// https://github.com/ESGF/esgf-utils/blob/master/node_status/query_prom.py
-export const nodeStatusURL = `${
- process.env.REACT_APP_ESGF_NODE_STATUS_URL as string
-}`;
+export const nodeStatusURL = getConfig('REACT_APP_ESGF_NODE_STATUS_URL');
// Keycloak
// ------------------------------------------------------------------------------
// https://github.com/keycloak/keycloak
-export const keycloakRealm = process.env.REACT_APP_KEYCLOAK_REALM as string;
-export const keycloakUrl = process.env.REACT_APP_KEYCLOAK_URL as string;
-export const keycloakClientId = process.env
- .REACT_APP_KEYCLOAK_CLIENT_ID as string;
+export const keycloakRealm = getConfig('REACT_APP_KEYCLOAK_REALM');
+export const keycloakUrl = getConfig('REACT_APP_KEYCLOAK_URL');
+export const keycloakClientId = getConfig('REACT_APP_KEYCLOAK_CLIENT_ID');
// react-hotjar
// ------------------------------------------------------------------------------
// https://github.com/abdalla/react-hotjar
-export const hjid = (process.env.REACT_APP_HOTJAR_ID as unknown) as number;
-export const hjsv = (process.env.REACT_APP_HOTJAR_SV as unknown) as number;
+export const hjid = +getConfig('REACT_APP_HOTJAR_ID');
+export const hjsv = +getConfig('REACT_APP_HOTJAR_SV');
// Django Auth URLs
-export const djangoLoginUrl = process.env.REACT_APP_DJANGO_LOGIN_URL as string;
-export const djangoLogoutUrl = process.env
- .REACT_APP_DJANGO_LOGOUT_URL as string;
+export const djangoLoginUrl = getConfig('REACT_APP_DJANGO_LOGIN_URL');
+export const djangoLogoutUrl = getConfig('REACT_APP_DJANGO_LOGOUT_URL');
// Authentication Method
-export const authenticationMethod = process.env
- .REACT_APP_AUTHENTICATION_METHOD as string;
+export const authenticationMethod = getConfig(
+ 'REACT_APP_AUTHENTICATION_METHOD'
+);
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 483d3c65e..5142fc182 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -11,7 +11,7 @@ import {
} from './contexts/AuthContext';
import { ReactJoyrideProvider } from './contexts/ReactJoyrideContext';
import { keycloak, keycloakProviderInitConfig } from './lib/keycloak';
-import { authenticationMethod } from './env';
+import { authenticationMethod, publicUrl } from './env';
import './index.css';
const container = document.getElementById('root');
@@ -19,7 +19,7 @@ const container = document.getElementById('root');
const root = createRoot(container!);
const appRouter = (
-
+
diff --git a/frontend/src/test/custom-render.tsx b/frontend/src/test/custom-render.tsx
index e556b76ea..152fd3ce5 100644
--- a/frontend/src/test/custom-render.tsx
+++ b/frontend/src/test/custom-render.tsx
@@ -11,6 +11,7 @@ import {
} from '../contexts/AuthContext';
import { keycloakProviderInitConfig } from '../lib/keycloak';
import { ReactJoyrideProvider } from '../contexts/ReactJoyrideContext';
+import { publicUrl } from '../env';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const keycloak = new Keycloak();
@@ -41,7 +42,7 @@ export const KeycloakProvidersAuthenticated = ({
pk: '1',
}}
>
-
+
{children}
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 90a22306a..44feaadf5 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -8,16 +8,16 @@
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==
"@adobe/css-tools@^4.0.1":
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.2.0.tgz#e1a84fca468f4b337816fcb7f0964beb620ba855"
- integrity sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11"
+ integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==
"@alloc/quick-lru@^5.2.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
-"@ampproject/remapping@^2.1.0", "@ampproject/remapping@^2.2.0":
+"@ampproject/remapping@^2.2.0":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==
@@ -86,7 +86,7 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.5", "@babel/code-frame@^7.8.3":
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.22.5", "@babel/code-frame@^7.8.3":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658"
integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==
@@ -101,30 +101,43 @@
"@babel/highlight" "^7.22.13"
chalk "^2.4.2"
+"@babel/code-frame@^7.23.5":
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244"
+ integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==
+ dependencies:
+ "@babel/highlight" "^7.23.4"
+ chalk "^2.4.2"
+
"@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730"
integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==
-"@babel/core@7.17.9":
- version "7.17.9"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.9.tgz#6bae81a06d95f4d0dec5bb9d74bbc1f58babdcfe"
- integrity sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==
- dependencies:
- "@ampproject/remapping" "^2.1.0"
- "@babel/code-frame" "^7.16.7"
- "@babel/generator" "^7.17.9"
- "@babel/helper-compilation-targets" "^7.17.7"
- "@babel/helper-module-transforms" "^7.17.7"
- "@babel/helpers" "^7.17.9"
- "@babel/parser" "^7.17.9"
- "@babel/template" "^7.16.7"
- "@babel/traverse" "^7.17.9"
- "@babel/types" "^7.17.0"
+"@babel/compat-data@^7.23.5":
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98"
+ integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==
+
+"@babel/core@7.22.5":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.5.tgz#d67d9747ecf26ee7ecd3ebae1ee22225fe902a89"
+ integrity sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==
+ dependencies:
+ "@ampproject/remapping" "^2.2.0"
+ "@babel/code-frame" "^7.22.5"
+ "@babel/generator" "^7.22.5"
+ "@babel/helper-compilation-targets" "^7.22.5"
+ "@babel/helper-module-transforms" "^7.22.5"
+ "@babel/helpers" "^7.22.5"
+ "@babel/parser" "^7.22.5"
+ "@babel/template" "^7.22.5"
+ "@babel/traverse" "^7.22.5"
+ "@babel/types" "^7.22.5"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
- json5 "^2.2.1"
+ json5 "^2.2.2"
semver "^6.3.0"
"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
@@ -148,6 +161,15 @@
json5 "^2.2.2"
semver "^6.3.1"
+"@babel/eslint-parser@7.22.5":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.22.5.tgz#fa032503b9e2d188e25b1b95d29e8b8431042d78"
+ integrity sha512-C69RWYNYtrgIRE5CmTd77ZiLDXqgBipahJc/jHP3sLcAGj6AJzxNIuKNpVnICqbyK7X3pFUfEvL++rvtbQpZkQ==
+ dependencies:
+ "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1"
+ eslint-visitor-keys "^2.1.0"
+ semver "^6.3.0"
+
"@babel/eslint-parser@^7.16.3":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.22.9.tgz#75f8aa978d1e76c87cc6f26c1ea16ae58804d390"
@@ -157,7 +179,17 @@
eslint-visitor-keys "^2.1.0"
semver "^6.3.1"
-"@babel/generator@^7.17.9", "@babel/generator@^7.22.7", "@babel/generator@^7.22.9", "@babel/generator@^7.7.2":
+"@babel/generator@^7.22.5", "@babel/generator@^7.23.6":
+ version "7.23.6"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e"
+ integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==
+ dependencies:
+ "@babel/types" "^7.23.6"
+ "@jridgewell/gen-mapping" "^0.3.2"
+ "@jridgewell/trace-mapping" "^0.3.17"
+ jsesc "^2.5.1"
+
+"@babel/generator@^7.22.7", "@babel/generator@^7.22.9", "@babel/generator@^7.7.2":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d"
integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==
@@ -181,7 +213,7 @@
dependencies:
"@babel/types" "^7.22.5"
-"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.22.9":
+"@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz#f9d0a7aaaa7cd32a3f31c9316a69f5a9bcacb892"
integrity sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==
@@ -192,6 +224,17 @@
lru-cache "^5.1.1"
semver "^6.3.1"
+"@babel/helper-compilation-targets@^7.23.6":
+ version "7.23.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991"
+ integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==
+ dependencies:
+ "@babel/compat-data" "^7.23.5"
+ "@babel/helper-validator-option" "^7.23.5"
+ browserslist "^4.22.2"
+ lru-cache "^5.1.1"
+ semver "^6.3.1"
+
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.22.5", "@babel/helper-create-class-features-plugin@^7.22.6", "@babel/helper-create-class-features-plugin@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz#c36ea240bb3348f942f08b0fbe28d6d979fab236"
@@ -227,6 +270,33 @@
lodash.debounce "^4.0.8"
resolve "^1.14.2"
+"@babel/helper-define-polyfill-provider@^0.4.4":
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz#64df615451cb30e94b59a9696022cffac9a10088"
+ integrity sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==
+ dependencies:
+ "@babel/helper-compilation-targets" "^7.22.6"
+ "@babel/helper-plugin-utils" "^7.22.5"
+ debug "^4.1.1"
+ lodash.debounce "^4.0.8"
+ resolve "^1.14.2"
+
+"@babel/helper-define-polyfill-provider@^0.5.0":
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz#465805b7361f461e86c680f1de21eaf88c25901b"
+ integrity sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==
+ dependencies:
+ "@babel/helper-compilation-targets" "^7.22.6"
+ "@babel/helper-plugin-utils" "^7.22.5"
+ debug "^4.1.1"
+ lodash.debounce "^4.0.8"
+ resolve "^1.14.2"
+
+"@babel/helper-environment-visitor@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
+ integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
+
"@babel/helper-environment-visitor@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98"
@@ -240,6 +310,14 @@
"@babel/template" "^7.22.5"
"@babel/types" "^7.22.5"
+"@babel/helper-function-name@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
+ integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
+ dependencies:
+ "@babel/template" "^7.22.15"
+ "@babel/types" "^7.23.0"
+
"@babel/helper-hoist-variables@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
@@ -247,6 +325,13 @@
dependencies:
"@babel/types" "^7.22.5"
+"@babel/helper-member-expression-to-functions@^7.22.15":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366"
+ integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==
+ dependencies:
+ "@babel/types" "^7.23.0"
+
"@babel/helper-member-expression-to-functions@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz#0a7c56117cad3372fbf8d2fb4bf8f8d64a1e76b2"
@@ -261,7 +346,7 @@
dependencies:
"@babel/types" "^7.22.5"
-"@babel/helper-module-transforms@^7.17.7", "@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9":
+"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129"
integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==
@@ -284,6 +369,15 @@
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295"
integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==
+"@babel/helper-remap-async-to-generator@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0"
+ integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.22.5"
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-wrap-function" "^7.22.20"
+
"@babel/helper-remap-async-to-generator@^7.22.5":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz#53a25b7484e722d7efb9c350c75c032d4628de82"
@@ -293,6 +387,15 @@
"@babel/helper-environment-visitor" "^7.22.5"
"@babel/helper-wrap-function" "^7.22.9"
+"@babel/helper-replace-supers@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793"
+ integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-member-expression-to-functions" "^7.22.15"
+ "@babel/helper-optimise-call-expression" "^7.22.5"
+
"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz#cbdc27d6d8d18cd22c81ae4293765a5d9afd0779"
@@ -328,6 +431,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
+"@babel/helper-string-parser@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83"
+ integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==
+
"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.22.5":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
@@ -338,6 +446,20 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac"
integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==
+"@babel/helper-validator-option@^7.23.5":
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307"
+ integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==
+
+"@babel/helper-wrap-function@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569"
+ integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==
+ dependencies:
+ "@babel/helper-function-name" "^7.22.5"
+ "@babel/template" "^7.22.15"
+ "@babel/types" "^7.22.19"
+
"@babel/helper-wrap-function@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.9.tgz#189937248c45b0182c1dcf32f3444ca153944cb9"
@@ -347,7 +469,16 @@
"@babel/template" "^7.22.5"
"@babel/types" "^7.22.5"
-"@babel/helpers@^7.17.9", "@babel/helpers@^7.22.6":
+"@babel/helpers@^7.22.5":
+ version "7.23.9"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d"
+ integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==
+ dependencies:
+ "@babel/template" "^7.23.9"
+ "@babel/traverse" "^7.23.9"
+ "@babel/types" "^7.23.9"
+
+"@babel/helpers@^7.22.6":
version "7.22.6"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.6.tgz#8e61d3395a4f0c5a8060f309fb008200969b5ecd"
integrity sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==
@@ -365,11 +496,25 @@
chalk "^2.4.2"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.17.9", "@babel/parser@^7.20.7", "@babel/parser@^7.22.5", "@babel/parser@^7.22.7":
+"@babel/highlight@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b"
+ integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.22.20"
+ chalk "^2.4.2"
+ js-tokens "^4.0.0"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.5", "@babel/parser@^7.22.7":
version "7.22.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae"
integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==
+"@babel/parser@^7.23.9":
+ version "7.23.9"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b"
+ integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==
+
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e"
@@ -637,6 +782,16 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
+"@babel/plugin-transform-async-generator-functions@^7.22.5":
+ version "7.23.9"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz#9adaeb66fc9634a586c5df139c6240d41ed801ce"
+ integrity sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-plugin-utils" "^7.22.5"
+ "@babel/helper-remap-async-to-generator" "^7.22.20"
+ "@babel/plugin-syntax-async-generators" "^7.8.4"
+
"@babel/plugin-transform-async-generator-functions@^7.22.7":
version "7.22.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.7.tgz#053e76c0a903b72b573cb1ab7d6882174d460a1b"
@@ -687,6 +842,20 @@
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-class-static-block" "^7.14.5"
+"@babel/plugin-transform-classes@^7.22.5":
+ version "7.23.8"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz#d08ae096c240347badd68cdf1b6d1624a6435d92"
+ integrity sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.22.5"
+ "@babel/helper-compilation-targets" "^7.23.6"
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-function-name" "^7.23.0"
+ "@babel/helper-plugin-utils" "^7.22.5"
+ "@babel/helper-replace-supers" "^7.22.20"
+ "@babel/helper-split-export-declaration" "^7.22.6"
+ globals "^11.1.0"
+
"@babel/plugin-transform-classes@^7.22.6":
version "7.22.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz#e04d7d804ed5b8501311293d1a0e6d43e94c3363"
@@ -1099,6 +1268,92 @@
"@babel/helper-create-regexp-features-plugin" "^7.22.5"
"@babel/helper-plugin-utils" "^7.22.5"
+"@babel/preset-env@7.22.5":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.5.tgz#3da66078b181f3d62512c51cf7014392c511504e"
+ integrity sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==
+ dependencies:
+ "@babel/compat-data" "^7.22.5"
+ "@babel/helper-compilation-targets" "^7.22.5"
+ "@babel/helper-plugin-utils" "^7.22.5"
+ "@babel/helper-validator-option" "^7.22.5"
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.5"
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.5"
+ "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2"
+ "@babel/plugin-syntax-async-generators" "^7.8.4"
+ "@babel/plugin-syntax-class-properties" "^7.12.13"
+ "@babel/plugin-syntax-class-static-block" "^7.14.5"
+ "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+ "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
+ "@babel/plugin-syntax-import-assertions" "^7.22.5"
+ "@babel/plugin-syntax-import-attributes" "^7.22.5"
+ "@babel/plugin-syntax-import-meta" "^7.10.4"
+ "@babel/plugin-syntax-json-strings" "^7.8.3"
+ "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+ "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+ "@babel/plugin-syntax-numeric-separator" "^7.10.4"
+ "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
+ "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+ "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
+ "@babel/plugin-syntax-top-level-await" "^7.14.5"
+ "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6"
+ "@babel/plugin-transform-arrow-functions" "^7.22.5"
+ "@babel/plugin-transform-async-generator-functions" "^7.22.5"
+ "@babel/plugin-transform-async-to-generator" "^7.22.5"
+ "@babel/plugin-transform-block-scoped-functions" "^7.22.5"
+ "@babel/plugin-transform-block-scoping" "^7.22.5"
+ "@babel/plugin-transform-class-properties" "^7.22.5"
+ "@babel/plugin-transform-class-static-block" "^7.22.5"
+ "@babel/plugin-transform-classes" "^7.22.5"
+ "@babel/plugin-transform-computed-properties" "^7.22.5"
+ "@babel/plugin-transform-destructuring" "^7.22.5"
+ "@babel/plugin-transform-dotall-regex" "^7.22.5"
+ "@babel/plugin-transform-duplicate-keys" "^7.22.5"
+ "@babel/plugin-transform-dynamic-import" "^7.22.5"
+ "@babel/plugin-transform-exponentiation-operator" "^7.22.5"
+ "@babel/plugin-transform-export-namespace-from" "^7.22.5"
+ "@babel/plugin-transform-for-of" "^7.22.5"
+ "@babel/plugin-transform-function-name" "^7.22.5"
+ "@babel/plugin-transform-json-strings" "^7.22.5"
+ "@babel/plugin-transform-literals" "^7.22.5"
+ "@babel/plugin-transform-logical-assignment-operators" "^7.22.5"
+ "@babel/plugin-transform-member-expression-literals" "^7.22.5"
+ "@babel/plugin-transform-modules-amd" "^7.22.5"
+ "@babel/plugin-transform-modules-commonjs" "^7.22.5"
+ "@babel/plugin-transform-modules-systemjs" "^7.22.5"
+ "@babel/plugin-transform-modules-umd" "^7.22.5"
+ "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5"
+ "@babel/plugin-transform-new-target" "^7.22.5"
+ "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.5"
+ "@babel/plugin-transform-numeric-separator" "^7.22.5"
+ "@babel/plugin-transform-object-rest-spread" "^7.22.5"
+ "@babel/plugin-transform-object-super" "^7.22.5"
+ "@babel/plugin-transform-optional-catch-binding" "^7.22.5"
+ "@babel/plugin-transform-optional-chaining" "^7.22.5"
+ "@babel/plugin-transform-parameters" "^7.22.5"
+ "@babel/plugin-transform-private-methods" "^7.22.5"
+ "@babel/plugin-transform-private-property-in-object" "^7.22.5"
+ "@babel/plugin-transform-property-literals" "^7.22.5"
+ "@babel/plugin-transform-regenerator" "^7.22.5"
+ "@babel/plugin-transform-reserved-words" "^7.22.5"
+ "@babel/plugin-transform-shorthand-properties" "^7.22.5"
+ "@babel/plugin-transform-spread" "^7.22.5"
+ "@babel/plugin-transform-sticky-regex" "^7.22.5"
+ "@babel/plugin-transform-template-literals" "^7.22.5"
+ "@babel/plugin-transform-typeof-symbol" "^7.22.5"
+ "@babel/plugin-transform-unicode-escapes" "^7.22.5"
+ "@babel/plugin-transform-unicode-property-regex" "^7.22.5"
+ "@babel/plugin-transform-unicode-regex" "^7.22.5"
+ "@babel/plugin-transform-unicode-sets-regex" "^7.22.5"
+ "@babel/preset-modules" "^0.1.5"
+ "@babel/types" "^7.22.5"
+ babel-plugin-polyfill-corejs2 "^0.4.3"
+ babel-plugin-polyfill-corejs3 "^0.8.1"
+ babel-plugin-polyfill-regenerator "^0.5.0"
+ core-js-compat "^3.30.2"
+ semver "^6.3.0"
+
"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.9.tgz#57f17108eb5dfd4c5c25a44c1977eba1df310ac7"
@@ -1231,7 +1486,16 @@
dependencies:
regenerator-runtime "^0.13.11"
-"@babel/template@^7.16.7", "@babel/template@^7.22.5", "@babel/template@^7.3.3":
+"@babel/template@^7.22.15", "@babel/template@^7.23.9":
+ version "7.23.9"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a"
+ integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==
+ dependencies:
+ "@babel/code-frame" "^7.23.5"
+ "@babel/parser" "^7.23.9"
+ "@babel/types" "^7.23.9"
+
+"@babel/template@^7.22.5", "@babel/template@^7.3.3":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==
@@ -1240,7 +1504,23 @@
"@babel/parser" "^7.22.5"
"@babel/types" "^7.22.5"
-"@babel/traverse@^7.17.9", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.7.2":
+"@babel/traverse@^7.22.5", "@babel/traverse@^7.23.9":
+ version "7.23.9"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950"
+ integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==
+ dependencies:
+ "@babel/code-frame" "^7.23.5"
+ "@babel/generator" "^7.23.6"
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-function-name" "^7.23.0"
+ "@babel/helper-hoist-variables" "^7.22.5"
+ "@babel/helper-split-export-declaration" "^7.22.6"
+ "@babel/parser" "^7.23.9"
+ "@babel/types" "^7.23.9"
+ debug "^4.3.1"
+ globals "^11.1.0"
+
+"@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.7.2":
version "7.22.8"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.8.tgz#4d4451d31bc34efeae01eac222b514a77aa4000e"
integrity sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==
@@ -1265,6 +1545,15 @@
"@babel/helper-validator-identifier" "^7.22.5"
to-fast-properties "^2.0.0"
+"@babel/types@^7.22.19", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.23.9":
+ version "7.23.9"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002"
+ integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==
+ dependencies:
+ "@babel/helper-string-parser" "^7.23.4"
+ "@babel/helper-validator-identifier" "^7.22.20"
+ to-fast-properties "^2.0.0"
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -3301,6 +3590,15 @@ babel-plugin-named-asset-import@^0.3.8:
resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz#6b7fa43c59229685368683c28bc9734f24524cc2"
integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==
+babel-plugin-polyfill-corejs2@^0.4.3:
+ version "0.4.8"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz#dbcc3c8ca758a290d47c3c6a490d59429b0d2269"
+ integrity sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==
+ dependencies:
+ "@babel/compat-data" "^7.22.6"
+ "@babel/helper-define-polyfill-provider" "^0.5.0"
+ semver "^6.3.1"
+
babel-plugin-polyfill-corejs2@^0.4.4:
version "0.4.5"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c"
@@ -3310,6 +3608,14 @@ babel-plugin-polyfill-corejs2@^0.4.4:
"@babel/helper-define-polyfill-provider" "^0.4.2"
semver "^6.3.1"
+babel-plugin-polyfill-corejs3@^0.8.1:
+ version "0.8.7"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz#941855aa7fdaac06ed24c730a93450d2b2b76d04"
+ integrity sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==
+ dependencies:
+ "@babel/helper-define-polyfill-provider" "^0.4.4"
+ core-js-compat "^3.33.1"
+
babel-plugin-polyfill-corejs3@^0.8.2:
version "0.8.3"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52"
@@ -3318,6 +3624,13 @@ babel-plugin-polyfill-corejs3@^0.8.2:
"@babel/helper-define-polyfill-provider" "^0.4.2"
core-js-compat "^3.31.0"
+babel-plugin-polyfill-regenerator@^0.5.0:
+ version "0.5.5"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz#8b0c8fc6434239e5d7b8a9d1f832bb2b0310f06a"
+ integrity sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==
+ dependencies:
+ "@babel/helper-define-polyfill-provider" "^0.5.0"
+
babel-plugin-polyfill-regenerator@^0.5.1:
version "0.5.2"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326"
@@ -3493,6 +3806,16 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4
node-releases "^2.0.12"
update-browserslist-db "^1.0.11"
+browserslist@^4.22.2, browserslist@^4.22.3:
+ version "4.23.0"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab"
+ integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==
+ dependencies:
+ caniuse-lite "^1.0.30001587"
+ electron-to-chromium "^1.4.668"
+ node-releases "^2.0.14"
+ update-browserslist-db "^1.0.13"
+
bser@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
@@ -3571,6 +3894,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001503:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8"
integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==
+caniuse-lite@^1.0.30001587:
+ version "1.0.30001588"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz#07f16b65a7f95dba82377096923947fb25bce6e3"
+ integrity sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==
+
case-sensitive-paths-webpack-plugin@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"
@@ -3868,6 +4196,13 @@ copy-to-clipboard@^3.2.0:
dependencies:
toggle-selection "^1.0.6"
+core-js-compat@^3.30.2, core-js-compat@^3.33.1:
+ version "3.36.0"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.36.0.tgz#087679119bc2fdbdefad0d45d8e5d307d45ba190"
+ integrity sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==
+ dependencies:
+ browserslist "^4.22.3"
+
core-js-compat@^3.31.0:
version "3.31.1"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.31.1.tgz#5084ad1a46858df50ff89ace152441a63ba7aae0"
@@ -4164,7 +4499,7 @@ debug@2.6.9, debug@^2.6.0:
dependencies:
ms "2.0.0"
-debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.0, debug@^4.3.2, debug@^4.3.4:
+debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -4480,6 +4815,11 @@ electron-to-chromium@^1.4.431:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.471.tgz#14cb056d0ce4bfa99df57946d57fe46c2330dac3"
integrity sha512-GpmGRC1vTl60w/k6YpQ18pSiqnmr0j3un//5TV1idPi6aheNfkT1Ye71tMEabWyNDO6sBMgAR+95Eb0eUUr1tA==
+electron-to-chromium@^1.4.668:
+ version "1.4.677"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.677.tgz#49ee77713516740bdde32ac2d1443c444f0dafe7"
+ integrity sha512-erDa3CaDzwJOpyvfKhOiJjBVNnMM0qxHq47RheVVwsSQrgBA9ZSGV9kdaOfZDPXcHzhG7lBxhj6A7KvfLJBd6Q==
+
emittery@^0.10.2:
version "0.10.2"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933"
@@ -6887,7 +7227,7 @@ json5@^1.0.2:
dependencies:
minimist "^1.2.0"
-json5@^2.1.2, json5@^2.2.0, json5@^2.2.1, json5@^2.2.2:
+json5@^2.1.2, json5@^2.2.0, json5@^2.2.2:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
@@ -7547,7 +7887,7 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
-nanoid@^3.3.4, nanoid@^3.3.6:
+nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
@@ -7607,6 +7947,11 @@ node-releases@^2.0.12:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
+node-releases@^2.0.14:
+ version "2.0.14"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
+ integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
+
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@@ -8499,12 +8844,12 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
-postcss@8.4.21:
- version "8.4.21"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
- integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
+postcss@8.4.31, postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.4:
+ version "8.4.31"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
+ integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
dependencies:
- nanoid "^3.3.4"
+ nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
@@ -8516,15 +8861,6 @@ postcss@^7.0.35:
picocolors "^0.2.1"
source-map "^0.6.1"
-postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.4:
- version "8.4.27"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
- integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
- dependencies:
- nanoid "^3.3.6"
- picocolors "^1.0.0"
- source-map-js "^1.0.2"
-
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -10726,6 +11062,14 @@ update-browserslist-db@^1.0.11:
escalade "^3.1.1"
picocolors "^1.0.0"
+update-browserslist-db@^1.0.13:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"
+ integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==
+ dependencies:
+ escalade "^3.1.1"
+ picocolors "^1.0.0"
+
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
diff --git a/metagrid_configs/metagrid_config b/metagrid_configs/metagrid_config
index 3c4371a97..ee2adaa36 100644
--- a/metagrid_configs/metagrid_config
+++ b/metagrid_configs/metagrid_config
@@ -24,6 +24,16 @@ KEYCLOAK_URL=
KEYCLOAK_REALM=
KEYCLOAK_CLIENT_ID=
+# Django redirects
+# https://docs.djangoproject.com/en/4.2/ref/settings/#logout-redirect-url
+DJANGO_LOGIN_REDIRECT_URL=
+DJANGO_LOGOUT_REDIRECT_URL=
+
+# Globus
+# https://app.globus.org/settings/developers/registration/confidential_client
+GLOBUS_CLIENT_KEY=
+GLOBUS_CLIENT_SECRET=
+
# postgress
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
@@ -40,6 +50,9 @@ REACT_APP_PREVIOUS_URL=metagrid
# https://github.com/aims-group/metagrid/tree/master/backend
REACT_APP_METAGRID_API_URL=
+# Authentication Method
+REACT_APP_AUTHENTICATION_METHOD=globus
+
# Globus
REACT_APP_GLOBUS_REDIRECT=http://localhost:3000/cart/items
REACT_APP_CLIENT_ID=7fa7ac4a-a051-4b26-836f-b292b5c5b268
@@ -66,6 +79,10 @@ REACT_APP_KEYCLOAK_URL=
REACT_APP_KEYCLOAK_REALM=
REACT_APP_KEYCLOAK_CLIENT_ID=
+# Django All Auth URLs
+REACT_APP_DJANGO_LOGIN_URL=
+REACT_APP_DJANGO_LOGOUT_URL=
+
# react-hotjar
# https://github.com/abdalla/react-hotjar
REACT_APP_HOTJAR_ID=