From 1993b8b1430787ab82a9cfeea6f49065a1ab2a8b Mon Sep 17 00:00:00 2001 From: Jimmy Date: Thu, 30 May 2024 13:28:44 -0700 Subject: [PATCH] Feature/swodlr UI 70 - add filtering to my data page and re-generation button (#105) * /version v1.0.0-rc.1 * /version v1.0.0-rc.2 * Pull in recent changes to 1.0.0 * /version v1.0.0-rc.3 * Fix umm-t record * /version v1.0.0-rc.4 * Fix umm-t record * /version v1.0.0-rc.5 * Fix umm-t record * /version v1.0.0-rc.6 * /version v1.0.0-rc.7 * issue/manual-granule-input-hotfix: fixed how ranged of scenes are processed (#86) Co-authored-by: jbyrne * Issues/swodlr UI 72 essential UI bug fixes (#87) * issues/swodlr-ui-72: fix cps url params bug * issues/swodlr-ui-72 * issues/swodlr-ui-75: fixed a couple bugs * issues/swodlr-ui-72: fixed map movement, adjust options, data page limit, etc * issues/swodlr-ui-72: changed spatial search beginning date in range * issues/swodlr-ui-72-essential-fixes: cleaned up comments --------- Co-authored-by: jbyrne * /version v1.0.0-rc.8 * feature/swodlr-ui-70: add my data filters * feature/swodlr-ui-70: fix re-generation button * feature/swodlr-ui-70: changed action buttons * feature/swodlr-ui-70: changed filter css * feature/swodlr-ui-70: add cycle pass scene input validation * feature/swodlr-ui-70: fix href for nav links * feature/swodlr-ui-70: --------- Co-authored-by: frankinspace Co-authored-by: Frank Greguska Co-authored-by: Jonathan M Smolenski Co-authored-by: jonathansmolenski Co-authored-by: jbyrne --- package-lock.json | 43 +++ package.json | 1 + src/components/app/App.css | 7 +- src/components/history/DataPagination.tsx | 148 +++----- .../history/GeneratedProductHistory.tsx | 272 ++++++++++----- src/components/history/HistoryFilters.tsx | 327 ++++++++++++++++++ .../history/ReGenerateProductsModal.tsx | 48 +++ src/components/navbar/MainNavbar.tsx | 7 +- src/components/navbar/PodaacFooter.tsx | 7 +- .../sidebar/GenerateProductsModal.tsx | 2 +- src/components/sidebar/actions/modalSlice.ts | 18 +- .../sidebar/actions/productSlice.ts | 92 +++-- src/constants/rasterParameterConstants.ts | 53 ++- src/types/constantTypes.ts | 2 +- src/types/graphqlTypes.ts | 2 +- src/types/historyPageTypes.ts | 22 ++ 16 files changed, 799 insertions(+), 252 deletions(-) create mode 100644 src/components/history/HistoryFilters.tsx create mode 100644 src/components/history/ReGenerateProductsModal.tsx create mode 100644 src/types/historyPageTypes.ts diff --git a/package-lock.json b/package-lock.json index a26521c..01c986d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "array-buffer-to-hex": "^1.0.0", "bootstrap": "^5.2.3", "dotenv": "^16.3.1", + "formik": "^2.4.6", "graphql": "^16.6.0", "graphql-request": "^6.1.0", "leaflet": "^1.9.3", @@ -8547,6 +8548,43 @@ "node": ">= 6" } }, + "node_modules/formik": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", + "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/formik/node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/formik/node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -15699,6 +15737,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 910daf8..933df60 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "array-buffer-to-hex": "^1.0.0", "bootstrap": "^5.2.3", "dotenv": "^16.3.1", + "formik": "^2.4.6", "graphql": "^16.6.0", "graphql-request": "^6.1.0", "leaflet": "^1.9.3", diff --git a/src/components/app/App.css b/src/components/app/App.css index bd33b87..a624a4c 100644 --- a/src/components/app/App.css +++ b/src/components/app/App.css @@ -310,10 +310,15 @@ tfoot { } .table-responsive-generatedProducts { - max-height: 65vh; + max-height: 62vh; overflow-y: auto; } +.table-filter { + background-color:#1a2535; + border: solid white 1px +} + .center { width:100%; display: flex; diff --git a/src/components/history/DataPagination.tsx b/src/components/history/DataPagination.tsx index c176efe..95f8d14 100644 --- a/src/components/history/DataPagination.tsx +++ b/src/components/history/DataPagination.tsx @@ -1,92 +1,35 @@ import { Col, Pagination, Row, Spinner } from "react-bootstrap"; -import { Product } from "../../types/graphqlTypes"; -import { getUserProducts } from "../../user/userData"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; -import { addPageToHistoryPageState, setUserProducts } from "../sidebar/actions/productSlice"; +import { setUserProducts } from "../sidebar/actions/productSlice"; import { productsPerPage } from "../../constants/rasterParameterConstants"; -import { useEffect, useState } from "react"; +import { useState } from "react"; -const DataPagination = () => { +const DataPagination = (props: {totalNumberOfProducts: number, totalNumberOfFilteredProducts: number, }) => { + const {totalNumberOfProducts, totalNumberOfFilteredProducts} = props const dispatch = useAppDispatch() - const historyPageState = useAppSelector((state) => state.product.historyPageState) - const firstHistoryPageData = useAppSelector((state) => state.product.firstHistoryPageData) const userProducts = useAppSelector((state) => state.product.userProducts) + const allUserProducts = useAppSelector((state) => state.product.allUserProducts) const [noNextPage, setNoNextPage] = useState(false) const [noPreviousPage, setNoPreviousPage] = useState(true) - const [waitingForPagination, setWaitingForPagination] = useState(true) + const [waitingForPagination, setWaitingForPagination] = useState(false) const [currentPageNumber, setCurrentPageNumber] = useState(1) - const [totalNumberOfProducts, setTotalNumberOfProducts] = useState(0) - - useEffect(() => { - // get the data for the first page - // go through all the user product data to get the id of each one so that - const fetchData = async () => { - await getUserProducts({limit: '1000000'}).then(response => { - const currentPageProducts = response.products as Product[] - const totalNumberOfProducts = currentPageProducts.length - setTotalNumberOfProducts(totalNumberOfProducts) - if(response.status === 'success' && currentPageProducts.length !== 0) { - const productsPerPageToInt = parseInt(productsPerPage) - const numberOfPages = Math.ceil(currentPageProducts.length/productsPerPageToInt) - for (let pageIndex = 0; pageIndex < numberOfPages; pageIndex++) { - // get index of the last product of each [productsPerPage] - const indexToUse = totalNumberOfProducts - (pageIndex * productsPerPageToInt) < productsPerPageToInt ? totalNumberOfProducts - (pageIndex * productsPerPageToInt) : productsPerPageToInt * (pageIndex + 1)-1 - const idToUse = currentPageProducts[indexToUse].id - dispatch(addPageToHistoryPageState(idToUse)) - } - } - }).then(() => setWaitingForPagination(false)) - } - fetchData() - .catch(console.error); - }, []); - - const handlePrevious = async () => { - if(noNextPage) setNoNextPage(false) - if (currentPageNumber <= 2) { - dispatch(setUserProducts(firstHistoryPageData)) - setCurrentPageNumber(currentPageNumber - 1) - setNoPreviousPage(true) - } else { - await getUserProducts({limit: productsPerPage, after: historyPageState[currentPageNumber-3]}).then(response => { - const currentPageProducts = response.products as Product[] - dispatch(setUserProducts(currentPageProducts)) - setCurrentPageNumber(currentPageNumber - 1) - }) - } - } - - const handleNext = async () => { - await getUserProducts({limit: productsPerPage, after: historyPageState[currentPageNumber-1]}).then(response => { - if(response.status === 'success') { - if(noPreviousPage) setNoPreviousPage(false) - const currentPageProducts = response.products as Product[] - dispatch(setUserProducts(currentPageProducts)) - setCurrentPageNumber(currentPageNumber + 1) - } else if (response.status === 'error') { - setNoNextPage(true) - } - }) - } + const numberOfTotalPages = Math.ceil(allUserProducts.length / parseInt(productsPerPage)) const handleSelectPage = async (pageNumber: number) => { - if (pageNumber === 1) { - if(noPreviousPage) setNoPreviousPage(false) - dispatch(setUserProducts(firstHistoryPageData)) - setCurrentPageNumber(pageNumber) + const firstNumber = (pageNumber-1) * parseInt(productsPerPage) + const lastNumber = firstNumber + parseInt(productsPerPage) + dispatch(setUserProducts(allUserProducts.slice(firstNumber, lastNumber))) + setCurrentPageNumber(pageNumber) + if(pageNumber === 1) { setNoPreviousPage(true) + setNoNextPage(false) + } else if (pageNumber === numberOfTotalPages) { + setNoPreviousPage(false) + setNoNextPage(true) } else { - await getUserProducts({limit: productsPerPage, after: historyPageState[pageNumber-2]}).then(response => { - if(response.status === 'success') { - if(noPreviousPage) setNoPreviousPage(false) - const currentPageProducts = response.products as Product[] - dispatch(setUserProducts(currentPageProducts)) - setCurrentPageNumber(pageNumber) - } else if (response.status === 'error') { - setNoNextPage(true) - } - }) + setNoPreviousPage(false) + setNoNextPage(false) } } @@ -102,8 +45,8 @@ const DataPagination = () => { const getPaginationItemsWithEllipsis = () => { let numberOfSlotsFreeLeft = 0 - if(currentPageNumber >= historyPageState.length-4) { - numberOfSlotsFreeLeft = 4 - (historyPageState.length - currentPageNumber) + if(currentPageNumber >= numberOfTotalPages-4) { + numberOfSlotsFreeLeft = 4 - (numberOfTotalPages - currentPageNumber) } let numberOfSlotsFreeRight = 0 @@ -112,8 +55,8 @@ const DataPagination = () => { } const pagesAllowed = [currentPageNumber-2, currentPageNumber-1, currentPageNumber, currentPageNumber+1, currentPageNumber+2] const pagesAllowedToLeftOfCurrent: number[] = [] - historyPageState.forEach((pageId, index) => { - if(numberOfSlotsFreeLeft !== 0 && index+1 <= historyPageState.length - 3) { + for(let index=0; index { pagesAllowed.push(index) numberOfSlotsFreeRight -= 1 } - }) + } + pagesAllowed.unshift(...pagesAllowedToLeftOfCurrent.reverse()) - const pagesToShow = historyPageState.map((pageId, index) => { - const pageNumberOfIndex = index + 1 - if (pageNumberOfIndex === 1 || pageNumberOfIndex === historyPageState.length) return null - if(pagesAllowed.includes(pageNumberOfIndex)) { - return handleSelectPage(pageNumberOfIndex)}>{pageNumberOfIndex} + const pagesAllowedFiltered = pagesAllowed.filter(value => value > 1) + const pagesToShow = [] + for(let pageIndex = 1; pageIndex < numberOfTotalPages-1; pageIndex++) { + const pageNumberOfIndex = pageIndex + 1 + if(pagesAllowedFiltered.includes(pageNumberOfIndex)) { + pagesToShow.push( handleSelectPage(pageNumberOfIndex)}>{pageNumberOfIndex}) } - return null - }) + } + if(pagesAllowed[0] > 2) pagesToShow.unshift() - if(pagesAllowed[pagesAllowed.length-1] < historyPageState.length-1) pagesToShow.push() + if(pagesAllowed[pagesAllowed.length-1] < numberOfTotalPages-1) pagesToShow.push() return pagesToShow } - + return waitingForPagination ? waitingForPaginationSpinner() : ( -
{totalNumberOfProducts} Total Products
- - - handlePrevious()} disabled={noPreviousPage} /> - handleSelectPage(1)}>1 - {getPaginationItemsWithEllipsis()} - handleSelectPage(historyPageState.length)}>{historyPageState.length} - handleNext()} disabled={userProducts.length < parseInt(productsPerPage) || noNextPage} /> - - + + { + totalNumberOfFilteredProducts !== 0 ? + + handleSelectPage(currentPageNumber-1)} disabled={noPreviousPage} /> + handleSelectPage(1)}>1 + {getPaginationItemsWithEllipsis()} + {numberOfTotalPages > 1 ? handleSelectPage(numberOfTotalPages)}>{numberOfTotalPages} : null} + handleSelectPage(currentPageNumber+1)} disabled={userProducts.length < parseInt(productsPerPage) || noNextPage} /> + + : null + } + +
{totalNumberOfProducts} Total Generated Products
) } export default DataPagination; + diff --git a/src/components/history/GeneratedProductHistory.tsx b/src/components/history/GeneratedProductHistory.tsx index 484ad84..a6acd37 100644 --- a/src/components/history/GeneratedProductHistory.tsx +++ b/src/components/history/GeneratedProductHistory.tsx @@ -1,45 +1,107 @@ -import { Alert, Col, OverlayTrigger, Row, Table, Tooltip, Button, Spinner } from "react-bootstrap"; +import { Alert, Col, OverlayTrigger, Row, Table, Tooltip, Spinner, Form, DropdownButton, Dropdown, Badge } from "react-bootstrap"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; -import { Product } from "../../types/graphqlTypes"; +import { Product, ProductState } from "../../types/graphqlTypes"; import { useEffect, useState } from "react"; -import { InfoCircle, Clipboard, Download } from "react-bootstrap-icons"; +import { InfoCircle } from "react-bootstrap-icons"; import { generatedProductsLabels, infoIconsToRender, parameterHelp, productsPerPage } from "../../constants/rasterParameterConstants"; import { getUserProducts } from "../../user/userData"; import { useLocation, useNavigate } from "react-router-dom"; import DataPagination from "./DataPagination"; -import { addPageToHistoryPageState, setFirstHistoryPageData, setUserProducts } from "../sidebar/actions/productSlice"; +import HistoryFilters from "./HistoryFilters"; +import { Adjust, FilterParameters, OutputGranuleExtentFlagOptions, OutputSamplingGridType, RasterResolution } from "../../types/historyPageTypes"; +import { setShowReGenerateProductModalTrue } from "../sidebar/actions/modalSlice"; +import ReGenerateProductsModal from "./ReGenerateProductsModal"; +import { setAllUserProducts, setGranulesToReGenerate, setUserProducts, setWaitingForMyDataFiltering } from "../sidebar/actions/productSlice"; + +export const productPassesFilterCheck = (currentFilters: FilterParameters, cycle: number, pass: number, scene: number, outputGranuleExtentFlag: boolean, status: string, outputSamplingGridType: string, rasterResolution: number, dateGenerated: string, utmZoneAdjust?: number, mgrsBandAdjust?: number): boolean => { + let productPassesFilter = true + const outputGranuleExtentFlagMap = ['128 x 128','256 x 128'] + + if(currentFilters.cycle !== 'none' && currentFilters.cycle !== String(cycle)) { + productPassesFilter = false + } + if (currentFilters.pass !== 'none' && currentFilters.pass !== String(pass)) { + productPassesFilter = false + } + if (currentFilters.scene !== 'none' && currentFilters.scene !== String(scene)) { + productPassesFilter = false + } + if (currentFilters.outputGranuleExtentFlag.length > 0 && !currentFilters.outputGranuleExtentFlag.includes(outputGranuleExtentFlagMap[+outputGranuleExtentFlag] as OutputGranuleExtentFlagOptions)) { + productPassesFilter = false + } + if (currentFilters.status.length > 0 && !currentFilters.status.includes(status as ProductState)) { + productPassesFilter = false + } + if (currentFilters.outputSamplingGridType.length > 0 && !currentFilters.outputSamplingGridType.includes(outputSamplingGridType as OutputSamplingGridType)) { + productPassesFilter = false + } + if (currentFilters.rasterResolution.length > 0 && !currentFilters.rasterResolution.includes(String(rasterResolution) as RasterResolution)) { + productPassesFilter = false + } + if (utmZoneAdjust !== undefined && currentFilters.utmZoneAdjust.length > 0 && !currentFilters.utmZoneAdjust.includes(String(utmZoneAdjust) as Adjust)) { + productPassesFilter = false + } + if (mgrsBandAdjust !== undefined && currentFilters.mgrsBandAdjust.length > 0 && !currentFilters.mgrsBandAdjust.includes(String(mgrsBandAdjust) as Adjust)) { + productPassesFilter = false + } + if(currentFilters.startDate !== 'none' && new Date(dateGenerated) < currentFilters.startDate) { + productPassesFilter = false + } + if(currentFilters.endDate !== 'none' && new Date(dateGenerated) > currentFilters.endDate) { + productPassesFilter = false + } + return productPassesFilter +} const GeneratedProductHistory = () => { const dispatch = useAppDispatch() const colorModeClass = useAppSelector((state) => state.navbar.colorModeClass) const userProducts = useAppSelector((state) => state.product.userProducts) - const { search } = useLocation(); + const currentFilters = useAppSelector((state) => state.product.currentFilters) + const { search } = useLocation() const navigate = useNavigate() + const [totalNumberOfProducts, setTotalNumberOfProducts] = useState(0) + const [totalNumberOfFilteredProducts, setTotalNumberOfFilteredProducts] = useState(0) + const [checkedProducts, setCheckedProducts] = useState([]) + const [allChecked, setAllChecked] = useState(false) useEffect(() => { + dispatch(setWaitingForMyDataFiltering(true)) // get the data for the first page // go through all the user product data to get the id of each one so that const fetchData = async () => { - await getUserProducts({limit: productsPerPage}).then(response => { - const currentPageProducts = response.products as Product[] - if(response.status === 'success' && currentPageProducts.length !== 0) { - const idToUse = currentPageProducts[currentPageProducts.length-1].id - dispatch(setUserProducts(currentPageProducts)) - dispatch(setFirstHistoryPageData(currentPageProducts)) - dispatch(addPageToHistoryPageState(idToUse)) - } + await getUserProducts({limit: '1000000'}).then(response => { + // filter products for what is in the filter + const allProducts = response.products as Product[] + setTotalNumberOfProducts(allProducts.length) + const filteredProducts = allProducts.filter(product => { + const {status, utmZoneAdjust, mgrsBandAdjust, outputGranuleExtentFlag, outputSamplingGridType, rasterResolution, timestamp: dateGenerated, cycle, pass, scene, granules} = product + const statusToUse = status[0].state + const outputSamplingGridTypeToUse = outputSamplingGridType === 'GEO' ? 'LAT/LON' : outputSamplingGridType + const productPassesFilter = productPassesFilterCheck(currentFilters, cycle, pass, scene, outputGranuleExtentFlag, statusToUse, outputSamplingGridTypeToUse, rasterResolution, dateGenerated, utmZoneAdjust, mgrsBandAdjust) + if(productPassesFilter) { + return product + } else { + return null + } + }) + setTotalNumberOfFilteredProducts(filteredProducts.length) + dispatch(setAllUserProducts(filteredProducts)) + const productsPerPageToInt = parseInt(productsPerPage) + dispatch(setUserProducts(filteredProducts.slice(0, productsPerPageToInt))) + dispatch(setWaitingForMyDataFiltering(false)) }) } - - if(userProducts.length === 0) fetchData().catch(console.error) - }, []); + fetchData().catch(console.error) + }, [currentFilters]); - // TODO: implement download link copy button - const [copyTooltipText, setCopyTooltipText] = useState('Click to Copy URL') + // reset all checked checkbox when going to next page + useEffect(() => { + setAllChecked(false) + }, [userProducts]); - const handleCopyClick = (downloadUrl: string) => { - navigator.clipboard.writeText(downloadUrl) - setCopyTooltipText('Copied!') + const handleCopyClick = (downloadUrls: string[]) => { + navigator.clipboard.writeText(downloadUrls.join('\n')) } const renderInfoIcon = (parameterId: string) => ( @@ -55,31 +117,16 @@ const GeneratedProductHistory = () => { ) - const renderCopyDownloadButton = (downloadUrlString: string) => ( - - Copy - - } - > - - - ) + const handleDownloadProduct = (downloadUrlStrings: string[]) => { + downloadUrlStrings.forEach((downloadUrl, index) => { + window.open(downloadUrl, String(index)) + }) + } - const renderDownloadButton = (downloadUrlString: string) => ( - - Download - - } - > - - - ) + const handleOnReGenerateClick = (granuleObjects: Product[]) => { + dispatch(setGranulesToReGenerate(granuleObjects)) + dispatch(setShowReGenerateProductModalTrue()) + } const renderColTitle = (labelEntry: string[], index: number) => { let infoIcon = infoIconsToRender.includes(labelEntry[0]) ? renderInfoIcon(labelEntry[0]) : null @@ -89,54 +136,102 @@ const GeneratedProductHistory = () => { ) } + // select product or remove if already selected + const handleProductChecked = (selectedProducts: Product[], checkType: 'single' | 'all') => { + let checkedProductsClone = [...checkedProducts] + + selectedProducts.forEach(selectedProduct => { + const productAlreadySelected: boolean = checkedProductsClone.map(product => product.id).includes(selectedProduct.id) + const productShouldBeRemoved: boolean = checkType === 'all' ? allChecked : productAlreadySelected + + if(productShouldBeRemoved) { + // remove product from checked list + checkedProductsClone = checkedProductsClone.filter(product => product.id !== selectedProduct.id) + } else { + // add product to checked list + if(!productAlreadySelected) checkedProductsClone.push(selectedProduct) + } + }) + if(checkType === 'all') setAllChecked(!allChecked) + setCheckedProducts(checkedProductsClone) + } + const renderHistoryTable = () => { + const downloadButtonDisabled = checkedProducts.length === 0 || checkedProducts.every(product => product.granules.length === 0) + const downloadUrlList = checkedProducts.map(product => product.granules.map(granule => granule.uri)).flat() return ( -
-
- - - - {Object.entries(generatedProductsLabels).map((labelEntry, index) => renderColTitle(labelEntry, index))} - - - - {userProducts.map((generatedProductObject, index) => { - const {status, utmZoneAdjust, mgrsBandAdjust, outputGranuleExtentFlag, outputSamplingGridType, rasterResolution, timestamp: dateGenerated, cycle, pass, scene, granules} = generatedProductObject - const statusToUse = status[0].state - const downloadUrl = granules && granules.length !== 0 ? granules[0].uri.split('/').pop() : 'N/A' - const utmZoneAdjustToUse = outputSamplingGridType === 'GEO' ? 'N/A' : utmZoneAdjust - const mgrsBandAdjustToUse = outputSamplingGridType === 'GEO' ? 'N/A' : mgrsBandAdjust - const outputSamplingGridTypeToUse = outputSamplingGridType === 'GEO' ? 'LAT/LON' : outputSamplingGridType - const outputGranuleExtentFlagToUse = outputGranuleExtentFlag ? '256 x 128 km' : '128 x 128 km' - const productRowValues = {cycle, pass, scene, status: statusToUse, outputGranuleExtentFlag: outputGranuleExtentFlagToUse, outputSamplingGridType: outputSamplingGridTypeToUse, rasterResolution, utmZoneAdjust: utmZoneAdjustToUse, mgrsBandAdjust: mgrsBandAdjustToUse, downloadUrl, dateGenerated} - return ( + + {} + + + +
+
+ { + totalNumberOfProducts === 0 ? + {productHistoryAlert()} + :<> + +
+ {checkedProducts.length} Actions}> + handleDownloadProduct(downloadUrlList)}>Download + handleCopyClick(downloadUrlList)}>Copy Download Url + handleOnReGenerateClick(checkedProducts)}>Re-Generate + + + + +
+
+ + + + {Object.entries(generatedProductsLabels).map((labelEntry, index) => renderColTitle(labelEntry, index))} + + + + {userProducts.map((generatedProductObject, index) => { + const {id, status, utmZoneAdjust, mgrsBandAdjust, outputGranuleExtentFlag, outputSamplingGridType, rasterResolution, timestamp: dateGenerated, cycle, pass, scene, granules} = generatedProductObject + const statusToUse = status[0].state + const downloadUrl = granules && granules.length !== 0 ? granules[0].uri.split('/').pop() : 'N/A' + const utmZoneAdjustToUse = outputSamplingGridType === 'GEO' ? 'N/A' : utmZoneAdjust + const mgrsBandAdjustToUse = outputSamplingGridType === 'GEO' ? 'N/A' : mgrsBandAdjust + const outputSamplingGridTypeToUse = outputSamplingGridType === 'GEO' ? 'LAT/LON' : outputSamplingGridType + const outputGranuleExtentFlagToUse = outputGranuleExtentFlag ? '256 x 128 km' : '128 x 128 km' + const productRowValues = {cycle, pass, scene, status: statusToUse, outputGranuleExtentFlag: outputGranuleExtentFlagToUse, outputSamplingGridType: outputSamplingGridTypeToUse, rasterResolution, utmZoneAdjust: utmZoneAdjustToUse, mgrsBandAdjust: mgrsBandAdjustToUse, downloadUrl, dateGenerated} + + return ( - {Object.entries(productRowValues).map((entry, index2) => { - let cellContents = null - if (entry[0] === 'downloadUrl' && entry[1] !== 'N/A') { - const downloadUrlString = granules[0].uri - cellContents = - - {entry[1]} - {(renderCopyDownloadButton(downloadUrlString))} - {renderDownloadButton(downloadUrlString)} - - } else { - cellContents = entry[1] - } - return - } )} + + {Object.entries(productRowValues).map((entry, index2) => )} - )})} - -
+
+
+ handleProductChecked(userProducts, 'all')}/> +
+
+
{cellContents} +
+
+ product.id).includes(id)} id='select-all-products-checkbox' label='' onChange={() => handleProductChecked([generatedProductObject], 'single')}/> +
+
+
{entry[1]}
+ ) + })} + +
- -
+ + } + + {} + + + ) } - const productHistoryAlert = () => { const alertMessage = 'No products have been generated. Go to the Product Customization page to generate products.' return navigate(`/generatedProductHistory${search}`)} style={{cursor: 'pointer'}}>{alertMessage} @@ -157,16 +252,15 @@ const GeneratedProductHistory = () => { return ( {renderHistoryTable()} - {userProducts.length === 0 ? {productHistoryAlert()} : null} ) } return ( <> -

Generated Products Data

- - {userProducts.length === 0 ? waitingForProductsToLoadSpinner() : renderProductHistoryViews()} +

Generated Products Data

+ + {totalNumberOfProducts === 0 ? waitingForProductsToLoadSpinner() : renderProductHistoryViews()} ); diff --git a/src/components/history/HistoryFilters.tsx b/src/components/history/HistoryFilters.tsx new file mode 100644 index 0000000..dbbf17c --- /dev/null +++ b/src/components/history/HistoryFilters.tsx @@ -0,0 +1,327 @@ +import { Accordion, Button, Col, Form, Row, Spinner } from "react-bootstrap"; +import { ProductState } from "../../types/graphqlTypes"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; +import { setCurrentFilter } from "../sidebar/actions/productSlice"; +import { defaultFilterParameters, defaultSpatialSearchEndDate, defaultSpatialSearchStartDate, inputBounds, parameterOptionValues, rasterResolutionOptions } from "../../constants/rasterParameterConstants"; +import { useState } from "react"; +import { OutputGranuleExtentFlagOptions, OutputSamplingGridType, RasterResolution, Adjust, FilterParameters, FilterAction } from "../../types/historyPageTypes"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import { Formik } from "formik"; + + +const HistoryFilters = () => { + const dispatch = useAppDispatch() + const [currentFilters, setCurrentFilters] = useState(defaultFilterParameters) + const [endDateToUse, setEndDateToUse] = useState(defaultSpatialSearchEndDate) + const [startDateToUse, setStartDateToUse] = useState(defaultSpatialSearchStartDate) + const waitingForMyDataFiltering = useAppSelector((state) => state.product.waitingForMyDataFiltering) + const [cycleIsValid, setCycleIsValid] = useState(true) + const [passIsValid, setPassIsValid] = useState(true) + const [sceneIsValid, setSceneIsValid] = useState(true) + + const handleChangeFilters = (filter: FilterAction, value: string, valueValidity?: boolean) => { + const currentFiltersToModify: FilterParameters = structuredClone(currentFilters) + switch(filter) { + case 'cycle': + if(value === '') { + currentFiltersToModify[filter] = 'none' + } else { + currentFiltersToModify[filter] = value + } + + const checkCycleIsValid = (parseInt(value) >= inputBounds.cycle.min && parseInt(value) <= inputBounds.cycle.max) || isNaN(parseInt(value)) + setCycleIsValid(checkCycleIsValid) + break; + case 'pass': + if(value === '') { + currentFiltersToModify[filter] = 'none' + } else { + currentFiltersToModify[filter] = value + } + + const checkPassIsValid = (parseInt(value) >= inputBounds.pass.min && parseInt(value) <= inputBounds.pass.max) || isNaN(parseInt(value)) + setPassIsValid(checkPassIsValid) + break; + case 'scene': + if(value === '') { + currentFiltersToModify[filter] = 'none' + } else { + currentFiltersToModify[filter] = value + } + + const checkSceneIsValid = (parseInt(value) >= inputBounds.scene.min && parseInt(value) <= inputBounds.scene.max) || isNaN(parseInt(value)) + setSceneIsValid(checkSceneIsValid) + break; + case 'status': + if(currentFiltersToModify[filter].includes(value as ProductState)) { + // remove the value + currentFiltersToModify[filter] = currentFiltersToModify[filter].filter(filterCopyObject => filterCopyObject !== value) + } else { + currentFiltersToModify[filter].push(value as ProductState) + } + break; + case 'outputGranuleExtentFlag': + if(currentFiltersToModify[filter].includes(value as OutputGranuleExtentFlagOptions)) { + // remove the value + currentFiltersToModify[filter] = currentFiltersToModify[filter].filter(filterCopyObject => filterCopyObject !== value) + } else { + currentFiltersToModify[filter].push(value as OutputGranuleExtentFlagOptions) + } + break; + case 'outputSamplingGridType': + if(currentFiltersToModify[filter].includes(value as OutputSamplingGridType)) { + // remove the value + currentFiltersToModify[filter] = currentFiltersToModify[filter].filter(filterCopyObject => filterCopyObject !== value) + } else { + currentFiltersToModify[filter].push(value as OutputSamplingGridType) + } + break; + case 'rasterResolution': + if(currentFiltersToModify[filter].includes(value as RasterResolution)) { + // remove the value + currentFiltersToModify[filter] = currentFiltersToModify[filter].filter(filterCopyObject => filterCopyObject !== value) + } else { + currentFiltersToModify[filter].push(value as RasterResolution) + } + break; + case 'utmZoneAdjust': + if(currentFiltersToModify[filter].includes(value as Adjust)) { + // remove the value + currentFiltersToModify[filter] = currentFiltersToModify[filter].filter(filterCopyObject => filterCopyObject !== value) + } else { + currentFiltersToModify[filter].push(value as Adjust) + } + break; + case 'mgrsBandAdjust': + if(currentFiltersToModify[filter].includes(value as Adjust)) { + // remove the value + currentFiltersToModify[filter] = currentFiltersToModify[filter].filter(filterCopyObject => filterCopyObject !== value) + } else { + currentFiltersToModify[filter].push(value as Adjust) + } + break; + case 'endDate': + if(value === 'none') { + currentFiltersToModify[filter] = 'none' + setEndDateToUse(defaultSpatialSearchEndDate) + } else { + currentFiltersToModify[filter] = new Date(value) + setEndDateToUse(new Date(value)) + } + break; + case 'startDate': + if(value === 'none') { + currentFiltersToModify[filter] = 'none' + setStartDateToUse(defaultSpatialSearchStartDate) + } else { + currentFiltersToModify[filter] = new Date(value) + setStartDateToUse(new Date(value)) + } + break; + default: + } + setCurrentFilters(currentFiltersToModify) + } + + const statusOptions = ['NEW', 'UNAVAILABLE', 'GENERATING', 'ERROR', 'READY', 'AVAILABLE'] + const outputGranuleExtentFlagOptions = ['128 x 128', '256 x 128'] + const outputSamplingGridTypeOptions = parameterOptionValues.outputSamplingGridType.values.map(value => { + const valueToUse = value as string + return valueToUse.toUpperCase()}) + const rasterResolutionOptionsUTMOptions = rasterResolutionOptions.UTM.map(value => value.toString()) + const rasterResolutionOptionsGEOOptions = rasterResolutionOptions.GEO.map(value => value.toString()) + const zoneAdjustOptions = ['+1', '0', '-1'] + const cycleIsInvalid = parseInt(currentFilters['cycle']) < inputBounds.cycle.min || parseInt(currentFilters['cycle']) > inputBounds.cycle.max + const passIsInvalid = parseInt(currentFilters['pass']) < inputBounds.pass.min || parseInt(currentFilters['pass']) > inputBounds.pass.max + const sceneIsInvalid = parseInt(currentFilters['scene']) < inputBounds.scene.min || parseInt(currentFilters['scene']) > inputBounds.scene.max + const applyFilterErrorMessage = `Not Valid: ${cycleIsValid ? '' : 'cycle'}${(!passIsValid || !sceneIsValid) && !cycleIsValid ? ',' : ''} ${passIsValid ? '' : 'pass'}${!sceneIsValid && !passIsValid ? ',' : ''} ${sceneIsValid ? '' : 'scene'}` + return ( + +
Filters
+ + + + Cycle + + +
+ + handleChangeFilters('cycle', String(e.target.value), !cycleIsInvalid)}/> +
{`Valid Values: ${inputBounds.cycle.min} - ${inputBounds.cycle.max}`}
+
+
+
+
+
+
+ + + Pass + +
+ + handleChangeFilters('pass', String(e.target.value), !passIsInvalid)}/> +
{`Valid Values: ${inputBounds.pass.min} - ${inputBounds.pass.max}`}
+
+
+
+
+
+ + + Scene + +
+ + handleChangeFilters('scene', String(e.target.value), !sceneIsInvalid)}/> +
{`Valid Values: ${inputBounds.scene.min} - ${inputBounds.scene.max}`}
+
+
+
+
+
+ + + Status + +
+
+ {statusOptions.map(statusString => { + return ( + handleChangeFilters('status', statusString)}/> + ) + })} +
+
+
+
+
+ + + Output Granule Extent Flag (km) + +
+
+ {outputGranuleExtentFlagOptions.map(flagString => { + return ( + handleChangeFilters('outputGranuleExtentFlag', flagString)}/> + ) + })} +
+
+
+
+
+ + + Output Sampling Grid Type + +
+
+ {outputSamplingGridTypeOptions.map(valueString => { + return ( + handleChangeFilters('outputSamplingGridType', valueString)}/> + ) + })} +
+
+
+
+
+ + + Raster Resolution + +
+
+
UTM
+ {rasterResolutionOptionsUTMOptions.map(valueString => { + return ( + handleChangeFilters('rasterResolution', valueString)}/> + ) + })} +
LAT/LON
+ {rasterResolutionOptionsGEOOptions.map(valueString => { + return ( + handleChangeFilters('rasterResolution', valueString)}/> + ) + })} +
+
+
+
+
+ + + UTM Zone Adjust + +
+
+ {zoneAdjustOptions.map(valueString => { + const strippedValueString = valueString.replace('+', '') + return ( + handleChangeFilters('utmZoneAdjust', strippedValueString)}/> + ) + })} +
+
+
+
+
+ + + MGRS Band Adjust + +
+
+ {zoneAdjustOptions.map(valueString => { + const strippedValueString = valueString.replace('+', '') + return ( + handleChangeFilters('mgrsBandAdjust', strippedValueString)}/> + ) + })} +
+
+
+
+
+ + + Date Generated + +
End Date
+ + handleChangeFilters('endDate', String(date as Date))} + timeInputLabel="Time:" + dateFormat="MM/dd/yyyy h:mm aa" + showTimeInput + /> + +
Start Date
+ + handleChangeFilters('startDate', String(date as Date))} + timeInputLabel="Time:" + dateFormat="MM/dd/yyyy h:mm aa" + showTimeInput + /> + +
+
+
+
+ + {!cycleIsValid || !passIsValid || !sceneIsValid ?
{applyFilterErrorMessage}
: null} + + ) +} + +export default HistoryFilters; diff --git a/src/components/history/ReGenerateProductsModal.tsx b/src/components/history/ReGenerateProductsModal.tsx new file mode 100644 index 0000000..90261eb --- /dev/null +++ b/src/components/history/ReGenerateProductsModal.tsx @@ -0,0 +1,48 @@ +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import { useAppSelector, useAppDispatch } from '../../redux/hooks' +import { Row } from 'react-bootstrap'; +import { granuleAlertMessageConstant } from '../../constants/rasterParameterConstants'; +import { alertMessageInput } from '../../types/constantTypes'; +import { setShowReGenerateProductModalFalse } from '../sidebar/actions/modalSlice'; +import { addGeneratedProducts, addGranuleTableAlerts } from '../sidebar/actions/productSlice'; + +const ReGenerateProductsModal = () => { + const showReGenerateProductModal = useAppSelector((state) => state.modal.showReGenerateProductModal) + const granulesToReGenerate = useAppSelector((state) => state.product.granulesToReGenerate) + 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({granuleIds: granulesToReGenerate.map(granule => granule.id), typeOfGenerate: 're-generate'})) + dispatch(setShowReGenerateProductModalFalse()) + setSaveGranulesAlert('successfullyReGenerated') + } + + return ( + dispatch(setShowReGenerateProductModalFalse())} id='re-generate-products-modal'> + + Re-Generate Product + + + + +
Are you sure you would like to re-generate products with the following granules:
+
{granulesToReGenerate.map((granuleObject, index) => index === granulesToReGenerate.length-1 ? `${granuleObject.cycle}_${granuleObject.pass}_${granuleObject.scene} ` : `${granuleObject.cycle}_${granuleObject.pass}_${granuleObject.scene}, `)}
+
+
+ + + + + +
+ ); +} + +export default ReGenerateProductsModal; \ No newline at end of file diff --git a/src/components/navbar/MainNavbar.tsx b/src/components/navbar/MainNavbar.tsx index e614fa2..923609d 100644 --- a/src/components/navbar/MainNavbar.tsx +++ b/src/components/navbar/MainNavbar.tsx @@ -13,6 +13,7 @@ const MainNavbar = () => { const colorModeClass = useAppSelector((state) => state.navbar.colorModeClass) const userData = useAppSelector((state) => state.app.currentUser) const navigate = useNavigate() + const BASE_REDIRECT_URI = process.env.REACT_APP_BASE_REDIRECT_URI; var { email, firstName, lastName } = userData || {}; const { search } = useLocation(); @@ -34,13 +35,13 @@ const MainNavbar = () => { diff --git a/src/components/navbar/PodaacFooter.tsx b/src/components/navbar/PodaacFooter.tsx index 5ae3e14..99f8d63 100644 --- a/src/components/navbar/PodaacFooter.tsx +++ b/src/components/navbar/PodaacFooter.tsx @@ -13,9 +13,10 @@ const PodaacFooter = () => { return ( - - {`Version ${packageJson.version} Beta of SWOT On-Demand Level-2 Raster Generator (SWODLR)`} - + + + {`Version ${packageJson.version} of SWOT On-Demand Level-2 Raster Generator (SWODLR)`} + diff --git a/src/components/sidebar/GenerateProductsModal.tsx b/src/components/sidebar/GenerateProductsModal.tsx index aa83945..9cde73a 100644 --- a/src/components/sidebar/GenerateProductsModal.tsx +++ b/src/components/sidebar/GenerateProductsModal.tsx @@ -19,7 +19,7 @@ const GenerateProductsModal = () => { const handleGenerate = () => { // unselect select-all box - dispatch(addGeneratedProducts(addedGranules.map(granuleObj => granuleObj.granuleId))) + dispatch(addGeneratedProducts({granuleIds: addedGranules.map(granuleObj => granuleObj.granuleId), typeOfGenerate: 'generate'})) dispatch(setShowGenerateProductModalFalse()) setSaveGranulesAlert('successfullyGenerated') } diff --git a/src/components/sidebar/actions/modalSlice.ts b/src/components/sidebar/actions/modalSlice.ts index 63d2f17..0d41e33 100644 --- a/src/components/sidebar/actions/modalSlice.ts +++ b/src/components/sidebar/actions/modalSlice.ts @@ -1,4 +1,6 @@ -import { createSlice } from '@reduxjs/toolkit' +import { PayloadAction, createSlice } from '@reduxjs/toolkit' +import { allProductParameters } from '../../../types/constantTypes' +import { Product } from '../../../types/graphqlTypes' // Define a type for the slice state interface AddCustomProductModalState { @@ -10,7 +12,8 @@ interface AddCustomProductModalState { selectedGranules: string[], showTutorialModal: boolean, skipTutorial: boolean, - showCloseTutorialModal: boolean + showCloseTutorialModal: boolean, + showReGenerateProductModal: boolean, } // Define the initial state using that type @@ -25,7 +28,8 @@ const initialState: AddCustomProductModalState = { // the key will be cycleId_passId_sceneId and the value will be a 'parameterOptionDefaults' type object sampleGranuleDataArray: [], selectedGranules: [], - showCloseTutorialModal: false + showCloseTutorialModal: false, + showReGenerateProductModal: false } export const modalSlice = createSlice({ @@ -61,6 +65,12 @@ export const modalSlice = createSlice({ setShowGenerateProductModalTrue: (state) => { state.showGenerateProductModal = true }, + setShowReGenerateProductModalFalse: (state) => { + state.showReGenerateProductModal = false + }, + setShowReGenerateProductModalTrue: (state) => { + state.showReGenerateProductModal = true + }, setShowTutorialModalFalse: (state) => { state.showTutorialModal = false }, @@ -92,6 +102,8 @@ export const { setShowDeleteProductModalTrue, setShowGenerateProductModalFalse, setShowGenerateProductModalTrue, + setShowReGenerateProductModalFalse, + setShowReGenerateProductModalTrue, setShowTutorialModalFalse, setShowTutorialModalTrue, setSkipTutorialFalse, diff --git a/src/components/sidebar/actions/productSlice.ts b/src/components/sidebar/actions/productSlice.ts index 59f23dc..8cde587 100644 --- a/src/components/sidebar/actions/productSlice.ts +++ b/src/components/sidebar/actions/productSlice.ts @@ -1,10 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { AlertMessageObject, allProductParameters, GeneratedProduct, GenerateProductParameters, MapFocusObject, RetrievedDataHistory, SpatialSearchResult } from '../../../types/constantTypes' +import { AlertMessageObject, allProductParameters, GeneratedProduct, GenerateProductParameters, MapFocusObject, SpatialSearchResult } from '../../../types/constantTypes' import L, { LatLngExpression } from 'leaflet' -import { parameterOptionDefaults } from '../../../constants/rasterParameterConstants' +import { defaultFilterParameters, defaultSpatialSearchEndDate, defaultSpatialSearchStartDate, parameterOptionDefaults, parameterOptionValues } from '../../../constants/rasterParameterConstants' import { v4 as uuidv4 } from 'uuid'; import { generateL2RasterProduct } from '../../../user/userData'; import { Product } from '../../../types/graphqlTypes'; +import { FilterParameters} from '../../../types/historyPageTypes'; // Define a type for the slice state interface GranuleState { @@ -12,7 +13,6 @@ interface GranuleState { addedProducts: allProductParameters[], selectedGranules: string[], granuleFocus: number[], - generatedProducts: GeneratedProduct[], generateProductParameters: GenerateProductParameters, granuleTableAlerts: AlertMessageObject[], productCustomizationTableAlerts: AlertMessageObject[], @@ -25,7 +25,11 @@ interface GranuleState { historyPageState: string[], historyPageIndex: number, firstHistoryPageData: Product[], - userProducts: Product[] + userProducts: Product[], + allUserProducts: Product[], + currentFilters: FilterParameters, + granulesToReGenerate: Product[], + waitingForMyDataFiltering: boolean, } const {name, cycle, pass, scene, ...generateProductParametersFiltered } = parameterOptionDefaults @@ -39,19 +43,22 @@ const initialState: GranuleState = { selectedGranules: [], granuleFocus: [33.854457, -118.709093], mapFocus: {center: [33.854457, -118.709093], zoom: 6}, - generatedProducts: [], generateProductParameters: generateProductParametersFiltered, granuleTableAlerts: [], productCustomizationTableAlerts: [], showUTMAdvancedOptions: false, spatialSearchResults: [], waitingForSpatialSearch: false, - spatialSearchStartDate: (new Date(2022, 11, 16)).toISOString(), - spatialSearchEndDate: (new Date()).toISOString(), + spatialSearchStartDate: defaultSpatialSearchStartDate.toISOString(), + spatialSearchEndDate: defaultSpatialSearchEndDate.toISOString(), userProducts: [], + allUserProducts: [], historyPageState: [], firstHistoryPageData: [], - historyPageIndex: 0 + historyPageIndex: 0, + currentFilters: defaultFilterParameters, + granulesToReGenerate: [], + waitingForMyDataFiltering: false, } @@ -86,37 +93,34 @@ export const productSlice = createSlice({ setMapFocus: (state, action: PayloadAction) => { state.mapFocus = action.payload }, - addGeneratedProducts: (state, action: PayloadAction) => { - const productsToBeGeneratedCopy = [...action.payload] + addGeneratedProducts: (state, action: PayloadAction<{granuleIds: string[], typeOfGenerate: 'generate' | 're-generate'}>) => { + action.payload.granuleIds.forEach(granuleId => { + if(action.payload.typeOfGenerate === 'generate') { + const relevantAddedProduct = state.addedProducts.find(productObj => productObj.granuleId === granuleId) as allProductParameters + const { cycle, pass, scene} = relevantAddedProduct + const utmZoneAdjust = relevantAddedProduct.utmZoneAdjust ?? parameterOptionValues.utmZoneAdjust.default + const mgrsBandAdjust = relevantAddedProduct.mgrsBandAdjust ?? parameterOptionValues.mgrsBandAdjust.default + const {outputGranuleExtentFlag, outputSamplingGridType, rasterResolutionUTM, rasterResolutionGEO} = state.generateProductParameters + const rasterResolution = outputSamplingGridType === "utm" ? rasterResolutionUTM : rasterResolutionGEO + const fetchData = async () => { + await generateL2RasterProduct(cycle, pass, scene, outputGranuleExtentFlag, outputSamplingGridType, rasterResolution, utmZoneAdjust, mgrsBandAdjust) + } + fetchData().catch(console.error); + } else if(action.payload.typeOfGenerate === 're-generate') { + const relevantAddedProduct = state.granulesToReGenerate.find(productObj => productObj.id === granuleId) as Product + const {cycle, pass, scene, outputGranuleExtentFlag, outputSamplingGridType, rasterResolution, utmZoneAdjust, mgrsBandAdjust} = relevantAddedProduct - const newGeneratedProducts: GeneratedProduct[] = productsToBeGeneratedCopy.map((granuleId, index) => { - const relevantAddedProduct = state.addedProducts.find(productObj => productObj.granuleId === granuleId) as allProductParameters - const {utmZoneAdjust, mgrsBandAdjust, cycle, pass, scene} = relevantAddedProduct - const {outputGranuleExtentFlag, outputSamplingGridType, rasterResolutionUTM, rasterResolutionGEO} = state.generateProductParameters - const rasterResolution = outputSamplingGridType === "utm" ? rasterResolutionUTM : rasterResolutionGEO - const fetchData = async () => { - await generateL2RasterProduct(cycle, pass, scene, outputGranuleExtentFlag, outputSamplingGridType, rasterResolution, utmZoneAdjust, mgrsBandAdjust) - } - - fetchData().catch(console.error); + let utmZoneAdjustToUse = String(utmZoneAdjust ?? parameterOptionValues.utmZoneAdjust.default) + if(utmZoneAdjustToUse === '1') utmZoneAdjustToUse = "+1" + let mgrsBandAdjustToUse = String(mgrsBandAdjust ?? parameterOptionValues.mgrsBandAdjust.default) + if(mgrsBandAdjustToUse === '1') mgrsBandAdjustToUse = "+1" - return ({ - productId: uuidv4(), - granuleId: granuleId, - status: index % 2 === 0 ? "In Progress" : "Complete", - cycle, - pass, - scene, - parametersUsedToGenerate: { - batchGenerateProductParameters: state.generateProductParameters, - utmZoneAdjust: utmZoneAdjust, - mgrsBandAdjust: mgrsBandAdjust - }, - downloadUrl: `https://test-download-url-${granuleId}.zip`, - dateGenerated: new Date(), - }) + const fetchData = async () => { + await generateL2RasterProduct(String(cycle), String(pass), String(scene), +outputGranuleExtentFlag, outputSamplingGridType, rasterResolution, utmZoneAdjustToUse, mgrsBandAdjustToUse) + } + fetchData().catch(console.error); + } }) - state.generatedProducts = [...state.generatedProducts, ...newGeneratedProducts] }, setGenerateProductParameters: (state, action: PayloadAction) => { state.generateProductParameters = action.payload @@ -162,6 +166,18 @@ export const productSlice = createSlice({ }, setUserProducts: (state, action: PayloadAction) => { state.userProducts = action.payload + }, + setAllUserProducts: (state, action: PayloadAction) => { + state.allUserProducts = action.payload + }, + setCurrentFilter: (state, action: PayloadAction) => { + state.currentFilters = action.payload + }, + setGranulesToReGenerate: (state, action: PayloadAction) => { + state.granulesToReGenerate = action.payload + }, + setWaitingForMyDataFiltering: (state, action: PayloadAction) => { + state.waitingForMyDataFiltering = action.payload } }, }) @@ -173,6 +189,7 @@ export const { setSelectedGranules, setGranuleFocus, addGeneratedProducts, + setGranulesToReGenerate, setGenerateProductParameters, addGranuleTableAlerts, removeGranuleTableAlerts, @@ -184,9 +201,12 @@ export const { setMapFocus, clearGranuleTableAlerts, setUserProducts, + setAllUserProducts, setFirstHistoryPageData, setHistoryPageState, addPageToHistoryPageState, + setCurrentFilter, + setWaitingForMyDataFiltering } = productSlice.actions export default productSlice.reducer \ No newline at end of file diff --git a/src/constants/rasterParameterConstants.ts b/src/constants/rasterParameterConstants.ts index 1eda59e..408fe36 100644 --- a/src/constants/rasterParameterConstants.ts +++ b/src/constants/rasterParameterConstants.ts @@ -1,5 +1,6 @@ import { LatLngExpression } from "leaflet" import { ParameterHelp, ParameterOptions, granuleAlertMessageConstantType, inputValuesDictionary, parameterValuesDictionary } from "../types/constantTypes" +import { FilterParameters } from "../types/historyPageTypes" export const rasterResolutionOptions = { UTM: [90, 100, 120, 125, 200, 250, 500, 1000, 2500, 5000, 10000], @@ -118,18 +119,18 @@ export interface InputBounds { } export const inputBounds: inputValuesDictionary = { -cycle: { - min: 0, - max: 399 -}, -pass: { - min: 1, - max: 584 -}, -scene: { - min: 1, - max: 154 -} + cycle: { + min: 0, + max: 578 + }, + pass: { + min: 1, + max: 584 + }, + scene: { + min: 1, + max: 154 + } } export const granuleAlertMessageConstant: granuleAlertMessageConstantType = { @@ -196,7 +197,11 @@ export const granuleAlertMessageConstant: granuleAlertMessageConstantType = { spatialSearchAreaTooLarge: { message: `The search area you've selected on the map is too large. Please choose a smaller area to search.`, variant: 'warning' - } + }, + successfullyReGenerated: { + message: `Successfully re-submitted product generation! Go to the 'My Data' page to track progress.`, + variant: 'success' + }, } export const parameterOptionHelp = { @@ -235,6 +240,24 @@ export const beforeCPS = '_x_x_x_' export const afterCPSR = 'F_' export const afterCPSL = 'F_' export const spatialSearchCollectionConceptId = 'C2799438271-POCLOUD' -// export const footprintSearchCollectionConceptId = 'C2799438271-POCLOUD' +// TODO: implement collection for calibration orbit use +// export const spatialSearchCollectionConceptId = 'C1261072637-POCLOUD' + +export const productsPerPage = '10' + +export const defaultFilterParameters: FilterParameters = { + cycle: 'none', + pass: 'none', + scene: 'none', + outputGranuleExtentFlag: [], + status: [], + outputSamplingGridType: [], + rasterResolution: [], + utmZoneAdjust: [], + mgrsBandAdjust: [], + startDate: 'none', + endDate: 'none' +} -export const productsPerPage = '20' +export const defaultSpatialSearchStartDate = new Date(2022, 11, 16) +export const defaultSpatialSearchEndDate = new Date() \ No newline at end of file diff --git a/src/types/constantTypes.ts b/src/types/constantTypes.ts index e97639f..618dfc0 100644 --- a/src/types/constantTypes.ts +++ b/src/types/constantTypes.ts @@ -138,7 +138,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' | 'spatialSearchAreaTooLarge' +export type alertMessageInput = 'success' | 'alreadyAdded' | 'allScenesNotAvailable' | 'alreadyAddedAndNotFound' | 'noScenesAdded' | 'readyForGeneration' | 'invalidCycle' | 'invalidPass' | 'invalidScene' | 'invalidScene' | 'someScenesNotAvailable' | 'granuleLimit' | 'notInTimeRange' | 'noScenesFound' | 'someSuccess' | 'successfullyGenerated' | 'spatialSearchAreaTooLarge' | 'successfullyReGenerated' export interface SpatialSearchResult { cycle: string, diff --git a/src/types/graphqlTypes.ts b/src/types/graphqlTypes.ts index 27f9137..5cc308a 100644 --- a/src/types/graphqlTypes.ts +++ b/src/types/graphqlTypes.ts @@ -60,7 +60,7 @@ export interface UserResponse { currentUser: CurrentUser } -export type ProductState = 'NEW' | 'UNAVAILABLE' | 'GENERATING' | 'ERROR' | 'READY' | 'AVAILABLE;' +export type ProductState = 'NEW' | 'UNAVAILABLE' | 'GENERATING' | 'ERROR' | 'READY' | 'AVAILABLE' export type GraphqlResponseStatus = 'success' | 'error' | 'unknown' diff --git a/src/types/historyPageTypes.ts b/src/types/historyPageTypes.ts new file mode 100644 index 0000000..0b51c4d --- /dev/null +++ b/src/types/historyPageTypes.ts @@ -0,0 +1,22 @@ +import { ProductState } from "./graphqlTypes"; +export type OutputGranuleExtentFlagOptions = '128 x 128' | '256 x 128' +export type OutputSamplingGridType = 'UTM' | 'LAT/LON' +export type Adjust = '1' | '0' | '-1' | 'N/A' +export type RasterResolution = '90' | '100' | '120' | '125' | '200' | '250' | '500' | '1000' | '2500' | '5000' | '10000' | '3' | '4' | '5' | '6' | '8' | '15' | '30' | '60' | '180' | '300' + +export interface FilterParameters { + cycle: string, + scene: string, + pass: string, + status: ProductState[], + outputGranuleExtentFlag: OutputGranuleExtentFlagOptions[], + outputSamplingGridType: OutputSamplingGridType[], + rasterResolution: RasterResolution[], + utmZoneAdjust: Adjust[], + mgrsBandAdjust: Adjust[], + startDate: Date | 'none', + endDate: Date | 'none' + +} + +export type FilterAction = 'cycle' | 'scene' | 'pass' | 'status' | 'outputGranuleExtentFlag' | 'outputSamplingGridType' | 'rasterResolution' | 'utmZoneAdjust' | 'mgrsBandAdjust' | 'startDate' | 'endDate' \ No newline at end of file