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)} + + } - 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); + }} > - - - {!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] !== '' && ( + + )} + {showQualityFlags && ( + + )} + + ), + }); + } + 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=