From 36991058eb72d1d498a5c73c9c53a798475f6bd1 Mon Sep 17 00:00:00 2001 From: Curtis Banh <30607061+cqbanh@users.noreply.github.com> Date: Wed, 27 Mar 2024 00:31:17 -0700 Subject: [PATCH] Merging main back to develop (#97) * /version v1.0.0-rc.1 * /version v1.0.0-rc.2 * Pull in recent changes to 1.0.0 * /version v1.0.0-rc.3 * Fix umm-t record * /version v1.0.0-rc.4 * Fix umm-t record * /version v1.0.0-rc.5 * Fix umm-t record * /version v1.0.0-rc.6 * /version v1.0.0-rc.7 * issue/manual-granule-input-hotfix: fixed how ranged of scenes are processed (#86) Co-authored-by: jbyrne * Issues/swodlr UI 72 essential UI bug fixes (#87) * issues/swodlr-ui-72: fix cps url params bug * issues/swodlr-ui-72 * issues/swodlr-ui-75: fixed a couple bugs * issues/swodlr-ui-72: fixed map movement, adjust options, data page limit, etc * issues/swodlr-ui-72: changed spatial search beginning date in range * issues/swodlr-ui-72-essential-fixes: cleaned up comments --------- Co-authored-by: jbyrne * /version v1.0.0-rc.8 * Deploying * Update build.yml - OPS environment variable for release * Update build.yml - add OPS choice * /version 1.0.0 * Update build.yml - check for staged changes * Deploying * Update build.yml - checks if tag exists * Update build.yml - check for tag on remote * Update build.yml - replace tag if exists * Update build.yml - trying force tag * Update build.yml - allow release update * Deploying * Issues/swodlr UI final fixes - bug fixed before version 1.0 release (#91) * issues/swodlr-ui-final-fixes: fixed spatial search speed and tutorial back fix * issues/swodlr-ui-final-fixes: fixed spinner, 10 limit, tutorial, map on reload * issues/swodlr-ui-final-fixes: fixed delete bug and added success alert for generation * issues/swodlr-ui-final-fixes: made alert message for search area too large * issues/swodlr-ui-final-fixes: changed search area too large message * issues/swodlr-ui-final-fixes: start search polygon url param * issues/swodlr-ui-final-fixed: removed skip from tutorial, added copy/download tooltips product url * issues/swodlr-ui-final-fixes: added tutorial close confirmation modal * issues/swodlr-ui-final-fixes: added cmr SWOT collection permissions check * issues/swodlr-ui-final-fixes: made cmr permissions alert conditional * issues/swodlr-ui-final-fixes: fixed tutorial back error and no data history tutorial error --------- Co-authored-by: jbyrne --------- Co-authored-by: frankinspace Co-authored-by: Frank Greguska Co-authored-by: Jonathan M Smolenski Co-authored-by: jonathansmolenski Co-authored-by: Jimmy Co-authored-by: jbyrne Co-authored-by: cqbanh --- .github/workflows/build.yml | 25 +- CHANGE | 1 + cmr/ops_swodlr_cmr_umm_t.json | 173 +++++----- cmr/sit_swodlr_cmr_umm_t.json | 6 +- cmr/uat_swodlr_cmr_umm_t.json | 173 +++++----- package-lock.json | 48 +-- package.json | 2 +- scss/custom.scss | 1 - src/components/about/About.tsx | 34 +- src/components/app/App.tsx | 46 ++- src/components/app/appSlice.ts | 13 +- .../edl/AuthorizationCodeHandler.tsx | 41 ++- .../history/GeneratedProductHistory.tsx | 75 ++-- src/components/map/WorldMap.tsx | 100 ++++-- src/components/navbar/PodaacFooter.tsx | 17 +- .../sidebar/CustomizeProductView.tsx | 3 + .../sidebar/CustomizeProductsSidebar.tsx | 5 +- .../sidebar/DeleteGranulesModal.tsx | 25 ++ .../sidebar/GenerateProductsModal.tsx | 10 +- .../GranuleSelectionAndConfigurationView.tsx | 26 ++ .../sidebar/GranuleSelectionView.tsx | 34 +- src/components/sidebar/GranuleTableAlerts.tsx | 5 +- src/components/sidebar/GranulesTable.tsx | 319 +++++++++++------- .../sidebar/ProductCustomization.tsx | 8 +- .../sidebar/SpatialSearchOptions.tsx | 12 - src/components/sidebar/actions/modalSlice.ts | 21 +- .../sidebar/actions/productSlice.ts | 12 +- .../sidebar/actions/sidebarSlice.ts | 3 - .../InteractiveTutorialModalClose.tsx | 44 +++ src/components/tutorial/tutorialConstants.ts | 15 +- src/components/welcome/Welcome.tsx | 6 +- src/constants/graphqlQueries.ts | 4 +- src/constants/rasterParameterConstants.ts | 33 +- src/redux/store.ts | 1 - src/types/constantTypes.ts | 10 +- src/user/userData.ts | 23 -- terraform/environments/ops.env | 6 + 37 files changed, 865 insertions(+), 515 deletions(-) create mode 100644 CHANGE create mode 100644 src/components/tutorial/InteractiveTutorialModalClose.tsx create mode 100644 terraform/environments/ops.env diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31b74f3..aba80d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,7 @@ on: options: - SIT - UAT + - OPS commit: type: string description: Custom commit hash @@ -113,6 +114,7 @@ jobs: THE_VERSION=${{ steps.package-version.outputs.current-version}} echo "software_version=${THE_VERSION//-*}" >> $GITHUB_ENV npm --no-git-tag-version version --allow-same-version ${THE_VERSION//-*} + echo "TARGET_ENV=OPS" >> $GITHUB_ENV - name: Set the target environment to ${{ env.TARGET_ENV }} id: set-env run: | @@ -172,10 +174,14 @@ jobs: steps.rc.conclusion == 'success' || steps.release.conclusion == 'success' run: | - git config user.name "${GITHUB_ACTOR}" - git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" - git commit -am "/version ${{ env.software_version }}" - git push + if ! git diff --cached --exit-code; then + git config user.name "${GITHUB_ACTOR}" + git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + git commit -am "/version ${{ env.software_version }}" + git push + else + echo "No staged changes" + fi - name: Push Tag if: | steps.alpha.conclusion == 'success' || @@ -184,8 +190,9 @@ jobs: run: | git config user.name "${GITHUB_ACTOR}" git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" - git tag -a "${{ env.software_version }}" -m "Version ${{ env.software_version }}" - git push origin "${{ env.software_version }}" + + git tag -a "${{ env.software_version }}" -m "Version ${{ env.software_version }}" --force + git push origin "${{ env.software_version }}" --force - name: Create GH release if: | steps.alpha.conclusion == 'success' || @@ -197,6 +204,7 @@ jobs: name: ${{ env.software_version }} prerelease: ${{ steps.alpha.conclusion == 'success' || steps.rc.conclusion == 'success'}} tag: ${{ env.software_version }} + allowUpdates: true - name: Set github SHA for deployment id: update-sha run: | @@ -256,7 +264,7 @@ jobs: run: aws s3 sync ${{steps.download.outputs.download-path}} s3://${{ env.SWODLR_UI_BUCKET }} --delete - name: Publish UMM-T with new version - uses: podaac/cmr-umm-updater@0.5.0 + uses: podaac/cmr-umm-updater@0.6.0 if: | needs.build.outputs.deploy_env == 'UAT' || needs.build.outputs.deploy_env == 'OPS' @@ -269,6 +277,7 @@ jobs: disable_removal: 'true' umm_type: 'umm-t' use_associations: 'false' + umm_version: '1.2.0' env: LAUNCHPAD_TOKEN_SIT: ${{secrets.LAUNCHPAD_TOKEN_SIT}} LAUNCHPAD_TOKEN_UAT: ${{secrets.LAUNCHPAD_TOKEN_UAT}} @@ -295,4 +304,4 @@ jobs: payload: | { "message": "ERROR: ${{ github.repository }} [version ${{ needs.build.outputs.software_version }}] has encountered an error while trying to deploy to the ${{ needs.build.outputs.deploy_env }} environment" - } \ No newline at end of file + } diff --git a/CHANGE b/CHANGE new file mode 100644 index 0000000..acca8be --- /dev/null +++ b/CHANGE @@ -0,0 +1 @@ +podaac/swodlr-ui deployment diff --git a/cmr/ops_swodlr_cmr_umm_t.json b/cmr/ops_swodlr_cmr_umm_t.json index 5ff615b..e6968fa 100644 --- a/cmr/ops_swodlr_cmr_umm_t.json +++ b/cmr/ops_swodlr_cmr_umm_t.json @@ -1,91 +1,90 @@ - { - "Name": "SWODLR", - "LongName": "SWOT On-Demand Level 2 Raster Generation", - "Type": "Web User Interface", - "Version": "#.#.#", - "URL": { - "URLContentType": "DistributionURL", - "Type": "GOTO WEB TOOL", - "URLValue": "http://swodlr.podaac.earthdatacloud.nasa.gov/" + "Name": "SWODLR", + "LongName": "SWOT On-Demand Level 2 Raster Generation", + "Type": "Web User Interface", + "Version": "#.#.#", + "Description": "SWODLR (swaa·dler) is an open-source software system developed to generate custom Level 2 raster data products for the SWOT mission. It provides an Application Programming Interface (API) and Graphical User Interface (GUI) that allows end-users to provide custom configurations to generate on-demand raster data products from underlying standard data products (PIXC, PIXCVec).", + "ToolKeywords": [ + { + "ToolCategory": "EARTH SCIENCE SERVICES", + "ToolTopic": "DATA ANALYSIS AND VISUALIZATION", + "ToolTerm": "DATA VISUALIZATION" }, - "Description": "SWODLR (swaa·dler) is an open-source software system developed to generate custom Level 2 raster data products for the SWOT mission. It provides an Application Programming Interface (API) and Graphical User Interface (GUI) that allows end-users to provide custom configurations to generate on-demand raster data products from underlying standard data products (PIXC, PIXCVec).", - "ToolKeywords" : [ - { - "ToolCategory": "EARTH SCIENCE SERVICES", - "ToolTopic": "DATA ANALYSIS AND VISUALIZATION", - "ToolTerm": "DATA VISUALIZATION" - }, - { - "ToolCategory": "EARTH SCIENCE SERVICES", - "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", - "ToolTerm": "SUBSETTING/SUPERSETTING", - "ToolSpecificTerm": "SPATIAL SUBSETTING" - }, - { - "ToolCategory": "EARTH SCIENCE SERVICES", - "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", - "ToolTerm": "SUBSETTING/SUPERSETTING", - "ToolSpecificTerm": "VARIABLE SUBSETTING" - }, - { - "ToolCategory": "EARTH SCIENCE SERVICES", - "ToolTopic": "DATA ANALYSIS AND VISUALIZATION", - "ToolTerm": "GEOGRAPHIC INFORMATION SYSTEMS", - "ToolSpecificTerm": "WEB-BASED GEOGRAPHIC INFORMATION SYSTEMS" - }, - { - "ToolCategory": "EARTH SCIENCE SERVICES", - "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", - "ToolTerm": "DATA ACCESS/RETRIEVAL" - } - ], - "PotentialAction": { - "Type": "SearchAction", - "Target": { - "Type": "EntryPoint", - "ResponseContentType": [ - "text/html" - ], - "UrlTemplate": "http://swodlr.podaac.earthdatacloud.nasa.gov/l={+layers}&ve={+bbox}&d={+date}", - "Description": "SWODLR (swaa·dler) is an open-source software system developed to generate custom Level 2 raster data products for the SWOT mission. It provides an Application Programming Interface (API) and Graphical User Interface (GUI) that allows end-users to provide custom configurations to generate on-demand raster data products from underlying standard data products (PIXC, PIXCVec).", - "HttpMethod": [ - "GET" - ] - }, - "QueryInput": [ - { - "ValueName": "layers", - "Description": "A comma-separated list of visualization layer ids, as defined by GIBS. These layers will be portrayed on the web application", - "ValueRequired": true, - "ValueType": "https://wiki.earthdata.nasa.gov/display/GIBS/GIBS+API+for+Developers#GIBSAPIforDevelopers-LayerNaming" - }, - { - "ValueName": "date", - "Description": "A UTC ISO DateTime. The layers portrayed will correspond to this date.", - "ValueRequired": false, - "ValueType": "https://schema.org/startDate" - }, - { - "ValueName": "bbox", - "Description": "A spatial bounding box that will set the spatial extent of the portrayed layers. The first point is the lower corner, the second point is the upper corner. A box is expressed as two points separated by a space character.", - "ValueRequired": false, - "ValueType": "https://schema.org/box" - } - ] + { + "ToolCategory": "EARTH SCIENCE SERVICES", + "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", + "ToolTerm": "SUBSETTING/SUPERSETTING", + "ToolSpecificTerm": "SPATIAL SUBSETTING" }, - "Organizations": [ - { - "Roles": [ - "ORIGINATOR" - ], - "ShortName": "NASA/JPL/PODAAC", - "LongName": "Physical Oceanography Distributed Active Archive Center, Jet Propulsion Laboratory, NASA" - } - ], - "MetadataSpecification": { - "URL": "https://cdn.earthdata.nasa.gov/umm/tool/v1.1.1", - "Name": "UMM-T", - "Version": "1.1.1" + { + "ToolCategory": "EARTH SCIENCE SERVICES", + "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", + "ToolTerm": "SUBSETTING/SUPERSETTING", + "ToolSpecificTerm": "VARIABLE SUBSETTING" + }, + { + "ToolCategory": "EARTH SCIENCE SERVICES", + "ToolTopic": "DATA ANALYSIS AND VISUALIZATION", + "ToolTerm": "GEOGRAPHIC INFORMATION SYSTEMS", + "ToolSpecificTerm": "WEB-BASED GEOGRAPHIC INFORMATION SYSTEMS" + }, + { + "ToolCategory": "EARTH SCIENCE SERVICES", + "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", + "ToolTerm": "DATA ACCESS/RETRIEVAL" } -} \ No newline at end of file + ], + "Organizations": [ + { + "Roles": [ + "ORIGINATOR" + ], + "ShortName": "NASA/JPL/PODAAC", + "LongName": "Physical Oceanography Distributed Active Archive Center, Jet Propulsion Laboratory, NASA" + } + ], + "URL": { + "URLContentType": "DistributionURL", + "Type": "GOTO WEB TOOL", + "URLValue": "http://swodlr.podaac.earthdatacloud.nasa.gov/" + }, + "MetadataSpecification": { + "URL": "https://cdn.earthdata.nasa.gov/umm/tool/v1.2.0", + "Name": "UMM-T", + "Version": "1.2.0" + }, + "PotentialAction": { + "Type": "SearchAction", + "Target": { + "Type": "EntryPoint", + "ResponseContentType": [ + "text/html" + ], + "UrlTemplate": "http://swodlr.podaac.earthdatacloud.nasa.gov/l={+layers}&ve={+bbox}&d={+date}", + "Description": "SWODLR (swaa·dler) is an open-source software system developed to generate custom Level 2 raster data products for the SWOT mission. It provides an Application Programming Interface (API) and Graphical User Interface (GUI) that allows end-users to provide custom configurations to generate on-demand raster data products from underlying standard data products (PIXC, PIXCVec).", + "HttpMethod": [ + "GET" + ] + }, + "QueryInput": [ + { + "ValueName": "layers", + "Description": "A comma-separated list of visualization layer ids, as defined by GIBS. These layers will be portrayed on the web application", + "ValueRequired": true, + "ValueType": "https://wiki.earthdata.nasa.gov/display/GIBS/GIBS+API+for+Developers#GIBSAPIforDevelopers-LayerNaming" + }, + { + "ValueName": "date", + "Description": "A UTC ISO DateTime. The layers portrayed will correspond to this date.", + "ValueRequired": false, + "ValueType": "https://schema.org/startDate" + }, + { + "ValueName": "bbox", + "Description": "A spatial bounding box that will set the spatial extent of the portrayed layers. The first point is the lower corner, the second point is the upper corner. A box is expressed as two points separated by a space character.", + "ValueRequired": false, + "ValueType": "https://schema.org/box" + } + ] + } +} diff --git a/cmr/sit_swodlr_cmr_umm_t.json b/cmr/sit_swodlr_cmr_umm_t.json index 8b41854..1011ff4 100644 --- a/cmr/sit_swodlr_cmr_umm_t.json +++ b/cmr/sit_swodlr_cmr_umm_t.json @@ -84,8 +84,8 @@ } ], "MetadataSpecification": { - "URL": "https://cdn.earthdata.nasa.gov/umm/tool/v1.1.1", + "URL": "https://cdn.earthdata.nasa.gov/umm/tool/v1.2.0", "Name": "UMM-T", - "Version": "1.1.1" + "Version": "1.2.0" } -} \ No newline at end of file +} diff --git a/cmr/uat_swodlr_cmr_umm_t.json b/cmr/uat_swodlr_cmr_umm_t.json index 0050391..dd3762c 100644 --- a/cmr/uat_swodlr_cmr_umm_t.json +++ b/cmr/uat_swodlr_cmr_umm_t.json @@ -1,91 +1,90 @@ - { - "Name": "SWODLR", - "LongName": "SWOT On-Demand Level 2 Raster Generation", - "Type": "Web User Interface", - "Version": "#.#.#", - "URL": { - "URLContentType": "DistributionURL", - "Type": "GOTO WEB TOOL", - "URLValue": "http://swodlr.podaac.uat.earthdatacloud.nasa.gov/" + "Name": "SWODLR", + "LongName": "SWOT On-Demand Level 2 Raster Generation", + "Type": "Web User Interface", + "Version": "#.#.#", + "Description": "SWODLR (swaa·dler) is an open-source software system developed to generate custom Level 2 raster data products for the SWOT mission. It provides an Application Programming Interface (API) and Graphical User Interface (GUI) that allows end-users to provide custom configurations to generate on-demand raster data products from underlying standard data products (PIXC, PIXCVec).", + "ToolKeywords": [ + { + "ToolCategory": "EARTH SCIENCE SERVICES", + "ToolTopic": "DATA ANALYSIS AND VISUALIZATION", + "ToolTerm": "DATA VISUALIZATION" }, - "Description": "SWODLR (swaa·dler) is an open-source software system developed to generate custom Level 2 raster data products for the SWOT mission. It provides an Application Programming Interface (API) and Graphical User Interface (GUI) that allows end-users to provide custom configurations to generate on-demand raster data products from underlying standard data products (PIXC, PIXCVec).", - "ToolKeywords" : [ - { - "ToolCategory": "EARTH SCIENCE SERVICES", - "ToolTopic": "DATA ANALYSIS AND VISUALIZATION", - "ToolTerm": "DATA VISUALIZATION" - }, - { - "ToolCategory": "EARTH SCIENCE SERVICES", - "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", - "ToolTerm": "SUBSETTING/SUPERSETTING", - "ToolSpecificTerm": "SPATIAL SUBSETTING" - }, - { - "ToolCategory": "EARTH SCIENCE SERVICES", - "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", - "ToolTerm": "SUBSETTING/SUPERSETTING", - "ToolSpecificTerm": "VARIABLE SUBSETTING" - }, - { - "ToolCategory": "EARTH SCIENCE SERVICES", - "ToolTopic": "DATA ANALYSIS AND VISUALIZATION", - "ToolTerm": "GEOGRAPHIC INFORMATION SYSTEMS", - "ToolSpecificTerm": "WEB-BASED GEOGRAPHIC INFORMATION SYSTEMS" - }, - { - "ToolCategory": "EARTH SCIENCE SERVICES", - "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", - "ToolTerm": "DATA ACCESS/RETRIEVAL" - } - ], - "PotentialAction": { - "Type": "SearchAction", - "Target": { - "Type": "EntryPoint", - "ResponseContentType": [ - "text/html" - ], - "UrlTemplate": "http://swodlr.podaac.uat.earthdatacloud.nasa.gov/l={+layers}&ve={+bbox}&d={+date}", - "Description": "SWODLR (swaa·dler) is an open-source software system developed to generate custom Level 2 raster data products for the SWOT mission. It provides an Application Programming Interface (API) and Graphical User Interface (GUI) that allows end-users to provide custom configurations to generate on-demand raster data products from underlying standard data products (PIXC, PIXCVec).", - "HttpMethod": [ - "GET" - ] - }, - "QueryInput": [ - { - "ValueName": "layers", - "Description": "A comma-separated list of visualization layer ids, as defined by GIBS. These layers will be portrayed on the web application", - "ValueRequired": true, - "ValueType": "https://wiki.earthdata.nasa.gov/display/GIBS/GIBS+API+for+Developers#GIBSAPIforDevelopers-LayerNaming" - }, - { - "ValueName": "date", - "Description": "A UTC ISO DateTime. The layers portrayed will correspond to this date.", - "ValueRequired": false, - "ValueType": "https://schema.org/startDate" - }, - { - "ValueName": "bbox", - "Description": "A spatial bounding box that will set the spatial extent of the portrayed layers. The first point is the lower corner, the second point is the upper corner. A box is expressed as two points separated by a space character.", - "ValueRequired": false, - "ValueType": "https://schema.org/box" - } - ] + { + "ToolCategory": "EARTH SCIENCE SERVICES", + "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", + "ToolTerm": "SUBSETTING/SUPERSETTING", + "ToolSpecificTerm": "SPATIAL SUBSETTING" }, - "Organizations": [ - { - "Roles": [ - "ORIGINATOR" - ], - "ShortName": "NASA/JPL/PODAAC", - "LongName": "Physical Oceanography Distributed Active Archive Center, Jet Propulsion Laboratory, NASA" - } - ], - "MetadataSpecification": { - "URL": "https://cdn.earthdata.nasa.gov/umm/tool/v1.1.1", - "Name": "UMM-T", - "Version": "1.1.1" + { + "ToolCategory": "EARTH SCIENCE SERVICES", + "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", + "ToolTerm": "SUBSETTING/SUPERSETTING", + "ToolSpecificTerm": "VARIABLE SUBSETTING" + }, + { + "ToolCategory": "EARTH SCIENCE SERVICES", + "ToolTopic": "DATA ANALYSIS AND VISUALIZATION", + "ToolTerm": "GEOGRAPHIC INFORMATION SYSTEMS", + "ToolSpecificTerm": "WEB-BASED GEOGRAPHIC INFORMATION SYSTEMS" + }, + { + "ToolCategory": "EARTH SCIENCE SERVICES", + "ToolTopic": "DATA MANAGEMENT/DATA HANDLING", + "ToolTerm": "DATA ACCESS/RETRIEVAL" } -} \ No newline at end of file + ], + "Organizations": [ + { + "Roles": [ + "ORIGINATOR" + ], + "ShortName": "NASA/JPL/PODAAC", + "LongName": "Physical Oceanography Distributed Active Archive Center, Jet Propulsion Laboratory, NASA" + } + ], + "URL": { + "URLContentType": "DistributionURL", + "Type": "GOTO WEB TOOL", + "URLValue": "http://swodlr.podaac.uat.earthdatacloud.nasa.gov/" + }, + "MetadataSpecification": { + "URL": "https://cdn.earthdata.nasa.gov/umm/tool/v1.2.0", + "Name": "UMM-T", + "Version": "1.2.0" + }, + "PotentialAction": { + "Type": "SearchAction", + "Target": { + "Type": "EntryPoint", + "ResponseContentType": [ + "text/html" + ], + "UrlTemplate": "http://swodlr.podaac.uat.earthdatacloud.nasa.gov/l={+layers}&ve={+bbox}&d={+date}", + "Description": "SWODLR (swaa·dler) is an open-source software system developed to generate custom Level 2 raster data products for the SWOT mission. It provides an Application Programming Interface (API) and Graphical User Interface (GUI) that allows end-users to provide custom configurations to generate on-demand raster data products from underlying standard data products (PIXC, PIXCVec).", + "HttpMethod": [ + "GET" + ] + }, + "QueryInput": [ + { + "ValueName": "layers", + "Description": "A comma-separated list of visualization layer ids, as defined by GIBS. These layers will be portrayed on the web application", + "ValueRequired": true, + "ValueType": "https://wiki.earthdata.nasa.gov/display/GIBS/GIBS+API+for+Developers#GIBSAPIforDevelopers-LayerNaming" + }, + { + "ValueName": "date", + "Description": "A UTC ISO DateTime. The layers portrayed will correspond to this date.", + "ValueRequired": false, + "ValueType": "https://schema.org/startDate" + }, + { + "ValueName": "bbox", + "Description": "A spatial bounding box that will set the spatial extent of the portrayed layers. The first point is the lower corner, the second point is the upper corner. A box is expressed as two points separated by a space character.", + "ValueRequired": false, + "ValueType": "https://schema.org/box" + } + ] + } +} diff --git a/package-lock.json b/package-lock.json index a0afe0d..a26521c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "swodlr-ui", - "version": "1.1.0-10", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "swodlr-ui", - "version": "1.1.0-10", + "version": "1.0.0", "dependencies": { "@hexagon/base64": "^1.1.28", "@reduxjs/toolkit": "^1.9.5", @@ -2900,12 +2900,12 @@ "integrity": "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==" }, "node_modules/@gilbarbara/helpers": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@gilbarbara/helpers/-/helpers-0.9.1.tgz", - "integrity": "sha512-B6q4qruzaurfbpmdGK85SSgnI36pFuJlewTul9hWHUv7u8VGxDwjj8anxSfuPyDZ3ovXF1H6ifCVFHQqRV2+Gg==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@gilbarbara/helpers/-/helpers-0.9.2.tgz", + "integrity": "sha512-vrydO6+8jOpzPaJ9Om2Ta6BStbpxBlg7j0uV27NnokG+k6bI95ys7rrw7P4hOcRYajkp+K/XpyLufFUUfYrKTQ==", "dependencies": { "@gilbarbara/types": "^0.2.2", - "is-lite": "^1.2.0" + "is-lite": "^1.2.1" } }, "node_modules/@gilbarbara/types": { @@ -2917,9 +2917,9 @@ } }, "node_modules/@gilbarbara/types/node_modules/type-fest": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.9.0.tgz", - "integrity": "sha512-KS/6lh/ynPGiHD/LnAobrEFq3Ad4pBzOlJ1wAnJx9N4EYoqFhMfLIBjUT2UEx4wg5ZE+cC1ob6DCSpppVo+rtg==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.1.tgz", + "integrity": "sha512-7ZnJYTp6uc04uYRISWtiX3DSKB/fxNQT0B5o1OUeCqiQiwF+JC9+rJiZIDrPrNCLLuTqyQmh4VdQqh/ZOkv9MQ==", "engines": { "node": ">=16" }, @@ -4287,9 +4287,9 @@ } }, "node_modules/@types/leaflet-draw": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/leaflet-draw/-/leaflet-draw-1.0.10.tgz", - "integrity": "sha512-1tV0QW5qAcTCmuZZwH19qngHayLxTE9vyDGicfMoASnrFf0oo3+lFOnn/ZRKSskyHKB7T8s6DTk7Henaq4ueyg==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/leaflet-draw/-/leaflet-draw-1.0.11.tgz", + "integrity": "sha512-dyedtNm3aSmnpi6FM6VSl28cQuvP+MD7pgpXyO3Q1ZOCvrJKmzaDq0P3YZTnnBs61fQCKSnNYmbvCkDgFT9FHQ==", "dependencies": { "@types/leaflet": "*" } @@ -4353,9 +4353,9 @@ } }, "node_modules/@types/react-datepicker": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.19.3.tgz", - "integrity": "sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==", + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.19.5.tgz", + "integrity": "sha512-tKpuj19p9T4sBQm3Bw13CPuhalo4CFOe/LcSUGJ5z6DmHoiBX3uq33iMKePeSEq7OxyU8O1rh5emAm92nyXZLg==", "dev": true, "dependencies": { "@popperjs/core": "^2.9.2", @@ -13370,9 +13370,9 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/proj4": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.9.2.tgz", - "integrity": "sha512-bdyfNmtlWjQN/rHEHEiqFvpTUHhuzDaeQ6Uu1G4sPGqk+Xkxae6ahh865fClJokSGPBmlDOQWWaO6465TCfv5Q==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.10.0.tgz", + "integrity": "sha512-0eyB8h1PDoWxucnq88/EZqt7UZlvjhcfbXCcINpE7hqRN0iRPWE/4mXINGulNa/FAvK+Ie7F+l2OxH/0uKV36A==", "dependencies": { "mgrs": "1.0.0", "wkt-parser": "^1.3.3" @@ -13640,9 +13640,9 @@ } }, "node_modules/react-datepicker": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.24.0.tgz", - "integrity": "sha512-2QUC2pP+x4v3Jp06gnFllxKsJR0yoT/K6y86ItxEsveTXUpsx+NBkChWXjU0JsGx/PL8EQnsxN0wHl4zdA1m/g==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz", + "integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==", "dependencies": { "@popperjs/core": "^2.11.8", "classnames": "^2.2.6", @@ -13798,9 +13798,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-joyride/node_modules/type-fest": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.9.0.tgz", - "integrity": "sha512-KS/6lh/ynPGiHD/LnAobrEFq3Ad4pBzOlJ1wAnJx9N4EYoqFhMfLIBjUT2UEx4wg5ZE+cC1ob6DCSpppVo+rtg==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.1.tgz", + "integrity": "sha512-7ZnJYTp6uc04uYRISWtiX3DSKB/fxNQT0B5o1OUeCqiQiwF+JC9+rJiZIDrPrNCLLuTqyQmh4VdQqh/ZOkv9MQ==", "engines": { "node": ">=16" }, diff --git a/package.json b/package.json index f31a57d..910daf8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "swodlr-ui", - "version": "1.1.0-10", + "version": "1.0.0", "private": true, "engines": { "node": ">=18.0.0" diff --git a/scss/custom.scss b/scss/custom.scss index 700b3fb..8ad80de 100644 --- a/scss/custom.scss +++ b/scss/custom.scss @@ -4,7 +4,6 @@ @import "node_modules/bootstrap/scss/mixins"; $theme-colors: ( - // "primary": #2275ac, "primary": red, "secondary": #24B363 ); \ No newline at end of file diff --git a/src/components/about/About.tsx b/src/components/about/About.tsx index e08c0a2..83c9a26 100644 --- a/src/components/about/About.tsx +++ b/src/components/about/About.tsx @@ -4,9 +4,19 @@ import CompareImage from '../../assets/comparing-images.png' import YukonImage from '../../assets/SWOT-YUKON.jpeg' import LatLongUTM from '../../assets/lat-lon-vs-utm.png' import UpSWOTResolution from '../../assets/swot-go-up-resolution.jpg' +import packageJson from '../../../package.json' +import { useEffect, useState } from "react"; const About = () => { - + const [backendVersion, setBackendVersion] = useState('') + useEffect(() => { + const fetchData = async () => { + setBackendVersion(await fetch('https://swodlr.podaac.sit.earthdatacloud.nasa.gov/api/about').then((version) => version.json()).then(response => response.version)) + } + fetchData() + .catch(console.error); + }, []); + return (

About: SWOT On-Demand Level-2 Raster Generator

@@ -18,7 +28,7 @@ const About = () => { SWODLR is an on-demand raster generation tool that generates customized Surface Water and Ocean Topography (SWOT) Level 2 raster products. SWOT standard products are released in geographically fixed tiles at 100m and 250m resolutions in a Universal Transverse Mercator (UTM) projection grid. SWODLR allows users to generate the same products at different resolutions in either the UTM or geodetic coordinate system (lat/lon). SWODLR also gives an option to change the output granule extent from a nonoverlapping square 128 km x 128 km to an overlapping rectangle 256 km x 128 km to assist with observing areas of interest near the along-track edges of the original square extent.
- Like the standard product, the on-demand product contains rasterized water surface elevation and inundation-extents. This is derived through resampling the upstream pixel cloud (L2_HR_PIXC) and pixel vector (L2_HR_PIXCVEC) datasets onto a uniform grid. A uniform grid is superimposed onto the pixel cloud from the source products, and all pixel-cloud samples within each grid cell are aggregated to produce a single value per raster cell. SWODLR uses the original algorithm that standard SWOT products use to generate products but at a different resolution; it does not just re-grid the standard products. + Like the standard product, the on-demand product contains rasterized water surface elevation and inundation-extents. This is derived through resampling the upstream pixel cloud (L2_HR_PIXC) and pixel vector (L2_HR_PIXCVEC) datasets onto a uniform grid. A uniform grid is superimposed onto the pixel cloud from the source products, and all pixel-cloud samples within each grid cell are aggregated to produce a single value per raster cell. SWODLR uses the original algorithm that standard SWOT products use to generate products but at a different resolution; it does not just re-grid the standard products.
@@ -70,8 +80,6 @@ const About = () => { - {/*

FAQ

*/} -

Definitions

@@ -131,10 +139,24 @@ const About = () => {
-

Version History

+

Current Version

-
Version 1 (9/05/2023)
+ +
+ SWODLR UI: + {packageJson.version} +
+
+
{`(Release Notes)`}
+
+ + +
+ SWODLR API: + {backendVersion} +
+
diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx index 24f26b8..d8984af 100644 --- a/src/components/app/App.tsx +++ b/src/components/app/App.tsx @@ -14,9 +14,12 @@ import { Session } from '../../authentication/session'; import { getCurrentUser, setStartTutorial } from './appSlice'; import { useEffect, useState } from 'react'; import GranuleSelectionAndConfigurationView from '../sidebar/GranuleSelectionAndConfigurationView'; -import Joyride from 'react-joyride'; +import Joyride, { ACTIONS, EVENTS } from 'react-joyride'; import { deleteProduct } from '../sidebar/actions/productSlice'; import { tutorialSteps } from '../tutorial/tutorialConstants'; +import InteractiveTutorialModal from '../tutorial/InteractiveTutorialModal'; +import { setShowCloseTutorialTrue, setSkipTutorialTrue } from '../sidebar/actions/modalSlice'; +import InteractiveTutorialModalClose from '../tutorial/InteractiveTutorialModalClose'; const App = () => { const dispatch = useAppDispatch() @@ -32,32 +35,44 @@ const App = () => { const [joyride, setState] = useState({ run: startTutorial, - steps: tutorialSteps + steps: tutorialSteps, + stepIndex: 0 }) - + useEffect(() => { - setState({...joyride, run: startTutorial }) - + setState({...joyride, run: startTutorial, stepIndex: 0}) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [startTutorial]); const handleJoyrideCallback = (data: { action: any; index: any; status: any; type: any; step: any; lifecycle: any; }) => { - const { action, step, type, lifecycle } = data; + const { action, step, type, lifecycle, index } = data; const stepTarget = step.target - if (stepTarget === '#configure-options-breadcrumb' && action === 'update') { + + if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(type)) { + // Update state to advance the tour + setState({...joyride, stepIndex: index + (action === ACTIONS.PREV ? -1 : 1) }); + } + + if (action === 'close') { + dispatch(setShowCloseTutorialTrue()) + } else if (stepTarget === '#configure-options-breadcrumb' && action === 'update') { navigate(`/customizeProduct/configureOptions${search}`) - } else if (stepTarget === '#configure-options-breadcrumb' && action === 'prev') { + } else if (stepTarget === '#configure-options-breadcrumb' && action === 'prev' && lifecycle === 'complete') { navigate(`/customizeProduct/selectScenes${search}`) - } else if (stepTarget === '#my-data-page' && action === 'prev') { + } + else if (stepTarget === '#my-data-page' && action === 'prev' && lifecycle === 'complete') { navigate(`/customizeProduct/configureOptions${search}`) - } else if (stepTarget === '#added-scenes' && action === 'update') { - navigate(`/customizeProduct/selectScenes?cyclePassScene=1_413_120&showUTMAdvancedOptions=true`) + } + else if (stepTarget === '#added-scenes' && action === 'update') { + navigate(`/customizeProduct/selectScenes?cyclePassScene=9_515_130&showUTMAdvancedOptions=true`) } else if (stepTarget === '#customization-tab' && action === 'start') { navigate('/customizeProduct/selectScenes') - } else if ((stepTarget === '#generate-products-button' && action === 'close' && lifecycle === 'complete') || (stepTarget === '#my-data-page' && action === 'next')) { + } else if (action === 'next' && stepTarget === '#my-data-page') { navigate(`/generatedProductHistory${search}`) } else if (type === 'tour:end') { - dispatch(deleteProduct(addedProducts.map(product => product.granuleId))) + dispatch(setSkipTutorialTrue()) dispatch(setStartTutorial(false)) + dispatch(deleteProduct(addedProducts.map(product => product.granuleId))) navigate(`/customizeProduct/selectScenes`) } }; @@ -103,9 +118,8 @@ const App = () => { callback={(data) => handleJoyrideCallback(data)} run={joyride.run} steps={joyride.steps} + stepIndex={joyride.stepIndex} showProgress - showSkipButton - hideCloseButton continuous scrollToFirstStep /> @@ -118,6 +132,8 @@ const App = () => { , true) } /> , true)}/> + +
); } diff --git a/src/components/app/appSlice.ts b/src/components/app/appSlice.ts index 2194e64..624b694 100644 --- a/src/components/app/appSlice.ts +++ b/src/components/app/appSlice.ts @@ -1,5 +1,5 @@ import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit' -import { PageTypes, UserData } from '../../types/constantTypes' +import { PageTypes } from '../../types/constantTypes' import { CurrentUserData } from '../../types/graphqlTypes' import { Session } from '../../authentication/session'; import { getUserData } from '../../user/userData'; @@ -9,7 +9,8 @@ interface AppState { userAuthenticated: boolean, currentPage: PageTypes, currentUser: CurrentUserData | null, - startTutorial: boolean + startTutorial: boolean, + userHasCorrectEdlPermissions: boolean } export const getCurrentUser = createAsyncThunk('currentUser', async () => { @@ -21,7 +22,8 @@ const initialState: AppState = { userAuthenticated: false, currentPage: 'welcome', currentUser: null, - startTutorial: false + startTutorial: false, + userHasCorrectEdlPermissions: true } export const appSlice = createSlice({ @@ -37,6 +39,9 @@ export const appSlice = createSlice({ setStartTutorial: (state, action: PayloadAction) => { state.startTutorial = action.payload }, + setUserHasCorrectEdlPermissions: (state, action: PayloadAction) => { + state.userHasCorrectEdlPermissions = action.payload + }, }, extraReducers(builder) { builder.addCase(getCurrentUser.fulfilled, (state, action) => { @@ -55,6 +60,6 @@ export const appSlice = createSlice({ }, }); -export const { logoutCurrentUser, setStartTutorial } = appSlice.actions +export const { logoutCurrentUser, setStartTutorial, setUserHasCorrectEdlPermissions } = appSlice.actions export default appSlice.reducer diff --git a/src/components/edl/AuthorizationCodeHandler.tsx b/src/components/edl/AuthorizationCodeHandler.tsx index 30e12be..4d9be22 100644 --- a/src/components/edl/AuthorizationCodeHandler.tsx +++ b/src/components/edl/AuthorizationCodeHandler.tsx @@ -5,6 +5,46 @@ import { exchangeAuthenticationCode } from "../../authentication/edl"; import { OAuthTokenExchangeFailed } from "../../authentication/exception"; import { Session } from "../../authentication/session"; +import { spatialSearchCollectionConceptId, spatialSearchResultLimit } from "../../constants/rasterParameterConstants"; + +export const checkUseHasCorrectEdlPermissions = async () => { + try { + // get session token to use in spatial search query + const session = await Session.getCurrent(); + if (session === null) { + throw new Error('No current session'); + } + const authToken = await session.getAccessToken(); + if (authToken === null) { + throw new Error('Failed to get authentication token'); + } + + const polygonUrlString = '&polygon[]=-49.921875,68.58850924263909,-50.06469726562501,68.56844733448305,-50.06469726562501,68.52223694881727,-49.91638183593751,68.52424806853186,-49.921875,68.58850924263909' + const spatialSearchUrl = `https://cmr.earthdata.nasa.gov/search/granules?collection_concept_id=${spatialSearchCollectionConceptId}${polygonUrlString}&page_size=${spatialSearchResultLimit}` + const userHasCorrectEdlPermissions = await fetch(spatialSearchUrl, { + method: 'GET', + credentials: 'omit', + headers: { + Authorization: `Bearer ${authToken}` + } + }).then(response => response.text()).then(data => { + const parser = new DOMParser(); + const xml = parser.parseFromString(data, "application/xml"); + const userHasCorrectEdlPermissions = parseInt(xml.getElementsByTagName("hits")[0].textContent ?? '0') > 0 + return userHasCorrectEdlPermissions + }) + return userHasCorrectEdlPermissions + } catch (err) { + if (err instanceof Error) { + // return err + return false + } else { + // return 'something happened' + return false + } + } +} + export default function AuthorizationCodeHandler(): ReactElement { const dispatch = useAppDispatch(); const [searchParams] = useSearchParams(); @@ -30,7 +70,6 @@ export default function AuthorizationCodeHandler(): ReactElement { } else if (ex instanceof TypeError) { console.debug('Network error') } - // TODO: improve this handling resetAuth(); }); diff --git a/src/components/history/GeneratedProductHistory.tsx b/src/components/history/GeneratedProductHistory.tsx index 0ff7986..474a0b3 100644 --- a/src/components/history/GeneratedProductHistory.tsx +++ b/src/components/history/GeneratedProductHistory.tsx @@ -1,4 +1,4 @@ -import { Alert, Col, OverlayTrigger, Row, Table, Tooltip, Button } from "react-bootstrap"; +import { Alert, Col, OverlayTrigger, Row, Table, Tooltip, Button, Spinner } from "react-bootstrap"; import { useAppSelector } from "../../redux/hooks"; import { getUserProductsResponse, Product } from "../../types/graphqlTypes"; import { useEffect, useState } from "react"; @@ -8,15 +8,18 @@ import { getUserProducts } from "../../user/userData"; import { useLocation, useNavigate } from "react-router-dom"; const GeneratedProductHistory = () => { - // const generatedProducts = useAppSelector((state) => state.product.generatedProducts) const colorModeClass = useAppSelector((state) => state.navbar.colorModeClass) const { search } = useLocation(); const navigate = useNavigate() const [userProducts, setUserProducts] = useState([]) + const [waitingForProductsToLoad, setWaitingForProductsToLoad] = useState(true) useEffect(() => { const fetchData = async () => { - const userProductsResponse: getUserProductsResponse = await getUserProducts() + const userProductsResponse: getUserProductsResponse = await getUserProducts().then((response) => { + setWaitingForProductsToLoad(false) + return response + }) if (userProductsResponse.status === 'success') setUserProducts(userProductsResponse.products as Product[]) } fetchData() @@ -43,6 +46,32 @@ const GeneratedProductHistory = () => { ) + + const renderCopyDownloadButton = (downloadUrlString: string) => ( + + Copy + + } + > + + + ) + + const renderDownloadButton = (downloadUrlString: string) => ( + + Download + + } + > + + + ) const renderColTitle = (labelEntry: string[], index: number) => { let infoIcon = infoIconsToRender.includes(labelEntry[0]) ? renderInfoIcon(labelEntry[0]) : null @@ -66,7 +95,6 @@ const GeneratedProductHistory = () => { {userProducts.map((generatedProductObject, index) => { const {status, utmZoneAdjust, mgrsBandAdjust, outputGranuleExtentFlag, outputSamplingGridType, rasterResolution, timestamp: dateGenerated, cycle, pass, scene, granules} = generatedProductObject const statusToUse = status[0].state - // const downloadUrl = granules && granules.length !== 0 ? {granules[0].uri.split('/').pop()} : 'N/A' const downloadUrl = granules && granules.length !== 0 ? granules[0].uri.split('/').pop() : 'N/A' const utmZoneAdjustToUse = outputSamplingGridType === 'GEO' ? 'N/A' : utmZoneAdjust const mgrsBandAdjustToUse = outputSamplingGridType === 'GEO' ? 'N/A' : mgrsBandAdjust @@ -80,10 +108,10 @@ const GeneratedProductHistory = () => { if (entry[0] === 'downloadUrl' && entry[1] !== 'N/A') { const downloadUrlString = granules[0].uri cellContents = - + {entry[1]} - - + {(renderCopyDownloadButton(downloadUrlString))} + {renderDownloadButton(downloadUrlString)} } else { cellContents = entry[1] @@ -105,26 +133,33 @@ const GeneratedProductHistory = () => { return navigate(`/generatedProductHistory${search}`)} style={{cursor: 'pointer'}}>{alertMessage} } + const waitingForProductsToLoadSpinner = () => { + return ( +
+
Loading Data Table...
+ + Loading... + +
+ ) + } + const renderProductHistoryViews = () => { - let viewToShow - // if (userProducts.length === 0) { - // viewToShow = productHistoryAlert() - // } else { - // viewToShow = renderHistoryTable() - // } return ( - -

Generated Products Data

- {renderHistoryTable()} - {userProducts.length === 0 ? {productHistoryAlert()} : null} + + {renderHistoryTable()} + {userProducts.length === 0 ? {productHistoryAlert()} : null} ) } return ( - - {renderProductHistoryViews()} - + <> +

Generated Products Data

+ + {waitingForProductsToLoad ? waitingForProductsToLoadSpinner() : renderProductHistoryViews()} + + ); } diff --git a/src/components/map/WorldMap.tsx b/src/components/map/WorldMap.tsx index 70e84c9..75b413f 100644 --- a/src/components/map/WorldMap.tsx +++ b/src/components/map/WorldMap.tsx @@ -1,4 +1,4 @@ -import { MapContainer, Polygon, TileLayer, Tooltip, ZoomControl, useMap, FeatureGroup } from 'react-leaflet' +import { MapContainer, Polygon, TileLayer, Tooltip, ZoomControl, useMap, FeatureGroup, useMapEvent } from 'react-leaflet' import L, { LatLngExpression } from 'leaflet'; import 'leaflet/dist/leaflet.css' import { useAppDispatch, useAppSelector } from '../../redux/hooks' @@ -12,7 +12,8 @@ import booleanClockwise from '@turf/boolean-clockwise'; import { afterCPSL, afterCPSR, beforeCPS, spatialSearchCollectionConceptId, spatialSearchResultLimit } from '../../constants/rasterParameterConstants'; import { addSpatialSearchResults, setMapFocus, setWaitingForSpatialSearch } from '../sidebar/actions/productSlice'; import { SpatialSearchResult } from '../../types/constantTypes'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useSearchParams } from 'react-router-dom'; +import { useEffect } from 'react'; let DefaultIcon = L.icon({ iconUrl: icon, @@ -20,17 +21,63 @@ let DefaultIcon = L.icon({ }); L.Marker.prototype.options.icon = DefaultIcon; +const UpdateMapCenter = () => { + const dispatch = useAppDispatch() + const mapFocus = useAppSelector((state) => state.product.mapFocus) + // search parameters + const [searchParams, setSearchParams] = useSearchParams() + + // put the current center and zoom into the url parameters + const handleMapFocus = (center: number[], zoom: number) => { + const currentSearchParams = Object.fromEntries(searchParams.entries()) + currentSearchParams.center = `${center[0]},${center[1]}` + currentSearchParams.zoom = String(zoom) + setSearchParams(currentSearchParams) + dispatch(setMapFocus({center, zoom})) + } + + const map = useMapEvent('moveend', () => { + const center = [map.getCenter().lat, map.getCenter().lng] + const zoom = map.getZoom() + if ((mapFocus.center[0] !== center[0] && mapFocus.center[1] !== center[1]) || mapFocus.zoom !== zoom) handleMapFocus(center, zoom) + }) + return null +} + +const ChangeView = () => { + const mapFocus = useAppSelector((state) => state.product.mapFocus) + const map = useMap() + map.setView(mapFocus.center as LatLngExpression, mapFocus.zoom) + return null +} + const WorldMap = () => { const addedProducts = useAppSelector((state) => state.product.addedProducts) const mapFocus = useAppSelector((state) => state.product.mapFocus) + const userHasCorrectEdlPermissions = useAppSelector((state) => state.app.userHasCorrectEdlPermissions) const dispatch = useAppDispatch() const footprintStyleOptions = { color: 'limegreen' } + // search parameters + const [searchParams, setSearchParams] = useSearchParams() - const ChangeView = () => { - const map = useMap(); - map.setView(mapFocus.center as LatLngExpression, mapFocus.zoom); - return null - } + useEffect(() => { + // if center and zoom are in url params, set the current center to them + const center = searchParams.get('center') + const zoom = searchParams.get('zoom') + if (center && zoom) { + const centerParamSplit = center.split(',') + const centerToUse: number[] = [parseFloat(centerParamSplit[0]), parseFloat(centerParamSplit[1])] + const zoomToUse = parseInt(zoom) + if (centerToUse !== mapFocus.center || zoomToUse !== mapFocus.zoom) { + dispatch(setMapFocus({center: centerToUse, zoom: zoomToUse})) + } + } + + // TODO: implement search polygon search param + // const searchPolygon = searchParams.get('searchPolygon') + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const getScenesWithinCoordinates = async (coordinatesToSearch: {lat: number, lng: number}[][]) => { try { @@ -43,7 +90,6 @@ const WorldMap = () => { if (authToken === null) { throw new Error('Failed to get authentication token'); } - dispatch(setWaitingForSpatialSearch(true)) const polygonUrlString = coordinatesToSearch.map((polygon) => { @@ -72,24 +118,24 @@ const WorldMap = () => { headers: { Authorization: `Bearer ${authToken}` } - }).then(response => response.text()).then(data => { + }).then(async data => { + const responseText = await data.text() + // TODO: make subsequent calls to get granules in spatial search area till everything is found. + // current issue is that 1000 (2000 total divided by 2) is limited by the cmr api. const parser = new DOMParser(); - const xml = parser.parseFromString(data, "application/xml"); + const xml = parser.parseFromString(responseText, "application/xml"); const references: SpatialSearchResult[] = Array.from(new Set(Array.from(xml.getElementsByTagName("name")).map(nameElement => { return (nameElement.textContent)?.match(`${beforeCPS}([0-9]+(_[0-9]+)+)(${afterCPSR}|${afterCPSL})`)?.[1] }))).map(foundIdString => { const cyclePassSceneStringArray = foundIdString?.split('_').map(id => parseInt(id).toString()) const tileValue = parseInt(cyclePassSceneStringArray?.[2] as string) - // const sceneToUse = String(Math.floor(tileValue / 2)) const sceneToUse = String(Math.floor(tileValue)) return {cycle: cyclePassSceneStringArray?.[0], pass: cyclePassSceneStringArray?.[1], scene : sceneToUse} as SpatialSearchResult }) return references }) dispatch(addSpatialSearchResults(spatialSearchResponse as SpatialSearchResult[])) - dispatch(setWaitingForSpatialSearch(false)) } catch (err) { - dispatch(setWaitingForSpatialSearch(false)) if (err instanceof Error) { return err } else { @@ -99,9 +145,17 @@ const WorldMap = () => { } const onCreate = async (createEvent: any) => { - await getScenesWithinCoordinates([createEvent.layer.getLatLngs()[0]]) - // set the new map focus location to what it was when polygon created so it will stay the same after map reload - dispatch(setMapFocus({center: [createEvent.layer._renderer._center.lat, createEvent.layer._renderer._center.lng], zoom: createEvent.target._zoom})) + const searchPolygonLatLngs = createEvent.layer.getLatLngs()[0] + + // TODO: implement search polygon search param + // const currentSearchParams = Object.fromEntries(searchParams.entries()) + // const searchPolygonLatLngsString = searchPolygonLatLngs.map((latLngObject: {lat: number, lng: number}) => `${latLngObject.lat},${latLngObject.lng}`).join('_') + // currentSearchParams.searchPolygon = searchPolygonLatLngsString + // setSearchParams(currentSearchParams) + + await getScenesWithinCoordinates([searchPolygonLatLngs]) + // set the new map focus location to what it was when polygon created so it will stay the same after map reload + dispatch(setMapFocus({center: [createEvent.layer._renderer._center.lat, createEvent.layer._renderer._center.lng], zoom: createEvent.target._zoom})) } const onEdit = async (editEvent: any) => { @@ -115,16 +169,14 @@ const WorldMap = () => { - {useLocation().pathname.includes('selectScenes') ? ( + {(useLocation().pathname.includes('selectScenes') && userHasCorrectEdlPermissions) ? ( onCreate(createEvent)} onEdited={(editEvent) => onEdit(editEvent)} - // onDeleted={(deleteEvent) => onDelete(deleteEvent)} draw={{ rectangle: false, polyline: false, @@ -141,12 +193,20 @@ const WorldMap = () => { url='https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}' attribution='Esri, Maxar, Earthstar Geographics, and the GIS User Community' maxZoom = {18} + noWrap + bounds={ + [ + [-89.9999, -179.9999], + [89.9999, 179.9999] + ] + } /> + {addedProducts.map((productObject, index) => ( - {[
{`Cycle: ${productObject.cycle}`}
,
{`Pass: ${productObject.pass}`}
,
{`Scene: ${productObject.scene}`}
]}
+ {[
{`Cycle: ${productObject.cycle}`}
,
{`Pass: ${productObject.pass}`}
,
{`Scene: ${productObject.scene}`}
]}
))}
diff --git a/src/components/navbar/PodaacFooter.tsx b/src/components/navbar/PodaacFooter.tsx index 3a4725e..5ae3e14 100644 --- a/src/components/navbar/PodaacFooter.tsx +++ b/src/components/navbar/PodaacFooter.tsx @@ -2,9 +2,11 @@ import Navbar from 'react-bootstrap/Navbar'; import { useAppSelector } from '../../redux/hooks' import { Col, Row } from 'react-bootstrap'; import { useLocation, useNavigate } from 'react-router-dom'; +import packageJson from '../../../package.json' const PodaacFooter = () => { const colorModeClass = useAppSelector((state) => state.navbar.colorModeClass) + const userAuthenticated = useAppSelector((state) => state.app.userAuthenticated) const navigate = useNavigate(); const { search } = useLocation(); @@ -12,7 +14,7 @@ const PodaacFooter = () => { - Version 1.0 Pre-Alpha of SWOT On-Demand Level-2 Raster Generator (SWODLR) + {`Version ${packageJson.version} Beta of SWOT On-Demand Level-2 Raster Generator (SWODLR)`} @@ -21,11 +23,14 @@ const PodaacFooter = () => { Privacy - - navigate(`/about${search}`)}> - About SWODLR - - + { userAuthenticated ? + ( + navigate(`/about${search}`)}> + About SWODLR + + ) + : null + } window.open('mailto:podaac@podaac.jpl.nasa.gov')}> Contact diff --git a/src/components/sidebar/CustomizeProductView.tsx b/src/components/sidebar/CustomizeProductView.tsx index bc47460..3eba245 100644 --- a/src/components/sidebar/CustomizeProductView.tsx +++ b/src/components/sidebar/CustomizeProductView.tsx @@ -1,6 +1,7 @@ import GranuleTable from './GranulesTable'; import ProductCustomization from './ProductCustomization'; import GenerateProducts from './GenerateProducts'; +import GranuleTableAlerts from './GranuleTableAlerts'; const GranuleSelectionView = () => { return ( @@ -9,6 +10,8 @@ const GranuleSelectionView = () => {
+ + ); } diff --git a/src/components/sidebar/CustomizeProductsSidebar.tsx b/src/components/sidebar/CustomizeProductsSidebar.tsx index 32f243e..c88379e 100644 --- a/src/components/sidebar/CustomizeProductsSidebar.tsx +++ b/src/components/sidebar/CustomizeProductsSidebar.tsx @@ -8,7 +8,6 @@ import CustomizeProductView from './CustomizeProductView'; import GranuleSelectionView from './GranuleSelectionView'; import { CustomizeProductSidebarProps } from '../../types/constantTypes'; import { ArrowsExpand } from 'react-bootstrap-icons'; -import InteractiveTutorialModal from '../tutorial/InteractiveTutorialModal'; const CustomizeProductsSidebar = (props: CustomizeProductSidebarProps) => { const { mode } = props @@ -48,6 +47,7 @@ const CustomizeProductsSidebar = (props: CustomizeProductSidebarProps) => { setLocalSidebarWidth(sidebarWidthNumber) setSidebarWidth(sidebarWidthNumber) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [resizeEndLocation]) return ( @@ -58,12 +58,9 @@ const CustomizeProductsSidebar = (props: CustomizeProductSidebarProps) => { {renderSidebarContents()} - - {/* TODO: uncomment when granule footprints are being retrieved to display on map */}
handleResizeClickDown(event)}> handleResizeClickDown(event)}/>
- ); } diff --git a/src/components/sidebar/DeleteGranulesModal.tsx b/src/components/sidebar/DeleteGranulesModal.tsx index 781f215..1f1b10c 100644 --- a/src/components/sidebar/DeleteGranulesModal.tsx +++ b/src/components/sidebar/DeleteGranulesModal.tsx @@ -4,13 +4,38 @@ import { useAppSelector, useAppDispatch } from '../../redux/hooks' import { setShowDeleteProductModalFalse } from './actions/modalSlice' import { Row } from 'react-bootstrap'; import { deleteProduct, setSelectedGranules } from './actions/productSlice'; +import { useSearchParams } from 'react-router-dom'; const GenerateProductsModal = () => { const showDeleteProductModal = useAppSelector((state) => state.modal.showDeleteProductModal) const selectedGranules = useAppSelector((state) => state.product.selectedGranules) const dispatch = useAppDispatch() + const [searchParams, setSearchParams] = useSearchParams() + + const removeCPSFromUrl = (cpsCombosToRemove: string[]) => { + const cyclePassSceneParameters = searchParams.get('cyclePassScene')?.split('-') + if (cyclePassSceneParameters) { + const cyclePassSceneParametersToKeep = cyclePassSceneParameters.filter(cpsCombo => { + const urlCPSSplit = cpsCombo.split('_') + const reconstructedUrlCPS = `${urlCPSSplit[0]}_${urlCPSSplit[1]}_${urlCPSSplit[2]}` + const keepCPSCombo = !cpsCombosToRemove.includes(reconstructedUrlCPS) + return keepCPSCombo + }).join('-') + const currentUrlParameters = Object.fromEntries(searchParams.entries()) + if (cyclePassSceneParametersToKeep.length === 0) { + const {cyclePassScene, ...restOfCurrentUrlParameters} = currentUrlParameters + setSearchParams(restOfCurrentUrlParameters) + } else { + setSearchParams({...currentUrlParameters, cyclePassScene: cyclePassSceneParametersToKeep}) + } + } + } + const handleDelete = () => { dispatch(deleteProduct(selectedGranules)) + // remove url parameters of selectedGranules + removeCPSFromUrl(selectedGranules) + // addSearchParamToCurrentUrlState dispatch(setSelectedGranules([])) // unselect select-all box dispatch(setShowDeleteProductModalFalse()) diff --git a/src/components/sidebar/GenerateProductsModal.tsx b/src/components/sidebar/GenerateProductsModal.tsx index de92f11..aa83945 100644 --- a/src/components/sidebar/GenerateProductsModal.tsx +++ b/src/components/sidebar/GenerateProductsModal.tsx @@ -2,18 +2,26 @@ import Button from 'react-bootstrap/Button'; import Modal from 'react-bootstrap/Modal'; import { useAppSelector, useAppDispatch } from '../../redux/hooks' import { setShowGenerateProductModalFalse } from './actions/modalSlice' -import { addGeneratedProducts } from './actions/productSlice' +import { addGeneratedProducts, addGranuleTableAlerts } from './actions/productSlice' import { Row } from 'react-bootstrap'; +import { granuleAlertMessageConstant } from '../../constants/rasterParameterConstants'; +import { alertMessageInput } from '../../types/constantTypes'; const GenerateProductsModal = () => { const showGenerateProductModal = useAppSelector((state) => state.modal.showGenerateProductModal) const addedGranules = useAppSelector((state) => state.product.addedProducts) const dispatch = useAppDispatch() + const setSaveGranulesAlert = (alert: alertMessageInput, additionalParameters?: any[]) => { + const {message, variant} = granuleAlertMessageConstant[alert] + dispatch(addGranuleTableAlerts({type: alert, message, variant, tableType: 'productCustomization' })) + } + const handleGenerate = () => { // unselect select-all box dispatch(addGeneratedProducts(addedGranules.map(granuleObj => granuleObj.granuleId))) dispatch(setShowGenerateProductModalFalse()) + setSaveGranulesAlert('successfullyGenerated') } return ( diff --git a/src/components/sidebar/GranuleSelectionAndConfigurationView.tsx b/src/components/sidebar/GranuleSelectionAndConfigurationView.tsx index 737f637..17bac93 100644 --- a/src/components/sidebar/GranuleSelectionAndConfigurationView.tsx +++ b/src/components/sidebar/GranuleSelectionAndConfigurationView.tsx @@ -1,10 +1,36 @@ import CustomizeProductsSidebar from './CustomizeProductsSidebar'; import { GranuleSelectionAndConfigurationViewProps } from '../../types/constantTypes'; import WorldMap from '../map/WorldMap' +import { setShowTutorialModalTrue, setSkipTutorialTrue } from './actions/modalSlice'; +import { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../../redux/hooks'; +import { checkUseHasCorrectEdlPermissions } from '../edl/AuthorizationCodeHandler'; +import { setUserHasCorrectEdlPermissions } from '../app/appSlice'; const GranuleSelectionAndConfigurationView = (props: GranuleSelectionAndConfigurationViewProps) => { + const dispatch = useAppDispatch() + const skipTutorial = useAppSelector((state) => state.modal.skipTutorial) + const userAuthenticated = useAppSelector((state) => state.app.userAuthenticated) const {mode} = props + useEffect(() => { + const fetchData = async () => { + const userHasCorrectEdlPermissions = await checkUseHasCorrectEdlPermissions() + dispatch(setUserHasCorrectEdlPermissions(userHasCorrectEdlPermissions)) + } + + // call the function + fetchData() + }, []) + + useEffect(() => { + if (!skipTutorial && userAuthenticated) { + dispatch(setShowTutorialModalTrue()) + dispatch(setSkipTutorialTrue()) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userAuthenticated]) + return ( <> diff --git a/src/components/sidebar/GranuleSelectionView.tsx b/src/components/sidebar/GranuleSelectionView.tsx index ca17591..bd1f46d 100644 --- a/src/components/sidebar/GranuleSelectionView.tsx +++ b/src/components/sidebar/GranuleSelectionView.tsx @@ -1,28 +1,28 @@ -import { Button, Col, Row } from 'react-bootstrap'; -import { ArrowReturnRight} from 'react-bootstrap-icons'; +import { Alert, Col, Row } from 'react-bootstrap'; import GranuleTable from './GranulesTable'; -import { useAppSelector } from '../../redux/hooks' import GranuleTableAlerts from './GranuleTableAlerts'; -import { useLocation, useNavigate } from 'react-router-dom'; import SpatialSearchOptions from './SpatialSearchOptions'; +import { useAppSelector } from '../../redux/hooks'; const GranuleSelectionView = () => { - const addedProducts = useAppSelector((state) => state.product.addedProducts) - const navigate = useNavigate(); - const { search } = useLocation(); - + const userHasCorrectEdlPermissions = useAppSelector((state) => state.app.userHasCorrectEdlPermissions) return ( - <> +
- -
- - - - - - + + { + userHasCorrectEdlPermissions ? + null : +
+ + + The SWOT dataset is not public yet. Until then, some functionality of this site will be limited. You are not yet able to add scenes, configure scenes, or generate products. + + +
+ } +
); } diff --git a/src/components/sidebar/GranuleTableAlerts.tsx b/src/components/sidebar/GranuleTableAlerts.tsx index f832aa7..48bfe99 100644 --- a/src/components/sidebar/GranuleTableAlerts.tsx +++ b/src/components/sidebar/GranuleTableAlerts.tsx @@ -1,13 +1,14 @@ import { useAppSelector } from '../../redux/hooks' import { Alert, Col, Row } from 'react-bootstrap'; import DeleteGranulesModal from './DeleteGranulesModal'; +import { TableTypes } from '../../types/constantTypes'; -const GranuleTableAlerts = () => { +const GranuleTableAlerts: React.FC<{tableType: TableTypes}> = ({tableType}) => { const granuleTableAlerts = useAppSelector((state) => state.product.granuleTableAlerts) return (
- {granuleTableAlerts.map((alertObject, index) => ( + {granuleTableAlerts.filter(item => item.tableType === tableType).map((alertObject, index) => ( {alertObject.message} diff --git a/src/components/sidebar/GranulesTable.tsx b/src/components/sidebar/GranulesTable.tsx index 3fdad95..3e42b76 100644 --- a/src/components/sidebar/GranulesTable.tsx +++ b/src/components/sidebar/GranulesTable.tsx @@ -5,8 +5,8 @@ import { granuleAlertMessageConstant, granuleSelectionLabels, productCustomizati footprintSearchCollectionConceptId } from '../../constants/rasterParameterConstants'; import { Button, Col, Form, OverlayTrigger, Row, Tooltip, Spinner } from 'react-bootstrap'; import { InfoCircle, Plus, Trash } from 'react-bootstrap-icons'; -import { AdjustType, AdjustValueDecoder, GranuleForTable, GranuleTableProps, InputType, SaveType, SpatialSearchResult, TableTypes, alertMessageInput, allProductParameters, validScene } from '../../types/constantTypes'; -import { addProduct, setSelectedGranules, setGranuleFocus, addGranuleTableAlerts, editProduct, addSpatialSearchResults, setWaitingForFootprintSearch, clearGranuleTableAlerts } from './actions/productSlice'; +import { AdjustType, AdjustValueDecoder, GranuleForTable, GranuleTableProps, InputType, SaveType, SpatialSearchResult, TableTypes, alertMessageInput, allProductParameters, handleSaveResult, validScene } from '../../types/constantTypes'; +import { addProduct, setSelectedGranules, setGranuleFocus, addGranuleTableAlerts, editProduct, addSpatialSearchResults, clearGranuleTableAlerts, setWaitingForSpatialSearch } from './actions/productSlice'; import { setShowDeleteProductModalTrue } from './actions/modalSlice'; import DeleteGranulesModal from './DeleteGranulesModal'; import { graphQLClient } from '../../user/userData'; @@ -23,9 +23,10 @@ const GranuleTable = (props: GranuleTableProps) => { const generateProductParameters = useAppSelector((state) => state.product.generateProductParameters) const showUTMAdvancedOptions = useAppSelector((state) => state.product.showUTMAdvancedOptions) const waitingForSpatialSearch = useAppSelector((state) => state.product.waitingForSpatialSearch) - const waitingForFootprintSearch = useAppSelector((state) => state.product.waitingForFootprintSearch) const spatialSearchStartDate = useAppSelector((state) => state.product.spatialSearchStartDate) const spatialSearchEndDate = useAppSelector((state) => state.product.spatialSearchEndDate) + const startTutorial = useAppSelector((state) => state.app.startTutorial) + const userHasCorrectEdlPermissions = useAppSelector((state) => state.app.userHasCorrectEdlPermissions) const dispatch = useAppDispatch() @@ -33,52 +34,106 @@ const GranuleTable = (props: GranuleTableProps) => { const {outputSamplingGridType} = generateProductParameters // search parameters - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams() // set the default url state parameters useEffect(() => { - dispatch(clearGranuleTableAlerts()) // if any cycle scene and pass parameters in url, add them to table const cyclePassSceneParameters = searchParams.get('cyclePassScene') if (cyclePassSceneParameters) { const sceneParamArray = Array.from(new Set(cyclePassSceneParameters.split('-'))) sceneParamArray.forEach((sceneParams, index) => { const splitSceneParams = sceneParams.split('_') - if (splitSceneParams.length > 3) { - // update zone and band adjust values - const zoneAdjustValue = adjustParamDecoder('value', splitSceneParams[3]) - const bandAdjustValue = adjustParamDecoder('value', splitSceneParams[4]) - const productToEdit = addedProducts.find(granuleObj => granuleObj.cycle === splitSceneParams[0] && granuleObj.pass === splitSceneParams[1] && granuleObj.scene === splitSceneParams[2]) - if (productToEdit?.utmZoneAdjust !== zoneAdjustValue || productToEdit?.mgrsBandAdjust !== bandAdjustValue) { - const editedProduct = {...productToEdit, utmZoneAdjust: zoneAdjustValue, mgrsBandAdjust: bandAdjustValue} - dispatch(editProduct(editedProduct as allProductParameters)) - } - } handleSave('urlParameter', sceneParamArray.length, index, splitSceneParams[0], splitSceneParams[1], splitSceneParams[2]) }) } - }, [tableType === 'granuleSelection' ? null : addedProducts]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tableType === 'granuleSelection' ? null : addedProducts, startTutorial ? searchParams : null]) + const validateSceneAvailability = async (cycleToUse: number, passToUse: number, sceneToUse: number[], cpsList?: {cycle: string, pass: string, scene: string}[]): Promise => { + try { + // build graphql availableScene query with all cycle/pass/scene combos requested + let queryAliasString = `` + // if there is a list of cycle pass and scenes go through them (spatial search) and if not, use first 3 function params (manual search) + if (cpsList) { + for(const specificCPS of cpsList) { + const {cycle, pass, scene} = specificCPS + const comboId = `${cycle}_${pass}_${scene}` + queryAliasString += ` s_${comboId}: availableScene(cycle: ${cycle}, pass: ${pass}, scene: ${scene}) ` + } + } else { + for(const specificScene of sceneToUse) { + const comboId = `${cycleToUse}_${passToUse}_${specificScene}` + queryAliasString += ` s_${comboId}: availableScene(cycle: ${cycleToUse}, pass: ${passToUse}, scene: ${specificScene}) ` + } + } + const queryAliasObject = `{${queryAliasString}}` + const res: {availableScene: boolean} = await graphQLClient.request(queryAliasObject).then(response => { + const responseToReturn = Object.fromEntries(Object.entries(response as {availableScene: boolean}).map(responseObj => [responseObj[0].replace('s_', ''), responseObj[1]])) + return responseToReturn as {availableScene: boolean} + }) + return res + + } catch (err) { + console.log (err) + return {} + } + } + + // Spatial search use effect useEffect(() => { dispatch(clearGranuleTableAlerts()) if (spatialSearchResults.length > 0) { let scenesFoundArray: string[] = [] + let addedScenes: string[] = [] + const fetchData = async () => { - for(let i=0; i { - - scenesFoundArray.push(result) - }) + if (spatialSearchResults.length < 1000) { + // check validity before saving + const validationResult = await validateSceneAvailability(0,0,[0],spatialSearchResults).then(result => Object.entries(result).filter(resultEntry => resultEntry[1]).map(valuePair => { + const cpsSplit = valuePair[0].split('_') + return {cycle: cpsSplit[0], pass: cpsSplit[1], scene: cpsSplit[2]} + })) + + if (validationResult.length > 0) { + for(let i=0; i result === 'found something').length) >= granuleTableLimit) { + // don't let more than 10 be added + scenesFoundArray.push('hit granule limit') + } else { + await handleSave('spatialSearch', validationResult.length, i, validationResult[i].cycle, validationResult[i].pass, validationResult[i].scene).then(result => { + if(result.savedScenes) { + addedScenes.push(...(result.savedScenes).map(productObject => productObject.granuleId)) + } + scenesFoundArray.push(result.result) + }) + } + + } + if(addedScenes.length > 0) { + // add parameters + addSearchParamToCurrentUrlState({'cyclePassScene': addedScenes.join('-')}) + } + } else { + scenesFoundArray.push('noScenesFound') + } + } else { + // If too many spatial search results, the search doesn't work because there too many granules and a limit was reached. + // In this scenario, make an alert that indicates that the search area was too large. + // TODO: remove this alert when there is a fix implemented for cmr spatial search limit. + // The valid granules are sometimes not a part of the first 1000 results which is the bug here. + scenesFoundArray.push('spatialSearchAreaTooLarge') } + dispatch(setWaitingForSpatialSearch(false)) return scenesFoundArray } // call the function fetchData() - .then((noScenesFoundResult) => { - if(scenesFoundArray.includes('noScenesFound') && !scenesFoundArray.includes('found something')){ - setSaveGranulesAlert('noScenesFound') - } + .then((noScenesFoundResults) => { + if(noScenesFoundResults.includes('noScenesFound') && !noScenesFoundResults.includes('found something')) setSaveGranulesAlert('noScenesFound') + if(noScenesFoundResults.includes('hit granule limit')) setSaveGranulesAlert('granuleLimit') + if(noScenesFoundResults.includes('spatialSearchAreaTooLarge')) setSaveGranulesAlert('spatialSearchAreaTooLarge') }) // make sure to catch any error .catch(console.error); @@ -86,19 +141,54 @@ const GranuleTable = (props: GranuleTableProps) => { // clear spatial results out of redux after use if(spatialSearchResults.length !== 0) dispatch(addSpatialSearchResults([] as SpatialSearchResult[])) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [spatialSearchResults]) const addSearchParamToCurrentUrlState = (newPairsObject: object, remove?: string) => { - const currentSearchParams = Object.fromEntries(searchParams.entries()) - Object.entries(newPairsObject).forEach(pair => { + const currentSearchParams = Object.fromEntries(searchParams.entries()) + const cyclePassSceneParameters = searchParams.get('cyclePassScene') + Object.entries(newPairsObject).forEach(pair => { + if (pair[0] === 'cyclePassScene') { + if(cyclePassSceneParameters !== null) { + // check if cps already exists in cyclePassSceneParameters + const currentCpsUrlSplit = cyclePassSceneParameters.split('-') + const paramsToAddSplit = pair[1].toString().split('-') + // let combinedParamsArray = currentCpsUrlSplit + let newParamsArray: string[] = [] + paramsToAddSplit.forEach((newParam: string) => { + if(!currentCpsUrlSplit.includes(newParam)) { + newParamsArray.push(newParam) + // if cps combo not already in url param, add it + // NOTE FOR WHEN I GET BACK: making sure no duplicates of cps + } + }) + if (newParamsArray.length > 0) { + // if cps without adjust params is in currentCpsUrlSplit and newParamsArray has cps with added adjust params + const newCPSParams: string[] = [] + newParamsArray.forEach(newParam => { + currentCpsUrlSplit.forEach(oldParam => { + const splitOldParam = oldParam.split('_') + if(!newParam.includes(`${splitOldParam[0]}_${splitOldParam[1]}_${splitOldParam[2]}`) && !newCPSParams.includes(oldParam)) { + // remove old param + newCPSParams.push(oldParam) + } + }) + }) + currentSearchParams[pair[0]] = [...newCPSParams, ...newParamsArray].join('-') + } + } else { currentSearchParams[pair[0]] = pair[1].toString() - }) - - // remove unused search param - if (remove) { - delete currentSearchParams[remove] + } + } else { + currentSearchParams[pair[0]] = pair[1].toString() } - setSearchParams(currentSearchParams) + }) + + // remove unused search param + if (remove) { + delete currentSearchParams[remove] + } + setSearchParams(currentSearchParams) } // add granules @@ -107,26 +197,7 @@ const GranuleTable = (props: GranuleTableProps) => { const [scene, setScene] = useState(''); const allAddedGranules = addedProducts.map(parameterObject => parameterObject.granuleId) const [waitingForScenesToBeAdded, setWaitingForScenesToBeAdded] = useState(false) - -const validateSceneAvailability = async (cycleToUse: number, passToUse: number, sceneToUse: number[]): Promise => { - try { - // build graphql availableScene query with all cycle/pass/scene combos requested - let queryAliasString = `` - for(const specificScene of sceneToUse) { - const comboId = `${cycleToUse}_${passToUse}_${specificScene}` - queryAliasString += ` s_${comboId}: availableScene(cycle: ${cycleToUse}, pass: ${passToUse}, scene: ${specificScene}) ` - } - const queryAliasObject = `{${queryAliasString}}` - const res: {availableScene: boolean} = await graphQLClient.request(queryAliasObject, {cycle: cycleToUse, pass: passToUse, scene: sceneToUse[0]}).then(response => { - const responseToReturn = Object.fromEntries(Object.entries(response as {availableScene: boolean}).map(responseObj => [responseObj[0].replace('s_', ''), responseObj[1]])) - return responseToReturn as {availableScene: boolean} - }) - return res - } catch (err) { - console.log (err) - return {} - } -} + const [waitingForFootprintSearch, setWaitingForFootprintSearch] = useState(false) const getScenesArray = (sceneString: string): string[] => { const scenesArray = [] @@ -143,7 +214,7 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number, return scenesArray } - const setSaveGranulesAlert = (alert: alertMessageInput) => { + const setSaveGranulesAlert = (alert: alertMessageInput, additionalParameters?: any[]) => { const {message, variant} = granuleAlertMessageConstant[alert] dispatch(addGranuleTableAlerts({type: alert, message, variant, tableType: 'granuleSelection' })) } @@ -159,7 +230,6 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number, // check for characters other than integers and one - if (inputValue.includes('-')) { const inputBoundsValue = inputValue.split('-') - // const allInputsValidNumbers = inputBoundsValue.every(inputString => !isNaN(+inputString)) const min: string = inputBoundsValue[0].trim() const max: string = inputBoundsValue[1].trim() const minIsValid = checkInBounds(inputType, min) @@ -214,7 +284,7 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number, if (authToken === null) { throw new Error('Failed to get authentication token'); } - dispatch(setWaitingForFootprintSearch(true)) + setWaitingForFootprintSearch(true) // convert the tileId in the cps string to a sceneId (divide by 2) const granuleIdTileToSceneArray = granuleId.split('_') let sceneString = String(parseInt(granuleIdTileToSceneArray[2])/2).padStart(3, '0'); @@ -246,10 +316,10 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number, return [[], true] } }) - dispatch(setWaitingForFootprintSearch(false)) + setWaitingForFootprintSearch(false) return footprintResult } catch (err) { - dispatch(setWaitingForFootprintSearch(false)) + setWaitingForFootprintSearch(false) console.log (err) if (err instanceof Error) { return err @@ -270,13 +340,13 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number, return cpsValueToReturn } - const handleSave = async (saveType: SaveType, totalRuns: number, index: number, cycleParam?: string, passParam?: string, sceneParam?: string): Promise => { + const handleSave = async (saveType: SaveType, totalRuns: number, index: number, cycleParam?: string, passParam?: string, sceneParam?: string): Promise => { if (saveType === 'manual') dispatch(clearGranuleTableAlerts()) setWaitingForScenesToBeAdded(true) // String(+(stringParam)) is used to remove the leading zeros const cycleToUse = String(+(cycleParam ?? cycle)) const passToUse = String(+(passParam ?? pass)) - const sceneToUse = String(+(sceneParam ?? scene)) + const sceneToUse = (sceneParam ?? scene).split('-').map((sceneValueSplit: string) => String(+sceneValueSplit)).join('-') // check if cycle pass and scene are all within a valid range const validCycle = inputIsValid('cycle', cycleToUse) const validPass = inputIsValid('pass', passToUse) @@ -287,15 +357,14 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number, if (!validCycle) setSaveGranulesAlert('invalidCycle') if (!validPass) setSaveGranulesAlert('invalidPass') if (!validScene) setSaveGranulesAlert('invalidScene') - return 'first step' - } else if (addedProducts.length >= granuleTableLimit) { - setSaveGranulesAlert('granuleLimit') - return 'second-step' - } else { + return {result: 'first step'} + } + else { const granulesToAdd: allProductParameters[] = [] let someGranulesAlreadyAdded = false let cyclePassSceneSearchParams = searchParams.get('cyclePassScene') ? String(searchParams.get('cyclePassScene')) : '' const sceneArray = getScenesArray(sceneToUse) + let validScenesThatCouldNotBeAdded: string[] = [] // check scenes availability const validationResult = await validateSceneAvailability(parseInt(cycleToUse), parseInt(passToUse), sceneArray.map(sceneId => parseInt(sceneId))).then(scenesAvailable => { // return response @@ -309,34 +378,39 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number, }) // TODO: make alert more verbose if some granules are added and others are not when adding more than one with scene hyphen sceneArray.filter(sceneNumber => scenesAvailable[`${cycleToUse}_${passToUse}_${sceneNumber}`]).forEach(async sceneId => { - // check if granule exists with that scene, cycle, and pass - const comboAlreadyAdded = alreadyAddedCyclePassScene(cycleToUse, passToUse, sceneId) - const cyclePassSceneInBounds = checkInBounds('cycle', cycleToUse) && checkInBounds('pass', passToUse) && checkInBounds('scene', sceneId) - if (cyclePassSceneInBounds && !comboAlreadyAdded) { - // get the granuleId from it and pass it to the parameters - const parameters: allProductParameters = { - granuleId: `${cycleToUse}_${passToUse}_${sceneId}`, - name: '', - cycle: cycleToUse, - pass: passToUse, - scene: sceneId, - outputGranuleExtentFlag: parameterOptionValues.outputGranuleExtentFlag.default as number, - outputSamplingGridType: parameterOptionValues.outputSamplingGridType.default as string, - rasterResolution: parameterOptionValues.rasterResolutionUTM.default as number, - utmZoneAdjust: parameterOptionValues.utmZoneAdjust.default as string, - mgrsBandAdjust: parameterOptionValues.mgrsBandAdjust.default as string, - footprint: sampleFootprint - } - // add cycle/pass/scene to url parameters - if (!searchParamSceneComboAlreadyInUrl(cyclePassSceneSearchParams, cycleToUse, passToUse, sceneId)) { - cyclePassSceneSearchParams += `${cyclePassSceneSearchParams.length === 0 ? '' : '-'}${cycleToUse}_${passToUse}_${sceneId}` + if ((granulesToAdd.length + addedProducts.length) >= granuleTableLimit) { + validScenesThatCouldNotBeAdded.push(sceneId) + setSaveGranulesAlert('granuleLimit') + } else { + // check if granule exists with that scene, cycle, and pass + const comboAlreadyAdded = alreadyAddedCyclePassScene(cycleToUse, passToUse, sceneId) + const cyclePassSceneInBounds = checkInBounds('cycle', cycleToUse) && checkInBounds('pass', passToUse) && checkInBounds('scene', sceneId) + if (cyclePassSceneInBounds && !comboAlreadyAdded) { + // get the granuleId from it and pass it to the parameters + const parameters: allProductParameters = { + granuleId: `${cycleToUse}_${passToUse}_${sceneId}`, + name: '', + cycle: cycleToUse, + pass: passToUse, + scene: sceneId, + outputGranuleExtentFlag: parameterOptionValues.outputGranuleExtentFlag.default as number, + outputSamplingGridType: parameterOptionValues.outputSamplingGridType.default as string, + rasterResolution: parameterOptionValues.rasterResolutionUTM.default as number, + utmZoneAdjust: parameterOptionValues.utmZoneAdjust.default as string, + mgrsBandAdjust: parameterOptionValues.mgrsBandAdjust.default as string, + footprint: sampleFootprint + } + // add cycle/pass/scene to url parameters + if (!searchParamSceneComboAlreadyInUrl(cyclePassSceneSearchParams, cycleToUse, passToUse, sceneId)) { + cyclePassSceneSearchParams += `${cyclePassSceneSearchParams.length === 0 ? '' : '-'}${cycleToUse}_${passToUse}_${sceneId}` + } + granulesToAdd.push(parameters) + } else if (comboAlreadyAdded) { + someGranulesAlreadyAdded = true } - granulesToAdd.push(parameters) - } else if (comboAlreadyAdded) { - someGranulesAlreadyAdded = true } }) - if (saveType !== 'spatialSearch') { + if (saveType !== 'spatialSearch' && saveType !== 'urlParameter') { // check if any granules could not be found or they were already added if (someGranulesAlreadyAdded) { setSaveGranulesAlert('alreadyAdded') @@ -364,38 +438,48 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number, })) })).then(async productsWithFootprints => { // don't run time range check if granule was manually entered - const productsInTimeRange: allProductParameters[] = [] - const productsNotInTimeRange:allProductParameters[] = [] - productsWithFootprints.forEach(product => { - if (product.inTimeRange){ - delete product.inTimeRange - productsInTimeRange.push(product) - } else if (!product.inTimeRange) { - delete product.inTimeRange - productsNotInTimeRange.push(product) - } - }) - if (productsInTimeRange.length > 0) { - setSaveGranulesAlert('success') - dispatch(addProduct(productsInTimeRange)) + if (saveType === 'manual' || saveType === 'urlParameter') { addSearchParamToCurrentUrlState({'cyclePassScene': cyclePassSceneSearchParams}) - } - if (productsNotInTimeRange.length > 0) { - // set alerts for not in range - setSaveGranulesAlert('notInTimeRange') + if (saveType !== 'urlParameter' || startTutorial) { + if (validScenesThatCouldNotBeAdded.length > 0) { + setSaveGranulesAlert('someSuccess') + } else { + setSaveGranulesAlert('success') + } + } + dispatch(addProduct(productsWithFootprints)) + } else { + const productsInTimeRange: allProductParameters[] = [] + const productsNotInTimeRange:allProductParameters[] = [] + productsWithFootprints.forEach(product => { + if (product.inTimeRange){ + delete product.inTimeRange + productsInTimeRange.push(product) + } else if (!product.inTimeRange) { + delete product.inTimeRange + productsNotInTimeRange.push(product) + } + }) + if (productsInTimeRange.length > 0) { + setSaveGranulesAlert('success') + dispatch(addProduct(productsInTimeRange)) + } + if (productsNotInTimeRange.length > 0) { + // set alerts for not in range + setSaveGranulesAlert('notInTimeRange') + } } }) - return 'found something' + return {result: 'found something', savedScenes: granulesToAdd} } else { if (index+1 === totalRuns){ - return 'noScenesFound' + return {result: 'noScenesFound'} } else { - return 'not applicable' + return {result: 'not applicable'} } } }) return validationResult - // return 'third step' } } @@ -634,7 +718,9 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number, - setCycle(event.target.value)}/> + { + setCycle(event.target.value) + }}/> setPass(event.target.value)}/> setScene(event.target.value)}/> @@ -651,20 +737,23 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number,
- {tableType === 'granuleSelection' ? To add multiple scenes at once, enter two numbers into the scene input field separated by a hyphen (e.g. 1-10) : null} {tableType === 'granuleSelection' ? ( + <> + To add multiple scenes at once, enter two numbers into the scene input field separated by a hyphen (e.g. 1-10) {waitingForScenesToBeAdded || waitingForSpatialSearch || waitingForFootprintSearch ? Loading... : - } + {`${addedProducts.length}/${granuleTableLimit} scenes added`} {renderInfoIcon('granuleTableLimit')} + ) : null } diff --git a/src/components/sidebar/ProductCustomization.tsx b/src/components/sidebar/ProductCustomization.tsx index 363e7eb..9b7ad43 100644 --- a/src/components/sidebar/ProductCustomization.tsx +++ b/src/components/sidebar/ProductCustomization.tsx @@ -81,7 +81,6 @@ const ProductCustomization = () => { if (showUTMAdvancedOptions) { handleShowUTMAdvancedOptions() } - } else { gridType = "rasterResolutionUTM" searchParamToRemove = "rasterResolutionGEO" @@ -113,13 +112,13 @@ const ProductCustomization = () => { if (outputSamplingGridType === 'utm') { return ( setRasterResolutionUTM(parseInt(event.target.value))}> - {parameterOptionValues.rasterResolutionUTM.values.map(parameterValue => )} + {parameterOptionValues.rasterResolutionUTM.values.map((parameterValue, index) => )} ) } else if (outputSamplingGridType === 'lat/lon') { return ( setRasterResolutionGEO(parseInt(event.target.value))}> - {parameterOptionValues.rasterResolutionGEO.values.map(parameterValue => )} + {parameterOptionValues.rasterResolutionGEO.values.map((parameterValue, index) => )} ) } @@ -162,6 +161,7 @@ const ProductCustomization = () => { type={'radio'} id={`outputSamplingGridTypeGroup-radio-${index}`} onChange={() => setOutputSamplingGridType(value as string, resolutionToUse)} + key={`outputSamplingGridTypeGroup-radio-key-${index}`} /> )} ) @@ -176,6 +176,7 @@ const ProductCustomization = () => { label={'advanced options'} style={{marginTop: '10px'}} disabled={!(outputSamplingGridType === 'utm')} + key={`outputGranuleExtentFlag-switch-key`} /> ) ) @@ -206,6 +207,7 @@ const ProductCustomization = () => { type={'radio'} id={`outputGranuleExtentFlagTypeGroup-radio-${index}`} onChange={() => setOutputGranuleExtentFlag(value)} + key={`outputGranuleExtentFlagTypeGroup-radio-key-${index}`} /> ) })} diff --git a/src/components/sidebar/SpatialSearchOptions.tsx b/src/components/sidebar/SpatialSearchOptions.tsx index 34394af..8a1c091 100644 --- a/src/components/sidebar/SpatialSearchOptions.tsx +++ b/src/components/sidebar/SpatialSearchOptions.tsx @@ -81,20 +81,8 @@ const SpatialSearchOptions = () => { />
- {/* - - - - */} - {/* -

- Draw areas to search spatially on the map by using the controls on the top right -

-
*/}

Draw areas to search spatially on the map by using the controls on the top right. The scene search will start once you finish drawing the search area shape.

diff --git a/src/components/sidebar/actions/modalSlice.ts b/src/components/sidebar/actions/modalSlice.ts index 0587281..63d2f17 100644 --- a/src/components/sidebar/actions/modalSlice.ts +++ b/src/components/sidebar/actions/modalSlice.ts @@ -1,5 +1,4 @@ import { createSlice } from '@reduxjs/toolkit' -import { allProductParameters } from '../../../types/constantTypes' // Define a type for the slice state interface AddCustomProductModalState { @@ -7,11 +6,11 @@ interface AddCustomProductModalState { showEditProductModal: boolean, showDeleteProductModal: boolean, showGenerateProductModal: boolean, - addedProducts: allProductParameters[], sampleGranuleDataArray: number[], selectedGranules: string[], showTutorialModal: boolean, skipTutorial: boolean, + showCloseTutorialModal: boolean } // Define the initial state using that type @@ -20,13 +19,13 @@ const initialState: AddCustomProductModalState = { showEditProductModal: false, showDeleteProductModal: false, showGenerateProductModal: false, - showTutorialModal: true, - skipTutorial: true, + showTutorialModal: false, + skipTutorial: false, // allProducts: this will be like a 'database' for the local state of all the products added // the key will be cycleId_passId_sceneId and the value will be a 'parameterOptionDefaults' type object - addedProducts: [], sampleGranuleDataArray: [], - selectedGranules: [] + selectedGranules: [], + showCloseTutorialModal: false } export const modalSlice = createSlice({ @@ -73,6 +72,12 @@ export const modalSlice = createSlice({ }, setSkipTutorialTrue: (state) => { state.skipTutorial = true + }, + setShowCloseTutorialFalse: (state) => { + state.showCloseTutorialModal = false + }, + setShowCloseTutorialTrue: (state) => { + state.showCloseTutorialModal = true } }, }) @@ -90,7 +95,9 @@ export const { setShowTutorialModalFalse, setShowTutorialModalTrue, setSkipTutorialFalse, - setSkipTutorialTrue + setSkipTutorialTrue, + setShowCloseTutorialFalse, + setShowCloseTutorialTrue, } = modalSlice.actions export default modalSlice.reducer \ No newline at end of file diff --git a/src/components/sidebar/actions/productSlice.ts b/src/components/sidebar/actions/productSlice.ts index 7682a9d..ce4ea8e 100644 --- a/src/components/sidebar/actions/productSlice.ts +++ b/src/components/sidebar/actions/productSlice.ts @@ -18,7 +18,6 @@ interface GranuleState { showUTMAdvancedOptions: boolean, spatialSearchResults: SpatialSearchResult[], waitingForSpatialSearch: boolean, - waitingForFootprintSearch: boolean, spatialSearchStartDate: string, spatialSearchEndDate: string, mapFocus: MapFocusObject @@ -26,8 +25,6 @@ interface GranuleState { const {name, cycle, pass, scene, ...generateProductParametersFiltered } = parameterOptionDefaults -const date = new Date() - // Define the initial state using that type const initialState: GranuleState = { // allProducts: this will be like a 'database' for the local state of all the products added @@ -36,7 +33,7 @@ const initialState: GranuleState = { sampleGranuleDataArray: [], selectedGranules: [], granuleFocus: [33.854457, -118.709093], - mapFocus: {center: [33.854457, -118.709093], zoom: 7}, + mapFocus: {center: [33.854457, -118.709093], zoom: 6}, generatedProducts: [], generateProductParameters: generateProductParametersFiltered, granuleTableAlerts: [], @@ -44,8 +41,7 @@ const initialState: GranuleState = { showUTMAdvancedOptions: false, spatialSearchResults: [], waitingForSpatialSearch: false, - waitingForFootprintSearch: false, - spatialSearchStartDate: (new Date(date.setMonth(date.getMonth() - 1))).toISOString(), + spatialSearchStartDate: (new Date(2022, 11, 16)).toISOString(), spatialSearchEndDate: (new Date()).toISOString() } @@ -137,9 +133,6 @@ export const productSlice = createSlice({ setWaitingForSpatialSearch: (state, action: PayloadAction) => { state.waitingForSpatialSearch = action.payload }, - setWaitingForFootprintSearch: (state, action: PayloadAction) => { - state.waitingForFootprintSearch = action.payload - }, setSpatialSearchStartDate: (state, action: PayloadAction) => { state.spatialSearchStartDate = action.payload }, @@ -162,7 +155,6 @@ export const { setShowUTMAdvancedOptions, addSpatialSearchResults, setWaitingForSpatialSearch, - setWaitingForFootprintSearch, setSpatialSearchStartDate, setSpatialSearchEndDate, setMapFocus, diff --git a/src/components/sidebar/actions/sidebarSlice.ts b/src/components/sidebar/actions/sidebarSlice.ts index 3c70317..0a1cd5c 100644 --- a/src/components/sidebar/actions/sidebarSlice.ts +++ b/src/components/sidebar/actions/sidebarSlice.ts @@ -77,7 +77,4 @@ export const { setSidebarWidth } = sidebarSlice.actions -// Other code such as selectors can use the imported `RootState` type -// export const selectShowAddProductModal = (state: RootState) => state.addCustomProductModal.showAddProductModal - export default sidebarSlice.reducer \ No newline at end of file diff --git a/src/components/tutorial/InteractiveTutorialModalClose.tsx b/src/components/tutorial/InteractiveTutorialModalClose.tsx new file mode 100644 index 0000000..3c7a7c8 --- /dev/null +++ b/src/components/tutorial/InteractiveTutorialModalClose.tsx @@ -0,0 +1,44 @@ +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import { useAppSelector, useAppDispatch } from '../../redux/hooks' +import { setShowCloseTutorialFalse, setSkipTutorialTrue } from '../sidebar/actions/modalSlice' +import { Row } from 'react-bootstrap'; +import { setStartTutorial } from '../app/appSlice'; +import { deleteProduct } from '../sidebar/actions/productSlice'; +import { useNavigate } from 'react-router-dom'; + +const InteractiveTutorialModalClose = () => { + const showCloseTutorialModal = useAppSelector((state) => state.modal.showCloseTutorialModal) + const dispatch = useAppDispatch() + const navigate = useNavigate() + const addedProducts = useAppSelector((state) => state.product.addedProducts) + + const handleCloseTutorial = () => { + dispatch(setSkipTutorialTrue()) + dispatch(setStartTutorial(false)) + dispatch(deleteProduct(addedProducts.map(product => product.granuleId))) + navigate(`/customizeProduct/selectScenes`) + dispatch(setShowCloseTutorialFalse()) + } + + return ( + dispatch(setShowCloseTutorialFalse())}> + + Exit Interactive Tutorial + + + + +
Are you sure you want to exit the tutorial?
+
+
+ + + + + +
+ ); +} + +export default InteractiveTutorialModalClose; \ No newline at end of file diff --git a/src/components/tutorial/tutorialConstants.ts b/src/components/tutorial/tutorialConstants.ts index 215d207..0647899 100644 --- a/src/components/tutorial/tutorialConstants.ts +++ b/src/components/tutorial/tutorialConstants.ts @@ -34,7 +34,6 @@ export const tutorialSteps = [ } }, { - // target: "#spatial-search-map", target: '#map-tutorial-target', content: "This map allows you to search for scenes by drawing a search area and will display the footprints of scenes once they have been added to your Added Scenes list.", disableBeacon: true, @@ -43,7 +42,6 @@ export const tutorialSteps = [ options: { zIndex: 1000, primaryColor: '#0d6efd', - // offset: 40, } } }, @@ -116,17 +114,6 @@ export const tutorialSteps = [ } } }, - { - target: "#configure-products-button", - content: "Click the Configure Products button to proceed to the Configure Options page where you can select options for how your selected scenes can be made into custom products.", - disableBeacon: true, - styles: { - options: { - zIndex: 1000, - primaryColor: '#0d6efd', - } - } - }, { target: "#configure-options-breadcrumb", content: "You can also click on the Configure Options tab to proceed to the Configure Options view.", @@ -151,7 +138,7 @@ export const tutorialSteps = [ }, { target: "#scenes-to-customize", - content: "This is this table showing the scenes you can customize. Also shown are the options which are scene specific which are not applied to all the scenes in the list.", + content: "This is the table showing the scenes you can customize. Also shown are the options which are scene specific which are not applied to all the scenes in the list.", disableBeacon: true, styles: { options: { diff --git a/src/components/welcome/Welcome.tsx b/src/components/welcome/Welcome.tsx index c681a0b..908e1ec 100644 --- a/src/components/welcome/Welcome.tsx +++ b/src/components/welcome/Welcome.tsx @@ -29,7 +29,7 @@ const Welcome = () => { return ( -

SWOT Level-2 On-demand Raster Generator

+

SWOT On-Demand Level-2 Raster Generator

@@ -73,8 +73,8 @@ const Welcome = () => {
4.
-
Download
-
Download generated products once processing is complete.
+
Download
+
Download generated products once processing is complete.
diff --git a/src/constants/graphqlQueries.ts b/src/constants/graphqlQueries.ts index 6fe47d6..e1c1151 100644 --- a/src/constants/graphqlQueries.ts +++ b/src/constants/graphqlQueries.ts @@ -1,3 +1,5 @@ +import { userProductQueryLimit } from "./rasterParameterConstants" + export const userQuery = ` { currentUser { @@ -27,7 +29,7 @@ export const generateL2RasterProductQuery = ` export const userProductsQuery = ` { currentUser { - products { + products (limit: ${userProductQueryLimit}) { id timestamp cycle diff --git a/src/constants/rasterParameterConstants.ts b/src/constants/rasterParameterConstants.ts index a171c46..322db77 100644 --- a/src/constants/rasterParameterConstants.ts +++ b/src/constants/rasterParameterConstants.ts @@ -95,8 +95,9 @@ export const parameterOptionDefaults = { mgrsBandAdjust: '0', } +export const granuleTableLimit = 10 + export const parameterHelp: ParameterHelp = { - // outputGranuleExtentFlag: `There are two sizing options for raster granules: square (128 km x 128 km) or rectangular (256 km x 128 km). The square granule extent utilizes the data from only the specific square scene ID indicated, whereas the rectangular granule extent utilizes the specific square scene ID indicated and data from the two adjacent scene IDs along the SWOT swath. At the very edges of scenes, there is a risk that the pixels SWOT measures will not be aggregated as accurately into the raster product. The rectangular extent addresses this issue and could be most helpful with points of interest near the edges of scenes.`, outputGranuleExtentFlag: `There are two sizing options for raster granules: nonoverlapping square (128 km x 128 km) or overlapping rectangular (256 km x 128 km). The rectangular granule extent is 64 km longer in along-track on both sides of the granule and can be useful for observing areas of interest near the along-track edges of the nonoverlapping granules without the need to stitch sequential granules together.`, outputSamplingGridType: `Specifies the type of the raster sampling grid. It can be either a Universal Transverse Mercator (UTM) grid or a geodetic latitude-longitude grid.`, rasterResolution: `Resolution of the raster sampling grid in units of integer meters for UTM grids and integer arc-seconds for latitude-longitude grids.`, @@ -105,7 +106,8 @@ export const parameterHelp: ParameterHelp = { cycle: `The repeat orbit cycle number of the observation. SWOT’s orbit is 21 days and thus observations in the same 21-day orbit period would have the same cycle number.`, pass: `Predefined sections of the orbit between the maximum and minimum latitudes. SWOT has 584 passes in one cycle, split into ascending and descending passes`, scene: `Predefined 128 x 128 km squares of the SWOT observations.`, - status: `The processing status of your custom product. The status types are as follows: NEW, UNAVAILABLE, GENERATING, ERROR, READY, AVAILABLE` + status: `The processing status of your custom product. The status types are as follows: NEW, UNAVAILABLE, GENERATING, ERROR, READY, AVAILABLE`, + granuleTableLimit: `There is a limit of ${granuleTableLimit} scenes allowed to be added to the scene table at a time. This is to ensure our scene processing pipeline can handle the demand of all of SWODLR's users.` } export interface InputBounds { @@ -130,8 +132,6 @@ scene: { } } -export const granuleTableLimit = 10 - export const granuleAlertMessageConstant: granuleAlertMessageConstantType = { success: { message: 'Successfully added scenes!', @@ -178,12 +178,24 @@ export const granuleAlertMessageConstant: granuleAlertMessageConstantType = { variant: 'danger', }, granuleLimit: { - message: `You can only process ${granuleTableLimit} scenes at a time.`, + message: `You can only process ${granuleTableLimit} scenes at a time so some scenes could not be added.`, variant: 'danger' }, notInTimeRange: { message: `Some scenes were not within the specified spatial search time range.`, variant: 'danger' + }, + someSuccess: { + message: `Successfully added some scenes.`, + variant: 'success' + }, + successfullyGenerated: { + message: `Successfully started product generation! Go to the 'My Data' page to track progress.`, + variant: 'success' + }, + spatialSearchAreaTooLarge: { + message: `The search area you've selected on the map is too large. Please choose a smaller area to search.`, + variant: 'warning' } } @@ -219,15 +231,10 @@ export const granuleAlertMessageConstant: granuleAlertMessageConstantType = { ] export const spatialSearchResultLimit = 2000 -// export const beforeCPS = '_PIXC_' -// export const afterCPSR = 'R_' -// export const afterCPSL = 'L_' -// export const spatialSearchCollectionConceptId = 'C2799438266-POCLOUD' -// export const spatialSearchCollectionConceptId = 'C2799438271-POCLOUD' - export const beforeCPS = '_x_x_x_' export const afterCPSR = 'F_' export const afterCPSL = 'F_' export const spatialSearchCollectionConceptId = 'C2799438271-POCLOUD' -// export const footprintSearchCollectionConceptId = 'C2799438266-POCLOUD' -export const footprintSearchCollectionConceptId = 'C2799438271-POCLOUD' \ No newline at end of file +export const footprintSearchCollectionConceptId = 'C2799438271-POCLOUD' + +export const userProductQueryLimit = 1000 \ No newline at end of file diff --git a/src/redux/store.ts b/src/redux/store.ts index 84eda7e..66b8e76 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -4,7 +4,6 @@ import productSlice from '../components/sidebar/actions/productSlice' import navbarSlice from '../components/navbar/navbarSlice' import appSlice from '../components/app/appSlice' import sidebarSlice from '../components/sidebar/actions/sidebarSlice' -// ... export const store = configureStore({ reducer: { diff --git a/src/types/constantTypes.ts b/src/types/constantTypes.ts index 66de6a3..65cefa4 100644 --- a/src/types/constantTypes.ts +++ b/src/types/constantTypes.ts @@ -100,7 +100,6 @@ export interface AlertMessageObject { type: string, message: string, variant: "danger" | "success" | "warning", - // timeoutId: ReturnType, tableType: TableTypes } @@ -138,7 +137,7 @@ export interface validScene { [key: string]: boolean } -export type alertMessageInput = 'success' | 'alreadyAdded' | 'allScenesNotAvailable' | 'alreadyAddedAndNotFound' | 'noScenesAdded' | 'readyForGeneration' | 'invalidCycle' | 'invalidPass' | 'invalidScene' | 'invalidScene' | 'someScenesNotAvailable' | 'granuleLimit' | 'notInTimeRange' | 'noScenesFound' +export type alertMessageInput = 'success' | 'alreadyAdded' | 'allScenesNotAvailable' | 'alreadyAddedAndNotFound' | 'noScenesAdded' | 'readyForGeneration' | 'invalidCycle' | 'invalidPass' | 'invalidScene' | 'invalidScene' | 'someScenesNotAvailable' | 'granuleLimit' | 'notInTimeRange' | 'noScenesFound' | 'someSuccess' | 'successfullyGenerated' | 'spatialSearchAreaTooLarge' export interface SpatialSearchResult { cycle: string, @@ -152,4 +151,9 @@ export interface MapFocusObject { zoom: number } -export type SaveType = 'manual' | 'urlParameter' | 'spatialSearch' \ No newline at end of file +export type SaveType = 'manual' | 'urlParameter' | 'spatialSearch' + +export interface handleSaveResult { + result: string, + savedScenes?: allProductParameters[] +} \ No newline at end of file diff --git a/src/user/userData.ts b/src/user/userData.ts index f3b5558..c75aa95 100644 --- a/src/user/userData.ts +++ b/src/user/userData.ts @@ -78,21 +78,6 @@ export const generateL2RasterProduct = async ( ) => { try { // TODO: why doesn't typescript like when I don't specifiy 2 different objects for variables utm and geo??? - // const variablesToUse: ProductGenerationVariables = { - // cycle: parseInt(cycle), - // pass: parseInt(pass), - // scene: parseInt(scene), - // outputGranuleExtentFlag: Boolean(outputGranuleExtentFlag), - // outputSamplingGridType: 'GEO', - // rasterResolution, - // } - - // // if outputSamplingGridType is lat/lon (UTM) - // if (outputSamplingGridType === 'lat/lon') { - // variablesToUse.utmZoneAdjust = parseInt(utmZoneAdjust) - // variablesToUse.mgrsBandAdjust = parseInt(mgrsBandAdjust) - // variablesToUse.outputSamplingGridType = outputSamplingGridType.toUpperCase() - // } const utmVariables = { cycle: parseInt(cycle), @@ -129,14 +114,6 @@ export const getUserProducts = async () => { try { const userProductResponse = await graphQLClient.request(userProductsQuery).then(result => { const userProductsResult = (result as UserResponse).currentUser.products - // const userProductsGeneratedForm = userProductResponse.result.map(productResult => { - // const {cycle, pass, scene, rasterResolution, outputGranuleExtentFlag, outputSamplingGridType, utmZoneAdjust, timestamp, id: productId, status} = productResult - // // const generatedFormToReuturn: GeneratedProduct = - // // return generatedFormToReuturn - // }) - - // turn into GeneratedProduct - // const generatedProduct: GeneratedProduct = {} return {status: 'success', products: userProductsResult} as getUserProductsResponse }) return userProductResponse diff --git a/terraform/environments/ops.env b/terraform/environments/ops.env new file mode 100644 index 0000000..49238da --- /dev/null +++ b/terraform/environments/ops.env @@ -0,0 +1,6 @@ +export REGION=us-west-2 + +export REACT_APP_SWODLR_API_BASE_URI="https://swodlr.podaac.earthdatacloud.nasa.gov/api" +export REACT_APP_EDL_BASE_URI="https://swodlr.podaac.earthdatacloud.nasa.gov/api/edl/" +export REACT_APP_BASE_REDIRECT_URI="https://swodlr.podaac.earthdatacloud.nasa.gov/" +export REACT_APP_EDL_CLIENT_ID="n0oQvwh7W8Y1d7GVtNsFsQ"