From d0a2cef08a90c8447a89866a69f8b2e9f78f14c2 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Tue, 23 Apr 2024 09:51:52 -0700 Subject: [PATCH] Feature/swodlr UI 71 - add pagination to my data page (#98) * /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-71: added pagination * feature/swodlr-ui-71: change pagination button color and fix index * feature/swodlr-ui-71: make end pagination faster * feature/swodlr-ui-71: rebase with develop * feature/swodlr-ui-71: change pagination button color and fix index * feature/swodlr-ui-71: make end pagination faster * feature/swodlr-ui-71: add page numbers to pagination * feature/swodlr-ui-71: made products per page in history 20 * feature/swodlr-ui-71: remove first and last pagination buttons --------- Co-authored-by: frankinspace Co-authored-by: Frank Greguska Co-authored-by: Jonathan M Smolenski Co-authored-by: jonathansmolenski Co-authored-by: jbyrne --- src/components/app/App.css | 20 ++- src/components/app/App.tsx | 2 +- src/components/history/DataPagination.tsx | 158 ++++++++++++++++++ .../history/GeneratedProductHistory.tsx | 115 +++++++------ .../sidebar/actions/productSlice.ts | 36 +++- src/constants/graphqlQueries.ts | 5 +- src/constants/rasterParameterConstants.ts | 2 +- src/types/constantTypes.ts | 5 + src/types/graphqlTypes.ts | 6 +- src/user/userData.ts | 14 +- 10 files changed, 292 insertions(+), 71 deletions(-) create mode 100644 src/components/history/DataPagination.tsx diff --git a/src/components/app/App.css b/src/components/app/App.css index 0d85248..bd33b87 100644 --- a/src/components/app/App.css +++ b/src/components/app/App.css @@ -310,10 +310,26 @@ tfoot { } .table-responsive-generatedProducts { - max-height: 75vh; + max-height: 65vh; overflow-y: auto; } +.center { + width:100%; + display: flex; + justify-content: center; +} + +.pagination-link-item { + background-color: #1A2535; + border-color: #1A2535; +} + +.pagination-link-item:active { + background-color: #1A2535; + border-color: #1A2535; +} + .dark-mode-table { color: #FAF9F6; border: #FAF9F6 solid 1px; @@ -398,7 +414,7 @@ ABOUT PAGE .about-page { max-height: 82vh; - overflow-y: auto; + /* overflow-y: auto; */ } .about-card { diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx index 61ee332..2ab0f20 100644 --- a/src/components/app/App.tsx +++ b/src/components/app/App.tsx @@ -64,7 +64,7 @@ const App = () => { navigate(`/customizeProduct/configureOptions${search}`) } else if (stepTarget === '#added-scenes' && action === 'update') { - navigate(`/customizeProduct/selectScenes?cyclePassScene=9_515_130&showUTMAdvancedOptions=true`) + navigate(`/customizeProduct/selectScenes?cyclePassScene=12_468_45&showUTMAdvancedOptions=true`) } else if (stepTarget === '#customization-tab' && action === 'start') { navigate('/customizeProduct/selectScenes') } else if (action === 'next' && stepTarget === '#my-data-page') { diff --git a/src/components/history/DataPagination.tsx b/src/components/history/DataPagination.tsx new file mode 100644 index 0000000..c176efe --- /dev/null +++ b/src/components/history/DataPagination.tsx @@ -0,0 +1,158 @@ +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 { productsPerPage } from "../../constants/rasterParameterConstants"; +import { useEffect, useState } from "react"; + + +const DataPagination = () => { + 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 [noNextPage, setNoNextPage] = useState(false) + const [noPreviousPage, setNoPreviousPage] = useState(true) + const [waitingForPagination, setWaitingForPagination] = useState(true) + 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 handleSelectPage = async (pageNumber: number) => { + if (pageNumber === 1) { + if(noPreviousPage) setNoPreviousPage(false) + dispatch(setUserProducts(firstHistoryPageData)) + setCurrentPageNumber(pageNumber) + setNoPreviousPage(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) + } + }) + } + } + + const waitingForPaginationSpinner = () => { + return ( +
+ + Loading... + +
+ ) + } + + const getPaginationItemsWithEllipsis = () => { + let numberOfSlotsFreeLeft = 0 + if(currentPageNumber >= historyPageState.length-4) { + numberOfSlotsFreeLeft = 4 - (historyPageState.length - currentPageNumber) + } + + let numberOfSlotsFreeRight = 0 + if(currentPageNumber <= 4) { + numberOfSlotsFreeRight = 5 - currentPageNumber + } + 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) { + // let another number go on right + pagesAllowedToLeftOfCurrent.push(currentPageNumber - index - 3) + numberOfSlotsFreeLeft -= 1 + } + if(numberOfSlotsFreeRight !== 0 && index+1 > currentPageNumber + 3) { + // let another number go on left + 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} + } + return null + }) + if(pagesAllowed[0] > 2) pagesToShow.unshift() + if(pagesAllowed[pagesAllowed.length-1] < historyPageState.length-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} /> + + + +
+ ) +} + +export default DataPagination; diff --git a/src/components/history/GeneratedProductHistory.tsx b/src/components/history/GeneratedProductHistory.tsx index 474a0b3..484ad84 100644 --- a/src/components/history/GeneratedProductHistory.tsx +++ b/src/components/history/GeneratedProductHistory.tsx @@ -1,29 +1,37 @@ import { Alert, Col, OverlayTrigger, Row, Table, Tooltip, Button, Spinner } from "react-bootstrap"; -import { useAppSelector } from "../../redux/hooks"; -import { getUserProductsResponse, Product } from "../../types/graphqlTypes"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; +import { Product } from "../../types/graphqlTypes"; import { useEffect, useState } from "react"; import { InfoCircle, Clipboard, Download } from "react-bootstrap-icons"; -import { generatedProductsLabels, infoIconsToRender, parameterHelp } from "../../constants/rasterParameterConstants"; +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"; const GeneratedProductHistory = () => { + const dispatch = useAppDispatch() const colorModeClass = useAppSelector((state) => state.navbar.colorModeClass) + const userProducts = useAppSelector((state) => state.product.userProducts) const { search } = useLocation(); const navigate = useNavigate() - const [userProducts, setUserProducts] = useState([]) - const [waitingForProductsToLoad, setWaitingForProductsToLoad] = useState(true) 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 () => { - const userProductsResponse: getUserProductsResponse = await getUserProducts().then((response) => { - setWaitingForProductsToLoad(false) - return response + 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)) + } }) - if (userProductsResponse.status === 'success') setUserProducts(userProductsResponse.products as Product[]) } - fetchData() - .catch(console.error); + + if(userProducts.length === 0) fetchData().catch(console.error) }, []); // TODO: implement download link copy button @@ -73,57 +81,58 @@ const GeneratedProductHistory = () => { ) - const renderColTitle = (labelEntry: string[], index: number) => { + const renderColTitle = (labelEntry: string[], index: number) => { let infoIcon = infoIconsToRender.includes(labelEntry[0]) ? renderInfoIcon(labelEntry[0]) : null let labelId = (labelEntry[0] === 'downloadUrl') ? 'download-url' : '' return ( - {labelEntry[1]} {infoIcon} + {labelEntry[1]} {infoIcon} ) - } + } const renderHistoryTable = () => { 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 ( - - {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 - } )} - - )})} - -
{cellContents}
+ + + + {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 ( + + {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 + } )} + + )})} + +
{cellContents}
+
+
- ) } @@ -157,7 +166,7 @@ const GeneratedProductHistory = () => { <>

Generated Products Data

- {waitingForProductsToLoad ? waitingForProductsToLoadSpinner() : renderProductHistoryViews()} + {userProducts.length === 0 ? waitingForProductsToLoadSpinner() : renderProductHistoryViews()} ); diff --git a/src/components/sidebar/actions/productSlice.ts b/src/components/sidebar/actions/productSlice.ts index ce4ea8e..59f23dc 100644 --- a/src/components/sidebar/actions/productSlice.ts +++ b/src/components/sidebar/actions/productSlice.ts @@ -1,9 +1,10 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { AlertMessageObject, allProductParameters, GeneratedProduct, GenerateProductParameters, MapFocusObject, SpatialSearchResult } from '../../../types/constantTypes' +import { AlertMessageObject, allProductParameters, GeneratedProduct, GenerateProductParameters, MapFocusObject, RetrievedDataHistory, SpatialSearchResult } from '../../../types/constantTypes' import L, { LatLngExpression } from 'leaflet' import { parameterOptionDefaults } from '../../../constants/rasterParameterConstants' import { v4 as uuidv4 } from 'uuid'; import { generateL2RasterProduct } from '../../../user/userData'; +import { Product } from '../../../types/graphqlTypes'; // Define a type for the slice state interface GranuleState { @@ -20,7 +21,11 @@ interface GranuleState { waitingForSpatialSearch: boolean, spatialSearchStartDate: string, spatialSearchEndDate: string, - mapFocus: MapFocusObject + mapFocus: MapFocusObject, + historyPageState: string[], + historyPageIndex: number, + firstHistoryPageData: Product[], + userProducts: Product[] } const {name, cycle, pass, scene, ...generateProductParametersFiltered } = parameterOptionDefaults @@ -42,7 +47,11 @@ const initialState: GranuleState = { spatialSearchResults: [], waitingForSpatialSearch: false, spatialSearchStartDate: (new Date(2022, 11, 16)).toISOString(), - spatialSearchEndDate: (new Date()).toISOString() + spatialSearchEndDate: (new Date()).toISOString(), + userProducts: [], + historyPageState: [], + firstHistoryPageData: [], + historyPageIndex: 0 } @@ -139,6 +148,21 @@ export const productSlice = createSlice({ setSpatialSearchEndDate: (state, action: PayloadAction) => { state.spatialSearchEndDate = action.payload }, + addPageToHistoryPageState: (state, action: PayloadAction) => { + const idInHistory = state.historyPageState.includes(action.payload) + if (!idInHistory) { + state.historyPageState = [...state.historyPageState, action.payload] + } + }, + setHistoryPageState: (state, action: PayloadAction) => { + state.historyPageIndex = action.payload + }, + setFirstHistoryPageData: (state, action: PayloadAction) => { + state.firstHistoryPageData = action.payload + }, + setUserProducts: (state, action: PayloadAction) => { + state.userProducts = action.payload + } }, }) @@ -158,7 +182,11 @@ export const { setSpatialSearchStartDate, setSpatialSearchEndDate, setMapFocus, - clearGranuleTableAlerts + clearGranuleTableAlerts, + setUserProducts, + setFirstHistoryPageData, + setHistoryPageState, + addPageToHistoryPageState, } = productSlice.actions export default productSlice.reducer \ No newline at end of file diff --git a/src/constants/graphqlQueries.ts b/src/constants/graphqlQueries.ts index 0622932..c60eb96 100644 --- a/src/constants/graphqlQueries.ts +++ b/src/constants/graphqlQueries.ts @@ -1,5 +1,5 @@ import { padCPSForCmrQuery } from "../components/sidebar/GranulesTable" -import { spatialSearchCollectionConceptId, userProductQueryLimit } from "./rasterParameterConstants" +import { spatialSearchCollectionConceptId } from "./rasterParameterConstants" export const userQuery = ` { @@ -28,9 +28,10 @@ export const generateL2RasterProductQuery = ` ` export const userProductsQuery = ` + query getUserProducts($limit: Int, $after: ID) { currentUser { - products (limit: ${userProductQueryLimit}) { + products (limit: $limit, after: $after) { id timestamp cycle diff --git a/src/constants/rasterParameterConstants.ts b/src/constants/rasterParameterConstants.ts index 9d06a92..1eda59e 100644 --- a/src/constants/rasterParameterConstants.ts +++ b/src/constants/rasterParameterConstants.ts @@ -237,4 +237,4 @@ export const afterCPSL = 'F_' export const spatialSearchCollectionConceptId = 'C2799438271-POCLOUD' // export const footprintSearchCollectionConceptId = 'C2799438271-POCLOUD' -export const userProductQueryLimit = 1000 \ No newline at end of file +export const productsPerPage = '20' diff --git a/src/types/constantTypes.ts b/src/types/constantTypes.ts index 65cefa4..9268d3c 100644 --- a/src/types/constantTypes.ts +++ b/src/types/constantTypes.ts @@ -156,4 +156,9 @@ export type SaveType = 'manual' | 'urlParameter' | 'spatialSearch' export interface handleSaveResult { result: string, savedScenes?: allProductParameters[] +} + +// key is the page number and the value is the product ID of the last element on a page +export interface RetrievedDataHistory { + [key: string]: string } \ No newline at end of file diff --git a/src/types/graphqlTypes.ts b/src/types/graphqlTypes.ts index 0dcaf4b..27f9137 100644 --- a/src/types/graphqlTypes.ts +++ b/src/types/graphqlTypes.ts @@ -69,6 +69,10 @@ export interface getUserProductsResponse { status: GraphqlResponseStatus, products?: Product[], error?: Error | string, + } + +export interface UserProductQueryVariables { + [key: string]: string } export interface cpsValidationResponse { @@ -83,4 +87,4 @@ export type cycleGroup = {cycle: string, pass: string, scene: string}[] export interface cycleGroups { [key: string]: {cycle: string, pass: string, scene: string}[] -} \ No newline at end of file +} diff --git a/src/user/userData.ts b/src/user/userData.ts index 6ecb5be..33f2620 100644 --- a/src/user/userData.ts +++ b/src/user/userData.ts @@ -1,6 +1,6 @@ import { gql, GraphQLClient, RequestMiddleware } from 'graphql-request' import { generateL2RasterProductQuery, userProductsQuery, userQuery } from '../constants/graphqlQueries'; -import { CurrentUser, UserResponse, getUserProductsResponse } from '../types/graphqlTypes'; +import { CurrentUser, UserProductQueryVariables, UserResponse, getUserProductsResponse } from '../types/graphqlTypes'; import { Session } from '../authentication/session'; const userIdQuery = gql`${userQuery}` @@ -118,13 +118,13 @@ export const generateL2RasterProduct = async ( } } -export const getUserProducts = async () => { +export const getUserProducts = async (userProductsQueryVariables?: UserProductQueryVariables) => { try { - const userProductResponse = await graphQLClient.request(userProductsQuery).then(result => { - const userProductsResult = (result as UserResponse).currentUser.products - return {status: 'success', products: userProductsResult} as getUserProductsResponse - }) - return userProductResponse + const userProductResponse = await graphQLClient.request(userProductsQuery, userProductsQueryVariables).then(result => { + const userProductsResult = (result as UserResponse).currentUser.products + return {status: 'success', products: userProductsResult} as getUserProductsResponse + }) + return userProductResponse } catch (err) { console.log (err) if (err instanceof Error) {