From 8b6a9894a04ce2c16408b490f803c8ed07041bc2 Mon Sep 17 00:00:00 2001 From: Carlos Downie <42552189+downiec@users.noreply.github.com> Date: Fri, 9 Jun 2023 16:59:34 -0700 Subject: [PATCH] V1.0.8 (#475) * Incrementing version number for next set of minor updates. * Pin dependencies (#461) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency @types/jest to v28.1.8 (#462) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Support backend settings in config (#482) * enable settings on the backend for urls * add the settings for workflow * add missing import * add newline * Update react monorepo to v18 (#478) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Pin dependencies (#477) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fixed tiny flake8 complaint * hotfix to the last update so that local containers have the required environment variables. Otherwise local build of backend will fail. * Update .django * Updating a few dependencies in the backend * Updating the Flake8 pre-commit config file to see if that resolves pre-commit issues... * Fixed typo in .coveragerc which caused migrations to be included in tests. Removed unneccessary type checking in the import headers of the test models. Updated coverage for some files that didn't have 100% coverage. Updated some django dependencies after testing to make sure nothing broke. Updated some deprecated imports to remove warnings. Updated pytest and coverage libraries. * Upgraded to Django 4.1.7, along with some other python packages. * Upgraded more python packages while testing to make sure nothing breaks. * Reverted dj-rest-auth as it caused issues with tests failing (although the application from users perspective was working fine) * Updated dj-rest-auth to latest (before breaking changes) * Updating front-end to use the latest react-router-dom major version 6 * Removed some comments and updated some tests. Tests still failing, need to troubleshoot. * Updated tests to use memory router to account for the useNavigate changes that came with react-router-dom v6. Tests are passing now. * Updated some more frontend packages, however a few updates are breaking and will cause errors, so ignored those. * Updated yarn.lock file to see if tests pass. * Feature/500 info notifications (#520) * Refactored the joyrideTutorials in order to reduce the chances for errors with misspelling target names. Created unique target calssnames to simplify the process of adding and using targets. Removed the chance of duplicate target names causing errors etc. Added a test startup component that should show when first opening metagrid. Startup window still needs some work to improve visuals. * Added a welcome and changelog template component to use with thh startup display. Made the startup display change based on existing version so that a new changelog will appear when version is changed. Modified the startup component behavior to hadle a welcome and changelog template based on whether its first-time startup or not. * Updated django-all-auth to 0.54.0 * Upgraded the antd library to latest version. Updated components to work with the latests antd library and added some styling to correct some undesired changes. Added a temporary drawer component which can be opened by clicking a new message button in the top-right navbar. Fixed some styling issues with window resizing to make the top navbar work better with smaller windows and improve visual behavior. Removed redundant components that are no longer needed with the new antd library (which is now working with typescript well). Created a popup window that shows at the start for new users. Created a startup template system that allows different templates to be used for displaying popup messages. More fixed needed for tests and the welcome popup tutorials need work. * Worked to correct issues with tests hanging by reverting back the antd library. Also updated to node version 18. Restored previously deleted files and will remove them later when all tests are passing. Still need to correct tests to pass. Still need to complete messaging feature and update tutorial. * Updated tests so that they pass, coverage will still be needed. * Updated the Startup template functionality and added actions that can be triggered by the templates. Updated the welcome message buttons to work properly and select between pages to start tutorials. * Removed redundant and unneccessary components as they are now directly taken from antd library. Removed commented out import statements. * Refactored some names and added messages for the right drawer. Added ability to create messages for the message bar and provide content from markdown files. Updated the changelog to have useful information on latest changes. Need to troubleshoot tests and complete coverage. * Added markdown file support for the messages. Did refactoring so messages on the right drawer and popups will show the markdown. Created new markdown files with version info and example message. Added flexibility to popup so that styles can be modified on a per message basis. Modified the changelog template and types. * Created a new welcome tour which will show users the help buttons to encourage them to use them if they have issues. Updated the navbar tour to include the new news button. Created new test files and updated tests to bring back 100% text coverage. Removed obsolete tour target related files, as they've been updated with target object class. Troubleshooted and ran tests to fix some bugs and added more functionality for jest tests. Deleted test markdown files and updated message markdown slightly. * modify the messages for release (#522) --------- Co-authored-by: Sasha Ames * Feature/500 info notifications (#523) * Refactored the joyrideTutorials in order to reduce the chances for errors with misspelling target names. Created unique target calssnames to simplify the process of adding and using targets. Removed the chance of duplicate target names causing errors etc. Added a test startup component that should show when first opening metagrid. Startup window still needs some work to improve visuals. * Added a welcome and changelog template component to use with thh startup display. Made the startup display change based on existing version so that a new changelog will appear when version is changed. Modified the startup component behavior to hadle a welcome and changelog template based on whether its first-time startup or not. * Updated django-all-auth to 0.54.0 * Upgraded the antd library to latest version. Updated components to work with the latests antd library and added some styling to correct some undesired changes. Added a temporary drawer component which can be opened by clicking a new message button in the top-right navbar. Fixed some styling issues with window resizing to make the top navbar work better with smaller windows and improve visual behavior. Removed redundant components that are no longer needed with the new antd library (which is now working with typescript well). Created a popup window that shows at the start for new users. Created a startup template system that allows different templates to be used for displaying popup messages. More fixed needed for tests and the welcome popup tutorials need work. * Worked to correct issues with tests hanging by reverting back the antd library. Also updated to node version 18. Restored previously deleted files and will remove them later when all tests are passing. Still need to correct tests to pass. Still need to complete messaging feature and update tutorial. * Updated tests so that they pass, coverage will still be needed. * Updated the Startup template functionality and added actions that can be triggered by the templates. Updated the welcome message buttons to work properly and select between pages to start tutorials. * Removed redundant and unneccessary components as they are now directly taken from antd library. Removed commented out import statements. * Refactored some names and added messages for the right drawer. Added ability to create messages for the message bar and provide content from markdown files. Updated the changelog to have useful information on latest changes. Need to troubleshoot tests and complete coverage. * Added markdown file support for the messages. Did refactoring so messages on the right drawer and popups will show the markdown. Created new markdown files with version info and example message. Added flexibility to popup so that styles can be modified on a per message basis. Modified the changelog template and types. * Created a new welcome tour which will show users the help buttons to encourage them to use them if they have issues. Updated the navbar tour to include the new news button. Created new test files and updated tests to bring back 100% text coverage. Removed obsolete tour target related files, as they've been updated with target object class. Troubleshooted and ran tests to fix some bugs and added more functionality for jest tests. Deleted test markdown files and updated message markdown slightly. * modify the messages for release (#522) * Updated the node status information alert so that text displays if there is an api error. * Updated Django to 4.1.9 * Fixed a test regarding the changes to the error message. --------- Co-authored-by: Sasha Ames * Added mip_era and data_node facets to the input4mips project. * Updated the general facet order for input 4 mips * Minor format fix applied to help resolve pre-commit error. * Updated the requests package to latest to resolve pyup.io/safety issue... --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Sasha Ames --- .github/workflows/backend.yml | 4 + .github/workflows/frontend.yml | 4 +- .github/workflows/pre-commit.yml | 4 +- .pre-commit-config.yaml | 4 +- backend/.coveragerc | 4 +- backend/.envs/.django | 12 + backend/config/settings/base.py | 5 + backend/metagrid/api_proxy/views.py | 25 +- .../metagrid/cart/migrations/0001_initial.py | 7 +- backend/metagrid/cart/tests/factories.py | 4 +- backend/metagrid/cart/tests/test_models.py | 6 +- backend/metagrid/initial_projects_data.py | 7 +- .../projects/migrations/0001_initial.py | 2 +- .../migrations/scripts/import_functions.py | 6 +- .../metagrid/projects/tests/test_models.py | 7 +- backend/metagrid/users/admin.py | 2 +- .../metagrid/users/migrations/0001_initial.py | 6 +- backend/metagrid/users/models.py | 2 +- backend/requirements/base.txt | 36 +- backend/requirements/local.txt | 22 +- docs/requirements.txt | 2 +- frontend/.envs/.react | 20 +- frontend/package.json | 62 +- frontend/public/changelog/v1.0.7-beta.md | 10 + frontend/public/changelog/v1.0.8-beta.md | 15 + frontend/public/messages/metagrid_messages.md | 13 + frontend/public/messages/test_message.md | 3 + frontend/src/common/JoyrideTour.ts | 8 + frontend/src/common/TargetObject.test.ts | 41 + frontend/src/common/TargetObject.ts | 56 + frontend/src/common/TourTargets.test.ts | 47 - frontend/src/common/TourTargets.ts | 72 - frontend/src/common/reactJoyrideSteps.ts | 427 ++-- frontend/src/components/App/App.test.tsx | 18 +- frontend/src/components/App/App.tsx | 93 +- frontend/src/components/Cart/Items.tsx | 11 +- frontend/src/components/Cart/Searches.tsx | 2 +- .../src/components/Cart/SearchesCard.test.tsx | 15 +- frontend/src/components/Cart/SearchesCard.tsx | 20 +- frontend/src/components/Cart/Summary.tsx | 5 +- frontend/src/components/Cart/index.test.tsx | 8 +- frontend/src/components/Cart/index.tsx | 15 +- .../src/components/DataDisplay/Popover.tsx | 2 +- .../src/components/DataDisplay/ToolTip.tsx | 2 +- frontend/src/components/Facets/FacetsForm.tsx | 28 +- .../src/components/Facets/ProjectForm.tsx | 15 +- frontend/src/components/Facets/index.tsx | 6 +- frontend/src/components/Feedback/Alert.tsx | 25 - frontend/src/components/Feedback/Modal.tsx | 9 +- .../src/components/Feedback/Skeleton.test.tsx | 19 - frontend/src/components/Feedback/Skeleton.tsx | 16 - frontend/src/components/Feedback/Spin.tsx | 6 - .../components/Messaging/MessageCard.test.tsx | 13 + .../src/components/Messaging/MessageCard.tsx | 22 + .../components/Messaging/RightDrawer.test.tsx | 11 + .../src/components/Messaging/RightDrawer.tsx | 57 + .../components/Messaging/StartPopup.test.tsx | 150 ++ .../src/components/Messaging/StartPopup.tsx | 129 ++ .../Messaging/Templates/ChangeLog.tsx | 21 + .../Messaging/Templates/Welcome.tsx | 120 + .../Messaging/messageDisplayData.ts | 44 + frontend/src/components/Messaging/types.ts | 46 + .../src/components/NavBar/LeftMenu.test.tsx | 2 +- frontend/src/components/NavBar/LeftMenu.tsx | 20 +- frontend/src/components/NavBar/NavBar.css | 32 +- .../src/components/NavBar/RightMenu.test.tsx | 43 +- frontend/src/components/NavBar/RightMenu.tsx | 64 +- frontend/src/components/NavBar/index.test.tsx | 21 +- frontend/src/components/NavBar/index.tsx | 5 +- .../src/components/NodeStatus/NodeSummary.tsx | 2 +- .../src/components/NodeStatus/index.test.tsx | 18 +- frontend/src/components/NodeStatus/index.tsx | 15 +- frontend/src/components/Search/Citation.tsx | 3 +- frontend/src/components/Search/FilesTable.tsx | 19 +- frontend/src/components/Search/Table.tsx | 34 +- frontend/src/components/Search/Tabs.tsx | 12 +- frontend/src/components/Search/index.tsx | 15 +- .../src/components/Support/index.test.tsx | 21 +- frontend/src/components/Support/index.tsx | 1 + .../src/contexts/ReactJoyrideContext.test.tsx | 11 +- frontend/src/contexts/ReactJoyrideContext.tsx | 14 +- frontend/src/index.tsx | 6 +- frontend/src/setupTests.ts | 28 + frontend/src/test/custom-render.tsx | 6 +- frontend/yarn.lock | 2005 +++++++++++++---- 85 files changed, 3047 insertions(+), 1223 deletions(-) create mode 100644 frontend/public/changelog/v1.0.7-beta.md create mode 100644 frontend/public/changelog/v1.0.8-beta.md create mode 100644 frontend/public/messages/metagrid_messages.md create mode 100644 frontend/public/messages/test_message.md create mode 100644 frontend/src/common/TargetObject.test.ts create mode 100644 frontend/src/common/TargetObject.ts delete mode 100644 frontend/src/common/TourTargets.test.ts delete mode 100644 frontend/src/common/TourTargets.ts delete mode 100644 frontend/src/components/Feedback/Alert.tsx delete mode 100644 frontend/src/components/Feedback/Skeleton.test.tsx delete mode 100644 frontend/src/components/Feedback/Skeleton.tsx delete mode 100644 frontend/src/components/Feedback/Spin.tsx create mode 100644 frontend/src/components/Messaging/MessageCard.test.tsx create mode 100644 frontend/src/components/Messaging/MessageCard.tsx create mode 100644 frontend/src/components/Messaging/RightDrawer.test.tsx create mode 100644 frontend/src/components/Messaging/RightDrawer.tsx create mode 100644 frontend/src/components/Messaging/StartPopup.test.tsx create mode 100644 frontend/src/components/Messaging/StartPopup.tsx create mode 100644 frontend/src/components/Messaging/Templates/ChangeLog.tsx create mode 100644 frontend/src/components/Messaging/Templates/Welcome.tsx create mode 100644 frontend/src/components/Messaging/messageDisplayData.ts create mode 100644 frontend/src/components/Messaging/types.ts diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 338b41cfd..ffa942ac2 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -17,6 +17,10 @@ env: KEYCLOAK_REALM: metagrid KEYCLOAK_CLIENT_ID: backend DATABASE_URL: pgsql://postgres:postgres@localhost:5432/postgres + REACT_APP_ESGF_NODE_URL: https://esgf-node.llnl.gov/esg-search/search + REACT_APP_WGET_API_URL: https://greyworm1-rh7.llnl.gov/wget + REACT_APP_ESGF_NODE_STATUS_URL: https://aims4.llnl.gov/prometheus/api/v1/query?query=probe_success%7Bjob%3D%22http_2xx%22%2C+target%3D~%22.%2Athredds.%2A%22%7D + jobs: build: diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 7fbdba7ad..fb94d4792 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -24,10 +24,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Use Node.js 17.x + - name: Use Node.js 18.x uses: actions/setup-node@v2 with: - node-version: "17.x" + node-version: "18.x" - name: Cache node modules uses: actions/cache@v2 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index acf60454e..b85a9e083 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -13,10 +13,10 @@ jobs: uses: actions/checkout@v2 # Required to run the local ESLint hook - - name: Use Node.js 17.x + - name: Use Node.js 18.x uses: actions/setup-node@v2 with: - node-version: "17.x" + node-version: "18.x" - name: Cache node modules uses: actions/cache@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5310ff047..3276b46c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,8 +12,8 @@ repos: # Back-end # ------------------------------------------------------------------------------ - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.0 + - repo: https://github.com/pycqa/flake8 + rev: 3.9.2 hooks: - id: flake8 args: ["--config=backend/setup.cfg"] diff --git a/backend/.coveragerc b/backend/.coveragerc index 013c1eda4..6d00222ba 100644 --- a/backend/.coveragerc +++ b/backend/.coveragerc @@ -5,8 +5,8 @@ omit = *urls.py, *manage.py, *wsgi.py*, - *docs/* - *migrations/*, + *docs/*, + */migrations/*, *settings*, *postgres-data/*, *tests/*, diff --git a/backend/.envs/.django b/backend/.envs/.django index 3d53483b2..d1d6ba6b0 100644 --- a/backend/.envs/.django +++ b/backend/.envs/.django @@ -12,3 +12,15 @@ CORS_ORIGIN_WHITELIST=http://localhost:3000 KEYCLOAK_URL=http://keycloak:8080/auth KEYCLOAK_REALM=metagrid KEYCLOAK_CLIENT_ID=backend + +# ESGF wget API +# https://github.com/ESGF/esgf-wget +REACT_APP_WGET_API_URL=https://nimbus3.llnl.gov/wget + +# ESGF Search API +# https://esgf.github.io/esg-search/ESGF_Search_RESTful_API.html +REACT_APP_ESGF_NODE_URL=https://esgf-node.llnl.gov/esg-search/search + +# ESGF Node Status API +# https://github.com/ESGF/esgf-utils/blob/master/node_status/query_prom.py +REACT_APP_ESGF_NODE_STATUS_URL=https://aims2.llnl.gov/metagrid-backend/proxy/status diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index 197c4ff48..da674717f 100755 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -296,3 +296,8 @@ # https://github.com/adamchainz/django-cors-headers#setup CORS_ORIGIN_ALLOW_ALL = False CORS_ORIGIN_WHITELIST = env.list("CORS_ORIGIN_WHITELIST") + + +SEARCH_URL = env("REACT_APP_ESGF_NODE_URL") +WGET_URL = env("REACT_APP_WGET_API_URL") +STATUS_URL = env("REACT_APP_ESGF_NODE_STATUS_URL") diff --git a/backend/metagrid/api_proxy/views.py b/backend/metagrid/api_proxy/views.py index 0ffb77d9c..9724f75e3 100644 --- a/backend/metagrid/api_proxy/views.py +++ b/backend/metagrid/api_proxy/views.py @@ -2,6 +2,7 @@ from urllib.parse import urlparse import requests +from django.conf import settings from django.http import HttpResponse, HttpResponseBadRequest from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods @@ -10,8 +11,12 @@ @require_http_methods(["GET", "POST"]) @csrf_exempt def do_search(request): - - return do_request(request, "https://esgf-node.llnl.gov/esg-search/search") + esgf_host = getattr( + settings, + "REACT_APP_SEARCH_URL", + "https://esgf-node.llnl.gov/esg-search/search", + ) + return do_request(request, esgf_host) @require_http_methods(["POST"]) @@ -49,9 +54,12 @@ def do_citation(request): @require_http_methods(["GET", "POST"]) @csrf_exempt def do_status(request): - resp = requests.get( - "https://aims4.llnl.gov/prometheus/api/v1/query?query=probe_success%7Bjob%3D%22http_2xx%22%2C+target%3D~%22.%2Athredds.%2A%22%7D" + status_url = getattr( + settings, + "REACT_APP_ESGF_NODE_STATUS_URL", + "https://aims4.llnl.gov/prometheus/api/v1/query?query=probe_success%7Bjob%3D%22http_2xx%22%2C+target%3D~%22.%2Athredds.%2A%22%7D", ) + resp = requests.get(status_url) if resp.status_code == 200: # pragma: no cover return HttpResponse(resp.text) else: # pragma: no cover @@ -61,7 +69,14 @@ def do_status(request): @require_http_methods(["GET", "POST"]) @csrf_exempt def do_wget(request): - return do_request(request, "https://esgf-node.llnl.gov/esg-search/wget") + return do_request( + request, + getattr( + settings, + "REACT_APP_WGET_API_URL", + "https://esgf-node.llnl.gov/esg-search/wget", + ), + ) def do_request(request, urlbase): diff --git a/backend/metagrid/cart/migrations/0001_initial.py b/backend/metagrid/cart/migrations/0001_initial.py index 7abc58cd5..08f8888d6 100644 --- a/backend/metagrid/cart/migrations/0001_initial.py +++ b/backend/metagrid/cart/migrations/0001_initial.py @@ -1,10 +1,11 @@ # Generated by Django 3.2.13 on 2022-06-01 02:13 -from django.conf import settings +import uuid + import django.contrib.postgres.fields -from django.db import migrations, models import django.db.models.deletion -import uuid +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/backend/metagrid/cart/tests/factories.py b/backend/metagrid/cart/tests/factories.py index 20cedce60..4e9730f57 100644 --- a/backend/metagrid/cart/tests/factories.py +++ b/backend/metagrid/cart/tests/factories.py @@ -14,8 +14,8 @@ class JSONFactory(factory.DictFactory): @classmethod def _generate(cls, create, attrs): - obj = super()._generate(create, attrs) - return json.dumps(obj) + obj = super()._generate(create, attrs) # pragma: no cover + return json.dumps(obj) # pragma: no cover class CartFactory(factory.django.DjangoModelFactory): diff --git a/backend/metagrid/cart/tests/test_models.py b/backend/metagrid/cart/tests/test_models.py index df936a7b5..bf55375bd 100644 --- a/backend/metagrid/cart/tests/test_models.py +++ b/backend/metagrid/cart/tests/test_models.py @@ -1,10 +1,6 @@ -from typing import TYPE_CHECKING - +from metagrid.cart.models import Cart, Search from metagrid.cart.tests.factories import CartFactory, SearchFactory -if TYPE_CHECKING: - from metagrid.cart.models import Cart, Search - class TestCart: def test__str__(self): diff --git a/backend/metagrid/initial_projects_data.py b/backend/metagrid/initial_projects_data.py index 8af7e23fe..45db76d89 100644 --- a/backend/metagrid/initial_projects_data.py +++ b/backend/metagrid/initial_projects_data.py @@ -147,7 +147,12 @@ "project_url": "https://esgf-node.llnl.gov/projects/input4mips/", "description": "input4MIPS (input datasets for Model Intercomparison Projects) is an activity to make available via ESGF the boundary condition and forcing datasets needed for CMIP6. Various datasets are needed for the pre-industrial control (piControl), AMIP, and historical simulations, and additional datasets are needed for many of the CMIP6-endorsed model intercomparison projects (MIPs) experiments. Earlier versions of many of these datasets were used in the 5th Coupled Model Intercomparison Project (CMIP5).", "facets_by_group": { - GROUPS[0]: ["target_mip_list", "dataset_status"], + GROUPS[0]: [ + "mip_era", + "target_mip_list", + "dataset_status", + "data_node", + ], GROUPS[1]: [ "institution_id", "source_id", diff --git a/backend/metagrid/projects/migrations/0001_initial.py b/backend/metagrid/projects/migrations/0001_initial.py index fbaec3d32..cf4dd88a7 100644 --- a/backend/metagrid/projects/migrations/0001_initial.py +++ b/backend/metagrid/projects/migrations/0001_initial.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.13 on 2022-05-13 03:10 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/backend/metagrid/projects/migrations/scripts/import_functions.py b/backend/metagrid/projects/migrations/scripts/import_functions.py index be2e463a1..b82b66ca7 100644 --- a/backend/metagrid/projects/migrations/scripts/import_functions.py +++ b/backend/metagrid/projects/migrations/scripts/import_functions.py @@ -8,11 +8,7 @@ ProjectFacet, ) - -from metagrid.initial_projects_data import ( - projects, - group_descriptions, -) +from metagrid.initial_projects_data import group_descriptions, projects def insert_data(apps, schema_editor): diff --git a/backend/metagrid/projects/tests/test_models.py b/backend/metagrid/projects/tests/test_models.py index 0a992ad50..8034d92c6 100644 --- a/backend/metagrid/projects/tests/test_models.py +++ b/backend/metagrid/projects/tests/test_models.py @@ -1,8 +1,6 @@ -from typing import TYPE_CHECKING - import pytest -from metagrid.projects.models import ProjectFacet +from metagrid.projects.models import Facet, FacetGroup, Project, ProjectFacet from metagrid.projects.tests.factories import ( FacetFactory, FacetGroupFactory, @@ -12,9 +10,6 @@ pytestmark = pytest.mark.django_db -if TYPE_CHECKING: - from metagrid.projects.models import Facet, FacetGroup, Project - class TestProjectModel: @pytest.fixture(autouse=True) diff --git a/backend/metagrid/users/admin.py b/backend/metagrid/users/admin.py index 916bf4621..f825c972a 100644 --- a/backend/metagrid/users/admin.py +++ b/backend/metagrid/users/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .models import User diff --git a/backend/metagrid/users/migrations/0001_initial.py b/backend/metagrid/users/migrations/0001_initial.py index 4d9424193..5f591751e 100644 --- a/backend/metagrid/users/migrations/0001_initial.py +++ b/backend/metagrid/users/migrations/0001_initial.py @@ -1,9 +1,11 @@ # Generated by Django 3.2.13 on 2022-06-01 02:56 -from django.db import migrations, models +import uuid + import django.utils.timezone +from django.db import migrations, models + import metagrid.users.models -import uuid class Migration(migrations.Migration): diff --git a/backend/metagrid/users/models.py b/backend/metagrid/users/models.py index 2ca3b058c..b7f28626a 100644 --- a/backend/metagrid/users/models.py +++ b/backend/metagrid/users/models.py @@ -3,7 +3,7 @@ from django.contrib.auth.models import AbstractUser, BaseUserManager from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from metagrid.cart.models import Cart diff --git a/backend/requirements/base.txt b/backend/requirements/base.txt index 4da005046..ec8631ac1 100644 --- a/backend/requirements/base.txt +++ b/backend/requirements/base.txt @@ -1,34 +1,34 @@ # Core # ------------------------------------------------------------------------------ -pytz==2022.1 # https://github.com/stub42/pytz -django==3.2.15 # https://www.djangoproject.com/ -django-environ==0.8.1 # https://github.com/joke2k/django-environ +pytz==2022.7.1 # https://github.com/stub42/pytz +django==4.1.9 # https://www.djangoproject.com/ +django-environ==0.9.0 # https://github.com/joke2k/django-environ gunicorn==20.1.0 # https://github.com/benoitc/gunicorn -newrelic==7.6.0.173 # https://pypi.org/project/newrelic/ -argon2-cffi==20.1.0 # https://github.com/hynek/argon2_cffi -requests==2.27.1 # https://github.com/psf/requests -whitenoise==5.3.0 # https://github.com/evansd/whitenoise +newrelic==8.7.0 # https://pypi.org/project/newrelic/ +argon2-cffi==21.3 # https://github.com/hynek/argon2_cffi +requests==2.31.0 # https://github.com/psf/requests +whitenoise==6.4.0 # https://github.com/evansd/whitenoise # Database # ------------------------------------------------------------------------------ -psycopg2-binary==2.9.3 # https://github.com/psycopg/psycopg2 +psycopg2-binary==2.9.5 # https://github.com/psycopg/psycopg2 # REST API # ------------------------------------------------------------------------------ -djangorestframework==3.13.1 # https://github.com/encode/django-rest-framework -Markdown==3.3.6 # https://pypi.org/project/Markdown/ -django-filter==2.4.0 # https://github.com/carltongibson/django-filter -django-cors-headers==3.11.0 # https://github.com/adamchainz/django-cors-headers -django-allauth==0.47.0 # https://github.com/pennersr/django-allauth -dj-rest-auth==2.2.2 # https://github.com/jazzband/dj-rest-auth -djangorestframework-simplejwt==4.8.0 # https://github.com/SimpleJWT/django-rest-framework-simplejwt/ -drf-yasg==1.20.0 # https://github.com/axnsan12/drf-yasg +djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework +Markdown==3.4.1 # https://pypi.org/project/Markdown/ +django-filter==22.1 # https://github.com/carltongibson/django-filter +django-cors-headers==3.14.0 # https://github.com/adamchainz/django-cors-headers +django-allauth==0.54.0 # https://github.com/pennersr/django-allauth +dj-rest-auth==2.2.8 # https://github.com/jazzband/dj-rest-auth +djangorestframework-simplejwt==5.2.2 # https://github.com/SimpleJWT/django-rest-framework-simplejwt/ +drf-yasg==1.21.5 # https://github.com/axnsan12/drf-yasg # Code quality # ------------------------------------------------------------------------------ -pre-commit==2.17.0 # https://github.com/pre-commit/pre-commit +pre-commit==3.1.1 # https://github.com/pre-commit/pre-commit # Model Tools # ------------------------------------------------------------------------------ -django-model-utils==4.2.0 # https://github.com/jazzband/django-model-utils +django-model-utils==4.3.1 # https://github.com/jazzband/django-model-utils django_unique_upload==0.2.1 # https://github.com/agconti/django-unique-upload diff --git a/backend/requirements/local.txt b/backend/requirements/local.txt index 023c91970..5bf32df78 100644 --- a/backend/requirements/local.txt +++ b/backend/requirements/local.txt @@ -2,23 +2,23 @@ # Code quality # ------------------------------------------------------------------------------ -flake8==3.9.2 # https://github.com/PyCQA/flake8 -flake8-isort==4.1.1 # https://github.com/gforcada/flake8-isort -black==20.8b1 # https://github.com/ambv/black -mypy==0.931 # https://github.com/python/mypy +flake8==6.0.0 # https://github.com/PyCQA/flake8 +flake8-isort==6.0.0 # https://github.com/gforcada/flake8-isort +black==23.1.0 # https://github.com/ambv/black +mypy==1.0.1 # https://github.com/python/mypy # Testing # ------------------------------------------------------------------------------ -django-stubs==1.9.0 # https://github.com/typeddjango/django-stubs -pytest==6.2.5 # https://github.com/pytest-dev/pytest -pytest-cov==2.12.1 # https://github.com/pytest-dev/pytest-cov +django-stubs==1.15.0 # https://github.com/typeddjango/django-stubs +pytest==7.2.1 # https://github.com/pytest-dev/pytest +pytest-cov==4.0.0 # https://github.com/pytest-dev/pytest-cov pytest-django==4.5.2 # https://github.com/pytest-dev/pytest-django -pytest-sugar==0.9.4 # https://github.com/Frozenball/pytest-sugar +pytest-sugar==0.9.6 # https://github.com/Frozenball/pytest-sugar pytest-watch==4.2.0 # https://github.com/joeyespo/pytest-watch factory-boy==3.2.1 # https://github.com/FactoryBoy/factory_boy # Developer Tools # ------------------------------------------------------------------------------ -ipdb==0.13.9 # https://github.com/gotcha/ipdb -ipython==7.31.1 # https://github.com/ipython/ipython -django-extensions==3.1.5 # https://github.com/django-extensions/django-extensions +ipdb==0.13.11 # https://github.com/gotcha/ipdb +ipython==8.11.0 # https://github.com/ipython/ipython +django-extensions==3.2.1 # https://github.com/django-extensions/django-extensions diff --git a/docs/requirements.txt b/docs/requirements.txt index 6512ec63f..44d40820b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -mkdocs==1.3.1 # https://www.mkdocs.org/ +mkdocs==1.4.2 # https://www.mkdocs.org/ mdx_truly_sane_lists==1.3 # https://github.com/radude/mdx_truly_sane_lists diff --git a/frontend/.envs/.react b/frontend/.envs/.react index 4bb42ac7e..e619961b9 100644 --- a/frontend/.envs/.react +++ b/frontend/.envs/.react @@ -1,31 +1,39 @@ +# =====================FRONTEND CONFIG==================== + +# Redirect the frontend to home page when old subdirectory is used (optional) +REACT_APP_PREVIOUS_URL=/metagrid + # MetaGrid API -# ------------------------------------------------------------------------------ # https://github.com/aims-group/metagrid/tree/master/backend REACT_APP_METAGRID_API_URL=http://localhost:8000 # ESGF wget API -# ------------------------------------------------------------------------------ # https://github.com/ESGF/esgf-wget REACT_APP_WGET_API_URL=https://esgf-node.llnl.gov/esg-search/wget # ESGF Search API -# ------------------------------------------------------------------------------ # https://esgf.github.io/esg-search/ESGF_Search_RESTful_API.html REACT_APP_ESGF_NODE_URL=https://esgf-node.llnl.gov # ESGF Node Status API -# ------------------------------------------------------------------------------ # https://github.com/ESGF/esgf-utils/blob/master/node_status/query_prom.py REACT_APP_ESGF_NODE_STATUS_URL=https://aims4.llnl.gov/prometheus/api/v1/query?query=probe_success%7Bjob%3D%22http_2xx%22%2C+target%3D~%22.%2Athredds.%2A%22%7D - # Keycloak -# ------------------------------------------------------------------------------ # https://github.com/keycloak/keycloak REACT_APP_KEYCLOAK_REALM=metagrid REACT_APP_KEYCLOAK_URL=http://keycloak:8080/auth REACT_APP_KEYCLOAK_CLIENT_ID=frontend +# react-hotjar +# https://github.com/abdalla/react-hotjar +REACT_APP_HOTJAR_ID= +REACT_APP_HOTJAR_SV= + +# react-ga +# https://github.com/react-ga/react-ga +REACT_APP_GOOGLE_ANALYTICS_TRACKING_ID= + # Temporary fix to remove Keycloak sourcemap error # ------------------------------------------------------------------------------ # https://github.com/react-keycloak/react-keycloak/issues/176 diff --git a/frontend/package.json b/frontend/package.json index 87271d35e..c94b15481 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "1.0.7-beta", + "version": "1.0.8-beta", "private": true, "scripts": { "build:local": "env-cmd -f .envs/.react react-scripts build", @@ -53,61 +53,65 @@ "./src/components/App/App.tsx": { "lines": 100 } + }, + "moduleNameMapper": { + "react-markdown": "/node_modules/react-markdown/react-markdown.min.js" } }, "dependencies": { - "@ant-design/icons": "4.6.2", - "@babel/plugin-syntax-flow": "^7.16.7", - "@babel/plugin-transform-react-jsx": "^7.17.3", + "@ant-design/icons": "5.0.1", + "@babel/plugin-syntax-flow": "7.16.7", + "@babel/plugin-transform-react-jsx": "7.17.3", "@react-keycloak/web": "3.4.0", "antd": "4.15.1", - "autoprefixer": "^10.4.4", - "axios": "^0.26.1", + "autoprefixer": "10.4.14", + "axios": "0.26.1", "dotenv": "8.2.0", "env-cmd": "10.1.0", "humps": "2.0.1", "keycloak-js": "17.0.1", "moment": "2.29.4", - "prop-types": "^15.8.1", + "prop-types": "15.8.1", "query-string": "7.0.0", - "react": "17.0.2", + "react": "18.2.0", "react-async": "10.0.1", - "react-dom": "17.0.2", + "react-dom": "18.2.0", "react-hotjar": "2.2.1", - "react-joyride": "^2.4.0", - "react-router-dom": "5.3.3", - "react-scripts": "^5.0.1", - "typescript": "^4.6.3", + "react-joyride": "2.5.3", + "react-markdown": "^8.0.7", + "react-router-dom": "^6.9.0", + "react-scripts": "5.0.1", + "typescript": "4.6.3", "uuid": "8.3.2" }, "devDependencies": { - "@babel/core": "^7.17.9", - "@testing-library/dom": "^8.13.0", - "@testing-library/jest-dom": "5.11.10", + "@babel/core": "7.17.9", + "@testing-library/dom": "8.13.0", + "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "11.2.6", "@testing-library/user-event": "13.1.2", - "@types/humps": "2.0.0", - "@types/jest": "28.1.6", + "@types/humps": "2.0.2", + "@types/jest": "28.1.8", "@types/node": "14.14.37", "@types/react": "17.0.3", "@types/react-dom": "17.0.3", - "@types/react-router-dom": "5.1.7", + "@types/react-router-dom": "5.3.3", "@types/uuid": "8.3.4", "@typescript-eslint/eslint-plugin": "5.30.7", "@typescript-eslint/parser": "5.30.7", - "dayjs": "^1.11.1", - "eslint": "^8.12.0", + "dayjs": "1.11.7", + "eslint": "8.12.0", "eslint-config-airbnb": "19.0.4", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.4", + "eslint-config-airbnb-typescript": "17.0.0", + "eslint-config-prettier": "8.5.0", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-jsx-a11y": "6.7.1", + "eslint-plugin-prettier": "4.0.0", + "eslint-plugin-react": "7.29.4", "eslint-plugin-react-hooks": "4.6.0", "msw": "0.28.1", - "postcss": "^8.4.12", + "postcss": "8.4.21", "prettier": "2.2.1", - "setimmediate": "^1.0.5" + "setimmediate": "1.0.5" } } diff --git a/frontend/public/changelog/v1.0.7-beta.md b/frontend/public/changelog/v1.0.7-beta.md new file mode 100644 index 000000000..d5af88acb --- /dev/null +++ b/frontend/public/changelog/v1.0.7-beta.md @@ -0,0 +1,10 @@ +## Summary + +This is the 'Deployment' update! Moving forward, deployment configuration has been improved, along with a few front-end UI improvements such as a new collapse/expand functionality added to the search facets section. + +**Changes** + +1. Added expand/collapse button for the search facets. +2. Updated deployment configuration implementation. +3. Fixed wget download issues with multiple dataset results. +4. Various package updates and some other minor bug fixes. diff --git a/frontend/public/changelog/v1.0.8-beta.md b/frontend/public/changelog/v1.0.8-beta.md new file mode 100644 index 000000000..f1d809fae --- /dev/null +++ b/frontend/public/changelog/v1.0.8-beta.md @@ -0,0 +1,15 @@ +## Summary + +This is the 'Messages' update! Moving forward, when a new Metagrid version is released, a window will open listing what are the latest changes in the update. This version includes a new 'News' tab at the top where you can view any important Metagrid related messages. First time users will also be prompted with a welcome message which includes links to the current U.I tours. For more details regarding this update, view the list below. + +**Changes** + +1. Added new notification drawer on the right which provides admins a way to communicate with users information relevant to Metagrid. Markdown docs can be displayed and content modified at run-time will be shown. +2. Created new Welcome dialog for first time users which includes buttons to start feature tours or view latest changes. +3. Created Change Log dialog that allows users to see details about latest update +4. Refactored the Joyride tours to improve ease and reliability of future updates +5. Updated test suite to handle latest major package updates and modifications +6. Migrated to the react-router-dom major version 6 +7. Upgraded to Django 4.1.7 and upgraded various backend dependencies +8. Added support for backend url settings +9. Updated various minor frontend dependencies including keycloak-js diff --git a/frontend/public/messages/metagrid_messages.md b/frontend/public/messages/metagrid_messages.md new file mode 100644 index 000000000..f8459c586 --- /dev/null +++ b/frontend/public/messages/metagrid_messages.md @@ -0,0 +1,13 @@ +# Welcome to the Metagrid Beta test v1.0.8 + +Use the Help link to find information on how to contact support or report any issues you find. + +## CORDEX data _not_ supported + +Metagrid uses an updated user accounts system. Unfortunately for anyone looking for CORDEX data, these new accounts cannot be used to authenticate when running a CORDEX Wget script. Please use an ESGF _legacy_ OpenID obtained at any of the ESGF CoG instances listed here: https://esgf.github.io/nodes.html + +## Upcoming changes to ESGF @LLNL + +We are excited to be planning to have an "official" release of the Metagrid platform onto scalable infrastructure. In the meantime we will be testing new features. + +- Globus Transfer feature planned to be released in v1.0.9. diff --git a/frontend/public/messages/test_message.md b/frontend/public/messages/test_message.md new file mode 100644 index 000000000..2a47d9a16 --- /dev/null +++ b/frontend/public/messages/test_message.md @@ -0,0 +1,3 @@ +# This is just a test + +Blah blah diff --git a/frontend/src/common/JoyrideTour.ts b/frontend/src/common/JoyrideTour.ts index cdf89f6ce..2ef6f63e3 100644 --- a/frontend/src/common/JoyrideTour.ts +++ b/frontend/src/common/JoyrideTour.ts @@ -55,6 +55,14 @@ export class JoyrideTour { await func(); } + /** + * + * @param target The element to highlight (CSS selector or an HTML Element) + * @param content The content of the tour window + * @param placement Default location for displaying the window + * @param action A function to call when the tour passes this step + * @returns This tour object + */ addNextStep( target: string, content: string, diff --git a/frontend/src/common/TargetObject.test.ts b/frontend/src/common/TargetObject.test.ts new file mode 100644 index 000000000..d5e6dbee3 --- /dev/null +++ b/frontend/src/common/TargetObject.test.ts @@ -0,0 +1,41 @@ +import { TargetObject, createTestTour } from './TargetObject'; + +describe('Test TourTarget class', () => { + it('returns target object with uuid', () => { + const targetObject: TargetObject = new TargetObject(); + expect(targetObject).toBeTruthy(); + expect(targetObject.class()).toContain('joyrideTarget-'); + expect(targetObject.selector()).toContain('.joyrideTarget-'); + }); + it('returns target object with specified className', () => { + const targetObject: TargetObject = new TargetObject('testClass'); + expect(targetObject).toBeTruthy(); + expect(targetObject.class()).toEqual('testClass'); + expect(targetObject.selector()).toEqual('#root .testClass'); + expect(targetObject.class('testState')).toEqual( + 'testClass target-state_testState' + ); + expect(targetObject.selector('testState')).toEqual( + '#root .testClass .target-state_testState' + ); + }); + it('returns target object with uuid', () => { + const targetObject: TargetObject = new TargetObject('testGroup', 'testId'); + expect(targetObject).toBeTruthy(); + expect(targetObject.class()).toEqual('testGroup_target-testId'); + expect(targetObject.selector()).toEqual('.testGroup_target-testId'); + }); + + it('Successfully creates an empty test joyride tour', () => { + createTestTour('testGroup', {}); + }); + + it('Successfully creates joyride tour fron test targets', () => { + const testTargets = { + test1: new TargetObject('test1'), + test2: new TargetObject('test2'), + test3: new TargetObject('test3'), + }; + createTestTour('testGroup', testTargets); + }); +}); diff --git a/frontend/src/common/TargetObject.ts b/frontend/src/common/TargetObject.ts new file mode 100644 index 000000000..2931f080e --- /dev/null +++ b/frontend/src/common/TargetObject.ts @@ -0,0 +1,56 @@ +import { v4 as uuidv4 } from 'uuid'; +import { JoyrideTour } from './JoyrideTour'; + +export class TargetObject { + private className: string; + + private selectorName: string; + + /** + * + * @param args + */ + public constructor(...args: string[]) { + if (args.length === 0) { + const targetId = uuidv4(); + this.className = `joyrideTarget-${targetId}`; + this.selectorName = `.joyrideTarget-${targetId}`; + } else if (args.length === 1) { + const className = args[0]; + this.className = className; + this.selectorName = `#root .${className}`; + } else { + const groupName = args[0]; + const id = args[1]; + this.className = `${groupName}_target-${id}`; + this.selectorName = `.${groupName}_target-${id}`; + } + } + + class(state?: string): string { + return `${this.className}${state ? ` target-state_${state}` : ''}`; + } + + selector(state?: string): string { + return `${this.selectorName}${state ? ` .target-state_${state}` : ''}`; + } +} + +export const createTestTour = ( + groupName: string, + targetGroup: { + [target: string]: TargetObject; + } +): JoyrideTour => { + const testTour = new JoyrideTour(`Test Tour of ${Object.name} Targets`); + Object.entries(targetGroup).forEach((entry) => { + const name = entry[0]; + const target = entry[1]; + testTour.addNextStep( + target.selector(), + `Group: ${groupName}__ Name: ${name}__ Selector: ${target.selector()}` + ); + }); + + return testTour; +}; diff --git a/frontend/src/common/TourTargets.test.ts b/frontend/src/common/TourTargets.test.ts deleted file mode 100644 index b3e18fe87..000000000 --- a/frontend/src/common/TourTargets.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { TourTargets } from './TourTargets'; - -describe('Test TourTarget class', () => { - it('returns default tour targets', () => { - expect(new TourTargets('Test Targets')).toBeTruthy(); - }); - - it('Creates appropriate tour targets and strings', () => { - const testTargets = new TourTargets('TestTargets'); - testTargets.create('firstTarget').create('nextTarget'); - - // Make sure bad target id returns default - expect(testTargets.getTarget('badTargetId')).toBeUndefined(); - expect(testTargets.getClass('badTargetId')).toEqual('navbar-logo'); - expect(testTargets.getSelector('badTargetId')).toEqual( - '#root .navbar-logo' - ); - - // Good target returns proper values - expect(testTargets.getTarget('firstTarget')?.name).toEqual('firstTarget'); - expect(testTargets.getTarget('firstTarget')?.class).toEqual( - testTargets.getClass('firstTarget') - ); - expect(testTargets.getTarget('firstTarget')?.selector).toEqual( - testTargets.getSelector('firstTarget') - ); - expect(testTargets.getClass('firstTarget', 'test')).toContain( - 'target-state_test' - ); - expect(testTargets.getSelector('firstTarget', 'test')).toContain( - '.target-state_test' - ); - }); - - it('returns a joyride tour of targets', () => { - const testTargets = new TourTargets('TestTargets'); - testTargets.create('firstTarget').create('nextTarget'); - - const testTour = testTargets.createTestTourOfTargets(); - - // Test that a test tour is created - expect(testTour.getSteps().length).toEqual(2); - expect(testTour.getActionByStepIndex(0)?.step.target).toEqual( - testTargets.getSelector('firstTarget') - ); - }); -}); diff --git a/frontend/src/common/TourTargets.ts b/frontend/src/common/TourTargets.ts deleted file mode 100644 index 3ecbafd05..000000000 --- a/frontend/src/common/TourTargets.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { JoyrideTour } from './JoyrideTour'; - -/** - * name: The identifier to use when locating the target - * class: The actual classname of the target - * selector: The css selector which can be used to get the target - */ -export type TourTarget = { - name: string; - class: string; - selector: string; -}; - -export class TourTargets { - private name: string; - - private targets: Map; - - private defaultTarget: TourTarget; - - constructor(name: string) { - this.name = name; - this.targets = new Map(); - this.defaultTarget = { - name: 'default', - class: 'navbar-logo', - selector: '#root .navbar-logo', - }; - } - - create(targetId: string): TourTargets { - const newTarget: TourTarget = { - name: targetId, - class: `${this.name}_target-${targetId}`, - selector: `.${this.name}_target-${targetId}`, - }; - this.targets.set(targetId, newTarget); - return this; - } - - getClass(targetId: string, state?: string): string { - const target = this.targets.get(targetId); - if (target) { - return `${target.class}${state ? ` target-state_${state}` : ''}`; - } - return this.defaultTarget.class; - } - - getSelector(targetId: string, state?: string): string { - const target = this.targets.get(targetId); - if (target) { - return `${target.selector}${state ? `.target-state_${state}` : ''}`; - } - return this.defaultTarget.selector; - } - - getTarget(targetId: string): TourTarget | undefined { - return this.targets.get(targetId); - } - - createTestTourOfTargets(): JoyrideTour { - const testTour = new JoyrideTour(`Test Tour of ${this.name} Targets`); - this.targets.forEach((target) => { - testTour.addNextStep( - target.selector, - `Target Name: ${target.name} __ Target Selector: ${target.selector}` - ); - }); - - return testTour; - } -} diff --git a/frontend/src/common/reactJoyrideSteps.ts b/frontend/src/common/reactJoyrideSteps.ts index 4a01bbcbb..c536bf54a 100644 --- a/frontend/src/common/reactJoyrideSteps.ts +++ b/frontend/src/common/reactJoyrideSteps.ts @@ -1,5 +1,5 @@ import { JoyrideTour } from './JoyrideTour'; -import { TourTargets } from './TourTargets'; +import { TargetObject } from './TargetObject'; import { AppPage } from './types'; export const getCurrentAppPage = (): number => { @@ -75,220 +75,246 @@ const searchLibraryIsEmpty = (): boolean => { return false; }; +export const defaultTarget = new TargetObject('navbar-logo'); + +export const miscTargets = { + defaultTarget, + questionBtn: new TargetObject(), +}; + +export const navBarTargets = { + topSearchBar: new TargetObject(), + topNavBar: new TargetObject(), + searchPageBtn: new TargetObject(), + cartPageBtn: new TargetObject(), + savedSearchPageBtn: new TargetObject(), + nodeStatusBtn: new TargetObject(), + newsBtn: new TargetObject(), + signInBtn: new TargetObject(), + helpBtn: new TargetObject(), +}; + +export const searchTableTargets = { + queryString: new TargetObject(), + resultsFoundText: new TargetObject(), + searchResultsTable: new TargetObject(), + addSelectedToCartBtn: new TargetObject(), + saveSearchBtn: new TargetObject(), + copySearchLinkBtn: new TargetObject(), +}; + +export const leftSidebarTargets = { + leftSideBar: new TargetObject(), + selectProjectBtn: new TargetObject(), + projectSelectLeftSideBtn: new TargetObject(), + projectWebsiteBtn: new TargetObject(), + searchFacetsForm: new TargetObject(), + facetFormGeneral: new TargetObject(), + facetFormFields: new TargetObject(), + facetFormCollapseAllBtn: new TargetObject(), + facetFormExpandAllBtn: new TargetObject(), + facetFormAdditional: new TargetObject(), + facetFormAdditionalFields: new TargetObject(), + facetFormFilename: new TargetObject(), + facetFormFilenameFields: new TargetObject(), +}; + +export const topDataRowTargets = { + searchResultsRowExpandIcon: new TargetObject(), + searchResultsRowContractIcon: new TargetObject(), + cartAddBtn: new TargetObject(), + nodeStatusIcon: new TargetObject(), + datasetTitle: new TargetObject(), + fileCount: new TargetObject(), + totalSize: new TargetObject(), + versionText: new TargetObject(), + downloadScriptForm: new TargetObject(), + downloadScriptOptions: new TargetObject(), + downloadScriptBtn: new TargetObject(), +}; + +export const innerDataRowTargets = { + filesTab: new TargetObject(), + metadataTab: new TargetObject(), + metadataLookupField: new TargetObject(), + citationTab: new TargetObject(), + additionalTab: new TargetObject(), + filesTitle: new TargetObject(), + dataSize: new TargetObject(), + downloadDataBtn: new TargetObject(), + copyUrlBtn: new TargetObject(), + checksum: new TargetObject(), +}; + +export const cartTourTargets = { + cartSummary: new TargetObject(), + datasetBtn: new TargetObject(), + libraryBtn: new TargetObject(), + downloadAllType: new TargetObject(), + downloadAllBtn: new TargetObject(), + removeItemsBtn: new TargetObject(), +}; + +export const savedSearchTourTargets = { + savedSearches: new TargetObject(), + projectDescription: new TargetObject(), + searchQueryString: new TargetObject(), + applySearch: new TargetObject(), + jsonBtn: new TargetObject(), + removeBtn: new TargetObject(), +}; + +export const nodeTourTargets = { + updateTime: new TargetObject(), + nodeStatusSummary: new TargetObject(), + nodeColHeader: new TargetObject(), + onlineColHeader: new TargetObject(), + sourceColHeader: new TargetObject(), +}; + // Used when creating the tour, as the title that user sees export enum TourTitles { Main = 'Main Search Page Tour', Cart = 'Data Cart Tour', Searches = 'Saved Searches Tour', Node = 'Node Status Tour', + Welcome = 'Welcome Tour', } -export const navBarTargets = new TourTargets('nav-bar-tour') - .create('topSearchBar') - .create('topNavBar') - .create('searchPageBtn') - .create('cartPageBtn') - .create('savedSearchPageBtn') - .create('nodeStatusBtn') - .create('signInBtn'); - -export const searchTableTargets = new TourTargets('search-table-tour') - .create('queryString') - .create('resultsFoundText') - .create('searchResultsTable') - .create('addSelectedToCartBtn') - .create('saveSearchBtn') - .create('copySearchLinkBtn'); - -export const leftSidebarTargets = new TourTargets('left-sidebar-tour') - .create('leftSideBar') - .create('selectProjectBtn') - .create('projectSelectLeftSideBtn') - .create('projectWebsiteBtn') - .create('searchFacetsForm') - .create('facetFormExpandAllBtn') - .create('facetFormCollapseAllBtn') - .create('facetFormGeneral') - .create('facetFormFields') - .create('facetFormAdditional') - .create('facetFormAdditionalFields') - .create('facetFormFilename') - .create('facetFormFilenameFields'); - -export const topDataRowTargets = new TourTargets('top-data-row-tour') - .create('searchResultsRowExpandIcon') - .create('searchResultsRowContractIcon') - .create('cartAddBtn') - .create('nodeStatusIcon') - .create('datasetTitle') - .create('fileCount') - .create('totalSize') - .create('versionText') - .create('downloadScriptForm') - .create('downloadScriptOptions') - .create('downloadScriptBtn'); - -export const innerDataRowTargets = new TourTargets('inner-data-row-tour') - .create('filesTab') - .create('metadataTab') - .create('metadataLookupField') - .create('citationTab') - .create('additionalTab') - .create('filesTitle') - .create('dataSize') - .create('downloadDataBtn') - .create('copyUrlBtn') - .create('checksum'); - -export const cartTourTargets = new TourTargets('cart-tour') - .create('cartSummary') - .create('datasetBtn') - .create('libraryBtn') - .create('downloadAllType') - .create('downloadAllBtn') - .create('removeItemsBtn'); - -export const savedSearchTourTargets = new TourTargets('saved-search-tour') - .create('savedSearches') - .create('projectDescription') - .create('searchQueryString') - .create('applySearch') - .create('jsonBtn') - .create('removeBtn'); - -export const nodeTourTargets = new TourTargets('node-tour') - .create('updateTime') - .create('nodeStatusSummary') - .create('nodeColHeader') - .create('onlineColHeader') - .create('sourceColHeader'); - const addDataRowTourSteps = (tour: JoyrideTour): JoyrideTour => { tour .addNextStep( - topDataRowTargets.getSelector('datasetTitle'), + topDataRowTargets.datasetTitle.selector(), 'Each row provides access to a specific dataset. The title of the dataset is shown here.', 'top-start' ) .addNextStep( - topDataRowTargets.getSelector('nodeStatusIcon'), + topDataRowTargets.nodeStatusIcon.selector(), "This icon shows the current status of the node which hosts this dataset. When hovering over the icon you will see more detail as to the node's status.", 'top-start' ) .addNextStep( - topDataRowTargets.getSelector('fileCount'), + topDataRowTargets.fileCount.selector(), 'This shows how many separate files are contained in this dataset.', 'top-start' ) .addNextStep( - topDataRowTargets.getSelector('totalSize'), + topDataRowTargets.totalSize.selector(), 'This shows the total size of the dataset with all of its files.', 'top-start' ) .addNextStep( - topDataRowTargets.getSelector('versionText'), + topDataRowTargets.versionText.selector(), 'The version number or preparation date is shown in this column (depending on the dataset).', 'top-start' ) .addNextStep( - topDataRowTargets.getSelector('downloadScriptForm'), + topDataRowTargets.downloadScriptForm.selector(), 'If you wish to download the entire dataset, you can do so by first obtaining the download script.', 'top-start' ) .addNextStep( - topDataRowTargets.getSelector('downloadScriptOptions'), + topDataRowTargets.downloadScriptOptions.selector(), 'This drop-down allows you to select which type of script you wish to download. Currently wget is the only form supported.', 'top' ) .addNextStep( - topDataRowTargets.getSelector('downloadScriptBtn'), + topDataRowTargets.downloadScriptBtn.selector(), 'Clicking this button will begin the download of your script.', 'top' ) .addNextStep( - topDataRowTargets.getSelector('searchResultsRowExpandIcon'), + topDataRowTargets.searchResultsRowExpandIcon.selector(), 'To view more information about a specific dataset, you can expand the row by clicking this little arrow icon...', 'top-start', /* istanbul ignore next */ async () => { clickFirstElement( - topDataRowTargets.getSelector('searchResultsRowExpandIcon') + topDataRowTargets.searchResultsRowExpandIcon.selector() ); await delay(500); } ) .addNextStep( - innerDataRowTargets.getSelector('filesTab'), + innerDataRowTargets.filesTab.selector(), 'The file information tab is open by default. Within this tab, it is possible to view individual files in the dataset for access and download.', 'top-start' ) .addNextStep( - innerDataRowTargets.getSelector('filesTitle'), + innerDataRowTargets.filesTitle.selector(), 'This shows the title of a specific file contained within the dataset.', 'top-start' ) .addNextStep( - innerDataRowTargets.getSelector('dataSize'), + innerDataRowTargets.dataSize.selector(), 'This shows the size of the specific file in the dataset.', 'top-start' ) .addNextStep( - innerDataRowTargets.getSelector('downloadDataBtn'), + innerDataRowTargets.downloadDataBtn.selector(), 'Clicking this button will initiate a direct download of this data file via HTTPS.', 'top-start' ) .addNextStep( - innerDataRowTargets.getSelector('copyUrlBtn'), + innerDataRowTargets.copyUrlBtn.selector(), 'Clicking this button will copy the OPEN DAP URL of this file directly to your clipboard.', 'top-start' ) .addNextStep( - innerDataRowTargets.getSelector('checksum'), + innerDataRowTargets.checksum.selector(), 'The checksum of the specified file is shown here.', 'top-start' ) .addNextStep( - innerDataRowTargets.getSelector('metadataTab'), + innerDataRowTargets.metadataTab.selector(), 'This is the Metadata tab. If you click it, you can view metadata for the dataset...', 'top-start', /* istanbul ignore next */ async () => { await delay(300); - clickFirstElement(innerDataRowTargets.getSelector('metadataTab')); + clickFirstElement(innerDataRowTargets.metadataTab.selector()); } ) .addNextStep( - innerDataRowTargets.getSelector('metadataLookupField'), + innerDataRowTargets.metadataLookupField.selector(), 'Besides seeing the metadata listed below, this field can help you search for a specific key/value pair of metadata.', 'top-start', /* istanbul ignore next */ async () => { await delay(300); - if (elementExists(innerDataRowTargets.getClass('citationTab'))) { - clickFirstElement(innerDataRowTargets.getSelector('citationTab')); + if (elementExists(innerDataRowTargets.citationTab.class())) { + clickFirstElement(innerDataRowTargets.citationTab.selector()); + } else if (!elementExists(innerDataRowTargets.additionalTab.class())) { + clickFirstElement( + topDataRowTargets.searchResultsRowContractIcon.selector() + ); } } ) .addNextStep( - innerDataRowTargets.getSelector('citationTab'), + innerDataRowTargets.citationTab.selector(), 'Citation information for the dataset can be viewed within this tab...', 'top-start', /* istanbul ignore next */ async () => { await delay(300); - if (elementExists(innerDataRowTargets.getClass('additionalTab'))) { - clickFirstElement(innerDataRowTargets.getSelector('additionalTab')); + if (elementExists(innerDataRowTargets.additionalTab.class())) { + clickFirstElement(innerDataRowTargets.additionalTab.selector()); + } else { + clickFirstElement( + topDataRowTargets.searchResultsRowContractIcon.selector() + ); } } ) .addNextStep( - innerDataRowTargets.getSelector('additionalTab'), + innerDataRowTargets.additionalTab.selector(), 'You can view additional data and sources by clicking this tab.', 'top-start', /* istanbul ignore next */ async () => { clickFirstElement( - topDataRowTargets.getSelector('searchResultsRowContractIcon') + topDataRowTargets.searchResultsRowContractIcon.selector() ); await delay(300); } @@ -297,6 +323,23 @@ const addDataRowTourSteps = (tour: JoyrideTour): JoyrideTour => { return tour; }; +export const welcomeTour = new JoyrideTour(TourTitles.Welcome) + .addNextStep( + 'body', + 'Just a note: We are continually striving to improve the Metagrid user interface and make it more intuitive. However, if you ever feel stuck, please try out the interface tours. The following is a quick tour showing where you can access support.', + 'center' + ) + .addNextStep( + navBarTargets.helpBtn.selector(), + 'This help button will open the Metagrid support dialog, which contains interface tours (like this one) as well as helpful resources.', + 'bottom' + ) + .addNextStep( + miscTargets.questionBtn.selector(), + 'This question button will also open the Metagrid support dialog. Note that the tour button shown in the support dialog will be specific to the current page you are on.', + 'top-end' + ); + export const createMainPageTour = (): JoyrideTour => { const tour = new JoyrideTour(TourTitles.Main) .addNextStep( @@ -305,42 +348,52 @@ export const createMainPageTour = (): JoyrideTour => { 'center' ) .addNextStep( - navBarTargets.getSelector('topSearchBar'), + navBarTargets.topSearchBar.selector(), 'This is the top search bar! You can select a project, then enter a search term and click the magnifying glass button to quickly start your search and view results in the table below.', 'bottom' ) .addNextStep( - navBarTargets.getSelector('topNavBar'), + navBarTargets.topNavBar.selector(), 'This area lets you navigate between pages of Metagrid.', 'bottom' ) .addNextStep( - navBarTargets.getSelector('searchPageBtn'), + navBarTargets.searchPageBtn.selector(), "Clicking this button takes you to the main search page (Metagrid's home page.)", 'bottom' ) .addNextStep( - navBarTargets.getSelector('cartPageBtn'), + navBarTargets.cartPageBtn.selector(), 'This button takes you to the data cart page where you can view the data you have selected for download.', 'bottom' ) .addNextStep( - navBarTargets.getSelector('savedSearchPageBtn'), + navBarTargets.savedSearchPageBtn.selector(), 'To view your currently saved searches, you would click here.', 'bottom' ) .addNextStep( - navBarTargets.getSelector('nodeStatusBtn'), + navBarTargets.nodeStatusBtn.selector(), 'If you are curious about data node status, you can visit the status page by clicking here.', 'bottom' ) .addNextStep( - navBarTargets.getSelector('signInBtn'), + navBarTargets.newsBtn.selector(), + "Clicking the news button will open up the message center to the right, where you'll find important notes from the admins and developers. You can also view changelog information regarding the latest version of Metagrid", + 'bottom' + ) + .addNextStep( + navBarTargets.signInBtn.selector(), 'Clicking this button will allow you to sign in to your profile.', 'bottom' ) .addNextStep( - leftSidebarTargets.getSelector('selectProjectBtn'), + navBarTargets.helpBtn.selector(), + "Clicking this 'Help' button will open the support dialog, where you can view interface tours (like this), or get links to helpful documentation.", + 'bottom' + ) + .addNextStep( + leftSidebarTargets.selectProjectBtn.selector(), 'To begin a search, you would first select a project from this drop-down.', 'right' ); @@ -349,17 +402,17 @@ export const createMainPageTour = (): JoyrideTour => { if (mainTableEmpty()) { tour .addNextStep( - leftSidebarTargets.getSelector('projectSelectLeftSideBtn'), + leftSidebarTargets.projectSelectLeftSideBtn.selector(), 'Then you click this button to load the results for the project you selected...', 'right', () => { clickFirstElement( - leftSidebarTargets.getSelector('projectSelectLeftSideBtn') + leftSidebarTargets.projectSelectLeftSideBtn.selector() ); } ) .addNextStep( - leftSidebarTargets.getSelector('projectSelectLeftSideBtn'), + leftSidebarTargets.projectSelectLeftSideBtn.selector(), "NOTE: The search results may take a few seconds to load... Click 'next' to continue.", 'right', async () => { @@ -370,7 +423,7 @@ export const createMainPageTour = (): JoyrideTour => { ); } else { tour.addNextStep( - leftSidebarTargets.getSelector('projectSelectLeftSideBtn'), + leftSidebarTargets.projectSelectLeftSideBtn.selector(), 'Then you click this button to load results for the project you selected.', 'right' ); @@ -378,116 +431,112 @@ export const createMainPageTour = (): JoyrideTour => { tour .addNextStep( - leftSidebarTargets.getSelector('projectWebsiteBtn'), + leftSidebarTargets.projectWebsiteBtn.selector(), 'Once a project is selected, if you wish, you can go view the project website by clicking this button.', 'right' ) .addNextStep( - leftSidebarTargets.getSelector('searchFacetsForm'), + leftSidebarTargets.searchFacetsForm.selector(), 'This area contains various groups of facets and parameters that you can use to filter results from your selected project.', 'right' ) .addNextStep( - leftSidebarTargets.getSelector('facetFormGeneral'), + leftSidebarTargets.facetFormGeneral.selector(), 'To filter by facets provided within this group, you would open this collapsable form by clicking on it...', 'right-end', /* istanbul ignore next */ async () => { // Open general facets - clickFirstElement(leftSidebarTargets.getSelector('facetFormGeneral')); + clickFirstElement(leftSidebarTargets.facetFormGeneral.selector()); await delay(300); } ) .addNextStep( - leftSidebarTargets.getSelector('facetFormFields'), + leftSidebarTargets.facetFormFields.selector(), 'These are facets that are available within this group. The drop-downs allow you to select multiple items you wish to include in your search. Note that you can search for elements in the drop-down by typing within the input area.', 'right-start', /* istanbul ignore next */ async () => { // Close general facets - clickFirstElement(leftSidebarTargets.getSelector('facetFormGeneral')); + clickFirstElement(leftSidebarTargets.facetFormGeneral.selector()); await delay(300); // Close facet panels if more than one is open - if ( - elementExists(leftSidebarTargets.getClass('facetFormCollapseAllBtn')) - ) { + if (elementExists(leftSidebarTargets.facetFormCollapseAllBtn.class())) { clickFirstElement( - leftSidebarTargets.getSelector('facetFormCollapseAllBtn') + leftSidebarTargets.facetFormCollapseAllBtn.selector() ); await delay(50); } } ) .addNextStep( - leftSidebarTargets.getSelector('facetFormExpandAllBtn'), + leftSidebarTargets.facetFormExpandAllBtn.selector(), 'You can quickly expand all the facet panels by clicking this button.', 'right-end', /* istanbul ignore next */ async () => { // Expand all facets - clickFirstElement( - leftSidebarTargets.getSelector('facetFormExpandAllBtn') - ); + clickFirstElement(leftSidebarTargets.facetFormExpandAllBtn.selector()); await delay(300); } ) .addNextStep( - leftSidebarTargets.getSelector('facetFormCollapseAllBtn'), + leftSidebarTargets.facetFormCollapseAllBtn.selector(), "Note that there is a scroll bar on the right when the panels don't all fit on the page. Clicking the collapse all button will close all the open facet panels.", 'right-end', /* istanbul ignore next */ async () => { // Open general facets clickFirstElement( - leftSidebarTargets.getSelector('facetFormCollapseAllBtn') + leftSidebarTargets.facetFormCollapseAllBtn.selector() ); await delay(300); } ) .addNextStep( - leftSidebarTargets.getSelector('facetFormAdditionalFields'), + leftSidebarTargets.facetFormAdditionalFields.selector(), 'This section contains additional properties that you can select to further refine your search results, including the Version Type, Result Type and Version Date Range. Hovering over the question mark icon will further explain the parameter.', 'right-end', /* istanbul ignore next */ async () => { // Open filename section - clickFirstElement(leftSidebarTargets.getSelector('facetFormFilename')); + clickFirstElement(leftSidebarTargets.facetFormFilename.selector()); await delay(300); } ) .addNextStep( - leftSidebarTargets.getSelector('facetFormFilenameFields'), + leftSidebarTargets.facetFormFilenameFields.selector(), 'This section lets you filter your results to include a specific filename. To filter by filename, you would type in the name or names as a list of comma separated values then click the magnifying glass icon to add it as a search parameter.', 'right-end', /* istanbul ignore next */ () => { // Close filename section - clickFirstElement(leftSidebarTargets.getSelector('facetFormFilename')); + clickFirstElement(leftSidebarTargets.facetFormFilename.selector()); window.scrollTo(0, 0); } ) .addNextStep( - searchTableTargets.getSelector('queryString'), + searchTableTargets.queryString.selector(), "When performing a search, you'll be able to view the resulting query generated by your selections here.", 'bottom' ) .addNextStep( - searchTableTargets.getSelector('resultsFoundText'), + searchTableTargets.resultsFoundText.selector(), 'This will display how many results were returned from your search.', 'bottom' ) .addNextStep( - searchTableTargets.getSelector('saveSearchBtn'), + searchTableTargets.saveSearchBtn.selector(), 'If you are happy with your search results and plan to perform this search again, you can save your search by clicking this button.', 'left' ) .addNextStep( - searchTableTargets.getSelector('copySearchLinkBtn'), + searchTableTargets.copySearchLinkBtn.selector(), 'You can also share your search with others as a specific URL by clicking this button. The button will copy the link to your clipboard for you to then paste at your convenience.', 'bottom-start' ) .addNextStep( - searchTableTargets.getSelector('searchResultsTable'), + searchTableTargets.searchResultsTable.selector(), 'These are your search results! Each row in the results table is a specific dataset that matches your criteria.', 'top-start' ) @@ -504,7 +553,7 @@ export const createMainPageTour = (): JoyrideTour => { } ) .addNextStep( - searchTableTargets.getSelector('addSelectedToCartBtn'), + searchTableTargets.addSelectedToCartBtn.selector(), 'Then to add them to your cart, you would click this button.', 'bottom-start', /* istanbul ignore next */ @@ -516,12 +565,12 @@ export const createMainPageTour = (): JoyrideTour => { } ) .addNextStep( - topDataRowTargets.getSelector('cartAddBtn', 'plus'), + topDataRowTargets.cartAddBtn.selector('plus'), "You can also directly add a specific dataset to the cart by clicking it's plus button here.", 'top-start' ) .addNextStep( - topDataRowTargets.getSelector('cartAddBtn', 'minus'), + topDataRowTargets.cartAddBtn.selector('minus'), 'Or you can remove a dataset from the cart by clicking its minus button here.', 'top-start' ); @@ -559,11 +608,11 @@ export const createCartItemsTour = ( 'center' ) .addNextStep( - cartTourTargets.getSelector('datasetBtn'), + cartTourTargets.datasetBtn.selector(), 'Note that we are currently in the data cart tab.' ) .addNextStep( - cartTourTargets.getSelector('libraryBtn'), + cartTourTargets.libraryBtn.selector(), 'Clicking this would switch you to the search library tab. However we will stay in the data cart for this tour.' ); @@ -592,17 +641,17 @@ export const createCartItemsTour = ( if (mainTableEmpty()) { tour .addNextStep( - leftSidebarTargets.getSelector('projectSelectLeftSideBtn'), + leftSidebarTargets.projectSelectLeftSideBtn.selector(), 'First we will click this button to load results from a project into the search table...', 'right', () => { clickFirstElement( - leftSidebarTargets.getSelector('projectSelectLeftSideBtn') + leftSidebarTargets.projectSelectLeftSideBtn.selector() ); } ) .addNextStep( - leftSidebarTargets.getSelector('projectSelectLeftSideBtn'), + leftSidebarTargets.projectSelectLeftSideBtn.selector(), "NOTE: The search results may take a few seconds to load... Click 'next' to continue.", 'right', async () => { @@ -612,23 +661,19 @@ export const createCartItemsTour = ( } tour .addNextStep( - searchTableTargets.getSelector('searchResultsTable'), + searchTableTargets.searchResultsTable.selector(), "Let's go ahead and add some datasets to the cart...", 'top-start', /* istanbul ignore next */ async () => { - clickFirstElement( - topDataRowTargets.getSelector('cartAddBtn', 'plus') - ); + clickFirstElement(topDataRowTargets.cartAddBtn.selector('plus')); await delay(500); - clickFirstElement( - topDataRowTargets.getSelector('cartAddBtn', 'plus') - ); + clickFirstElement(topDataRowTargets.cartAddBtn.selector('plus')); await delay(500); } ) .addNextStep( - navBarTargets.getSelector('cartPageBtn'), + navBarTargets.cartPageBtn.selector(), 'Now that there are datasets in the cart, we will go view them in the cart page...', 'bottom', /* istanbul ignore next */ @@ -641,7 +686,7 @@ export const createCartItemsTour = ( tour .addNextStep( - cartTourTargets.getSelector('cartSummary'), + cartTourTargets.cartSummary.selector(), 'This shows a summary of all the datasets in the cart. From here you can see the total datasets, files and total file size at a glance. Note: The summary is visible to both the data cart and search library.' ) .addNextStep( @@ -649,7 +694,7 @@ export const createCartItemsTour = ( 'This table shows the datasets that have been added to the cart.' ) .addNextStep( - topDataRowTargets.getSelector('cartAddBtn', 'minus'), + topDataRowTargets.cartAddBtn.selector('minus'), 'You can remove a dataset from the cart by clicking its minus button here.', 'top-start' ); @@ -669,12 +714,12 @@ export const createCartItemsTour = ( } ) .addNextStep( - cartTourTargets.getSelector('downloadAllType'), + cartTourTargets.downloadAllType.selector(), 'This will select which download script to use (only wget is available currently).', 'top-start' ) .addNextStep( - cartTourTargets.getSelector('downloadAllBtn'), + cartTourTargets.downloadAllBtn.selector(), 'Then you would click this button to get the download script needed for all currently selected datasets in the cart.', 'top-start', /* istanbul ignore next */ @@ -686,7 +731,7 @@ export const createCartItemsTour = ( } ) .addNextStep( - cartTourTargets.getSelector('removeItemsBtn'), + cartTourTargets.removeItemsBtn.selector(), 'We can remove all items from the cart with this button.', 'right-start' ) @@ -697,7 +742,7 @@ export const createCartItemsTour = ( // Clean-up step for when the tour is complete (or skipped) return async () => { if (cartItemsAdded) { - clickFirstElement(cartTourTargets.getSelector('removeItemsBtn')); + clickFirstElement(cartTourTargets.removeItemsBtn.selector()); await delay(500); clickFirstElement('.ant-popover-buttons .ant-btn-primary'); await delay(300); @@ -724,11 +769,11 @@ export const createSearchCardTour = ( 'center' ) .addNextStep( - cartTourTargets.getSelector('libraryBtn'), + cartTourTargets.libraryBtn.selector(), 'Note that we are currently in the search library tab.' ) .addNextStep( - cartTourTargets.getSelector('datasetBtn'), + cartTourTargets.datasetBtn.selector(), 'Clicking this would switch you to the data cart tab. We will remain on the search tab for this tour.' ); @@ -757,17 +802,17 @@ export const createSearchCardTour = ( if (mainTableEmpty()) { tour .addNextStep( - leftSidebarTargets.getSelector('projectSelectLeftSideBtn'), + leftSidebarTargets.projectSelectLeftSideBtn.selector(), 'First we will click this button to load results from a project into the search table...', 'right', () => { clickFirstElement( - leftSidebarTargets.getSelector('projectSelectLeftSideBtn') + leftSidebarTargets.projectSelectLeftSideBtn.selector() ); } ) .addNextStep( - leftSidebarTargets.getSelector('projectSelectLeftSideBtn'), + leftSidebarTargets.projectSelectLeftSideBtn.selector(), "NOTE: The search results may take a few seconds to load... Click 'next' to continue.", 'right', async () => { @@ -779,17 +824,17 @@ export const createSearchCardTour = ( } tour .addNextStep( - searchTableTargets.getSelector('saveSearchBtn'), + searchTableTargets.saveSearchBtn.selector(), 'To save the current search to the library, we need to click this button...', 'bottom-start', /* istanbul ignore next */ async () => { - clickFirstElement(searchTableTargets.getSelector('saveSearchBtn')); + clickFirstElement(searchTableTargets.saveSearchBtn.selector()); await delay(500); } ) .addNextStep( - navBarTargets.getSelector('savedSearchPageBtn'), + navBarTargets.savedSearchPageBtn.selector(), 'We can now go back to the search library and view our recently added search...', 'bottom', /* istanbul ignore next */ @@ -801,34 +846,34 @@ export const createSearchCardTour = ( } tour .addNextStep( - cartTourTargets.getSelector('cartSummary'), + cartTourTargets.cartSummary.selector(), 'This shows a summary of all the datasets in the data cart. The summary is visible to both the data cart and search library.' ) .addNextStep( - savedSearchTourTargets.getSelector('savedSearches'), + savedSearchTourTargets.savedSearches.selector(), 'Your saved searches are shown as cards in this row.', 'bottom' ) .addNextStep( - savedSearchTourTargets.getSelector('projectDescription'), + savedSearchTourTargets.projectDescription.selector(), 'This is the project selected for the search.', 'top' ) .addNextStep( - savedSearchTourTargets.getSelector('searchQueryString'), + savedSearchTourTargets.searchQueryString.selector(), 'This shows the query used by the search to list results.' ) .addNextStep( - savedSearchTourTargets.getSelector('applySearch'), + savedSearchTourTargets.applySearch.selector(), 'Clicking this button will apply your saved search to the main results page.' ) .addNextStep( - savedSearchTourTargets.getSelector('jsonBtn'), + savedSearchTourTargets.jsonBtn.selector(), 'Clicking this button will show the JSON data associated with this search.', 'right' ) .addNextStep( - savedSearchTourTargets.getSelector('removeBtn'), + savedSearchTourTargets.removeBtn.selector(), 'This button will remove this search from your saved searches.', 'left-start' ) @@ -839,7 +884,7 @@ export const createSearchCardTour = ( // Clean-up step for when the tour is complete (or skipped) return async () => { if (searchSaved) { - clickFirstElement(savedSearchTourTargets.getSelector('removeBtn')); + clickFirstElement(savedSearchTourTargets.removeBtn.selector()); await delay(500); } }; @@ -857,38 +902,38 @@ export const createNodeStatusTour = (): JoyrideTour => { 'center' ) .addNextStep( - nodeTourTargets.getSelector('updateTime'), + nodeTourTargets.updateTime.selector(), 'This is the timestamp for the last time the node status was updated.' ) .addNextStep( - nodeTourTargets.getSelector('nodeStatusSummary'), + nodeTourTargets.nodeStatusSummary.selector(), 'This area provides an overall summary of the number of nodes that are available, how many are currently online and how many are currently offline.' ) .addNextStep( - nodeTourTargets.getSelector('nodeColHeader'), + nodeTourTargets.nodeColHeader.selector(), 'This column lists the various nodes that are registered to serve the data with Metagrid. Clicking the header will toggle the sort between ascending and descending like so...', 'top', /* istanbul ignore next */ async () => { - clickFirstElement(nodeTourTargets.getSelector('nodeColHeader')); + clickFirstElement(nodeTourTargets.nodeColHeader.selector()); await delay(500); } ) .addNextStep( - nodeTourTargets.getSelector('onlineColHeader'), + nodeTourTargets.onlineColHeader.selector(), 'This column shows the online status of each node. A green check-mark indicates the node is online whereas a red x mark indicates it is offline. As with the node column, you can click this to sort by node status like so...', 'top', /* istanbul ignore next */ async () => { - clickFirstElement(nodeTourTargets.getSelector('onlineColHeader')); + clickFirstElement(nodeTourTargets.onlineColHeader.selector()); await delay(700); - clickFirstElement(nodeTourTargets.getSelector('onlineColHeader')); + clickFirstElement(nodeTourTargets.onlineColHeader.selector()); await delay(700); - clickFirstElement(nodeTourTargets.getSelector('nodeColHeader')); + clickFirstElement(nodeTourTargets.nodeColHeader.selector()); } ) .addNextStep( - nodeTourTargets.getSelector('sourceColHeader'), + nodeTourTargets.sourceColHeader.selector(), 'This column shows links to the THREDDS catalog of its respective node.' ) .addNextStep( diff --git a/frontend/src/components/App/App.test.tsx b/frontend/src/components/App/App.test.tsx index bbdbd11e4..a451e6cbc 100644 --- a/frontend/src/components/App/App.test.tsx +++ b/frontend/src/components/App/App.test.tsx @@ -1125,22 +1125,28 @@ describe('User search library', () => { }); describe('User support', () => { - it('renders user support modal when clicking fixed button and is closeable', () => { - const { getByRole } = customRender(); + it('renders user support modal when clicking help button and is closeable', () => { + const { getByRole, getByText, findByText } = customRender( + + ); // support button renders - const supportBtn = getByRole('img', { name: 'question', hidden: true }); + const supportBtn = getByRole('img', { name: 'question' }); expect(supportBtn).toBeTruthy(); // click support button fireEvent.click(supportBtn); // GitHub icon renders - const githubIcon = getByRole('img', { name: 'github', hidden: true }); - expect(githubIcon).toBeTruthy(); + const metagridSupportHeader = findByText(' MetaGrid Support'); + expect(metagridSupportHeader).toBeTruthy(); + + // click close button + // const closeBtn = getAllByRole('img', { name: 'close' })[1]; + // fireEvent.click(closeBtn); // click close button - const closeBtn = getByRole('img', { name: 'close' }); + const closeBtn = getByText('Close Support'); fireEvent.click(closeBtn); }); }); diff --git a/frontend/src/components/App/App.tsx b/frontend/src/components/App/App.tsx index 44863c7d0..bf3814248 100644 --- a/frontend/src/components/App/App.tsx +++ b/frontend/src/components/App/App.tsx @@ -11,7 +11,7 @@ import { Affix, Breadcrumb, Button, Layout, message, Result } from 'antd'; import React, { ReactElement } from 'react'; import { useAsync } from 'react-async'; import { hotjar } from 'react-hotjar'; -import { Link, Redirect, Route, Switch } from 'react-router-dom'; +import { Link, Navigate, Route, Routes } from 'react-router-dom'; import { v4 as uuidv4 } from 'uuid'; import { addUserSearchQuery, @@ -51,7 +51,10 @@ import { VersionType, } from '../Search/types'; import Support from '../Support'; +import StartPopup from '../Messaging/StartPopup'; +import startupDisplayData from '../Messaging/messageDisplayData'; import './App.css'; +import { miscTargets } from '../../common/reactJoyrideSteps'; const styles: CSSinJS = { bodySider: { @@ -79,7 +82,7 @@ export type Props = { searchQuery: ActiveSearchQuery; }; -const metagridVersion = '1.0.7-beta'; +const metagridVersion: string = startupDisplayData.messageToShow; const App: React.FC = ({ searchQuery }) => { // Third-party tool integration @@ -203,11 +206,11 @@ const App: React.FC = ({ searchQuery }) => { }, [runFetchNodeStatus]); React.useEffect(() => { - void fetchProjects() + fetchProjects() .then((data) => { const projectName = searchQuery ? searchQuery.project.name : ''; /* istanbul ignore else */ - if (projectName && projectName !== '' && data) { + if (data && projectName && projectName !== '') { const rawProj: RawProject | undefined = data.results.find((proj) => { return ( proj.name.toLowerCase() === (projectName as string).toLowerCase() @@ -227,7 +230,7 @@ const App: React.FC = ({ searchQuery }) => { }); } ); - }, [searchQuery]); + }, [fetchProjects]); const handleTextSearch = ( selectedProject: RawProject, @@ -476,8 +479,7 @@ const App: React.FC = ({ searchQuery }) => { const generateRedirects = (): ReactElement => { /* istanbul ignore next */ if (!publicUrl && previousPublicUrl) { - const newFrom = `/${previousPublicUrl}`; - return ; + } />; } return <>; @@ -485,28 +487,30 @@ const App: React.FC = ({ searchQuery }) => { return ( <> - - - + + } /> + } /> {generateRedirects()} - +
- ( - - )} - /> + + + } + /> + - + ( + element={ = ({ searchQuery }) => { onSetActiveFacets={handleOnSetActiveFacets} /> - )} + } /> ( + element={ - )} + } /> ( + path="/cart/*" + element={ - )} + } /> - + - + ( + element={ <> @@ -573,11 +577,11 @@ const App: React.FC = ({ searchQuery }) => { onShareSearchQuery={handleShareSearchQuery} > - )} + } /> ( + element={ <> @@ -591,11 +595,11 @@ const App: React.FC = ({ searchQuery }) => { isLoading={nodeStatusIsLoading} /> - )} + } /> ( + path="/cart/*" + element={ <> @@ -612,10 +616,11 @@ const App: React.FC = ({ searchQuery }) => { onRemoveSearchQuery={handleRemoveSearchQuery} /> - )} + } /> ( + path="*" + element={ = ({ searchQuery }) => { } /> - )} + } /> - +

