Skip to content

Commit

Permalink
Issues/swodlr UI final fixes - bug fixed before version 1.0 release (#91
Browse files Browse the repository at this point in the history
)

* 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 <[email protected]>
  • Loading branch information
jbyrne6 and jbyrne authored Mar 25, 2024
1 parent db3d98e commit 7faf449
Show file tree
Hide file tree
Showing 23 changed files with 497 additions and 171 deletions.
34 changes: 28 additions & 6 deletions src/components/about/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Col className="about-page" style={{marginTop: '70px', paddingRight: '12px', marginLeft: '0px', height: '100%'}}>
<Row><h4 style={{marginTop: '10px', marginBottom: '20px'}}>About: SWOT On-Demand Level-2 Raster Generator</h4></Row>
Expand All @@ -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.
</h5>
<h5>
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 <a href='https://deotb6e7tfubr.cloudfront.net/s3-edaf5da92e0ce48fb61175c28b67e95d/podaac-ops-cumulus-docs.s3.us-west-2.amazonaws.com/web-misc/swot_mission_docs/atbd/D-105507_SWOT_ATBD_L2_HR_Raster_w-sigs.pdf?A-userid=None&Expires=1701977957&Signature=O8RO~hg2I0pIgH2wSebhos861vC9zG77fk-9LsTCzBnTbTysg1p56rUxOTLycm0M1TnRlwjo5jfLGOkyEpqj~x50J-cxUl16wS1c~pA327KSf8~LZ5170e-azmLUFOgYhACgl23A6qhF9KGhF6yX-Ba4oW756UMg33teMWAAkowFXbi0JOdzIr~bkIcONk7MTr~jzU9G-Tum-yDwk3PEh8ch0sW~9QCJGXq0BjIu6wAquU8bA9wbonqV76w5VrzOiR~42h8jYaNq0MJ18zLwZIWKYQIbXfKHqlm6tWJ6Cwd80QOMAPdEQ5AsF83bG1Q4TxzEgF-GZ8n4nLZlSQObgg__&Key-Pair-Id=K3CPO4G5OR7B1G' target="_">original algorithm</a> 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 <a href='https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-docs/web-misc/swot_mission_docs/atbd/D-105507_SWOT_ATBD_L2_HR_Raster_w-sigs.pdf' target="_">original algorithm</a> that standard SWOT products use to generate products but at a different resolution; it does not just re-grid the standard products.
</h5>
</div>
</Row>
Expand Down Expand Up @@ -70,8 +80,6 @@ const About = () => {
</div>
</Row>

{/* <Row className='about-card'><h4 style={{marginTop: '10px', marginBottom: '20px'}}>FAQ</h4></Row> */}

<Row className='about-card' style={{marginRight: '10%', marginLeft: '10%', marginBottom: '40px'}}>
<div className='about-card' style={{paddingTop: '20px', paddingBottom: '30px', paddingRight: '5%', paddingLeft: '5%'}}>
<h4 style={{marginBottom: '20px'}}>Definitions</h4>
Expand Down Expand Up @@ -131,10 +139,24 @@ const About = () => {

<Row className='about-card' style={{marginRight: '10%', marginLeft: '10%', marginBottom: '40px'}}>
<div className='howToListItem' style={{marginBottom: '20px', paddingRight: '5%', paddingLeft: '5%'}}>
<h4 style={{marginTop: '10px', marginBottom: '20px'}}>Version History</h4>
<h4 style={{marginTop: '10px', marginBottom: '20px'}}>Current Version</h4>
<ListGroup>
<ListGroup.Item className='howToListItem' style={{marginRight: '0%', marginLeft: '0%'}}>
<Row><h5><b>Version 1</b> (9/05/2023)</h5></Row>
<Row>
<h5>
<b>SWODLR UI: </b>
{packageJson.version}
</h5>
</Row>
<Row><h5><a href="https://github.com/podaac/swodlr-ui/releases" target="_">{`(Release Notes)`}</a></h5></Row>
</ListGroup.Item>
<ListGroup.Item className='howToListItem' style={{marginRight: '0%', marginLeft: '0%'}}>
<Row>
<h5>
<b>SWODLR API: </b>
{backendVersion}
</h5>
</Row>
</ListGroup.Item>
</ListGroup>
</div>
Expand Down
41 changes: 28 additions & 13 deletions src/components/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +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()
Expand All @@ -33,33 +35,46 @@ 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' && 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`)
}
// 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(() => {
Expand Down Expand Up @@ -103,9 +118,8 @@ const App = () => {
callback={(data) => handleJoyrideCallback(data)}
run={joyride.run}
steps={joyride.steps}
stepIndex={joyride.stepIndex}
showProgress
showSkipButton
hideCloseButton
continuous
scrollToFirstStep
/>
Expand All @@ -119,6 +133,7 @@ const App = () => {
<Route path='*' element={getPageWithFormatting(<NotFound errorCode='404'/>, true)}/>
</Routes>
<InteractiveTutorialModal />
<InteractiveTutorialModalClose />
</div>
);
}
Expand Down
13 changes: 9 additions & 4 deletions src/components/app/appSlice.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,7 +9,8 @@ interface AppState {
userAuthenticated: boolean,
currentPage: PageTypes,
currentUser: CurrentUserData | null,
startTutorial: boolean
startTutorial: boolean,
userHasCorrectEdlPermissions: boolean
}

export const getCurrentUser = createAsyncThunk<CurrentUserData | null>('currentUser', async () => {
Expand All @@ -21,7 +22,8 @@ const initialState: AppState = {
userAuthenticated: false,
currentPage: 'welcome',
currentUser: null,
startTutorial: false
startTutorial: false,
userHasCorrectEdlPermissions: true
}

export const appSlice = createSlice({
Expand All @@ -37,6 +39,9 @@ export const appSlice = createSlice({
setStartTutorial: (state, action: PayloadAction<boolean>) => {
state.startTutorial = action.payload
},
setUserHasCorrectEdlPermissions: (state, action: PayloadAction<boolean>) => {
state.userHasCorrectEdlPermissions = action.payload
},
},
extraReducers(builder) {
builder.addCase(getCurrentUser.fulfilled, (state, action) => {
Expand All @@ -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
40 changes: 40 additions & 0 deletions src/components/edl/AuthorizationCodeHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
67 changes: 55 additions & 12 deletions src/components/history/GeneratedProductHistory.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -12,10 +12,14 @@ const GeneratedProductHistory = () => {
const { search } = useLocation();
const navigate = useNavigate()
const [userProducts, setUserProducts] = useState<Product[]>([])
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()
Expand All @@ -42,6 +46,32 @@ const GeneratedProductHistory = () => {
<InfoCircle/>
</OverlayTrigger>
)

const renderCopyDownloadButton = (downloadUrlString: string) => (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id="button-tooltip">
Copy
</Tooltip>
}
>
<Button onClick={() => handleCopyClick(downloadUrlString as string)}><Clipboard color="white" size={18}/></Button>
</OverlayTrigger>
)

const renderDownloadButton = (downloadUrlString: string) => (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id="button-tooltip">
Download
</Tooltip>
}
>
<Button onClick={() => window.open(downloadUrlString, '_blank', 'noreferrer')}><Download color="white" size={18}/></Button>
</OverlayTrigger>
)

const renderColTitle = (labelEntry: string[], index: number) => {
let infoIcon = infoIconsToRender.includes(labelEntry[0]) ? renderInfoIcon(labelEntry[0]) : null
Expand Down Expand Up @@ -78,10 +108,10 @@ const GeneratedProductHistory = () => {
if (entry[0] === 'downloadUrl' && entry[1] !== 'N/A') {
const downloadUrlString = granules[0].uri
cellContents =
<Row>
<Row className='normal-row'>
<Col>{entry[1]}</Col>
<Col><Button onClick={() => handleCopyClick(downloadUrlString as string)}><Clipboard color="white" size={18}/></Button></Col>
<Col><Button onClick={() => window.open(downloadUrlString, '_blank', 'noreferrer')}><Download color="white" size={18}/></Button></Col>
<Col>{(renderCopyDownloadButton(downloadUrlString))}</Col>
<Col>{renderDownloadButton(downloadUrlString)}</Col>
</Row>
} else {
cellContents = entry[1]
Expand All @@ -103,20 +133,33 @@ const GeneratedProductHistory = () => {
return <Col style={{margin: '30px'}}><Alert variant='warning' onClick={() => navigate(`/generatedProductHistory${search}`)} style={{cursor: 'pointer'}}>{alertMessage}</Alert></Col>
}

const waitingForProductsToLoadSpinner = () => {
return (
<div>
<h5>Loading Data Table...</h5>
<Spinner animation="border" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
</div>
)
}

const renderProductHistoryViews = () => {
return (
<Col style={{marginRight: '50px', marginLeft: '50px', marginTop: '70px', height: '100%', width: '100%'}}>
<Row className='normal-row' style={{marginRight: '0px'}}><h4>Generated Products Data</h4></Row>
<Row className='normal-row' style={{marginRight: '0px'}}>{renderHistoryTable()}</Row>
{userProducts.length === 0 ? <Row className='normal-row' style={{marginRight: '0px'}}>{productHistoryAlert()}</Row> : null}
<Col>
<Row>{renderHistoryTable()}</Row>
{userProducts.length === 0 ? <Row>{productHistoryAlert()}</Row> : null}
</Col>
)
}

return (
<Row className='about-page' style={{marginRight: '0%'}}>
{renderProductHistoryViews()}
</Row>
<>
<h4 className='normal-row' style={{marginTop: '70px'}}>Generated Products Data</h4>
<Col className='about-page' style={{marginRight: '50px', marginLeft: '50px'}}>
<Row className='normal-row'>{waitingForProductsToLoad ? waitingForProductsToLoadSpinner() : renderProductHistoryViews()}</Row>
</Col>
</>
);
}

Expand Down
Loading

0 comments on commit 7faf449

Please sign in to comment.