From ab10eddeecf5822c0d239855746b69a48de61add Mon Sep 17 00:00:00 2001 From: jbyrne Date: Wed, 14 Feb 2024 00:40:22 -0800 Subject: [PATCH 01/11] issues/swodlr-ui-final-fixes: fixed spatial search speed and tutorial back fix --- src/components/app/App.tsx | 15 ++- src/components/sidebar/GranulesTable.tsx | 94 ++++++++++++------- .../sidebar/SpatialSearchOptions.tsx | 12 --- src/components/sidebar/actions/modalSlice.ts | 2 +- 4 files changed, 72 insertions(+), 51 deletions(-) diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx index eaf6719..d9f68d5 100644 --- a/src/components/app/App.tsx +++ b/src/components/app/App.tsx @@ -14,7 +14,7 @@ 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, STATUS } from 'react-joyride'; import { deleteProduct } from '../sidebar/actions/productSlice'; import { tutorialSteps } from '../tutorial/tutorialConstants'; import InteractiveTutorialModal from '../tutorial/InteractiveTutorialModal'; @@ -33,15 +33,20 @@ const App = () => { const [joyride, setState] = useState({ run: startTutorial, - steps: tutorialSteps + steps: tutorialSteps, + stepIndex: 0 }) useEffect(() => { setState({...joyride, run: startTutorial }) }, [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 ([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 (stepTarget === '#configure-options-breadcrumb' && action === 'update') { navigate(`/customizeProduct/configureOptions${search}`) } else if (stepTarget === '#configure-options-breadcrumb' && action === 'prev' && lifecycle === 'complete') { @@ -49,7 +54,7 @@ const App = () => { } else if (stepTarget === '#my-data-page' && action === 'prev') { navigate(`/customizeProduct/configureOptions${search}`) } else if (stepTarget === '#added-scenes' && action === 'update') { - navigate(`/customizeProduct/selectScenes?cyclePassScene=1_413_120&showUTMAdvancedOptions=true`) + navigate(`/customizeProduct/selectScenes?cyclePassScene=1_414_20&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')) { @@ -59,7 +64,6 @@ const App = () => { dispatch(setStartTutorial(false)) navigate(`/customizeProduct/selectScenes`) } - // TODO: Make condition to load previous page when clicking previous before trying to target component to highlight. Use conditions "stepTarget === '#alert-messages' && action === 'prev' && lifecycle === 'init'" }; useEffect(() => { @@ -103,6 +107,7 @@ const App = () => { callback={(data) => handleJoyrideCallback(data)} run={joyride.run} steps={joyride.steps} + stepIndex={joyride.stepIndex} showProgress showSkipButton hideCloseButton diff --git a/src/components/sidebar/GranulesTable.tsx b/src/components/sidebar/GranulesTable.tsx index 4bb8342..807ca82 100644 --- a/src/components/sidebar/GranulesTable.tsx +++ b/src/components/sidebar/GranulesTable.tsx @@ -6,7 +6,7 @@ import { granuleAlertMessageConstant, granuleSelectionLabels, productCustomizati 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, handleSaveResult, validScene } from '../../types/constantTypes'; -import { addProduct, setSelectedGranules, setGranuleFocus, addGranuleTableAlerts, editProduct, addSpatialSearchResults, setWaitingForFootprintSearch, clearGranuleTableAlerts } from './actions/productSlice'; +import { addProduct, setSelectedGranules, setGranuleFocus, addGranuleTableAlerts, editProduct, addSpatialSearchResults, setWaitingForFootprintSearch, clearGranuleTableAlerts, setWaitingForSpatialSearch } from './actions/productSlice'; import { setShowDeleteProductModalTrue } from './actions/modalSlice'; import DeleteGranulesModal from './DeleteGranulesModal'; import { graphQLClient } from '../../user/userData'; @@ -26,6 +26,7 @@ const GranuleTable = (props: GranuleTableProps) => { 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 dispatch = useAppDispatch() @@ -37,7 +38,7 @@ const GranuleTable = (props: GranuleTableProps) => { // set the default url state parameters useEffect(() => { - dispatch(clearGranuleTableAlerts()) + // dispatch(clearGranuleTableAlerts()) // if any cycle scene and pass parameters in url, add them to table const cyclePassSceneParameters = searchParams.get('cyclePassScene') if (cyclePassSceneParameters) { @@ -47,24 +48,71 @@ const GranuleTable = (props: GranuleTableProps) => { handleSave('urlParameter', sceneParamArray.length, index, splitSceneParams[0], splitSceneParams[1], splitSceneParams[2]) }) } - }, [tableType === 'granuleSelection' ? null : addedProducts]) + }, [tableType === 'granuleSelection' ? null : addedProducts, searchParams]) + 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 {} + } + } + useEffect(() => { dispatch(clearGranuleTableAlerts()) if (spatialSearchResults.length > 0) { let scenesFoundArray: string[] = [] let addedScenes: string[] = [] + const fetchData = async () => { - for(let i=0; i { - if(result.savedScenes) { - addedScenes.push(...(result.savedScenes).map(productObject => productObject.granuleId)) - } - scenesFoundArray.push(result.result) - }) + // check validity before saving + dispatch(setWaitingForSpatialSearch(true)) + 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 { + if(result.savedScenes) { + addedScenes.push(...(result.savedScenes).map(productObject => productObject.granuleId)) + } + scenesFoundArray.push(result.result) + }) + } + dispatch(setWaitingForSpatialSearch(false)) + if(addedScenes.length > 0) { + // add parameters + addSearchParamToCurrentUrlState({'cyclePassScene': addedScenes.join('-')}) + } + } else { + dispatch(setWaitingForSpatialSearch(false)) + scenesFoundArray.push('noScenesFound') } - // add parameters - addSearchParamToCurrentUrlState({'cyclePassScene': addedScenes.join('-')}) + return scenesFoundArray } @@ -137,26 +185,6 @@ const GranuleTable = (props: GranuleTableProps) => { 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 getScenesArray = (sceneString: string): string[] => { const scenesArray = [] if (sceneString.includes('-')) { @@ -394,7 +422,7 @@ const validateSceneAvailability = async (cycleToUse: number, passToUse: number, // don't run time range check if granule was manually entered if (saveType === 'manual' || saveType === 'urlParameter') { addSearchParamToCurrentUrlState({'cyclePassScene': cyclePassSceneSearchParams}) - if (saveType !== 'urlParameter') { + if (saveType !== 'urlParameter' || startTutorial) { setSaveGranulesAlert('success') } dispatch(addProduct(productsWithFootprints)) 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 ae3a743..404f0d4 100644 --- a/src/components/sidebar/actions/modalSlice.ts +++ b/src/components/sidebar/actions/modalSlice.ts @@ -19,7 +19,7 @@ const initialState: AddCustomProductModalState = { showDeleteProductModal: false, showGenerateProductModal: false, showTutorialModal: false, - skipTutorial: true, + 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 sampleGranuleDataArray: [], From 19ebc89856bc5ec34d5612153797f84835043439 Mon Sep 17 00:00:00 2001 From: jbyrne Date: Fri, 16 Feb 2024 00:52:03 -0800 Subject: [PATCH 02/11] issues/swodlr-ui-final-fixes: fixed spinner, 10 limit, tutorial, map on reload --- src/components/about/About.tsx | 42 +++++- src/components/app/App.tsx | 10 +- src/components/app/appSlice.ts | 2 +- src/components/map/WorldMap.tsx | 52 ++++++-- src/components/navbar/PodaacFooter.tsx | 17 ++- .../sidebar/CustomizeProductsSidebar.tsx | 3 +- .../GranuleSelectionAndConfigurationView.tsx | 6 +- src/components/sidebar/GranulesTable.tsx | 122 +++++++++++------- .../sidebar/actions/productSlice.ts | 8 -- src/constants/rasterParameterConstants.ts | 13 +- src/types/constantTypes.ts | 2 +- 11 files changed, 185 insertions(+), 92 deletions(-) diff --git a/src/components/about/About.tsx b/src/components/about/About.tsx index e08c0a2..9c4c8fe 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

@@ -129,12 +137,36 @@ const About = () => {
+ +
+

Current Version

+ + + +
+ SWODLR UI: + {packageJson.version} +
+
+
+ + +
+ SWODLR API: + {backendVersion} +
+
+
+
+
+
+

Version History

-
Version 1 (9/05/2023)
+
Version 1 Pre-Alpha (9/05/2023)
diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx index d9f68d5..5ea6df2 100644 --- a/src/components/app/App.tsx +++ b/src/components/app/App.tsx @@ -14,10 +14,11 @@ import { Session } from '../../authentication/session'; import { getCurrentUser, setStartTutorial } from './appSlice'; import { useEffect, useState } from 'react'; import GranuleSelectionAndConfigurationView from '../sidebar/GranuleSelectionAndConfigurationView'; -import Joyride, { ACTIONS, EVENTS, STATUS } 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 { setSkipTutorialTrue } from '../sidebar/actions/modalSlice'; const App = () => { const dispatch = useAppDispatch() @@ -36,8 +37,10 @@ const App = () => { 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; }) => { @@ -60,8 +63,9 @@ const App = () => { } else if ((stepTarget === '#generate-products-button' && action === 'close' && lifecycle === 'complete') || (stepTarget === '#my-data-page' && action === 'next')) { 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`) } }; diff --git a/src/components/app/appSlice.ts b/src/components/app/appSlice.ts index 2194e64..566fd5b 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'; diff --git a/src/components/map/WorldMap.tsx b/src/components/map/WorldMap.tsx index 0ae56e4..69131ef 100644 --- a/src/components/map/WorldMap.tsx +++ b/src/components/map/WorldMap.tsx @@ -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,29 +21,59 @@ let DefaultIcon = L.icon({ }); L.Marker.prototype.options.icon = DefaultIcon; -function UpdateMapCenter() { +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) dispatch(setMapFocus({center, zoom})) + 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 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})) + } + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const getScenesWithinCoordinates = async (coordinatesToSearch: {lat: number, lng: number}[][]) => { try { @@ -98,9 +129,7 @@ const WorldMap = () => { return references }) dispatch(addSpatialSearchResults(spatialSearchResponse as SpatialSearchResult[])) - dispatch(setWaitingForSpatialSearch(false)) } catch (err) { - dispatch(setWaitingForSpatialSearch(false)) if (err instanceof Error) { return err } else { @@ -150,13 +179,14 @@ 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 /> {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..b398c5b 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} Pre-Alpha 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/CustomizeProductsSidebar.tsx b/src/components/sidebar/CustomizeProductsSidebar.tsx index ee0eff9..c88379e 100644 --- a/src/components/sidebar/CustomizeProductsSidebar.tsx +++ b/src/components/sidebar/CustomizeProductsSidebar.tsx @@ -47,6 +47,7 @@ const CustomizeProductsSidebar = (props: CustomizeProductSidebarProps) => { setLocalSidebarWidth(sidebarWidthNumber) setSidebarWidth(sidebarWidthNumber) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [resizeEndLocation]) return ( @@ -57,8 +58,6 @@ 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/GranuleSelectionAndConfigurationView.tsx b/src/components/sidebar/GranuleSelectionAndConfigurationView.tsx index e8b766b..f1defb5 100644 --- a/src/components/sidebar/GranuleSelectionAndConfigurationView.tsx +++ b/src/components/sidebar/GranuleSelectionAndConfigurationView.tsx @@ -8,14 +8,16 @@ import { useAppDispatch, useAppSelector } from '../../redux/hooks'; 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(() => { - if (!skipTutorial) { + if (!skipTutorial && userAuthenticated) { dispatch(setShowTutorialModalTrue()) dispatch(setSkipTutorialTrue()) } - }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userAuthenticated]); return ( <> diff --git a/src/components/sidebar/GranulesTable.tsx b/src/components/sidebar/GranulesTable.tsx index 807ca82..74da0bc 100644 --- a/src/components/sidebar/GranulesTable.tsx +++ b/src/components/sidebar/GranulesTable.tsx @@ -6,7 +6,7 @@ import { granuleAlertMessageConstant, granuleSelectionLabels, productCustomizati 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, handleSaveResult, validScene } from '../../types/constantTypes'; -import { addProduct, setSelectedGranules, setGranuleFocus, addGranuleTableAlerts, editProduct, addSpatialSearchResults, setWaitingForFootprintSearch, clearGranuleTableAlerts, setWaitingForSpatialSearch } from './actions/productSlice'; +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,7 +23,6 @@ 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) @@ -38,7 +37,6 @@ const GranuleTable = (props: GranuleTableProps) => { // 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) { @@ -48,6 +46,7 @@ const GranuleTable = (props: GranuleTableProps) => { handleSave('urlParameter', sceneParamArray.length, index, splitSceneParams[0], splitSceneParams[1], splitSceneParams[2]) }) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [tableType === 'granuleSelection' ? null : addedProducts, searchParams]) const validateSceneAvailability = async (cycleToUse: number, passToUse: number, sceneToUse: number[], cpsList?: {cycle: string, pass: string, scene: string}[]): Promise => { @@ -80,6 +79,7 @@ const GranuleTable = (props: GranuleTableProps) => { } } + // Spatial search use effect useEffect(() => { dispatch(clearGranuleTableAlerts()) if (spatialSearchResults.length > 0) { @@ -88,7 +88,6 @@ const GranuleTable = (props: GranuleTableProps) => { const fetchData = async () => { // check validity before saving - dispatch(setWaitingForSpatialSearch(true)) 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]} @@ -96,23 +95,27 @@ const GranuleTable = (props: GranuleTableProps) => { if (validationResult.length > 0) { for(let i=0; i { - if(result.savedScenes) { - addedScenes.push(...(result.savedScenes).map(productObject => productObject.granuleId)) - } - scenesFoundArray.push(result.result) - }) + if ((addedProducts.length + scenesFoundArray.filter(result => 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) + }) + } + } - dispatch(setWaitingForSpatialSearch(false)) if(addedScenes.length > 0) { // add parameters addSearchParamToCurrentUrlState({'cyclePassScene': addedScenes.join('-')}) } } else { - dispatch(setWaitingForSpatialSearch(false)) scenesFoundArray.push('noScenesFound') } - + dispatch(setWaitingForSpatialSearch(false)) return scenesFoundArray } @@ -122,6 +125,9 @@ const GranuleTable = (props: GranuleTableProps) => { if(noScenesFoundResults.includes('noScenesFound') && !noScenesFoundResults.includes('found something')){ setSaveGranulesAlert('noScenesFound') } + if(noScenesFoundResults.includes('hit granule limit')) { + setSaveGranulesAlert('granuleLimit') + } }) // make sure to catch any error .catch(console.error); @@ -129,6 +135,7 @@ 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) => { @@ -184,6 +191,7 @@ const GranuleTable = (props: GranuleTableProps) => { const [scene, setScene] = useState(''); const allAddedGranules = addedProducts.map(parameterObject => parameterObject.granuleId) const [waitingForScenesToBeAdded, setWaitingForScenesToBeAdded] = useState(false) + const [waitingForFootprintSearch, setWaitingForFootprintSearch] = useState(false) const getScenesArray = (sceneString: string): string[] => { const scenesArray = [] @@ -200,8 +208,11 @@ const GranuleTable = (props: GranuleTableProps) => { return scenesArray } - const setSaveGranulesAlert = (alert: alertMessageInput) => { + const setSaveGranulesAlert = (alert: alertMessageInput, additionalParameters?: any[]) => { const {message, variant} = granuleAlertMessageConstant[alert] + if(alert === 'someSuccess') { + + } dispatch(addGranuleTableAlerts({type: alert, message, variant, tableType: 'granuleSelection' })) } @@ -270,7 +281,7 @@ const GranuleTable = (props: GranuleTableProps) => { 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'); @@ -302,10 +313,10 @@ const GranuleTable = (props: GranuleTableProps) => { 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 @@ -344,14 +355,13 @@ const GranuleTable = (props: GranuleTableProps) => { if (!validPass) setSaveGranulesAlert('invalidPass') if (!validScene) setSaveGranulesAlert('invalidScene') return {result: 'first step'} - } else if (addedProducts.length >= granuleTableLimit) { - setSaveGranulesAlert('granuleLimit') - return {result: 'second-step'} - } else { + } + 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 @@ -365,31 +375,36 @@ const GranuleTable = (props: GranuleTableProps) => { }) // 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' && saveType !== 'urlParameter') { @@ -423,7 +438,11 @@ const GranuleTable = (props: GranuleTableProps) => { if (saveType === 'manual' || saveType === 'urlParameter') { addSearchParamToCurrentUrlState({'cyclePassScene': cyclePassSceneSearchParams}) if (saveType !== 'urlParameter' || startTutorial) { - setSaveGranulesAlert('success') + if (validScenesThatCouldNotBeAdded.length > 0) { + setSaveGranulesAlert('someSuccess') + } else { + setSaveGranulesAlert('success') + } } dispatch(addProduct(productsWithFootprints)) } else { @@ -696,7 +715,9 @@ const GranuleTable = (props: GranuleTableProps) => { - setCycle(event.target.value)}/> + { + setCycle(event.target.value) + }}/> setPass(event.target.value)}/> setScene(event.target.value)}/> @@ -713,20 +734,23 @@ const GranuleTable = (props: GranuleTableProps) => { - {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/actions/productSlice.ts b/src/components/sidebar/actions/productSlice.ts index a98c05e..2b33478 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 @@ -44,7 +41,6 @@ const initialState: GranuleState = { showUTMAdvancedOptions: false, spatialSearchResults: [], waitingForSpatialSearch: false, - waitingForFootprintSearch: false, 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/constants/rasterParameterConstants.ts b/src/constants/rasterParameterConstants.ts index fad2e10..ed1510f 100644 --- a/src/constants/rasterParameterConstants.ts +++ b/src/constants/rasterParameterConstants.ts @@ -95,6 +95,8 @@ export const parameterOptionDefaults = { mgrsBandAdjust: '0', } +export const granuleTableLimit = 10 + export const parameterHelp: ParameterHelp = { 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.`, @@ -104,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 { @@ -129,8 +132,6 @@ scene: { } } -export const granuleTableLimit = 10 - export const granuleAlertMessageConstant: granuleAlertMessageConstantType = { success: { message: 'Successfully added scenes!', @@ -177,12 +178,16 @@ 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' } } diff --git a/src/types/constantTypes.ts b/src/types/constantTypes.ts index b61f34a..cdf3b61 100644 --- a/src/types/constantTypes.ts +++ b/src/types/constantTypes.ts @@ -137,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' export interface SpatialSearchResult { cycle: string, From 2977c09ef2ad2ac65619fe9742a9bb131177a9a8 Mon Sep 17 00:00:00 2001 From: jbyrne Date: Sun, 18 Feb 2024 17:15:19 -0800 Subject: [PATCH 03/11] issues/swodlr-ui-final-fixes: fixed delete bug and added success alert for generation --- src/components/app/App.tsx | 2 +- src/components/map/WorldMap.tsx | 15 ++++++++++++--- src/components/sidebar/CustomizeProductView.tsx | 3 +++ src/components/sidebar/DeleteGranulesModal.tsx | 7 ++++++- src/components/sidebar/GenerateProductsModal.tsx | 10 +++++++++- src/components/sidebar/GranuleSelectionView.tsx | 2 +- src/components/sidebar/GranuleTableAlerts.tsx | 5 +++-- src/components/sidebar/GranulesTable.tsx | 5 +---- src/components/sidebar/actions/productSlice.ts | 2 +- src/constants/rasterParameterConstants.ts | 4 ++++ src/types/constantTypes.ts | 2 +- 11 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx index 5ea6df2..d20ed83 100644 --- a/src/components/app/App.tsx +++ b/src/components/app/App.tsx @@ -57,7 +57,7 @@ const App = () => { } else if (stepTarget === '#my-data-page' && action === 'prev') { navigate(`/customizeProduct/configureOptions${search}`) } else if (stepTarget === '#added-scenes' && action === 'update') { - navigate(`/customizeProduct/selectScenes?cyclePassScene=1_414_20&showUTMAdvancedOptions=true`) + 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')) { diff --git a/src/components/map/WorldMap.tsx b/src/components/map/WorldMap.tsx index 69131ef..bfdaad8 100644 --- a/src/components/map/WorldMap.tsx +++ b/src/components/map/WorldMap.tsx @@ -86,7 +86,6 @@ const WorldMap = () => { if (authToken === null) { throw new Error('Failed to get authentication token'); } - dispatch(setWaitingForSpatialSearch(true)) const polygonUrlString = coordinatesToSearch.map((polygon) => { @@ -115,9 +114,13 @@ 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 responseHeaders = data.headers 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 => { @@ -180,6 +183,12 @@ const WorldMap = () => { attribution='Esri, Maxar, Earthstar Geographics, and the GIS User Community' maxZoom = {18} noWrap + bounds={ + [ + [-89.9999, -179.9999], + [89.9999, 179.9999] + ] + } /> 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/DeleteGranulesModal.tsx b/src/components/sidebar/DeleteGranulesModal.tsx index 2ed6d47..1f1b10c 100644 --- a/src/components/sidebar/DeleteGranulesModal.tsx +++ b/src/components/sidebar/DeleteGranulesModal.tsx @@ -15,7 +15,12 @@ const GenerateProductsModal = () => { const removeCPSFromUrl = (cpsCombosToRemove: string[]) => { const cyclePassSceneParameters = searchParams.get('cyclePassScene')?.split('-') if (cyclePassSceneParameters) { - const cyclePassSceneParametersToKeep = cyclePassSceneParameters.filter(cpsCombo => !cpsCombosToRemove.includes(cpsCombo)).join('-') + 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 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/GranuleSelectionView.tsx b/src/components/sidebar/GranuleSelectionView.tsx index c88d913..e1031a2 100644 --- a/src/components/sidebar/GranuleSelectionView.tsx +++ b/src/components/sidebar/GranuleSelectionView.tsx @@ -7,7 +7,7 @@ const GranuleSelectionView = () => {
- +
); } 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 74da0bc..f2d2db0 100644 --- a/src/components/sidebar/GranulesTable.tsx +++ b/src/components/sidebar/GranulesTable.tsx @@ -47,7 +47,7 @@ const GranuleTable = (props: GranuleTableProps) => { }) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tableType === 'granuleSelection' ? null : addedProducts, searchParams]) + }, [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 { @@ -210,9 +210,6 @@ const GranuleTable = (props: GranuleTableProps) => { const setSaveGranulesAlert = (alert: alertMessageInput, additionalParameters?: any[]) => { const {message, variant} = granuleAlertMessageConstant[alert] - if(alert === 'someSuccess') { - - } dispatch(addGranuleTableAlerts({type: alert, message, variant, tableType: 'granuleSelection' })) } diff --git a/src/components/sidebar/actions/productSlice.ts b/src/components/sidebar/actions/productSlice.ts index 2b33478..ce4ea8e 100644 --- a/src/components/sidebar/actions/productSlice.ts +++ b/src/components/sidebar/actions/productSlice.ts @@ -33,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: [], diff --git a/src/constants/rasterParameterConstants.ts b/src/constants/rasterParameterConstants.ts index ed1510f..b87fef1 100644 --- a/src/constants/rasterParameterConstants.ts +++ b/src/constants/rasterParameterConstants.ts @@ -188,6 +188,10 @@ export const granuleAlertMessageConstant: granuleAlertMessageConstantType = { 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' } } diff --git a/src/types/constantTypes.ts b/src/types/constantTypes.ts index cdf3b61..aa2d280 100644 --- a/src/types/constantTypes.ts +++ b/src/types/constantTypes.ts @@ -137,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' | 'someSuccess' +export type alertMessageInput = 'success' | 'alreadyAdded' | 'allScenesNotAvailable' | 'alreadyAddedAndNotFound' | 'noScenesAdded' | 'readyForGeneration' | 'invalidCycle' | 'invalidPass' | 'invalidScene' | 'invalidScene' | 'someScenesNotAvailable' | 'granuleLimit' | 'notInTimeRange' | 'noScenesFound' | 'someSuccess' | 'successfullyGenerated' export interface SpatialSearchResult { cycle: string, From 0ecd2280dbf1106b1b532f8ead1bbcca3850f3ac Mon Sep 17 00:00:00 2001 From: jbyrne Date: Mon, 26 Feb 2024 09:13:22 -0800 Subject: [PATCH 04/11] issues/swodlr-ui-final-fixes: made alert message for search area too large --- src/components/sidebar/GranulesTable.tsx | 65 ++++++++++++----------- src/constants/rasterParameterConstants.ts | 4 ++ src/types/constantTypes.ts | 2 +- 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/components/sidebar/GranulesTable.tsx b/src/components/sidebar/GranulesTable.tsx index f2d2db0..a81ab26 100644 --- a/src/components/sidebar/GranulesTable.tsx +++ b/src/components/sidebar/GranulesTable.tsx @@ -87,33 +87,41 @@ const GranuleTable = (props: GranuleTableProps) => { let addedScenes: string[] = [] const fetchData = async () => { - // 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 (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('-')}) + } + if(addedScenes.length > 0) { + // add parameters + addSearchParamToCurrentUrlState({'cyclePassScene': addedScenes.join('-')}) + } + } else { + scenesFoundArray.push('noScenesFound') } } else { - scenesFoundArray.push('noScenesFound') + // 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 @@ -122,12 +130,9 @@ const GranuleTable = (props: GranuleTableProps) => { // call the function fetchData() .then((noScenesFoundResults) => { - if(noScenesFoundResults.includes('noScenesFound') && !noScenesFoundResults.includes('found something')){ - setSaveGranulesAlert('noScenesFound') - } - if(noScenesFoundResults.includes('hit granule limit')) { - setSaveGranulesAlert('granuleLimit') - } + 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); diff --git a/src/constants/rasterParameterConstants.ts b/src/constants/rasterParameterConstants.ts index b87fef1..98f2e8e 100644 --- a/src/constants/rasterParameterConstants.ts +++ b/src/constants/rasterParameterConstants.ts @@ -192,6 +192,10 @@ export const granuleAlertMessageConstant: granuleAlertMessageConstantType = { successfullyGenerated: { message: `Successfully started product generation! Go to the 'My Data' page to track progress.`, variant: 'success' + }, + spatialSearchAreaTooLarge: { + message: `The search area you have drawn on the map is too large. Please search a smaller area.`, + variant: 'warning' } } diff --git a/src/types/constantTypes.ts b/src/types/constantTypes.ts index aa2d280..65cefa4 100644 --- a/src/types/constantTypes.ts +++ b/src/types/constantTypes.ts @@ -137,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' | 'someSuccess' | 'successfullyGenerated' +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, From 08da6d8a4751df9dce7dab8dde01be0c899cc243 Mon Sep 17 00:00:00 2001 From: jbyrne Date: Mon, 26 Feb 2024 09:19:23 -0800 Subject: [PATCH 05/11] issues/swodlr-ui-final-fixes: changed search area too large message --- src/constants/rasterParameterConstants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/rasterParameterConstants.ts b/src/constants/rasterParameterConstants.ts index 98f2e8e..322db77 100644 --- a/src/constants/rasterParameterConstants.ts +++ b/src/constants/rasterParameterConstants.ts @@ -194,7 +194,7 @@ export const granuleAlertMessageConstant: granuleAlertMessageConstantType = { variant: 'success' }, spatialSearchAreaTooLarge: { - message: `The search area you have drawn on the map is too large. Please search a smaller area.`, + message: `The search area you've selected on the map is too large. Please choose a smaller area to search.`, variant: 'warning' } } From 8ac3c9ca8b622c73e270e1ebf4ae2e78b40bcc45 Mon Sep 17 00:00:00 2001 From: jbyrne Date: Mon, 26 Feb 2024 14:28:47 -0800 Subject: [PATCH 06/11] issues/swodlr-ui-final-fixes: start search polygon url param --- src/components/about/About.tsx | 12 +------ src/components/app/App.tsx | 3 +- .../history/GeneratedProductHistory.tsx | 34 +++++++++++++------ src/components/map/WorldMap.tsx | 24 +++++++++---- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/components/about/About.tsx b/src/components/about/About.tsx index 9c4c8fe..83c9a26 100644 --- a/src/components/about/About.tsx +++ b/src/components/about/About.tsx @@ -148,6 +148,7 @@ const About = () => { {packageJson.version} +
{`(Release Notes)`}
@@ -160,17 +161,6 @@ const About = () => {
- - -
-

Version History

- - -
Version 1 Pre-Alpha (9/05/2023)
-
-
-
-
); } diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx index d20ed83..cd44004 100644 --- a/src/components/app/App.tsx +++ b/src/components/app/App.tsx @@ -60,7 +60,8 @@ const App = () => { 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 ((stepTarget === '#generate-products-button' && action === 'close' && lifecycle === 'complete') || (stepTarget === '#my-data-page' && action === 'next')) { + } else if ((action === 'next' || action === 'close') && stepTarget === '#my-data-page') { navigate(`/generatedProductHistory${search}`) } else if (type === 'tour:end') { dispatch(setSkipTutorialTrue()) diff --git a/src/components/history/GeneratedProductHistory.tsx b/src/components/history/GeneratedProductHistory.tsx index 8dfbee0..f3e2877 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"; @@ -12,10 +12,14 @@ const GeneratedProductHistory = () => { 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() @@ -103,20 +107,28 @@ const GeneratedProductHistory = () => { return navigate(`/generatedProductHistory${search}`)} style={{cursor: 'pointer'}}>{alertMessage} } - const renderProductHistoryViews = () => { + const waitingForProductsToLoadSpinner = () => { return ( - -

Generated Products Data

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

Generated Products Data

+ + {waitingForProductsToLoad ? waitingForProductsToLoadSpinner() : renderProductHistoryViews()} + + ); } diff --git a/src/components/map/WorldMap.tsx b/src/components/map/WorldMap.tsx index bfdaad8..5700349 100644 --- a/src/components/map/WorldMap.tsx +++ b/src/components/map/WorldMap.tsx @@ -30,7 +30,7 @@ const UpdateMapCenter = () => { // 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.center = `${center[0]},${center[1]}` currentSearchParams.zoom = String(zoom) setSearchParams(currentSearchParams) dispatch(setMapFocus({center, zoom})) @@ -64,14 +64,17 @@ const WorldMap = () => { const center = searchParams.get('center') const zoom = searchParams.get('zoom') if (center && zoom) { - const centerParamSplit = center.split('_') + 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 }, []); @@ -118,7 +121,6 @@ const WorldMap = () => { 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 responseHeaders = data.headers const parser = new DOMParser(); const xml = parser.parseFromString(responseText, "application/xml"); const references: SpatialSearchResult[] = Array.from(new Set(Array.from(xml.getElementsByTagName("name")).map(nameElement => { @@ -142,9 +144,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) => { From 22b413f8f6d611cacd54d4cad63afb6b7a723a8c Mon Sep 17 00:00:00 2001 From: jbyrne Date: Thu, 29 Feb 2024 14:29:24 -0800 Subject: [PATCH 07/11] issues/swodlr-ui-final-fixed: removed skip from tutorial, added copy/download tooltips product url --- src/components/app/App.tsx | 16 ++++++--- .../history/GeneratedProductHistory.tsx | 34 ++++++++++++++++--- src/components/welcome/Welcome.tsx | 2 +- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx index cd44004..1d2840c 100644 --- a/src/components/app/App.tsx +++ b/src/components/app/App.tsx @@ -46,11 +46,18 @@ const App = () => { const handleJoyrideCallback = (data: { action: any; index: any; status: any; type: any; step: any; lifecycle: any; }) => { const { action, step, type, lifecycle, index } = data; const stepTarget = step.target + 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 (stepTarget === '#configure-options-breadcrumb' && action === 'update') { + + if (action === 'close') { + dispatch(setSkipTutorialTrue()) + dispatch(setStartTutorial(false)) + dispatch(deleteProduct(addedProducts.map(product => product.granuleId))) + navigate(`/customizeProduct/selectScenes`) + } else if (stepTarget === '#configure-options-breadcrumb' && action === 'update') { navigate(`/customizeProduct/configureOptions${search}`) } else if (stepTarget === '#configure-options-breadcrumb' && action === 'prev' && lifecycle === 'complete') { navigate(`/customizeProduct/selectScenes${search}`) @@ -60,8 +67,7 @@ const App = () => { 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' || action === 'close') && stepTarget === '#my-data-page') { + } else if (action === 'next' && stepTarget === '#my-data-page') { navigate(`/generatedProductHistory${search}`) } else if (type === 'tour:end') { dispatch(setSkipTutorialTrue()) @@ -114,8 +120,8 @@ const App = () => { steps={joyride.steps} stepIndex={joyride.stepIndex} showProgress - showSkipButton - hideCloseButton + // showSkipButton + // hideCloseButton continuous scrollToFirstStep /> diff --git a/src/components/history/GeneratedProductHistory.tsx b/src/components/history/GeneratedProductHistory.tsx index f3e2877..628b8d7 100644 --- a/src/components/history/GeneratedProductHistory.tsx +++ b/src/components/history/GeneratedProductHistory.tsx @@ -46,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 @@ -82,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] @@ -124,7 +150,7 @@ const GeneratedProductHistory = () => { return ( <> -

Generated Products Data

+

Generated Products Data

{waitingForProductsToLoad ? waitingForProductsToLoadSpinner() : renderProductHistoryViews()} diff --git a/src/components/welcome/Welcome.tsx b/src/components/welcome/Welcome.tsx index 75eefc5..7e93c16 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

From 398c3c1774cf3c781f0a5ec76fdec83feba4111c Mon Sep 17 00:00:00 2001 From: jbyrne Date: Thu, 29 Feb 2024 17:00:14 -0800 Subject: [PATCH 08/11] issues/swodlr-ui-final-fixes: added tutorial close confirmation modal --- src/components/app/App.tsx | 11 ++--- src/components/navbar/PodaacFooter.tsx | 2 +- src/components/sidebar/actions/modalSlice.ts | 14 +++++- .../InteractiveTutorialModalClose.tsx | 44 +++++++++++++++++++ src/components/tutorial/tutorialConstants.ts | 2 +- src/components/welcome/Welcome.tsx | 2 +- 6 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 src/components/tutorial/InteractiveTutorialModalClose.tsx diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx index 1d2840c..a58db95 100644 --- a/src/components/app/App.tsx +++ b/src/components/app/App.tsx @@ -18,7 +18,8 @@ import Joyride, { ACTIONS, EVENTS } from 'react-joyride'; import { deleteProduct } from '../sidebar/actions/productSlice'; import { tutorialSteps } from '../tutorial/tutorialConstants'; import InteractiveTutorialModal from '../tutorial/InteractiveTutorialModal'; -import { setSkipTutorialTrue } from '../sidebar/actions/modalSlice'; +import { setShowCloseTutorialTrue, setSkipTutorialTrue } from '../sidebar/actions/modalSlice'; +import InteractiveTutorialModalClose from '../tutorial/InteractiveTutorialModalClose'; const App = () => { const dispatch = useAppDispatch() @@ -53,10 +54,7 @@ const App = () => { } if (action === 'close') { - dispatch(setSkipTutorialTrue()) - dispatch(setStartTutorial(false)) - dispatch(deleteProduct(addedProducts.map(product => product.granuleId))) - navigate(`/customizeProduct/selectScenes`) + dispatch(setShowCloseTutorialTrue()) } else if (stepTarget === '#configure-options-breadcrumb' && action === 'update') { navigate(`/customizeProduct/configureOptions${search}`) } else if (stepTarget === '#configure-options-breadcrumb' && action === 'prev' && lifecycle === 'complete') { @@ -120,8 +118,6 @@ const App = () => { steps={joyride.steps} stepIndex={joyride.stepIndex} showProgress - // showSkipButton - // hideCloseButton continuous scrollToFirstStep /> @@ -135,6 +131,7 @@ const App = () => { , true)}/> + ); } diff --git a/src/components/navbar/PodaacFooter.tsx b/src/components/navbar/PodaacFooter.tsx index b398c5b..5ae3e14 100644 --- a/src/components/navbar/PodaacFooter.tsx +++ b/src/components/navbar/PodaacFooter.tsx @@ -14,7 +14,7 @@ const PodaacFooter = () => { - {`Version ${packageJson.version} Pre-Alpha of SWOT On-Demand Level-2 Raster Generator (SWODLR)`} + {`Version ${packageJson.version} Beta of SWOT On-Demand Level-2 Raster Generator (SWODLR)`} diff --git a/src/components/sidebar/actions/modalSlice.ts b/src/components/sidebar/actions/modalSlice.ts index 404f0d4..63d2f17 100644 --- a/src/components/sidebar/actions/modalSlice.ts +++ b/src/components/sidebar/actions/modalSlice.ts @@ -10,6 +10,7 @@ interface AddCustomProductModalState { selectedGranules: string[], showTutorialModal: boolean, skipTutorial: boolean, + showCloseTutorialModal: boolean } // Define the initial state using that type @@ -23,7 +24,8 @@ const initialState: AddCustomProductModalState = { // 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 sampleGranuleDataArray: [], - selectedGranules: [] + selectedGranules: [], + showCloseTutorialModal: false } export const modalSlice = createSlice({ @@ -70,6 +72,12 @@ export const modalSlice = createSlice({ }, setSkipTutorialTrue: (state) => { state.skipTutorial = true + }, + setShowCloseTutorialFalse: (state) => { + state.showCloseTutorialModal = false + }, + setShowCloseTutorialTrue: (state) => { + state.showCloseTutorialModal = true } }, }) @@ -87,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/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 41c93e5..0647899 100644 --- a/src/components/tutorial/tutorialConstants.ts +++ b/src/components/tutorial/tutorialConstants.ts @@ -138,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 7e93c16..908e1ec 100644 --- a/src/components/welcome/Welcome.tsx +++ b/src/components/welcome/Welcome.tsx @@ -29,7 +29,7 @@ const Welcome = () => { return ( -

SWOT On-demand Level-2 Raster Generator

+

SWOT On-Demand Level-2 Raster Generator

From af25654491376d561ca72af48f6f55e036313de8 Mon Sep 17 00:00:00 2001 From: jbyrne Date: Thu, 29 Feb 2024 17:33:30 -0800 Subject: [PATCH 09/11] issues/swodlr-ui-final-fixes: added cmr SWOT collection permissions check --- src/components/app/appSlice.ts | 11 +++-- .../edl/AuthorizationCodeHandler.tsx | 40 +++++++++++++++++++ src/components/map/WorldMap.tsx | 3 +- .../GranuleSelectionAndConfigurationView.tsx | 14 ++++++- .../sidebar/GranuleSelectionView.tsx | 8 ++++ src/components/sidebar/GranulesTable.tsx | 3 +- 6 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/components/app/appSlice.ts b/src/components/app/appSlice.ts index 566fd5b..b5ffcfa 100644 --- a/src/components/app/appSlice.ts +++ b/src/components/app/appSlice.ts @@ -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: false } 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 5ada8e8..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(); diff --git a/src/components/map/WorldMap.tsx b/src/components/map/WorldMap.tsx index 5700349..75b413f 100644 --- a/src/components/map/WorldMap.tsx +++ b/src/components/map/WorldMap.tsx @@ -54,6 +54,7 @@ const ChangeView = () => { 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 @@ -170,7 +171,7 @@ const WorldMap = () => { id='spatial-search-map' zoom={7} scrollWheelZoom={true} zoomControl={false} > - {useLocation().pathname.includes('selectScenes') ? ( + {(useLocation().pathname.includes('selectScenes') && userHasCorrectEdlPermissions) ? ( { const dispatch = useAppDispatch() @@ -11,13 +13,23 @@ const GranuleSelectionAndConfigurationView = (props: GranuleSelectionAndConfigur 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]); + }, [userAuthenticated]) return ( <> diff --git a/src/components/sidebar/GranuleSelectionView.tsx b/src/components/sidebar/GranuleSelectionView.tsx index e1031a2..6f90183 100644 --- a/src/components/sidebar/GranuleSelectionView.tsx +++ b/src/components/sidebar/GranuleSelectionView.tsx @@ -1,3 +1,4 @@ +import { Alert, Col, Row } from 'react-bootstrap'; import GranuleTable from './GranulesTable'; import GranuleTableAlerts from './GranuleTableAlerts'; import SpatialSearchOptions from './SpatialSearchOptions'; @@ -8,6 +9,13 @@ const GranuleSelectionView = () => { +
+ + + 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/GranulesTable.tsx b/src/components/sidebar/GranulesTable.tsx index a81ab26..3e42b76 100644 --- a/src/components/sidebar/GranulesTable.tsx +++ b/src/components/sidebar/GranulesTable.tsx @@ -26,6 +26,7 @@ const GranuleTable = (props: GranuleTableProps) => { 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() @@ -745,7 +746,7 @@ const GranuleTable = (props: GranuleTableProps) => { Loading... : - } From f354d74a31497873dc8f0ab4203f40725fb5411e Mon Sep 17 00:00:00 2001 From: jbyrne Date: Thu, 29 Feb 2024 17:47:03 -0800 Subject: [PATCH 10/11] issues/swodlr-ui-final-fixes: made cmr permissions alert conditional --- src/components/app/appSlice.ts | 2 +- .../sidebar/GranuleSelectionView.tsx | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/app/appSlice.ts b/src/components/app/appSlice.ts index b5ffcfa..624b694 100644 --- a/src/components/app/appSlice.ts +++ b/src/components/app/appSlice.ts @@ -23,7 +23,7 @@ const initialState: AppState = { currentPage: 'welcome', currentUser: null, startTutorial: false, - userHasCorrectEdlPermissions: false + userHasCorrectEdlPermissions: true } export const appSlice = createSlice({ diff --git a/src/components/sidebar/GranuleSelectionView.tsx b/src/components/sidebar/GranuleSelectionView.tsx index 6f90183..bd1f46d 100644 --- a/src/components/sidebar/GranuleSelectionView.tsx +++ b/src/components/sidebar/GranuleSelectionView.tsx @@ -2,20 +2,26 @@ import { Alert, Col, Row } from 'react-bootstrap'; import GranuleTable from './GranulesTable'; import GranuleTableAlerts from './GranuleTableAlerts'; import SpatialSearchOptions from './SpatialSearchOptions'; +import { useAppSelector } from '../../redux/hooks'; const GranuleSelectionView = () => { + const userHasCorrectEdlPermissions = useAppSelector((state) => state.app.userHasCorrectEdlPermissions) return (
-
- - - 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. - - -
+ { + 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. + + +
+ }
); } From 92056ebb4841b8452b1b53438a47a01c83668b43 Mon Sep 17 00:00:00 2001 From: jbyrne Date: Mon, 18 Mar 2024 14:12:56 -0700 Subject: [PATCH 11/11] issues/swodlr-ui-final-fixes: fixed tutorial back error and no data history tutorial error --- src/components/app/App.tsx | 6 ++++-- src/components/history/GeneratedProductHistory.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx index a58db95..d8984af 100644 --- a/src/components/app/App.tsx +++ b/src/components/app/App.tsx @@ -59,9 +59,11 @@ const App = () => { navigate(`/customizeProduct/configureOptions${search}`) } 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') { + } + 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') diff --git a/src/components/history/GeneratedProductHistory.tsx b/src/components/history/GeneratedProductHistory.tsx index 628b8d7..474a0b3 100644 --- a/src/components/history/GeneratedProductHistory.tsx +++ b/src/components/history/GeneratedProductHistory.tsx @@ -145,7 +145,12 @@ const GeneratedProductHistory = () => { } const renderProductHistoryViews = () => { - return userProducts.length === 0 ? productHistoryAlert() : renderHistoryTable() + return ( + + {renderHistoryTable()} + {userProducts.length === 0 ? {productHistoryAlert()} : null} + + ) } return (