@@ -649,6 +654,7 @@ const App: React.FC = ({ searchQuery }) => { = ({ searchQuery }) => { type="primary" shape="circle" style={{ width: '48px', height: '48px' }} - icon={} + icon={} onClick={() => setSupportModalVisible(true)} > @@ -667,6 +673,7 @@ const App: React.FC = ({ searchQuery }) => { visible={supportModalVisible} onClose={() => setSupportModalVisible(false)} /> +

); diff --git a/frontend/src/components/Cart/Items.tsx b/frontend/src/components/Cart/Items.tsx index c510a7804..32199a99c 100644 --- a/frontend/src/components/Cart/Items.tsx +++ b/frontend/src/components/Cart/Items.tsx @@ -3,7 +3,7 @@ import { DownloadOutlined, QuestionCircleOutlined, } from '@ant-design/icons'; -import { Col, Form, message, Row, Select } from 'antd'; +import { Col, Form, message, Popconfirm, Row, Select } from 'antd'; import React from 'react'; import { fetchWgetScript, @@ -13,7 +13,7 @@ import { import { cartTourTargets } from '../../common/reactJoyrideSteps'; import { CSSinJS } from '../../common/types'; import Empty from '../DataDisplay/Empty'; -import Popconfirm from '../Feedback/Popconfirm'; +// import Popconfirm from '../Feedback/Popconfirm'; import Button from '../General/Button'; import Table from '../Search/Table'; import { RawSearchResults } from '../Search/types'; @@ -88,12 +88,13 @@ const Items: React.FC = ({ userCart, onUpdateCart, onClearCart }) => {
{userCart.length > 0 && ( } onConfirm={onClearCart} >
{!objectIsEmpty(availableFacets) && ( -
+
= ({ - message, - description, - type, - showIcon = true, -}) => ( - -); - -export default Alert; diff --git a/frontend/src/components/Feedback/Modal.tsx b/frontend/src/components/Feedback/Modal.tsx index b7f2f65c7..186463055 100644 --- a/frontend/src/components/Feedback/Modal.tsx +++ b/frontend/src/components/Feedback/Modal.tsx @@ -1,29 +1,34 @@ import { Button, Modal as ModalD } from 'antd'; -import React from 'react'; +import React, { CSSProperties } from 'react'; type Props = { visible: boolean; title?: React.ReactNode; + closeText: string; onClose?: () => void; centered?: boolean; children: React.ReactNode; + style?: CSSProperties; }; const Modal: React.FC = ({ visible, title, onClose, + closeText, centered, children, + style, }) => ( - Close + {closeText} , ]} > diff --git a/frontend/src/components/Feedback/Skeleton.test.tsx b/frontend/src/components/Feedback/Skeleton.test.tsx deleted file mode 100644 index 9c00699c4..000000000 --- a/frontend/src/components/Feedback/Skeleton.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - -import Skeleton from './Skeleton'; - -it('returns component', () => { - const { getByTestId } = render(); - const skeleton = getByTestId('skeleton'); - expect(skeleton).toBeTruthy(); -}); - -it('returns component without active animation effect', () => { - const { getByTestId } = render(); - const skeleton = getByTestId('skeleton'); - expect(skeleton).toBeTruthy(); - - const activeClass = document.querySelector('.ant-skeleton-active'); - expect(activeClass).toBeNull(); -}); diff --git a/frontend/src/components/Feedback/Skeleton.tsx b/frontend/src/components/Feedback/Skeleton.tsx deleted file mode 100644 index 75211235e..000000000 --- a/frontend/src/components/Feedback/Skeleton.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Skeleton as SkeletonD } from 'antd'; -import React from 'react'; - -type Props = { - title?: Record; - paragraph?: Record; - active?: boolean; -}; - -const Skeleton: React.FC = ({ title, paragraph, active = false }) => ( -
- -
-); - -export default Skeleton; diff --git a/frontend/src/components/Feedback/Spin.tsx b/frontend/src/components/Feedback/Spin.tsx deleted file mode 100644 index d55c7bdf9..000000000 --- a/frontend/src/components/Feedback/Spin.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Spin as SpinD } from 'antd'; -import React from 'react'; - -const Spin: React.FC = () => ; - -export default Spin; diff --git a/frontend/src/components/Messaging/MessageCard.test.tsx b/frontend/src/components/Messaging/MessageCard.test.tsx new file mode 100644 index 000000000..309786ff6 --- /dev/null +++ b/frontend/src/components/Messaging/MessageCard.test.tsx @@ -0,0 +1,13 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import MessageCard from './MessageCard'; + +it.only('renders message component with default markdown when file is wrong.', () => { + const { getByText } = render( + + ); + + // Check component renders + const text = getByText('Content is empty.'); + expect(text).toBeTruthy(); +}); diff --git a/frontend/src/components/Messaging/MessageCard.tsx b/frontend/src/components/Messaging/MessageCard.tsx new file mode 100644 index 000000000..352dfe0b7 --- /dev/null +++ b/frontend/src/components/Messaging/MessageCard.tsx @@ -0,0 +1,22 @@ +import React, { useEffect } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { MarkdownMessage } from './types'; + +const MessageCard: React.FC = ({ fileName }) => { + const [content, setContent] = React.useState('Content is empty.'); + + /* istanbul ignore next */ + useEffect(() => { + fetch(fileName) + .then((res) => res.text()) + .then((text) => setContent(text)) + .catch((error) => { + // eslint-disable-next-line + console.error(error); + }); + }, []); + + return {content}; +}; + +export default MessageCard; diff --git a/frontend/src/components/Messaging/RightDrawer.test.tsx b/frontend/src/components/Messaging/RightDrawer.test.tsx new file mode 100644 index 000000000..c063ba054 --- /dev/null +++ b/frontend/src/components/Messaging/RightDrawer.test.tsx @@ -0,0 +1,11 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import RightDrawer from './RightDrawer'; + +it('renders right drawer component.', () => { + const { getByText } = render( {}} />); + + // Check component renders + const text = getByText('Notifications'); + expect(text).toBeTruthy(); +}); diff --git a/frontend/src/components/Messaging/RightDrawer.tsx b/frontend/src/components/Messaging/RightDrawer.tsx new file mode 100644 index 000000000..c0f65d5ed --- /dev/null +++ b/frontend/src/components/Messaging/RightDrawer.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Drawer, Space, Button, Collapse, Card } from 'antd'; +import { rightDrawerChanges, rightDrawerMessages } from './messageDisplayData'; +import MessageCard from './MessageCard'; +import { MarkdownMessage } from './types'; + +export type Props = { + visible: boolean; + onClose: () => void; +}; + +const RightDrawer: React.FC = ({ visible, onClose }) => { + return ( + + + + } + > + + + {rightDrawerMessages.map((message: MarkdownMessage) => { + return ( + + + + ); + })} + + + + + {rightDrawerChanges.map((change: MarkdownMessage) => { + return ( + + + + ); + })} + + + + ); +}; + +export default RightDrawer; diff --git a/frontend/src/components/Messaging/StartPopup.test.tsx b/frontend/src/components/Messaging/StartPopup.test.tsx new file mode 100644 index 000000000..0d83176e4 --- /dev/null +++ b/frontend/src/components/Messaging/StartPopup.test.tsx @@ -0,0 +1,150 @@ +import { render, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import StartPopup from './StartPopup'; +import StartupMessages from './messageDisplayData'; +import { TourTitles } from '../../common/reactJoyrideSteps'; + +// const defaultMessageId = StartupMessages.defaultMessageId; +// const currentMessageId = StartupMessages.messageToShow; + +const { defaultMessageId, messageToShow } = StartupMessages; + +let mockNavigate: () => void; + +beforeEach(() => { + mockNavigate = jest.fn(); + jest.mock( + 'react-router-dom', + () => + ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => ({ + push: mockNavigate, + }), + } as Record) + ); + window.localStorage.clear(); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('Start popup tests', () => { + it('renders start popup with welcome message if no local data exists.', () => { + const { getByTestId } = render( + + + + ); + + // Check welcome template rendered (default) + const welcomeHeader = getByTestId('welcomeTemplate'); + expect(welcomeHeader).toBeTruthy(); + }); + + it('renders start popup with welcome message and starts search tour.', () => { + const { getByTestId, getByText } = render( + + + + ); + + // Check welcome template rendered (default) + const welcomeHeader = getByTestId('welcomeTemplate'); + expect(welcomeHeader).toBeTruthy(); + + const searchTourBtn = getByText(TourTitles.Main); + expect(welcomeHeader).toBeTruthy(); + fireEvent.click(searchTourBtn); + }); + + it('renders start popup with welcome message and starts cart tour.', () => { + const { getByTestId, getByText } = render( + + + + ); + + // Check welcome template rendered (default) + const welcomeHeader = getByTestId('welcomeTemplate'); + expect(welcomeHeader).toBeTruthy(); + + const cartTourBtn = getByText(TourTitles.Cart); + expect(welcomeHeader).toBeTruthy(); + fireEvent.click(cartTourBtn); + }); + + it('renders start popup with welcome message and starts saved search tour.', () => { + const { getByTestId, getByText } = render( + + + + ); + + // Check welcome template rendered (default) + const welcomeHeader = getByTestId('welcomeTemplate'); + expect(welcomeHeader).toBeTruthy(); + + const searchesTourBtn = getByText(TourTitles.Searches); + expect(welcomeHeader).toBeTruthy(); + fireEvent.click(searchesTourBtn); + }); + + it('renders start popup with welcome message and starts node page tour.', () => { + const { getByTestId, getByText } = render( + + + + ); + + // Check welcome template rendered (default) + const welcomeHeader = getByTestId('welcomeTemplate'); + expect(welcomeHeader).toBeTruthy(); + + const nodeTourBtn = getByText(TourTitles.Node); + expect(welcomeHeader).toBeTruthy(); + fireEvent.click(nodeTourBtn); + }); + + it('renders start popup with message data missing.', () => { + StartupMessages.defaultMessageId = 'test'; + const { getByText } = render( + + + + ); + StartupMessages.defaultMessageId = defaultMessageId; + + // Check welcome template rendered (default) + const missing = getByText('Message Data Missing'); + expect(missing).toBeTruthy(); + }); + + it('renders start popup with wrong version specified', () => { + window.localStorage.setItem('lastMessageSeen', 'test'); + const { getByTestId } = render( + + + + ); + + // Check changelog template rendered + const changelog = getByTestId('changelogTemplate'); + expect(changelog).toBeTruthy(); + }); + + it('start popup doesnt render when correct version is specified', () => { + window.localStorage.setItem('lastMessageSeen', messageToShow); + const { queryByText } = render( + + + + ); + + // Check that popup doesn't render + const github = queryByText('GitHub Issues'); + expect(github).toBeFalsy(); + }); +}); diff --git a/frontend/src/components/Messaging/StartPopup.tsx b/frontend/src/components/Messaging/StartPopup.tsx new file mode 100644 index 000000000..48278038e --- /dev/null +++ b/frontend/src/components/Messaging/StartPopup.tsx @@ -0,0 +1,129 @@ +import { GithubOutlined } from '@ant-design/icons'; +import React, { CSSProperties, useEffect } from 'react'; +import Modal from '../Feedback/Modal'; +import messageDisplayData from './messageDisplayData'; +import WelcomeTemplate from './Templates/Welcome'; +import ChangeLogTemplate from './Templates/ChangeLog'; +import { MessageActions, MessageData, MessageTemplates } from './types'; +import { + RawTourState, + ReactJoyrideContext, +} from '../../contexts/ReactJoyrideContext'; +import { welcomeTour } from '../../common/reactJoyrideSteps'; + +const getMessageSeen = (): string | null => { + return localStorage.getItem('lastMessageSeen'); +}; + +const setMessageSeen = (): void => { + localStorage.setItem('lastMessageSeen', messageDisplayData.messageToShow); +}; + +const getMessageData = (msgId: string): MessageData | undefined => { + const messages: MessageData[] = messageDisplayData.messageData; + const msgData = messages.find((msg) => { + return msg.messageId === msgId; + }); + return msgData; +}; + +const getMessageTemplate = ( + msgData: MessageData | undefined, + msgActions: MessageActions +): JSX.Element => { + if (!msgData) { + return

Message Data Missing

; + } + const { template, data: props } = msgData; + switch (template) { + case MessageTemplates.ChangeLog: + return ( + + ); + default: + return ( + + ); + } +}; + +const StartPopup: React.FC = () => { + const startData = messageDisplayData; + // Startup visibility + const [visible, setVisible] = React.useState(false); + const [title, setTitle] = React.useState(<>); + const [style, setStyle] = React.useState(); + + // Tutorial state + const tourState: RawTourState = React.useContext(ReactJoyrideContext); + const { setTour, startTour } = tourState; + + const hideMessage = (): void => { + setVisible(false); + + // Show welcome tour if welcome message is shown + const startupMessageSeen = getMessageSeen(); + if (!startupMessageSeen) { + setTour(welcomeTour); + startTour(); + } + setMessageSeen(); + }; + + const showMessage = (msgId: string): void => { + /* istanbul ignore next */ + const actions: MessageActions = { + close: hideMessage, + viewChanges: (): void => showMessage(startData.messageToShow), + }; + const messageData = getMessageData(msgId); + const titleComponent = getMessageTemplate(messageData, actions); + setStyle(messageData?.style); + setTitle(titleComponent); + setVisible(true); + }; + + useEffect(() => { + const startupMessageSeen = getMessageSeen(); + if (!startupMessageSeen) { + showMessage(startData.defaultMessageId); + } else if (startupMessageSeen !== startData.messageToShow) { + showMessage(startData.messageToShow); + } + }, []); + + return ( +
+ +

+ Questions, suggestions, or problems? Please visit our GitHub page to + open an issue. +

+ +
+
+ ); +}; + +export default StartPopup; diff --git a/frontend/src/components/Messaging/Templates/ChangeLog.tsx b/frontend/src/components/Messaging/Templates/ChangeLog.tsx new file mode 100644 index 000000000..4254ec9fc --- /dev/null +++ b/frontend/src/components/Messaging/Templates/ChangeLog.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { TemplateProps, ChangeLogData } from '../types'; +import MessageCard from '../MessageCard'; + +const ChangeLogTemplate: React.FC = ({ templateData }) => { + const props: ChangeLogData = templateData as ChangeLogData; + return ( + <> +

+ What's New with Metagrid v{props.version} +

+

+ {props.changesFile && ( + + )} +

+ + ); +}; + +export default ChangeLogTemplate; diff --git a/frontend/src/components/Messaging/Templates/Welcome.tsx b/frontend/src/components/Messaging/Templates/Welcome.tsx new file mode 100644 index 000000000..1b3c09852 --- /dev/null +++ b/frontend/src/components/Messaging/Templates/Welcome.tsx @@ -0,0 +1,120 @@ +import { Button, Card, Col, Row } from 'antd'; +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { JoyrideTour } from '../../../common/JoyrideTour'; +import { + createMainPageTour, + createCartItemsTour, + createSearchCardTour, + createNodeStatusTour, + TourTitles, +} from '../../../common/reactJoyrideSteps'; +import { + RawTourState, + ReactJoyrideContext, +} from '../../../contexts/ReactJoyrideContext'; +import { MessageActions, TemplateProps, WelcomeData } from '../types'; + +const WelcomeTemplate: React.FC = ({ + templateData, + templateActions, +}) => { + const data: WelcomeData = templateData as WelcomeData; + const actions: MessageActions = templateActions; + + // Tutorial state + const tourState: RawTourState = React.useContext(ReactJoyrideContext); + const { setTour, startTour, setCurrentAppPage } = tourState; + const navigator = useNavigate(); + + const startSpecificTour = (tour: JoyrideTour): void => { + setTour(tour); + actions.close(); + startTour(); + }; + + const startMainPageTour = (): void => { + navigator('/search'); + startSpecificTour(createMainPageTour()); + }; + + const startCartPageTour = (): void => { + navigator('/cart/items'); + startSpecificTour(createCartItemsTour(setCurrentAppPage)); + }; + + const startSearchCardTour = (): void => { + navigator('/cart/searches'); + startSpecificTour(createSearchCardTour(setCurrentAppPage)); + }; + + const startNodeStatusTour = (): void => { + navigator('/nodes'); + startSpecificTour(createNodeStatusTour()); + }; + + return ( + <> +

Welcome!

+

{data.welcomeMessage}

+ + + + + + + + + + + + + + + + +
+

+ To view the latest changes to Metagrid click button below: +
+ +

+

Documentation

+

+ To view the latest documentation and FAQ, please visit this page: +
+ + https://esgf.github.io/esgf-user-support/metagrid.html + +

+ + ); +}; + +export default WelcomeTemplate; diff --git a/frontend/src/components/Messaging/messageDisplayData.ts b/frontend/src/components/Messaging/messageDisplayData.ts new file mode 100644 index 000000000..b7ee1f2e9 --- /dev/null +++ b/frontend/src/components/Messaging/messageDisplayData.ts @@ -0,0 +1,44 @@ +import { StartPopupData, MessageTemplates, MarkdownMessage } from './types'; + +export const rightDrawerMessages: MarkdownMessage[] = [ + { title: 'Messages', fileName: 'messages/metagrid_messages.md' }, +]; + +export const rightDrawerChanges: MarkdownMessage[] = [ + { title: 'V1.0.8', fileName: 'changelog/v1.0.8-beta.md' }, + { title: 'V1.0.7', fileName: 'changelog/v1.0.7-beta.md' }, +]; + +const startupMessages: StartPopupData = { + messageToShow: 'v1.0.8-beta', + defaultMessageId: 'welcome', + messageData: [ + { + messageId: 'v1.0.8-beta', + template: MessageTemplates.ChangeLog, + style: { minWidth: '700px' }, + data: { + changesFile: 'changelog/v1.0.8-beta.md', + version: '1.0.8 Beta', + }, + }, + { + messageId: 'v1.0.7-beta', + template: MessageTemplates.ChangeLog, + data: { + changesFile: 'changelog/v1.0.7-beta.md', + version: '1.0.7 Beta', + }, + }, + { + messageId: 'welcome', + template: MessageTemplates.Welcome, + data: { + welcomeMessage: + "If you wish to become familiar with Metagrid's search and download features, we recommend checking out the interface tours below:", + }, + }, + ], +}; + +export default startupMessages; diff --git a/frontend/src/components/Messaging/types.ts b/frontend/src/components/Messaging/types.ts new file mode 100644 index 000000000..24dff0557 --- /dev/null +++ b/frontend/src/components/Messaging/types.ts @@ -0,0 +1,46 @@ +import { CSSProperties } from 'react'; + +export enum MessageTemplates { + Welcome, + ChangeLog, + Notice, +} + +export type ChangeLogData = { + version: string; + changesFile: string; +}; + +export type WelcomeData = { + welcomeMessage: string; +}; + +export type MessageActions = { + close: () => void; + viewChanges: () => void; +}; + +export type ValidTemplateData = ChangeLogData | WelcomeData; + +export type TemplateProps = { + templateData: ValidTemplateData; + templateActions: MessageActions; +}; + +export type MessageData = { + messageId: string; + template: MessageTemplates; + data: ValidTemplateData; + style?: CSSProperties; +}; + +export type StartPopupData = { + messageToShow: string; + defaultMessageId: string; + messageData: MessageData[]; +}; + +export type MarkdownMessage = { + fileName: string; + title: string; +}; diff --git a/frontend/src/components/NavBar/LeftMenu.test.tsx b/frontend/src/components/NavBar/LeftMenu.test.tsx index 28905ecae..576fcd5c7 100644 --- a/frontend/src/components/NavBar/LeftMenu.test.tsx +++ b/frontend/src/components/NavBar/LeftMenu.test.tsx @@ -1,5 +1,5 @@ -import { fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import { BrowserRouter as Router } from 'react-router-dom'; import { projectsFixture } from '../../api/mock/fixtures'; import LeftMenu, { Props } from './LeftMenu'; diff --git a/frontend/src/components/NavBar/LeftMenu.tsx b/frontend/src/components/NavBar/LeftMenu.tsx index 5e3da6839..e7dbc4db2 100644 --- a/frontend/src/components/NavBar/LeftMenu.tsx +++ b/frontend/src/components/NavBar/LeftMenu.tsx @@ -1,11 +1,10 @@ import { SearchOutlined } from '@ant-design/icons'; -import { Form, Input, Select, Spin } from 'antd'; +import { Alert, Form, Input, Select, Spin } from 'antd'; import React from 'react'; -import { useHistory } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { ResponseError } from '../../api'; import { navBarTargets } from '../../common/reactJoyrideSteps'; import { RawProject, RawProjects } from '../Facets/types'; -import Alert from '../Feedback/Alert'; import Button from '../General/Button'; const styles = { @@ -28,7 +27,8 @@ const LeftMenu: React.FC = ({ }) => { const [form] = Form.useForm(); const [text, setText] = React.useState(''); - const history = useHistory(); + const navigate = useNavigate(); + const location = useLocation(); /** * Sets the project and search value using the search form. @@ -36,8 +36,8 @@ const LeftMenu: React.FC = ({ */ const onFinish = (values: { [key: string]: string }): void => { /* istanbul ignore else */ - if (!history.location.pathname.endsWith('search')) { - history.push('/search'); + if (!location.pathname.endsWith('search')) { + navigate('/search'); } const selectedProj: RawProject | undefined = (projects as RawProjects).find( @@ -67,7 +67,7 @@ const LeftMenu: React.FC = ({ return (
= ({ = ({ placeholder="Search for a keyword" /> - + + {!authenticated ? ( +
); }; diff --git a/frontend/src/components/NavBar/index.test.tsx b/frontend/src/components/NavBar/index.test.tsx index d5b9fce32..32880bafc 100644 --- a/frontend/src/components/NavBar/index.test.tsx +++ b/frontend/src/components/NavBar/index.test.tsx @@ -1,6 +1,5 @@ -import { fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; +import { fireEvent, waitFor } from '@testing-library/react'; import { rest, server } from '../../api/mock/setup-env'; import apiRoutes from '../../api/routes'; import { customRender } from '../../test/custom-render'; @@ -14,11 +13,7 @@ const defaultProps: Props = { }; it('renders LeftMenu and RightMenu components', async () => { - const { getByTestId } = customRender( - - - - ); + const { getByTestId } = customRender(); const rightMenuComponent = await waitFor(() => getByTestId('right-menu')); expect(rightMenuComponent).toBeTruthy(); @@ -31,11 +26,7 @@ it('renders error message when projects can"t be fetched', async () => { server.use( rest.get(apiRoutes.projects.path, (_req, res, ctx) => res(ctx.status(404))) ); - const { getByRole } = customRender( - - - - ); + const { getByRole } = customRender(); const alertComponent = await waitFor(() => getByRole('img', { name: 'close-circle' }) @@ -44,11 +35,7 @@ it('renders error message when projects can"t be fetched', async () => { }); it('opens the drawer onClick and closes with onClose', async () => { - const { getByRole, getByTestId } = customRender( - - - - ); + const { getByRole, getByTestId } = customRender(); await waitFor(() => expect(getByTestId('left-menu')).toBeTruthy()); expect(getByTestId('right-menu')).toBeTruthy(); diff --git a/frontend/src/components/NavBar/index.tsx b/frontend/src/components/NavBar/index.tsx index f635f3d5a..d0ae68d7d 100644 --- a/frontend/src/components/NavBar/index.tsx +++ b/frontend/src/components/NavBar/index.tsx @@ -38,7 +38,6 @@ const NavBar: React.FC = ({ />
-
= ({ onTextSearch={onTextSearch} >
-
+
= ({ supportModalVisible={supportModalVisible} >
- - = ({ nodeStatus }) => { return (
Node diff --git a/frontend/src/components/NodeStatus/index.test.tsx b/frontend/src/components/NodeStatus/index.test.tsx index 83b1ac70f..2ac369aa4 100644 --- a/frontend/src/components/NodeStatus/index.test.tsx +++ b/frontend/src/components/NodeStatus/index.test.tsx @@ -43,14 +43,12 @@ it('renders the node status and columns sort', () => { it('renders an error message when no node status information is available', async () => { const { getByRole } = render(); - const alertMsg = await waitFor(() => - getByRole('img', { name: 'close-circle', hidden: true }) - ); + const alertMsg = await waitFor(() => getByRole('alert')); expect(alertMsg).toBeTruthy(); }); it('renders an error message when there is an api error', async () => { - const errorMsg = 'API error'; + const errorMsg = 'Node status information is currently unavailable.'; const { getByRole, getByText } = render( { > ); - const alertMsg = await waitFor(() => - getByRole('img', { name: 'close-circle', hidden: true }) - ); + const alertMsg = await waitFor(() => getByRole('alert')); expect(alertMsg).toBeTruthy(); const errorMsgDiv = getByText(errorMsg); @@ -76,9 +72,7 @@ it('renders error message that feature is disabled', async () => { ); - const alertMsg = await waitFor(() => - getByRole('img', { name: 'close-circle', hidden: true }) - ); + const alertMsg = await waitFor(() => getByRole('alert')); expect(alertMsg).toBeTruthy(); const errorMsgDiv = getByText(errorMsg); @@ -92,9 +86,7 @@ it('renders fallback network error msg', async () => { ); - const alertMsg = await waitFor(() => - getByRole('img', { name: 'close-circle', hidden: true }) - ); + const alertMsg = await waitFor(() => getByRole('alert')); expect(alertMsg).toBeTruthy(); const errorMsgDiv = getByText(errorMsg); diff --git a/frontend/src/components/NodeStatus/index.tsx b/frontend/src/components/NodeStatus/index.tsx index 93fb7d595..a58d208cf 100644 --- a/frontend/src/components/NodeStatus/index.tsx +++ b/frontend/src/components/NodeStatus/index.tsx @@ -1,12 +1,11 @@ import { CheckCircleTwoTone, CloseCircleTwoTone } from '@ant-design/icons'; -import { Table as TableD } from 'antd'; +import { Alert, Table as TableD } from 'antd'; import { SortOrder } from 'antd/lib/table/interface'; import React from 'react'; import { ResponseError } from '../../api'; import apiRoutes from '../../api/routes'; import { nodeTourTargets } from '../../common/reactJoyrideSteps'; import { CSSinJS } from '../../common/types'; -import Alert from '../Feedback/Alert'; import { NodeStatusArray, NodeStatusElement } from './types'; const styles = { headerContainer: { margin: '12px' } } as CSSinJS; @@ -47,7 +46,7 @@ const NodeStatus: React.FC = ({ nodeStatus, apiError, isLoading }) => { const columns = [ { title: ( -
Node
+
Node
), dataIndex: 'name', align: 'center' as const, @@ -57,9 +56,7 @@ const NodeStatus: React.FC = ({ nodeStatus, apiError, isLoading }) => { }, { title: ( -
- Online -
+
Online
), dataIndex: 'isOnline', align: 'center' as const, @@ -83,7 +80,7 @@ const NodeStatus: React.FC = ({ nodeStatus, apiError, isLoading }) => { dataIndex: 'source', render: (source: string) => ( = ({ nodeStatus, apiError, isLoading }) => { (
-

+

Status as of {timestamp}

@@ -136,7 +133,7 @@ const NodeStatus: React.FC = ({ nodeStatus, apiError, isLoading }) => { let errorMsg: string; if (apiError) { - errorMsg = apiError.message; + errorMsg = 'Node status information is currently unavailable.'; } else if (featureIsDisabled) { errorMsg = 'This feature is not enabled on this node or status information is currently unavailable.'; diff --git a/frontend/src/components/Search/Citation.tsx b/frontend/src/components/Search/Citation.tsx index a7e4f2ea9..b59385260 100644 --- a/frontend/src/components/Search/Citation.tsx +++ b/frontend/src/components/Search/Citation.tsx @@ -1,9 +1,8 @@ import React from 'react'; +import { Alert, Skeleton } from 'antd'; import { PromiseFn, useAsync } from 'react-async'; import { fetchDatasetCitation } from '../../api'; import { splitStringByChar } from '../../common/utils'; -import Alert from '../Feedback/Alert'; -import Skeleton from '../Feedback/Skeleton'; import { RawCitation } from './types'; type CitationInfoProps = { diff --git a/frontend/src/components/Search/FilesTable.tsx b/frontend/src/components/Search/FilesTable.tsx index 863f3ce29..948db2240 100644 --- a/frontend/src/components/Search/FilesTable.tsx +++ b/frontend/src/components/Search/FilesTable.tsx @@ -6,7 +6,7 @@ import { RightCircleOutlined, ShareAltOutlined, } from '@ant-design/icons'; -import { Form, message, Table as TableD } from 'antd'; +import { Alert, Form, message, Table as TableD } from 'antd'; import { SizeType } from 'antd/lib/config-provider/SizeContext'; import { TablePaginationConfig } from 'antd/lib/table'; import React from 'react'; @@ -16,7 +16,6 @@ import { innerDataRowTargets } from '../../common/reactJoyrideSteps'; import { CSSinJS } from '../../common/types'; import { formatBytes, splitStringByChar } from '../../common/utils'; import ToolTip from '../DataDisplay/ToolTip'; -import Alert from '../Feedback/Alert'; import Button from '../General/Button'; import { Pagination, @@ -197,9 +196,7 @@ const FilesTable: React.FC = ({ id, numResults = 0, filenameVars }) => { key: 'title', render: (title: string) => { return ( -

- {title} -
+
{title}
); }, }, @@ -210,7 +207,7 @@ const FilesTable: React.FC = ({ id, numResults = 0, filenameVars }) => { key: 'size', render: (size: number) => { return ( -
+
{formatBytes(size)}
); @@ -231,7 +228,7 @@ const FilesTable: React.FC = ({ id, numResults = 0, filenameVars }) => { >