Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues/swodlr UI final fixes - bug fixed before version 1.0 release #91

Merged
merged 11 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading