├── .eslintignore ├── .nvmrc ├── static └── src │ ├── js │ ├── util │ │ ├── bootstrap.js │ │ ├── polyfill │ │ │ ├── index.js │ │ │ └── matches.js │ │ ├── mocks │ │ │ └── fileMock.js │ │ ├── history.js │ │ ├── map │ │ │ ├── projections.js │ │ │ └── actions │ │ │ │ ├── panBoundsToCenter.js │ │ │ │ └── panFeatureGroupToCenter.js │ │ ├── variables.js │ │ ├── propTypes │ │ │ └── location.js │ │ ├── scienceKeywordTypes.js │ │ ├── isNumber.js │ │ ├── isBrowserCompatible.js │ │ ├── getFilenameFromPath.js │ │ ├── url │ │ │ ├── stringEncoders.js │ │ │ ├── integerEncoders.js │ │ │ ├── arrayEncoders.js │ │ │ ├── __tests__ │ │ │ │ ├── url.test.js │ │ │ │ ├── buildAuthenticatedRedirectUrl.test.js │ │ │ │ ├── focusedGranuleUrl.test.js │ │ │ │ ├── shapefileId.test.js │ │ │ │ ├── granuleDataFormatFacetUrl.test.js │ │ │ │ └── projectFacetUrl.test.js │ │ │ ├── gridEncoders.js │ │ │ ├── facetEncoders.js │ │ │ └── buildAuthenticatedRedirectUrl.js │ │ ├── facetCategoryAbbreviationsMap.js │ │ ├── object.js │ │ ├── request │ │ │ ├── accessMethodsRequest.js │ │ │ ├── providerRequest.js │ │ │ ├── regionRequest.js │ │ │ ├── preferencesRequest.js │ │ │ ├── dataQualitySummaryRequest.js │ │ │ ├── retrievalCollectionRequest.js │ │ │ ├── shapefileRequest.js │ │ │ ├── logoutRequest.js │ │ │ ├── contactInfoRequest.js │ │ │ ├── loggerRequest.js │ │ │ ├── admin │ │ │ │ └── retrievalRequest.js │ │ │ ├── __tests__ │ │ │ │ └── accessMethodsRequest.test.js │ │ │ ├── retrievalRequest.js │ │ │ └── projectRequest.js │ │ ├── autocompleteFacetsMap.js │ │ ├── convertRemsToPixels.js │ │ ├── __tests__ │ │ │ ├── pathStartsWith.test.js │ │ │ ├── isNumber.test.js │ │ │ ├── isLinkType.test.js │ │ │ ├── portals.test.js │ │ │ ├── limitDecimalPoints.test.js │ │ │ ├── pluralize.test.js │ │ │ └── getFilenameFromPath.test.js │ │ ├── buildPromise.js │ │ ├── focusedGranule.js │ │ ├── isLinkType.js │ │ ├── query.js │ │ ├── collectionMetadata │ │ │ ├── granuleLimit.js │ │ │ ├── scienceKeywords.js │ │ │ ├── dataCenters.js │ │ │ ├── doi.js │ │ │ └── __tests__ │ │ │ │ └── granuleLimit.test.js │ │ ├── datepicker.js │ │ ├── regions.js │ │ ├── pluralize.js │ │ ├── findProvider.js │ │ ├── commafy.js │ │ ├── alphabetic-list.js │ │ ├── scrollParent.js │ │ ├── isProjectCollectionValid.js │ │ ├── getPanelSizeMap.js │ │ ├── getActivePanelSize.js │ │ ├── pathStartsWith.js │ │ ├── limitDecimalPoints.js │ │ └── itemToRowColumnIndicies.js │ ├── components │ │ ├── Map │ │ │ └── ProjectionSwitcher.scss │ │ ├── Timeline │ │ │ └── Timeline.scss │ │ ├── AdvancedSearchModal │ │ │ ├── AdvancedSearchModal.scss │ │ │ ├── AdvancedSearchForm.scss │ │ │ └── RegionSearch.scss │ │ ├── Facets │ │ │ ├── FacetsModal.scss │ │ │ ├── FacetsSectionHeading.scss │ │ │ ├── Facets.scss │ │ │ ├── FacetsList.scss │ │ │ ├── FacetsSectionHeading.js │ │ │ └── FacetsModalNav.scss │ │ ├── Preferences │ │ │ ├── PreferencesForm.scss │ │ │ ├── Preferences.scss │ │ │ └── PreferencesRadioField.scss │ │ ├── CustomToggle │ │ │ ├── MoreActionsToggle.scss │ │ │ └── CustomToggle.scss │ │ ├── GranuleFilters │ │ │ ├── GranuleFiltersList.scss │ │ │ ├── GranuleFiltersActions.scss │ │ │ ├── GranuleFiltersItem.scss │ │ │ ├── GranuleFiltersHeader.scss │ │ │ ├── GranuleFiltersBody.js │ │ │ ├── GranuleFiltersList.js │ │ │ └── __tests__ │ │ │ │ └── GranuleFiltersHeader.test.js │ │ ├── OrderDropdownList │ │ │ ├── OrderDropdownList.scss │ │ │ └── OrderDropdownItem.scss │ │ ├── ButtonDropdown │ │ │ └── ButtonDropdown.scss │ │ ├── AdvancedSearchDisplay │ │ │ ├── AdvancedSearchDisplay.scss │ │ │ └── AdvancedSearchDisplayEntry.js │ │ ├── CollapsePanel │ │ │ └── CollapsePanel.scss │ │ ├── TemporalDisplay │ │ │ ├── TemporalDisplay.scss │ │ │ ├── TemporalSelectionDropdown.scss │ │ │ ├── TemporalSelectionDropdownMenu.scss │ │ │ └── TemporalSelectionDropdownToggle.js │ │ ├── ChunkedOrderModal │ │ │ └── ChunkedOrderModal.scss │ │ ├── OrderProgressList │ │ │ ├── OrderProgressList.scss │ │ │ └── OrderProgressList.js │ │ ├── GranuleDetails │ │ │ ├── skeleton.js │ │ │ ├── GranuleDetailsMetadata.scss │ │ │ ├── GranuleDetailsInfo.scss │ │ │ └── GranuleDetailsHeader.scss │ │ ├── GranuleResults │ │ │ ├── GranuleResultsList.scss │ │ │ ├── GranuleResultsDataLinksButton.scss │ │ │ └── GranuleResultsBrowseImageCell.scss │ │ ├── OrderStatus │ │ │ └── OrderStatusList.scss │ │ ├── CollectionResults │ │ │ ├── skeleton.js │ │ │ ├── CollectionResultsList.scss │ │ │ ├── CollectionResultsListItem.scss │ │ │ └── CollectionResultsBody.scss │ │ ├── CollectionDetails │ │ │ ├── CollectionDetailsMinimap.scss │ │ │ ├── skeleton.js │ │ │ ├── RelatedUrlsModal.scss │ │ │ ├── CollectionDetailsFeatureGroup.js │ │ │ └── CollectionDetailsHeader.scss │ │ ├── Panels │ │ │ ├── PanelGroupFooter.scss │ │ │ ├── PanelGroup.scss │ │ │ ├── PanelSection.scss │ │ │ ├── PanelGroupHeaderMeta.js │ │ │ ├── PanelGroupFooter.js │ │ │ └── PanelSection.js │ │ ├── TooManyPointsModal │ │ │ └── TooManyPointsModal.scss │ │ ├── EDSCAlert │ │ │ └── EDSCAlert.scss │ │ ├── SearchPanels │ │ │ └── SearchPanels.scss │ │ ├── ProjectPanels │ │ │ ├── VariableDetailsPanel.scss │ │ │ └── ProjectPanels.scss │ │ ├── OverrideTemporalModal │ │ │ └── OverrideTemporalModal.scss │ │ ├── EDSCTable │ │ │ ├── skeleton.js │ │ │ ├── EDSCTableCell.scss │ │ │ └── EDSCTableCell.js │ │ ├── MoreActionsDropdown │ │ │ └── MoreActionsDropdownItem.scss │ │ ├── Well │ │ │ ├── WellMain.js │ │ │ ├── WellHeading.js │ │ │ ├── WellFooter.js │ │ │ ├── WellSection.js │ │ │ ├── WellIntroduction.js │ │ │ └── Well.scss │ │ ├── AdminPage │ │ │ └── AdminPage.scss │ │ ├── FilterStack │ │ │ ├── FilterStack.scss │ │ │ └── FilterStack.js │ │ ├── EDSCModal │ │ │ ├── EDSCModalOverlay.scss │ │ │ └── EDSCModalOverlay.js │ │ ├── Dropzone │ │ │ ├── ShapefileDropzone.scss │ │ │ └── ShapefileDropzone.js │ │ ├── ContactInfo │ │ │ └── ContactInfo.scss │ │ ├── ProgressRing │ │ │ └── ProgressRing.scss │ │ ├── AccessMethod │ │ │ ├── EchoForm.scss │ │ │ └── AccessMethod.scss │ │ ├── SpatialDisplay │ │ │ ├── SpatialDisplayEntry.js │ │ │ └── __tests__ │ │ │ │ └── SpatialDisplayEntry.test.js │ │ ├── DownloadHistory │ │ │ └── DownloadHistory.scss │ │ ├── CollectionDownloadDisplay │ │ │ └── CollectionDownloadDisplay.scss │ │ ├── SplitBadge │ │ │ └── SplitBadge.scss │ │ ├── Sidebar │ │ │ ├── SidebarSection.scss │ │ │ ├── Sidebar.scss │ │ │ └── SidebarSection.js │ │ ├── ProjectCollections │ │ │ ├── ProjectCollectionsList.scss │ │ │ └── ProjectCollections.scss │ │ ├── EDSCTabs │ │ │ └── EDSCTabs.scss │ │ ├── CollectionDetailsHighlights │ │ │ └── skeleton.js │ │ ├── FormFields │ │ │ └── Radio │ │ │ │ └── Radio.scss │ │ └── AdminRetrievals │ │ │ └── __tests__ │ │ │ └── AdminRetrievalForm.test.js │ ├── actions │ │ ├── advancedSearch.js │ │ ├── map.js │ │ ├── browser.js │ │ ├── __tests__ │ │ │ └── advancedSearch.test.js │ │ ├── panels.js │ │ └── admin │ │ │ └── isAuthorized.js │ ├── selectors │ │ ├── focusedGranule.js │ │ ├── focusedCollection.js │ │ ├── __tests__ │ │ │ ├── granuleMetadata.test.js │ │ │ └── collectionMetadata.test.js │ │ ├── granuleMetadata.js │ │ └── collectionMetadata.js │ ├── reducers │ │ ├── authToken.js │ │ ├── providers.js │ │ ├── portals.js │ │ ├── admin │ │ │ ├── isAuthorized.js │ │ │ └── __tests__ │ │ │ │ └── isAuthorized.test.js │ │ ├── browser.js │ │ ├── focusedGranule.js │ │ ├── dataQualitySummaries.js │ │ ├── focusedCollection.js │ │ ├── errors.js │ │ ├── contactInfo.js │ │ └── __tests__ │ │ │ ├── authToken.test.js │ │ │ └── browser.test.js │ ├── containers │ │ ├── MapContainer │ │ │ └── MapContainer.scss │ │ └── AppLogoContainer │ │ │ └── AppLogoContainer.js │ ├── middleware │ │ └── metrics │ │ │ └── constants.js │ └── events │ │ └── events.js │ ├── css │ ├── utils │ │ ├── mixins │ │ │ └── mixins.scss │ │ ├── variables │ │ │ ├── variables.scss │ │ │ └── _shadows.scss │ │ └── utils.scss │ ├── vendor │ │ ├── simplebar.scss │ │ ├── bootstrap │ │ │ ├── overrides │ │ │ │ ├── _alert.scss │ │ │ │ └── _forms.scss │ │ │ ├── _bootstrap.scss │ │ │ ├── _utils.scss │ │ │ ├── _vars.scss │ │ │ └── _components.scss │ │ └── datepicker.scss │ ├── modules │ │ ├── modules.scss │ │ ├── _tooltip.scss │ │ ├── _tables.scss │ │ ├── _links.scss │ │ └── _dropdown.scss │ ├── globalUtils.js │ ├── main.scss │ └── _helpers.scss │ ├── public │ ├── favicon.ico │ ├── images │ │ └── browsers │ │ │ ├── browser_ie.gif │ │ │ ├── browser_opera.gif │ │ │ ├── browser_chrome.gif │ │ │ ├── browser_firefox.gif │ │ │ └── browser_safari.gif │ └── robots.txt │ ├── assets │ ├── fonts │ │ ├── edsc.eot │ │ ├── edsc.ttf │ │ └── edsc.woff │ └── images │ │ ├── drag.png │ │ ├── app-logo.png │ │ ├── app-logo_2x.png │ │ ├── app-logo_hover.png │ │ ├── app-logo_hover_2x.png │ │ ├── blue-bars-circle.png │ │ ├── projection-sprite.png │ │ ├── orange-bars-circle.png │ │ ├── projection-sprite@2x.png │ │ ├── plate_carree_earth_scaled.png │ │ └── plate_carree_earth_scaled@2x.png │ └── partials │ └── analytics.html ├── .travis └── travis.enc ├── portals ├── amd │ ├── images │ │ └── amd-logo.jpg │ ├── styles.scss │ └── config.json ├── idn │ ├── images │ │ └── logo-idn.png │ ├── styles.scss │ └── config.json ├── cwic │ ├── images │ │ └── ceos-logo.png │ ├── styles.scss │ └── config.json ├── soos │ ├── images │ │ └── soos-logo.png │ ├── styles.scss │ └── config.json ├── podaac │ ├── images │ │ └── podaac-logo.png │ ├── styles.scss │ └── config.json ├── simple │ └── config.json ├── carve │ ├── images │ │ ├── ornl-daac-logo-mono.png │ │ └── ornl-daac-logo-color.png │ ├── scripts.js │ ├── styles.scss │ └── config.json ├── airmoss │ ├── images │ │ ├── ornl-daac-logo-mono.png │ │ └── ornl-daac-logo-color.png │ ├── scripts.js │ ├── styles.scss │ └── config.json ├── ornldaac │ ├── images │ │ ├── ornl-daac-logo-color.png │ │ └── ornl-daac-logo-mono.png │ ├── scripts.js │ ├── styles.scss │ └── config.json ├── suborbital │ ├── images │ │ ├── suborbital-icon-grey-clear.png │ │ └── suborbital-icon-white-clear.png │ ├── scripts.js │ ├── styles.scss │ └── config.json ├── above │ └── config.json ├── complex │ └── config.json ├── standardproducts │ └── config.json └── default │ └── config.json ├── serverless └── src │ ├── getRetrieval │ └── __tests__ │ │ └── mocks.js │ ├── util │ ├── echoForms │ │ ├── namespaces.js │ │ ├── __tests__ │ │ │ ├── getEmail.test.js │ │ │ ├── getBoundingBox.test.js │ │ │ ├── getSwitchFields.test.js │ │ │ ├── getNameValuePairsForResample.test.js │ │ │ ├── getNameValuePairsForProjections.test.js │ │ │ ├── getTopLevelFields.test.js │ │ │ ├── getSubsetDataLayers.test.js │ │ │ └── getFieldElementValue.test.js │ │ ├── getEmail.js │ │ ├── findFieldElement.js │ │ ├── getFieldElementValue.js │ │ ├── getSwitchFields.js │ │ └── getNameValuePairsForResample.js │ ├── getUsernameFromToken.js │ ├── aws │ │ ├── getSqsConfig.js │ │ ├── getStepFunctionsConfig.js │ │ ├── getSecretsManagerConfig.js │ │ └── getLambdaConfig.js │ ├── obfuscation │ │ ├── obfuscateId.js │ │ └── deobfuscateId.js │ ├── __tests__ │ │ ├── generateFormDigest.test.js │ │ ├── getUsernameFromToken.test.js │ │ ├── getVerifiedJwtToken.test.js │ │ ├── getJwtToken.test.js │ │ ├── createJwtToken.test.js │ │ └── pick.test.js │ ├── getJwtToken.js │ ├── generateFormDigest.js │ ├── cmr │ │ ├── __tests__ │ │ │ └── prepareExposeHeaders.test.js │ │ ├── prepareExposeHeaders.js │ │ └── cmrUrl.js │ ├── getVerifiedJwtToken.js │ ├── requestTimeout.js │ ├── urs │ │ ├── getEchoToken.js │ │ ├── getUserAccessToken.js │ │ └── getAccessTokenFromJwtToken.js │ ├── database │ │ └── getDbConnection.js │ ├── createLimitedShapefile.js │ ├── createJwtToken.js │ ├── chunkArray.js │ └── pick.js │ ├── submitHarmonyOrder │ ├── __tests__ │ │ ├── bboxToPolygon.test.js │ │ └── ccwShapefile.test.js │ └── bboxToPolygon.js │ ├── decodeId │ └── handler.js │ ├── adminIsAuthorized │ └── handler.js │ ├── getAccessMethods │ ├── supportsVariableSubsetting.js │ ├── supportsShapefileSubsetting.js │ └── supportsBoundingBoxSubsetting.js │ └── relevancyLogger │ └── handler.js ├── cypress ├── integration │ ├── retrievals │ │ └── download_mocks │ │ │ ├── timeline.json │ │ │ └── retrievals.json │ └── utils │ │ ├── __tests__ │ │ └── getAuthHeaders.test.js │ │ └── getAuthHeaders.js ├── fixtures │ └── example.json ├── support │ ├── getByTestId.js │ ├── getJwtToken.js │ └── index.js └── plugins │ └── index.js ├── webpack.config.prod.js ├── migrations ├── 1585836600012_clear-site-preferences.js ├── 1562705069972_rename-order-search-params.js ├── 1564488272729_add-environment-to-users.js ├── 1563990176916_alter-retrieval-order-number.js ├── 1565375842170_add-order-information-to-orders.js ├── 1565744336991_add-default-to-order-state.js ├── 1598036919806_add-parent-shapefile-id-selected-features-to-shapefiles.js ├── 1564489093225_remove-environment-default-from-users.js ├── 1568146028532_change-users-not-null.js ├── 1559966487087_projects.js ├── 1556745445952_colormaps.js └── 1563477325342_access-configurations.js ├── cypress.json ├── .gitignore ├── sharedUtils ├── cmrEnv.js ├── limitedCollectionSize.js ├── prepareGranuleAccessParams.js ├── portalPath.js ├── outputFormatMaps.js └── prepKeysForCmr.js ├── schemas └── sitePreferencesUISchema.json ├── font-awesome.config.js ├── bin ├── travis-copy-secrets ├── cypress-prepare-travis └── ecc-sync ├── .github └── ISSUE_TEMPLATE │ ├── custom.md │ └── feature_request.md ├── serverless-infrastructure.yml ├── babel.config.js ├── jest.config.js ├── postcss.config.js ├── .travis.yml ├── secret.config.json.example └── static.webpack.config.dev.js /.eslintignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.16.3 2 | -------------------------------------------------------------------------------- /static/src/js/util/bootstrap.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/src/js/components/Map/ProjectionSwitcher.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/src/css/utils/mixins/mixins.scss: -------------------------------------------------------------------------------- 1 | @import "carat"; 2 | -------------------------------------------------------------------------------- /static/src/js/util/polyfill/index.js: -------------------------------------------------------------------------------- 1 | import './matches' 2 | -------------------------------------------------------------------------------- /static/src/js/util/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub' 2 | -------------------------------------------------------------------------------- /static/src/css/utils/variables/variables.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | @import "shadows"; 3 | -------------------------------------------------------------------------------- /static/src/css/utils/utils.scss: -------------------------------------------------------------------------------- 1 | @import "variables/variables"; 2 | @import "mixins/mixins"; 3 | -------------------------------------------------------------------------------- /static/src/js/components/Timeline/Timeline.scss: -------------------------------------------------------------------------------- 1 | .timeline-tooltip { 2 | color: white; 3 | } 4 | -------------------------------------------------------------------------------- /.travis/travis.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/.travis/travis.enc -------------------------------------------------------------------------------- /static/src/js/components/AdvancedSearchModal/AdvancedSearchModal.scss: -------------------------------------------------------------------------------- 1 | .advanced-search-modal { 2 | } 3 | -------------------------------------------------------------------------------- /static/src/css/vendor/simplebar.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * SimpleBar styles 3 | */ 4 | 5 | @import "~simplebar/dist/simplebar"; 6 | -------------------------------------------------------------------------------- /static/src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/public/favicon.ico -------------------------------------------------------------------------------- /portals/amd/images/amd-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/amd/images/amd-logo.jpg -------------------------------------------------------------------------------- /portals/idn/images/logo-idn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/idn/images/logo-idn.png -------------------------------------------------------------------------------- /static/src/js/components/Facets/FacetsModal.scss: -------------------------------------------------------------------------------- 1 | .facets-modal { 2 | &__hits { 3 | font-weight: 700; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /portals/cwic/images/ceos-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/cwic/images/ceos-logo.png -------------------------------------------------------------------------------- /portals/soos/images/soos-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/soos/images/soos-logo.png -------------------------------------------------------------------------------- /static/src/assets/fonts/edsc.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/fonts/edsc.eot -------------------------------------------------------------------------------- /static/src/assets/fonts/edsc.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/fonts/edsc.ttf -------------------------------------------------------------------------------- /static/src/assets/fonts/edsc.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/fonts/edsc.woff -------------------------------------------------------------------------------- /static/src/assets/images/drag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/drag.png -------------------------------------------------------------------------------- /portals/soos/styles.scss: -------------------------------------------------------------------------------- 1 | #SOOS-logo { 2 | height: 40px; 3 | width: 95px; 4 | background-image: url('./images/soos-logo.png'); 5 | } -------------------------------------------------------------------------------- /serverless/src/getRetrieval/__tests__/mocks.js: -------------------------------------------------------------------------------- 1 | export const retrievalPayload = { 2 | pathParameters: { 3 | id: 2 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /static/src/js/components/Preferences/PreferencesForm.scss: -------------------------------------------------------------------------------- 1 | .preferences-form { 2 | &__submit { 3 | width: 5rem; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /portals/amd/styles.scss: -------------------------------------------------------------------------------- 1 | #amd-logo { 2 | height: 40px; 3 | width: 57px; 4 | background-image: url('./images/amd-logo.jpg'); 5 | } 6 | -------------------------------------------------------------------------------- /portals/cwic/styles.scss: -------------------------------------------------------------------------------- 1 | #ceos-logo { 2 | height: 40px; 3 | width: 95px; 4 | background-image: url('./images/ceos-logo.png'); 5 | } 6 | -------------------------------------------------------------------------------- /portals/idn/styles.scss: -------------------------------------------------------------------------------- 1 | #idn-logo { 2 | height: 40px; 3 | width: 101px; 4 | background-image: url('./images/logo-idn.png'); 5 | } 6 | -------------------------------------------------------------------------------- /portals/podaac/images/podaac-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/podaac/images/podaac-logo.png -------------------------------------------------------------------------------- /portals/simple/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "parentConfig": "edsc", 3 | "query": { 4 | "echoCollectionId": "C203234523-LAADS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /static/src/assets/images/app-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/app-logo.png -------------------------------------------------------------------------------- /portals/podaac/styles.scss: -------------------------------------------------------------------------------- 1 | #podaac-logo { 2 | height: 40px; 3 | width: 40px; 4 | background-image: url('./images/podaac-logo.png'); 5 | } 6 | -------------------------------------------------------------------------------- /static/src/assets/images/app-logo_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/app-logo_2x.png -------------------------------------------------------------------------------- /portals/carve/images/ornl-daac-logo-mono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/carve/images/ornl-daac-logo-mono.png -------------------------------------------------------------------------------- /static/src/assets/images/app-logo_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/app-logo_hover.png -------------------------------------------------------------------------------- /static/src/js/components/CustomToggle/MoreActionsToggle.scss: -------------------------------------------------------------------------------- 1 | .more-actions-toggle { 2 | color: $color__black--400; 3 | font-size: 1.125rem; 4 | } 5 | -------------------------------------------------------------------------------- /portals/airmoss/images/ornl-daac-logo-mono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/airmoss/images/ornl-daac-logo-mono.png -------------------------------------------------------------------------------- /portals/carve/images/ornl-daac-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/carve/images/ornl-daac-logo-color.png -------------------------------------------------------------------------------- /static/src/assets/images/app-logo_hover_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/app-logo_hover_2x.png -------------------------------------------------------------------------------- /static/src/assets/images/blue-bars-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/blue-bars-circle.png -------------------------------------------------------------------------------- /static/src/assets/images/projection-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/projection-sprite.png -------------------------------------------------------------------------------- /static/src/js/components/GranuleFilters/GranuleFiltersList.scss: -------------------------------------------------------------------------------- 1 | .granule-filters-list { 2 | margin: 0; 3 | padding: 0; 4 | list-style: none; 5 | } 6 | -------------------------------------------------------------------------------- /static/src/js/components/OrderDropdownList/OrderDropdownList.scss: -------------------------------------------------------------------------------- 1 | .order-dropdown-list { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | -------------------------------------------------------------------------------- /portals/airmoss/images/ornl-daac-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/airmoss/images/ornl-daac-logo-color.png -------------------------------------------------------------------------------- /portals/ornldaac/images/ornl-daac-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/ornldaac/images/ornl-daac-logo-color.png -------------------------------------------------------------------------------- /portals/ornldaac/images/ornl-daac-logo-mono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/ornldaac/images/ornl-daac-logo-mono.png -------------------------------------------------------------------------------- /static/src/assets/images/orange-bars-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/orange-bars-circle.png -------------------------------------------------------------------------------- /static/src/assets/images/projection-sprite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/projection-sprite@2x.png -------------------------------------------------------------------------------- /static/src/css/vendor/bootstrap/overrides/_alert.scss: -------------------------------------------------------------------------------- 1 | .alert { 2 | border: 0; 3 | 4 | &-light { 5 | background-color: $color__theme--light; 6 | } 7 | } -------------------------------------------------------------------------------- /static/src/js/components/GranuleFilters/GranuleFiltersActions.scss: -------------------------------------------------------------------------------- 1 | .granule-filters-actions { 2 | &__action { 3 | margin-right: $spacer/2; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /static/src/public/images/browsers/browser_ie.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/public/images/browsers/browser_ie.gif -------------------------------------------------------------------------------- /cypress/integration/retrievals/download_mocks/timeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": [{"concept-id":"C1443528505-LAADS","intervals":[[1515943738,1579132800,113372]]}] 3 | } 4 | -------------------------------------------------------------------------------- /static/src/js/components/ButtonDropdown/ButtonDropdown.scss: -------------------------------------------------------------------------------- 1 | .button-dropdown { 2 | &__toggle { 3 | &:after { 4 | display: none; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /static/src/js/components/Facets/FacetsSectionHeading.scss: -------------------------------------------------------------------------------- 1 | .facets-section-heading { 2 | padding: $spacer; 3 | font-size: 1.25rem; 4 | font-weight: 700; 5 | } 6 | -------------------------------------------------------------------------------- /static/src/public/images/browsers/browser_opera.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/public/images/browsers/browser_opera.gif -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const ProdStaticWebpackConfig = require('./static.webpack.config.dev.js') 2 | 3 | module.exports = [ 4 | ProdStaticWebpackConfig 5 | ] 6 | -------------------------------------------------------------------------------- /portals/airmoss/scripts.js: -------------------------------------------------------------------------------- 1 | import logo from './images/ornl-daac-logo-color.png' 2 | 3 | // Pre-load logo hover image 4 | const image = new Image() 5 | image.src = logo 6 | -------------------------------------------------------------------------------- /portals/carve/scripts.js: -------------------------------------------------------------------------------- 1 | import logo from './images/ornl-daac-logo-color.png' 2 | 3 | // Pre-load logo hover image 4 | const image = new Image() 5 | image.src = logo 6 | -------------------------------------------------------------------------------- /portals/ornldaac/scripts.js: -------------------------------------------------------------------------------- 1 | import logo from './images/ornl-daac-logo-color.png' 2 | 3 | // Pre-load logo hover image 4 | const image = new Image() 5 | image.src = logo 6 | -------------------------------------------------------------------------------- /static/src/assets/images/plate_carree_earth_scaled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/plate_carree_earth_scaled.png -------------------------------------------------------------------------------- /static/src/public/images/browsers/browser_chrome.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/public/images/browsers/browser_chrome.gif -------------------------------------------------------------------------------- /static/src/public/images/browsers/browser_firefox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/public/images/browsers/browser_firefox.gif -------------------------------------------------------------------------------- /static/src/public/images/browsers/browser_safari.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/public/images/browsers/browser_safari.gif -------------------------------------------------------------------------------- /portals/suborbital/images/suborbital-icon-grey-clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/suborbital/images/suborbital-icon-grey-clear.png -------------------------------------------------------------------------------- /portals/suborbital/images/suborbital-icon-white-clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/portals/suborbital/images/suborbital-icon-white-clear.png -------------------------------------------------------------------------------- /portals/suborbital/scripts.js: -------------------------------------------------------------------------------- 1 | import logo from './images/suborbital-icon-grey-clear.png' 2 | 3 | // Pre-load logo hover image 4 | const image = new Image() 5 | image.src = logo 6 | -------------------------------------------------------------------------------- /static/src/assets/images/plate_carree_earth_scaled@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/earthdata-search/master/static/src/assets/images/plate_carree_earth_scaled@2x.png -------------------------------------------------------------------------------- /migrations/1585836600012_clear-site-preferences.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.db.query('UPDATE users SET site_preferences=\'{}\';') 5 | } 6 | -------------------------------------------------------------------------------- /static/src/js/components/AdvancedSearchDisplay/AdvancedSearchDisplay.scss: -------------------------------------------------------------------------------- 1 | .advanced-search-display { 2 | &__text { 3 | font-weight: 300; 4 | font-style: italic; 5 | } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /static/src/js/components/Facets/Facets.scss: -------------------------------------------------------------------------------- 1 | .facets { 2 | display: block; 3 | width: 100%; 4 | margin: 0; 5 | padding: 0; 6 | list-style: none; 7 | color: $color__white; 8 | } 9 | -------------------------------------------------------------------------------- /static/src/js/util/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history' 2 | 3 | // Get the browser history 4 | const history = createBrowserHistory() 5 | 6 | export default history 7 | -------------------------------------------------------------------------------- /static/src/js/util/map/projections.js: -------------------------------------------------------------------------------- 1 | const projections = { 2 | arctic: 'epsg3413', 3 | geographic: 'epsg4326', 4 | antarctic: 'epsg3031' 5 | } 6 | 7 | export default projections 8 | -------------------------------------------------------------------------------- /static/src/css/modules/modules.scss: -------------------------------------------------------------------------------- 1 | @import 'dropdown'; 2 | @import 'edsc-icons'; 3 | @import 'error-page'; 4 | @import 'links'; 5 | @import 'modal'; 6 | @import 'tables'; 7 | @import 'tooltip'; 8 | -------------------------------------------------------------------------------- /static/src/css/utils/variables/_shadows.scss: -------------------------------------------------------------------------------- 1 | $shadow__default: 0 0 7px rgba(0,0,0,0.1); 2 | $shadow__panel: 0 6px 12px rgba(0,0,0,0.175); 3 | $shadow__panel-dark: 0 6px 12px rgba(0,0,0,0.5); 4 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /static/src/js/components/CollapsePanel/CollapsePanel.scss: -------------------------------------------------------------------------------- 1 | .collapse-panel { 2 | &__button { 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /static/src/js/components/TemporalDisplay/TemporalDisplay.scss: -------------------------------------------------------------------------------- 1 | .temporal-display { 2 | color: white; 3 | padding: 10px 15px; 4 | background: rgb(17, 17, 17); 5 | font-size: 1em; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /static/src/js/components/TemporalDisplay/TemporalSelectionDropdown.scss: -------------------------------------------------------------------------------- 1 | .temporal-selection-dropdown { 2 | display: flex; 3 | 4 | .dropdown-toggle:after { 5 | display: none; 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /static/src/css/vendor/datepicker.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * React Datepicker default styles 3 | * 4 | * The default styles supplied for the React Datepicker 5 | */ 6 | 7 | @import "~react-datetime/css/react-datetime"; 8 | -------------------------------------------------------------------------------- /static/src/js/components/AdvancedSearchModal/AdvancedSearchForm.scss: -------------------------------------------------------------------------------- 1 | .advanced-search-form { 2 | &__label { 3 | color: $color__black--500; 4 | font-size: 0.925rem; 5 | font-weight: 700; 6 | } 7 | } -------------------------------------------------------------------------------- /static/src/js/actions/advancedSearch.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_ADVANCED_SEARCH } from '../constants/actionTypes' 2 | 3 | export const updateAdvancedSearch = payload => ({ 4 | type: UPDATE_ADVANCED_SEARCH, 5 | payload 6 | }) 7 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeVersion": "system", 3 | "baseUrl": "http://localhost:8080", 4 | "viewportWidth": 1400, 5 | "viewportHeight": 900, 6 | "testFiles": "**/*_spec.js", 7 | "fixturesFolder": "cypress/fixtures" 8 | } 9 | -------------------------------------------------------------------------------- /cypress/support/getByTestId.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds an element by using the data-test-id attribute 3 | * @param {String} id data-test-id attribute in markup 4 | */ 5 | export const getByTestId = id => cy.get(`[data-test-id=${id}]`) 6 | -------------------------------------------------------------------------------- /static/src/js/components/ChunkedOrderModal/ChunkedOrderModal.scss: -------------------------------------------------------------------------------- 1 | .chunked-order-modal { 2 | &__body-emphasis { 3 | font-style: italic; 4 | } 5 | 6 | &__body-strong { 7 | font-weight: 500; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /static/src/js/components/OrderProgressList/OrderProgressList.scss: -------------------------------------------------------------------------------- 1 | .order-progress-list { 2 | list-style: none; 3 | overflow: hidden; 4 | overflow-y: scroll; 5 | margin: 0; 6 | margin-top: $spacer; 7 | padding: 0; 8 | } -------------------------------------------------------------------------------- /static/src/css/globalUtils.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const resources = [ 4 | 'utils/utils.scss', 5 | 'vendor/bootstrap/_vars.scss' 6 | ] 7 | 8 | module.exports = resources.map(file => path.resolve(__dirname, file)) 9 | -------------------------------------------------------------------------------- /static/src/js/components/GranuleDetails/skeleton.js: -------------------------------------------------------------------------------- 1 | export const granuleTitle = [ 2 | { 3 | shape: 'rectangle', 4 | left: 0, 5 | top: 0, 6 | height: 22, 7 | width: 280, 8 | radius: 2 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /static/src/js/components/GranuleResults/GranuleResultsList.scss: -------------------------------------------------------------------------------- 1 | .granule-results-list { 2 | flex: 1; 3 | width: 100%; 4 | 5 | &__list { 6 | padding: 0 $spacer; 7 | list-style: none; 8 | margin: 0; 9 | } 10 | } -------------------------------------------------------------------------------- /static/src/js/selectors/focusedGranule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieve the id of the focused granule from Redux 3 | * @param {Object} state Current state of Redux 4 | */ 5 | export const getFocusedGranuleId = state => state.focusedGranule 6 | -------------------------------------------------------------------------------- /static/src/js/components/CustomToggle/CustomToggle.scss: -------------------------------------------------------------------------------- 1 | .custom-toggle { 2 | &:after { 3 | display: none; 4 | } 5 | 6 | &--icon { 7 | background-color: transparent; 8 | border: 0; 9 | outline: 0; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /static/src/js/components/OrderStatus/OrderStatusList.scss: -------------------------------------------------------------------------------- 1 | .order-status-list { 2 | margin-bottom: $spacer*2; 3 | 4 | &__list { 5 | position: relative; 6 | margin: 0; 7 | padding: 0; 8 | list-style: none; 9 | } 10 | } -------------------------------------------------------------------------------- /static/src/js/selectors/focusedCollection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieve the id of the focused collection from Redux 3 | * @param {Object} state Current state of Redux 4 | */ 5 | export const getFocusedCollectionId = state => state.focusedCollection 6 | -------------------------------------------------------------------------------- /static/src/js/util/variables.js: -------------------------------------------------------------------------------- 1 | export const allVariablesSelected = (variableIds, selectedVariableIds) => ( 2 | variableIds.every(variableId => selectedVariableIds.indexOf(variableId) > -1) 3 | ) 4 | 5 | export default allVariablesSelected 6 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionResults/skeleton.js: -------------------------------------------------------------------------------- 1 | export const collectionResultsTotal = [ 2 | { 3 | shape: 'rectangle', 4 | left: 0, 5 | top: 3, 6 | height: 12, 7 | width: 213, 8 | radius: 2 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/namespaces.js: -------------------------------------------------------------------------------- 1 | // Namespaces used for ECHO Forms 2 | export const namespaces = { 3 | xmlns: 'http://echo.nasa.gov/v9/echoforms', 4 | ecs: 'http://ecs.nasa.gov/options', 5 | info: 'http://eosdis.nasa.gov/esi/info' 6 | } 7 | -------------------------------------------------------------------------------- /static/src/js/components/GranuleDetails/GranuleDetailsMetadata.scss: -------------------------------------------------------------------------------- 1 | .granule-details-metadata { 2 | &__heading { 3 | font-size: 0.875rem; 4 | font-weight: 300; 5 | } 6 | &__list { 7 | padding: 0; 8 | list-style: none; 9 | } 10 | } -------------------------------------------------------------------------------- /portals/airmoss/styles.scss: -------------------------------------------------------------------------------- 1 | #ornl-daac-logo { 2 | height: 40px; 3 | width: 53px; 4 | background-image: url('./images/ornl-daac-logo-mono.png'); 5 | 6 | &:hover { 7 | background-image: url('./images/ornl-daac-logo-color.png'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /portals/carve/styles.scss: -------------------------------------------------------------------------------- 1 | #ornl-daac-logo { 2 | height: 40px; 3 | width: 53px; 4 | background-image: url('./images/ornl-daac-logo-mono.png'); 5 | 6 | &:hover { 7 | background-image: url('./images/ornl-daac-logo-color.png'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /portals/ornldaac/styles.scss: -------------------------------------------------------------------------------- 1 | #ornl-daac-logo { 2 | height: 40px; 3 | width: 53px; 4 | background-image: url('./images/ornl-daac-logo-mono.png'); 5 | 6 | &:hover { 7 | background-image: url('./images/ornl-daac-logo-color.png'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project build files 2 | .DS_Store 3 | .serverless 4 | coverage 5 | dist 6 | node_modules 7 | secret.config.json 8 | tmp 9 | 10 | # VSCode Settings 11 | .vscode 12 | 13 | cypress/screenshots 14 | cypress/videos 15 | overrideStatic.config.json 16 | -------------------------------------------------------------------------------- /static/src/js/util/propTypes/location.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | 3 | export const locationPropType = PropTypes.shape({ 4 | pathname: PropTypes.string, 5 | search: PropTypes.string, 6 | hash: PropTypes.string, 7 | key: PropTypes.string 8 | }) 9 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionDetails/CollectionDetailsMinimap.scss: -------------------------------------------------------------------------------- 1 | .collection-details-minimap { 2 | &.leaflet-container { 3 | width: 360px; 4 | height: 180px; 5 | } 6 | 7 | .leaflet-interactive { 8 | cursor: default; 9 | } 10 | } -------------------------------------------------------------------------------- /portals/above/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "org": "ABoVE", 3 | "pageTitle": "ABoVE", 4 | "parentConfig": "edsc", 5 | "query": { 6 | "project": "ABoVE" 7 | }, 8 | "ui": { 9 | "showOnlyGranulesCheckbox": false, 10 | "showNonEosdisCheckbox": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /portals/suborbital/styles.scss: -------------------------------------------------------------------------------- 1 | #suborbital-logo { 2 | height: 40px; 3 | width: 40px; 4 | background-image: url('./images/suborbital-icon-grey-clear.png'); 5 | 6 | &:hover { 7 | background-image: url('./images/suborbital-icon-white-clear.png'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /static/src/js/actions/map.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_MAP } from '../constants/actionTypes' 2 | 3 | export const updateMap = payload => ({ 4 | type: UPDATE_MAP, 5 | payload 6 | }) 7 | 8 | export const changeMap = map => (dispatch) => { 9 | dispatch(updateMap(map)) 10 | } 11 | -------------------------------------------------------------------------------- /static/src/js/components/Panels/PanelGroupFooter.scss: -------------------------------------------------------------------------------- 1 | .panel-group-footer { 2 | position: relative; 3 | justify-self: flex-end; 4 | flex-grow: 0; 5 | margin-top: auto; 6 | border-top: 1px solid $border-color; 7 | z-index: 1; 8 | background: $color__white; 9 | } 10 | -------------------------------------------------------------------------------- /static/src/js/util/scienceKeywordTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Listing of the science keyword types 3 | */ 4 | export const scienceKeywordTypes = [ 5 | 'topic', 6 | 'term', 7 | 'variable_level_1', 8 | 'variable_level_2', 9 | 'variable_level_3', 10 | 'detailed_variable' 11 | ] 12 | -------------------------------------------------------------------------------- /static/src/js/components/GranuleFilters/GranuleFiltersItem.scss: -------------------------------------------------------------------------------- 1 | .granule-filters-item { 2 | margin-bottom: $spacer*1.25; 3 | 4 | &__heading { 5 | font-size: 1rem; 6 | font-weight: 700; 7 | } 8 | 9 | &__description { 10 | margin-bottom: $spacer/2; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /static/src/js/components/TooManyPointsModal/TooManyPointsModal.scss: -------------------------------------------------------------------------------- 1 | .too-many-points-modal { 2 | &__actions { 3 | display: flex; 4 | justify-content: flex-start; 5 | margin-top: $spacer*2; 6 | } 7 | 8 | &__action { 9 | margin-right: $spacer/2; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionResults/CollectionResultsList.scss: -------------------------------------------------------------------------------- 1 | .collection-results-list { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | right: 0; 6 | left: 0; 7 | 8 | &__list { 9 | list-style: none; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /static/src/js/components/EDSCAlert/EDSCAlert.scss: -------------------------------------------------------------------------------- 1 | .edsc-alert { 2 | &--small { 3 | font-size: 0.75rem; 4 | } 5 | 6 | &--icon { 7 | display: flex; 8 | align-items: center; 9 | } 10 | 11 | &__icon { 12 | font-size: 0.825rem; 13 | margin-right: 0.75rem; 14 | } 15 | } -------------------------------------------------------------------------------- /static/src/js/components/Facets/FacetsList.scss: -------------------------------------------------------------------------------- 1 | .facets-list { 2 | margin: 0; 3 | padding: 0; 4 | list-style: none; 5 | 6 | &--light { 7 | .facets-item__label { 8 | &:hover { 9 | background-color: lighten($color__black--200, 5); 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionResults/CollectionResultsListItem.scss: -------------------------------------------------------------------------------- 1 | .collection-results-list-item { 2 | &--loading { 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | padding: $spacer 0; 7 | text-align: center; 8 | color: $color__black--600; 9 | } 10 | } -------------------------------------------------------------------------------- /static/src/js/util/isNumber.js: -------------------------------------------------------------------------------- 1 | const reg = new RegExp(/^\d+$/) 2 | 3 | /** 4 | * Returns true the string contains only number characters and false if there are any non-number characters 5 | * @return {boolean} 6 | */ 7 | export const isNumber = string => reg.test(string) 8 | 9 | export default isNumber 10 | -------------------------------------------------------------------------------- /cypress/integration/utils/__tests__/getAuthHeaders.test.js: -------------------------------------------------------------------------------- 1 | import { getAuthHeaders } from '../getAuthHeaders' 2 | 3 | describe('getAuthHeaders', () => { 4 | test('builds an object containing standard auth headers', () => { 5 | expect(Object.keys(getAuthHeaders())) 6 | .toEqual(['jwt-token']) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /portals/complex/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "logo": { 3 | "image": "http://placehold.it/75x40", 4 | "link": "https://example.com/logo" 5 | }, 6 | "pageTitle": "Complex", 7 | "parentConfig": "edsc", 8 | "query": { 9 | "echoCollectionId": "C203234523-LAADS" 10 | }, 11 | "title": "Complex" 12 | } 13 | -------------------------------------------------------------------------------- /static/src/js/actions/browser.js: -------------------------------------------------------------------------------- 1 | import browser from 'browser-detect' 2 | 3 | import { UPDATE_BROWSER_VERSION } from '../constants/actionTypes' 4 | 5 | export const updateBrowserVersion = () => ({ 6 | type: UPDATE_BROWSER_VERSION, 7 | payload: browser() 8 | }) 9 | 10 | export default updateBrowserVersion 11 | -------------------------------------------------------------------------------- /static/src/js/util/polyfill/matches.js: -------------------------------------------------------------------------------- 1 | /** 2 | * IE matches polyfill 3 | * https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill 4 | */ 5 | if (!Element.prototype.matches) { 6 | Element.prototype.matches = Element.prototype.msMatchesSelector 7 | || Element.prototype.webkitMatchesSelector 8 | } 9 | -------------------------------------------------------------------------------- /sharedUtils/cmrEnv.js: -------------------------------------------------------------------------------- 1 | import { getApplicationConfig } from './config' 2 | 3 | /** 4 | * Return the CMR environment to use 5 | */ 6 | export const cmrEnv = () => { 7 | const { env } = getApplicationConfig() 8 | 9 | if (env === 'dev') return 'prod' 10 | 11 | return env 12 | } 13 | 14 | export default cmrEnv 15 | -------------------------------------------------------------------------------- /static/src/js/util/isBrowserCompatible.js: -------------------------------------------------------------------------------- 1 | import browser from 'browser-detect' 2 | 3 | export const isBrowserCompatible = () => { 4 | const { 5 | name, 6 | versionNumber 7 | } = browser() 8 | 9 | if (name === 'ie' && versionNumber <= 10) { 10 | return false 11 | } 12 | 13 | return true 14 | } 15 | -------------------------------------------------------------------------------- /migrations/1562705069972_rename-order-search-params.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.renameColumn('retrieval_orders', 'search_params', 'granule_params') 5 | } 6 | 7 | exports.down = (pgm) => { 8 | pgm.renameColumn('retrieval_orders', 'granule_params', 'search_params') 9 | } 10 | -------------------------------------------------------------------------------- /static/src/js/components/Preferences/Preferences.scss: -------------------------------------------------------------------------------- 1 | .preferences { 2 | border-radius: 5px; 3 | padding: 30px; 4 | border-color: #e3e3e3; 5 | color: #333333; 6 | 7 | &__loading { 8 | min-height: 10rem; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /static/src/js/components/SearchPanels/SearchPanels.scss: -------------------------------------------------------------------------------- 1 | .search-panels { 2 | &__portal-escape { 3 | color: #424242; 4 | margin: 0; 5 | padding: 0.5rem 1rem; 6 | text-align: center; 7 | font-size: 0.875rem; 8 | } 9 | 10 | &__portal-escape-link { 11 | text-decoration: underline; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /schemas/sitePreferencesUISchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "panelState": { 3 | "ui:widget": "radio", 4 | "ui:field": "radio" 5 | }, 6 | "collectionListView": { 7 | "ui:widget": "radio", 8 | "ui:field": "radio" 9 | }, 10 | "granuleListView": { 11 | "ui:widget": "radio", 12 | "ui:field": "radio" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /serverless/src/util/getUsernameFromToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Strips the username out of the URS token endpoint 3 | * @param {Object} token URS token object 4 | */ 5 | export const getUsernameFromToken = (token) => { 6 | const { endpoint } = token 7 | const username = endpoint.split('/').pop() 8 | 9 | return username 10 | } 11 | -------------------------------------------------------------------------------- /static/src/js/components/AdvancedSearchModal/RegionSearch.scss: -------------------------------------------------------------------------------- 1 | .region-search { 2 | &__selected-region-id { 3 | margin-right: $spacer/2; 4 | font-weight: 700; 5 | } 6 | 7 | &__selected-region-name { 8 | margin-right: $spacer/2; 9 | font-style: italic; 10 | color: $color__black--400; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /static/src/js/components/GranuleDetails/GranuleDetailsInfo.scss: -------------------------------------------------------------------------------- 1 | .granule-details-info { 2 | &__content { 3 | white-space: pre-wrap; 4 | font-family: monospace; 5 | } 6 | 7 | &__spinner { 8 | position: absolute; 9 | top: 50%; 10 | left: 50%; 11 | transform: translate(-50%, -50%); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /static/src/js/components/GranuleFilters/GranuleFiltersHeader.scss: -------------------------------------------------------------------------------- 1 | .granule-filters-header { 2 | &__primary { 3 | margin-left: $spacer/2; 4 | display: inline-block; 5 | font-size: 1rem; 6 | } 7 | 8 | &__secondary { 9 | display: block; 10 | font-size: 1.125rem; 11 | font-weight: 700; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /static/src/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | User-Agent: * 5 | Disallow: /downloads 6 | Disallow: /projects 7 | Disallow: /search/granules 8 | Disallow: /contact_info 9 | -------------------------------------------------------------------------------- /migrations/1564488272729_add-environment-to-users.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.addColumns('users', { 5 | environment: { type: 'varchar(1000)', notNull: true, default: 'prod' } 6 | }) 7 | } 8 | 9 | exports.down = (pgm) => { 10 | pgm.dropColumns('users', ['environment']) 11 | } 12 | -------------------------------------------------------------------------------- /serverless/src/util/aws/getSqsConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns an environment specific configuration object for SQS 3 | * @return {Object} A configuration object for SQS 4 | */ 5 | export const getSqsConfig = () => { 6 | const productionConfig = { 7 | apiVersion: '2012-11-05' 8 | } 9 | 10 | return productionConfig 11 | } 12 | -------------------------------------------------------------------------------- /static/src/css/modules/_tooltip.scss: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | font-size: .75rem; 3 | 4 | &--large { 5 | font-size: 0.625rem; 6 | } 7 | 8 | &--ta-left { 9 | .tooltip-inner { 10 | text-align: left; 11 | } 12 | } 13 | 14 | &--wide { 15 | .tooltip-inner { 16 | max-width: 400px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /static/src/js/components/GranuleResults/GranuleResultsDataLinksButton.scss: -------------------------------------------------------------------------------- 1 | .granule-results-data-links-button { 2 | &__dropdown-item { 3 | color: $color__black--800; 4 | font-size: 0.875rem; 5 | 6 | &:hover { 7 | background-color: $color__blue--dark; 8 | color: $color__black--200; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /serverless/src/util/obfuscation/obfuscateId.js: -------------------------------------------------------------------------------- 1 | import ScatterSwap from 'scatter-swap' 2 | 3 | /** 4 | * Obfuscate a database ID to hide its identity 5 | * @param {Integer} value The value to be obfuscated 6 | */ 7 | export const obfuscateId = (value, spin = process.env.obfuscationSpin) => ( 8 | new ScatterSwap(value, spin).hash() 9 | ) 10 | -------------------------------------------------------------------------------- /static/src/css/vendor/bootstrap/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Bootstrap utils 3 | */ 4 | 5 | @import "utils"; 6 | 7 | /* 8 | * Bootstrap components 9 | */ 10 | 11 | @import "components"; 12 | 13 | /* 14 | * Bootstrap overrides 15 | */ 16 | 17 | @import "overrides/alert"; 18 | @import "overrides/dropdown"; 19 | @import "overrides/forms"; 20 | -------------------------------------------------------------------------------- /static/src/js/util/getFilenameFromPath.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Given a filepath, returns the filename and extension 3 | * @param {String} path A filepath 4 | * @returns {String} A filename with extension 5 | */ 6 | export const getFilenameFromPath = (path) => { 7 | if (!path) return '' 8 | return path.substring(path.lastIndexOf('/') + 1) 9 | } 10 | -------------------------------------------------------------------------------- /static/src/css/modules/_tables.scss: -------------------------------------------------------------------------------- 1 | .table { 2 | thead { 3 | th { 4 | font-size: 0.825rem; 5 | color: $color__black--500; 6 | border-bottom: 0; 7 | border-top: 0; 8 | } 9 | } 10 | 11 | td { 12 | vertical-align: middle; 13 | } 14 | 15 | td, 16 | th { 17 | padding: .625rem 1rem; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /static/src/js/components/GranuleDetails/GranuleDetailsHeader.scss: -------------------------------------------------------------------------------- 1 | .granule-details-header { 2 | &__primary { 3 | padding: 0.5rem 1rem; 4 | } 5 | 6 | &__title { 7 | display: inline; 8 | margin-right: 0.75rem; 9 | font-weight: 700; 10 | font-size: 1rem; 11 | vertical-align: middle; 12 | word-break: break-all; 13 | } 14 | } -------------------------------------------------------------------------------- /static/src/js/components/ProjectPanels/VariableDetailsPanel.scss: -------------------------------------------------------------------------------- 1 | .variable-details-panel { 2 | &__heading { 3 | margin: 0; 4 | font-size: 1.5rem; 5 | font-weight: 700; 6 | } 7 | 8 | &__longname { 9 | margin-bottom: $spacer; 10 | font-style: italic; 11 | color: $color__black--500; 12 | font-weight: 400; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cypress/integration/utils/getAuthHeaders.js: -------------------------------------------------------------------------------- 1 | import { getEnvironmentConfig } from '../../../sharedUtils/config' 2 | 3 | /** 4 | * Returns common authentication headers for integration tests 5 | */ 6 | export const getAuthHeaders = () => { 7 | const { jwtToken } = getEnvironmentConfig('test') 8 | return { 9 | 'jwt-token': jwtToken 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /migrations/1563990176916_alter-retrieval-order-number.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.alterColumn('retrieval_orders', 'order_number', { 5 | type: 'varchar(1000)' 6 | }) 7 | } 8 | 9 | exports.down = (pgm) => { 10 | pgm.alterColumn('retrieval_orders', 'order_number', { 11 | type: 'integer' 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /static/src/js/components/Preferences/PreferencesRadioField.scss: -------------------------------------------------------------------------------- 1 | .preferences-radio-field { 2 | &__radio { 3 | border: none; 4 | padding: none; 5 | } 6 | 7 | &__name { 8 | font-weight: bold; 9 | } 10 | 11 | &__description { 12 | margin-bottom: $spacer/2; 13 | color: $color__black--400; 14 | font-size: 0.875rem; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /static/src/js/util/url/stringEncoders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Decodes a string parameter (returns the same value) 3 | * @param {string} string 4 | */ 5 | export const decodeString = string => string 6 | 7 | 8 | /** 9 | * Encodes a string parameter (returns the same value) 10 | * @param {string} string 11 | */ 12 | export const encodeString = string => string || '' 13 | -------------------------------------------------------------------------------- /portals/standardproducts/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "org": "Standard Products", 3 | "pageTitle": "Standard Products", 4 | "parentConfig": "edsc", 5 | "query": { 6 | "tagKey": "gov.nasa.eosdis.standardproduct", 7 | "hasGranulesOrCwic": null 8 | }, 9 | "ui": { 10 | "showOnlyGranulesCheckbox": false, 11 | "showNonEosdisCheckbox": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /static/src/css/main.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Vendor styles 3 | */ 4 | @import 'vendor/bootstrap/bootstrap'; 5 | @import 'vendor/datepicker'; 6 | @import 'vendor/leaflet'; 7 | @import 'vendor/simplebar'; 8 | 9 | /* 10 | * Application styles 11 | */ 12 | @import 'utils/utils'; 13 | @import 'base'; 14 | @import 'helpers'; 15 | @import 'layout'; 16 | @import 'modules/modules'; 17 | -------------------------------------------------------------------------------- /serverless/src/util/aws/getStepFunctionsConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns an environment specific configuration object for Step Functions 3 | * @return {Object} A configuration object for Step Functions 4 | */ 5 | export const getStepFunctionsConfig = () => { 6 | const productionConfig = { 7 | apiVersion: '2016-11-23' 8 | } 9 | 10 | return productionConfig 11 | } 12 | -------------------------------------------------------------------------------- /serverless/src/util/aws/getSecretsManagerConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns an environment specific configuration object for Secrets Manager 3 | * @return {Object} A configuration object for Secrets Manager 4 | */ 5 | export const getSecretsManagerConfig = () => { 6 | const productionConfig = { 7 | apiVersion: '2017-10-17' 8 | } 9 | 10 | return productionConfig 11 | } 12 | -------------------------------------------------------------------------------- /serverless/src/util/obfuscation/deobfuscateId.js: -------------------------------------------------------------------------------- 1 | import ScatterSwap from 'scatter-swap' 2 | 3 | /** 4 | * Deobfuscate a database ID determine it's original value 5 | * @param {Integer} value The value to deobfuscate 6 | */ 7 | export const deobfuscateId = (value, spin = process.env.obfuscationSpin) => ( 8 | parseInt(new ScatterSwap(value, spin).reverseHash(), 10) 9 | ) 10 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionDetails/skeleton.js: -------------------------------------------------------------------------------- 1 | export const collectionTitleSkeleton = [ 2 | { 3 | shape: 'rectangle', 4 | left: 0, 5 | top: 0, 6 | height: 22, 7 | width: 280, 8 | radius: 2 9 | }, 10 | { 11 | shape: 'rectangle', 12 | left: 290, 13 | top: 2, 14 | height: 18, 15 | width: 65, 16 | radius: 2 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /serverless/src/submitHarmonyOrder/__tests__/bboxToPolygon.test.js: -------------------------------------------------------------------------------- 1 | import { bboxToPolygon } from '../bboxToPolygon' 2 | 3 | describe('bboxToPolygon', () => { 4 | test('returns a CW polygon', () => { 5 | expect(bboxToPolygon([[5, 0], [15, 10]])).toEqual([[ 6 | [15, 10], 7 | [5, 10], 8 | [5, 0], 9 | [15, 0], 10 | [15, 10] 11 | ]]) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /serverless/src/util/__tests__/generateFormDigest.test.js: -------------------------------------------------------------------------------- 1 | import { generateFormDigest } from '../generateFormDigest' 2 | 3 | describe('generateFormDigest', () => { 4 | test('retrieves the username from the endpoint field', () => { 5 | const form = 'mock echo form data' 6 | 7 | expect(generateFormDigest(form)).toEqual('e60e33c401915211e731cbcf993ecfe1102a4a22') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /static/src/js/reducers/authToken.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_AUTH } from '../constants/actionTypes' 2 | 3 | const initialState = '' 4 | 5 | const authTokenReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case UPDATE_AUTH: { 8 | return action.payload 9 | } 10 | default: 11 | return state 12 | } 13 | } 14 | 15 | export default authTokenReducer 16 | -------------------------------------------------------------------------------- /serverless/src/util/getJwtToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the JWT Token from our custom authorizer context 3 | * @param {Object} event Details about the HTTP request that it received 4 | */ 5 | export const getJwtToken = (event) => { 6 | const { requestContext = {} } = event 7 | const { authorizer = {} } = requestContext 8 | const { jwtToken } = authorizer 9 | 10 | return jwtToken 11 | } 12 | -------------------------------------------------------------------------------- /static/src/js/components/OverrideTemporalModal/OverrideTemporalModal.scss: -------------------------------------------------------------------------------- 1 | .choice { 2 | width: 50%; 3 | text-align: center; 4 | padding: 0.5em 0; 5 | 6 | button { 7 | display: inline-block; 8 | } 9 | } 10 | 11 | .choice-1 { 12 | float: left; 13 | } 14 | 15 | .choice-2 { 16 | float: right; 17 | } 18 | 19 | .override-temporal-modal__footer-actions { 20 | width: 100%; 21 | } 22 | -------------------------------------------------------------------------------- /font-awesome.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | styleLoader: 'style-loader!css-loader!sass-loader', 3 | styles: { 4 | mixins: true, 5 | 'bordered-pulled': true, 6 | core: true, 7 | 'fixed-width': true, 8 | icons: true, 9 | larger: true, 10 | list: true, 11 | path: true, 12 | 'rotated-flipped': true, 13 | animated: true, 14 | stacked: true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /serverless/src/util/__tests__/getUsernameFromToken.test.js: -------------------------------------------------------------------------------- 1 | import { getUsernameFromToken } from '../getUsernameFromToken' 2 | 3 | describe('getUsernameFromToken', () => { 4 | test('retrieves the username from the endpoint field', () => { 5 | const token = { 6 | endpoint: '/api/users/testuser' 7 | } 8 | 9 | expect(getUsernameFromToken(token)).toEqual('testuser') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /static/src/css/_helpers.scss: -------------------------------------------------------------------------------- 1 | .visually-hidden { 2 | position: absolute !important; 3 | height: 1px; width: 1px; 4 | overflow: hidden; 5 | clip: rect(1px 1px 1px 1px); 6 | clip: rect(1px, 1px, 1px, 1px); 7 | } 8 | 9 | .visually-hidden a:focus, 10 | .visually-hidden input:focus, 11 | .visually-hidden button:focus { 12 | position:static; 13 | width:auto; height:auto; 14 | } 15 | -------------------------------------------------------------------------------- /static/src/js/util/facetCategoryAbbreviationsMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Maps the CMR Autocomplete types to their abbriviations 3 | */ 4 | export const facetCategoryAbbreviationsMap = { 5 | organization: 'Org.', 6 | instrument: 'Inst.', 7 | granule_data_format: 'Gran. Fmt.', 8 | platform: 'Plat.', 9 | processing_level_id: 'Proc. Lvl.', 10 | project: 'Proj.', 11 | science_keywords: 'Kwd.' 12 | } 13 | -------------------------------------------------------------------------------- /static/src/js/util/object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lookup a object key given a value 3 | * @param {string} object JavaScript Object with key-value pairs 4 | * @param {string} value A value in the object 5 | * @return {string} A key in the object 6 | */ 7 | const getObjectKeyByValue = (object, value) => Object.keys(object) 8 | .find(key => object[key] === value) 9 | 10 | export default getObjectKeyByValue 11 | -------------------------------------------------------------------------------- /migrations/1565375842170_add-order-information-to-orders.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.addColumns('retrieval_orders', { 5 | order_information: { 6 | type: 'jsonb', 7 | notNull: true, 8 | default: '{}' 9 | } 10 | }) 11 | } 12 | 13 | exports.down = (pgm) => { 14 | pgm.dropColumns('retrieval_orders', ['order_information']) 15 | } 16 | -------------------------------------------------------------------------------- /static/src/css/vendor/bootstrap/overrides/_forms.scss: -------------------------------------------------------------------------------- 1 | .form-control-basic { 2 | width: auto; 3 | height: auto; 4 | margin: 0; 5 | padding: 0; 6 | border: none; 7 | border-radius: 0; 8 | color: inherit; 9 | background-color: transparent; 10 | background-clip: initial; 11 | 12 | &.is-invalid { 13 | padding: 0; 14 | background-image: none; 15 | border: none; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /static/src/js/reducers/providers.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_PROVIDERS 3 | } from '../constants/actionTypes' 4 | 5 | const initialState = [] 6 | 7 | const providersReducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case SET_PROVIDERS: { 10 | return action.payload 11 | } 12 | default: 13 | return state 14 | } 15 | } 16 | 17 | export default providersReducer 18 | -------------------------------------------------------------------------------- /static/src/js/components/EDSCTable/skeleton.js: -------------------------------------------------------------------------------- 1 | export const rowContentLarge = [ 2 | { 3 | shape: 'rectangle', 4 | left: 0, 5 | top: 3, 6 | height: 12, 7 | width: 213, 8 | radius: 2 9 | } 10 | ] 11 | 12 | export const rowContentSmall = [ 13 | { 14 | shape: 'rectangle', 15 | left: 0, 16 | top: 3, 17 | height: 12, 18 | width: 80, 19 | radius: 2 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /static/src/js/reducers/portals.js: -------------------------------------------------------------------------------- 1 | import { ADD_PORTAL } from '../constants/actionTypes' 2 | 3 | const initialState = { 4 | portalId: '' 5 | } 6 | 7 | const portalsReducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case ADD_PORTAL: { 10 | return action.payload 11 | } 12 | default: 13 | return state 14 | } 15 | } 16 | 17 | export default portalsReducer 18 | -------------------------------------------------------------------------------- /bin/travis-copy-secrets: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp secret.config.json.example secret.config.json 4 | 5 | # read in static.config.json 6 | config="`cat static.config.json`" 7 | 8 | # update keys for deployment 9 | config="`jq '.application.ciMode = $newValue' --arg newValue true <<< $config`" 10 | 11 | # overwrite static.config.json with new values 12 | echo $config > tmp.$$.json && mv tmp.$$.json static.config.json 13 | -------------------------------------------------------------------------------- /static/src/js/components/MoreActionsDropdown/MoreActionsDropdownItem.scss: -------------------------------------------------------------------------------- 1 | .more-actions-dropdown-item { 2 | padding: $spacer/4 $spacer/2; 3 | font-size: 0.75rem; 4 | 5 | &__icon { 6 | width: 1rem; 7 | margin-right: $spacer/4; 8 | text-align: center; 9 | color: $color__black--300; 10 | 11 | .more-actions-dropdown-item:hover & { 12 | color: $color__black--400; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /static/src/js/components/Well/WellMain.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export const WellMain = ({ 5 | children 6 | }) => ( 7 |
8 | {children} 9 |
10 | ) 11 | 12 | WellMain.defaultProps = { 13 | children: null 14 | } 15 | 16 | WellMain.propTypes = { 17 | children: PropTypes.node 18 | } 19 | 20 | export default WellMain 21 | -------------------------------------------------------------------------------- /static/src/js/components/AdminPage/AdminPage.scss: -------------------------------------------------------------------------------- 1 | .admin-page { 2 | &__header { 3 | margin-bottom: 1.25rem; 4 | border-bottom: 1px solid $color__black--200; 5 | } 6 | 7 | &__breadcrumbs { 8 | background: none; 9 | padding: 0; 10 | padding-bottom: 0.5rem; 11 | margin-bottom: 1.5rem; 12 | } 13 | 14 | &__page-title { 15 | font-size: 1.5em; 16 | margin-bottom: 0.5rem; 17 | } 18 | } -------------------------------------------------------------------------------- /static/src/js/components/Well/WellHeading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export const WellSection = ({ children }) => ( 5 |

6 | {children} 7 |

8 | ) 9 | 10 | WellSection.defaultProps = { 11 | children: null 12 | } 13 | 14 | WellSection.propTypes = { 15 | children: PropTypes.node 16 | } 17 | 18 | export default WellSection 19 | -------------------------------------------------------------------------------- /static/src/js/components/FilterStack/FilterStack.scss: -------------------------------------------------------------------------------- 1 | .filter-stack { 2 | height: 0; 3 | width: 0; 4 | margin: 0; 5 | padding: 0; 6 | list-style: none; 7 | background-color: $color__black--900; 8 | border-top: 1px solid $color__black--700; 9 | color: $color__white; 10 | visibility: hidden; 11 | 12 | &--is-open { 13 | height: auto; 14 | width: 100%; 15 | visibility: visible; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cypress/integration/retrievals/download_mocks/retrievals.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": {"id":"0684513294","user_id":20,"environment":"prod","jsondata":{"source":"?p=!C1443528505-LAADS&sb=-77.15071678161621%2C38.78817179999825%2C-76.89801406860352%2C38.99784152603538&m=38.379638671875!-77.1767578125!7!1!0!0%2C2&qt=2020-01-06T04%3A15%3A27.310Z%2C2020-01-13T07%3A32%3A50.962Z&ff=Map%20Imagery&tl=1563377338!4!!","portal_id":"","shapefile_id":""}} 3 | } 4 | -------------------------------------------------------------------------------- /portals/idn/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hasStyles": true, 3 | "logo": { 4 | "id": "idn-logo", 5 | "link": "https://idn.ceos.org/", 6 | "title": "CEOS IDN Search" 7 | }, 8 | "org": "IDN", 9 | "pageTitle": "IDN", 10 | "parentConfig": "edsc", 11 | "query": { 12 | "hasGranulesOrCwic": null 13 | }, 14 | "ui": { 15 | "showOnlyGranulesCheckbox": false, 16 | "showNonEosdisCheckbox": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /static/src/css/vendor/bootstrap/_utils.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Bootstrap Utils 3 | */ 4 | 5 | @import "~bootstrap/scss/_utilities"; 6 | @import "~bootstrap/scss/_reboot"; 7 | @import "~bootstrap/scss/_print"; 8 | @import "~bootstrap/scss/_grid"; 9 | @import "~bootstrap/scss/_images"; 10 | @import "~bootstrap/scss/_media"; 11 | @import "~bootstrap/scss/_type"; 12 | @import "~bootstrap/scss/_forms"; 13 | @import "~bootstrap/scss/_buttons"; 14 | -------------------------------------------------------------------------------- /static/src/js/components/EDSCModal/EDSCModalOverlay.scss: -------------------------------------------------------------------------------- 1 | .edsc-modal-overlay { 2 | display: flex; 3 | flex-direction: column; 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | padding: 1rem; 10 | background-color: #fff; 11 | z-index: 1; 12 | overflow: scroll; 13 | 14 | &__content { 15 | flex-grow: 1; 16 | } 17 | 18 | &__heading { 19 | flex-grow: 0; 20 | } 21 | } -------------------------------------------------------------------------------- /static/src/js/util/request/accessMethodsRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | export default class AccessMethodsRequest extends Request { 5 | constructor(authToken) { 6 | super(getEnvironmentConfig().apiHost) 7 | this.authenticated = true 8 | this.authToken = authToken 9 | this.searchPath = 'access_methods' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /static/src/js/util/url/integerEncoders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Decodes an integer parameter (returns the same value as an integer) 3 | * @param {Integer} integer 4 | */ 5 | export const decodeInteger = integer => parseInt(integer, 10) || undefined 6 | 7 | 8 | /** 9 | * Encodes an integer parameter (returns the same value as a string) 10 | * @param {Integer} integer 11 | */ 12 | export const encodeInteger = integer => integer || '' 13 | -------------------------------------------------------------------------------- /migrations/1565744336991_add-default-to-order-state.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.db.query('ALTER TABLE retrieval_orders ALTER COLUMN state SET DEFAULT \'creating\';') 5 | pgm.db.query('UPDATE retrieval_orders SET state=\'creating\' WHERE state IS NULL;') 6 | } 7 | 8 | exports.down = (pgm) => { 9 | pgm.db.query('ALTER TABLE retrieval_orders ALTER COLUMN state DROP DEFAULT;') 10 | } 11 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/__tests__/getEmail.test.js: -------------------------------------------------------------------------------- 1 | import { echoFormWithEmail } from './mocks' 2 | import { getEmail } from '../getEmail' 3 | 4 | describe('util#getEmail', () => { 5 | test('correctly discovers the correct fields from the provided xml', () => { 6 | const email = getEmail(echoFormWithEmail) 7 | 8 | expect(email).toEqual({ 9 | EMAIL: 'edsc-support@earthdata.nasa.gov' 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /static/src/js/components/Well/WellFooter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export const WellFooter = ({ 5 | children 6 | }) => ( 7 | 10 | ) 11 | 12 | WellFooter.defaultProps = { 13 | children: null 14 | } 15 | 16 | WellFooter.propTypes = { 17 | children: PropTypes.node 18 | } 19 | 20 | export default WellFooter 21 | -------------------------------------------------------------------------------- /static/src/js/components/Well/WellSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export const WellSection = ({ 5 | children 6 | }) => ( 7 |
8 | {children} 9 |
10 | ) 11 | 12 | WellSection.defaultProps = { 13 | children: null 14 | } 15 | 16 | WellSection.propTypes = { 17 | children: PropTypes.node 18 | } 19 | 20 | export default WellSection 21 | -------------------------------------------------------------------------------- /static/src/css/vendor/bootstrap/_vars.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Bootstrap Variables 3 | * 4 | * The default bootstrap variables are loaded in via the 'sass-resources-loader' 5 | */ 6 | 7 | // Override default Bootstrap variables 8 | @import "overrides"; 9 | 10 | // Import default Bootstrap functions, variables, and mixins 11 | @import "~bootstrap/scss/_functions"; 12 | @import "~bootstrap/scss/_variables"; 13 | @import "~bootstrap/scss/_mixins"; 14 | -------------------------------------------------------------------------------- /static/src/js/util/autocompleteFacetsMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Maps the CMR Autocomplete types to CMR Facet types 3 | */ 4 | export const autocompleteFacetsMap = { 5 | organization: 'data_center_h', 6 | instrument: 'instrument_h', 7 | granule_data_format: 'granule_data_format_h', 8 | platform: 'platform_h', 9 | processing_level_id: 'processing_level_id_h', 10 | project: 'project_h', 11 | science_keywords: 'science_keywords_h' 12 | } 13 | -------------------------------------------------------------------------------- /static/src/js/util/convertRemsToPixels.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Given the a number in rems, returns that number in pixels relative to the current 3 | * body font size. 4 | * @param {Number} rem The number in rems. 5 | * @returns {Number} The number in pixels. 6 | */ 7 | export const convertRemsToPixels = (rem) => { 8 | if (!window || !document) return 16 9 | return rem * parseFloat(getComputedStyle(document.documentElement).fontSize) 10 | } 11 | -------------------------------------------------------------------------------- /static/src/js/reducers/admin/isAuthorized.js: -------------------------------------------------------------------------------- 1 | import { SET_ADMIN_IS_AUTHORIZED } from '../../constants/actionTypes' 2 | 3 | const initialState = false 4 | 5 | const adminIsAuthorizedReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case SET_ADMIN_IS_AUTHORIZED: { 8 | return action.payload 9 | } 10 | default: 11 | return state 12 | } 13 | } 14 | 15 | export default adminIsAuthorizedReducer 16 | -------------------------------------------------------------------------------- /static/src/js/util/__tests__/pathStartsWith.test.js: -------------------------------------------------------------------------------- 1 | import { pathStartsWith } from '../pathStartsWith' 2 | 3 | describe('pathStartsWith', () => { 4 | test('returns true when a path is matched', () => { 5 | expect(pathStartsWith('/test/path', '/test')).toEqual(true) 6 | }) 7 | 8 | test('returns false with a path that does not match', () => { 9 | expect(pathStartsWith('/test/path', '/wrongtest')).toEqual(false) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /static/src/js/util/buildPromise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds a promise object with a resolve method to manually create/return promises 3 | * @param {Object} response The value to return when the promise is resolved 4 | * @see {@link https://stackoverflow.com/questions/27715275/whats-the-difference-between-returning-value-or-promise-resolve-from-then|Stack Overflow} 5 | */ 6 | export const buildPromise = response => new Promise(resolve => resolve(response)) 7 | -------------------------------------------------------------------------------- /static/src/js/util/request/providerRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | export default class ProviderRequest extends Request { 5 | constructor(authToken) { 6 | super(getEnvironmentConfig().apiHost) 7 | this.authenticated = true 8 | this.authToken = authToken 9 | } 10 | 11 | all() { 12 | return this.get('providers') 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /static/src/js/util/request/regionRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | export default class RegionRequest extends Request { 5 | constructor() { 6 | super(getEnvironmentConfig().apiHost) 7 | this.lambda = true 8 | this.searchPath = 'regions' 9 | } 10 | 11 | search(params) { 12 | return this.get(this.searchPath, params) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/__tests__/getBoundingBox.test.js: -------------------------------------------------------------------------------- 1 | import { echoFormXml } from './mocks' 2 | import { getBoundingBox } from '../getBoundingBox' 3 | 4 | describe('util#getBoundingBox', () => { 5 | test('correctly discovers the correct fields from the provided xml', () => { 6 | const boundingBox = getBoundingBox(echoFormXml) 7 | 8 | expect(boundingBox).toEqual({ 9 | BBOX: '-180,-90,180,90' 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /static/src/js/util/map/actions/panBoundsToCenter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pans the Leaflet bounds to the center with padding. 3 | * @param {Object} map - An instance of the Leaflet map. 4 | * @param {Object} bounds - The bounds of the area to be centered. 5 | */ 6 | export const panBoundsToCenter = (map, bounds) => { 7 | if (bounds.isValid && bounds.isValid()) { 8 | map.fitBounds(bounds, { padding: [200, 200] }).panTo(bounds.getCenter()) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/__tests__/getSwitchFields.test.js: -------------------------------------------------------------------------------- 1 | import { echoFormXml } from './mocks' 2 | import { getSwitchFields } from '../getSwitchFields' 3 | 4 | describe('util#getSwitchFields', () => { 5 | test('correctly discovers the correct fields from the provided xml', () => { 6 | const topLevelFields = getSwitchFields(echoFormXml) 7 | 8 | expect(topLevelFields).toEqual({ 9 | INCLUDE_META: 'N' 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /serverless/src/util/generateFormDigest.js: -------------------------------------------------------------------------------- 1 | import forge from 'node-forge' 2 | 3 | /** 4 | * Generates a SHA1 digest of the echoform, used to determine if the saved 5 | * EchoForm data is up to date with the current EchoForm 6 | * @param {String} form EchoForm string 7 | */ 8 | export const generateFormDigest = (form) => { 9 | const formDigest = forge.md.sha1.create() 10 | formDigest.update(form) 11 | 12 | return formDigest.digest().toHex() 13 | } 14 | -------------------------------------------------------------------------------- /static/src/js/components/Dropzone/ShapefileDropzone.scss: -------------------------------------------------------------------------------- 1 | .shapefile-dropzone { 2 | display: none; 3 | z-index: 5000; 4 | 5 | &.dropzone--is-active { 6 | display: block; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | right: 0; 11 | bottom: 0; 12 | background-color: transparent; 13 | } 14 | 15 | // Hides the default text inside the dropzone container 16 | .dz-default { 17 | @include invisible(hidden) 18 | } 19 | } -------------------------------------------------------------------------------- /static/src/js/components/Well/WellIntroduction.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export const WellIntroduction = ({ 5 | children 6 | }) => ( 7 |
8 | {children} 9 |
10 | ) 11 | 12 | WellIntroduction.defaultProps = { 13 | children: null 14 | } 15 | 16 | WellIntroduction.propTypes = { 17 | children: PropTypes.node 18 | } 19 | 20 | export default WellIntroduction 21 | -------------------------------------------------------------------------------- /static/src/js/util/__tests__/isNumber.test.js: -------------------------------------------------------------------------------- 1 | import { isNumber } from '../isNumber' 2 | 3 | describe('isNumber', () => { 4 | describe('when provided a non number string', () => { 5 | test('returns false', () => { 6 | expect(isNumber('test')).toEqual(false) 7 | }) 8 | }) 9 | 10 | describe('when provided a number string', () => { 11 | test('returns true', () => { 12 | expect(isNumber('100')).toEqual(true) 13 | }) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /static/src/js/components/ContactInfo/ContactInfo.scss: -------------------------------------------------------------------------------- 1 | .contact-info-form { 2 | border-radius: 5px; 3 | padding: 30px; 4 | border-color: #e3e3e3; 5 | color: #333333; 6 | 7 | &__list { 8 | list-style: none; 9 | padding: 0; 10 | 11 | &__item { 12 | margin: 10px, 0; 13 | display: list-item; 14 | } 15 | } 16 | 17 | &__label { 18 | display: inline-block; 19 | min-width: 200px; 20 | font-weight: bold; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /static/src/js/util/request/preferencesRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | export default class PreferencesRequest extends Request { 5 | constructor(authToken) { 6 | super(getEnvironmentConfig().apiHost) 7 | this.authenticated = true 8 | this.authToken = authToken 9 | } 10 | 11 | update(params) { 12 | return this.post('preferences', params) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /static/src/js/util/request/dataQualitySummaryRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | export default class DataQualitySummaryRequest extends Request { 5 | constructor(authToken) { 6 | super(getEnvironmentConfig().apiHost) 7 | this.authenticated = true 8 | this.authToken = authToken 9 | } 10 | 11 | fetch(params) { 12 | return this.post('dqs', params) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /static/src/js/reducers/browser.js: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATE_BROWSER_VERSION 3 | } from '../constants/actionTypes' 4 | 5 | const initialState = {} 6 | 7 | const browserReducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case UPDATE_BROWSER_VERSION: { 10 | const { payload } = action 11 | return { 12 | ...payload 13 | } 14 | } 15 | default: 16 | return state 17 | } 18 | } 19 | 20 | export default browserReducer 21 | -------------------------------------------------------------------------------- /sharedUtils/limitedCollectionSize.js: -------------------------------------------------------------------------------- 1 | import { getValueForTag } from './tags' 2 | 3 | /** 4 | * Retrieve the value stored within the limited collection tag for a provided collection 5 | * @param {Object} collectionMetadata CMR Collection metadata 6 | */ 7 | export const limitedCollectionSize = (collectionMetadata) => { 8 | const { tags } = collectionMetadata 9 | 10 | const { limit } = getValueForTag('limited_collections', tags, 'edsc') || {} 11 | 12 | return limit 13 | } 14 | -------------------------------------------------------------------------------- /static/src/js/components/ProgressRing/ProgressRing.scss: -------------------------------------------------------------------------------- 1 | .progress-ring { 2 | &__ring { 3 | position: relative; 4 | } 5 | 6 | &__progress { 7 | z-index: 1; 8 | position: relative; 9 | stroke: $color__green; 10 | transition: stroke-dashoffset 0.35s; 11 | transform: rotate(-90deg); 12 | transform-origin: 50% 50%, 13 | } 14 | 15 | &__circle-back { 16 | z-index: 0; 17 | position: relative; 18 | stroke: $color__black--200; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /portals/podaac/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hasStyles": true, 3 | "logo": { 4 | "id": "podaac-logo", 5 | "link": "https://podaac.jpl.nasa.gov", 6 | "title": "PO.DAAC Home" 7 | }, 8 | "org": "PO.DAAC", 9 | "pageTitle": "PO.DAAC", 10 | "parentConfig": "edsc", 11 | "query": { 12 | "dataCenter": "NASA/JPL/PODAAC", 13 | "hasGranulesOrCwic": null 14 | }, 15 | "ui": { 16 | "showOnlyGranulesCheckbox": false, 17 | "showNonEosdisCheckbox": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/getEmail.js: -------------------------------------------------------------------------------- 1 | import { getFieldElementValue } from './getFieldElementValue' 2 | 3 | /** 4 | * Get the email address from the provided XML Document 5 | * @param {String} xmlDocument ECHO Form xml as a string 6 | */ 7 | export const getEmail = (xmlDocument) => { 8 | const xmlElement = getFieldElementValue(xmlDocument, 'email') 9 | 10 | if (xmlElement.length) { 11 | return { 12 | EMAIL: xmlElement 13 | } 14 | } 15 | 16 | return {} 17 | } 18 | -------------------------------------------------------------------------------- /static/src/js/components/AccessMethod/EchoForm.scss: -------------------------------------------------------------------------------- 1 | #root { 2 | .tree-item__parent-button { 3 | &.btn { 4 | height: 1.5rem; 5 | } 6 | 7 | svg { 8 | vertical-align: initial; 9 | } 10 | } 11 | } 12 | 13 | .echoform { 14 | .card { 15 | background-color: transparent; 16 | } 17 | 18 | & > .card { 19 | border: 0; 20 | background-color: transparent; 21 | } 22 | 23 | .form__body { 24 | border: 0; 25 | padding: 0; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /static/src/js/util/request/retrievalCollectionRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | export default class RetrievalCollectionRequest extends Request { 5 | constructor(authToken) { 6 | super(getEnvironmentConfig().apiHost) 7 | this.authenticated = true 8 | this.authToken = authToken 9 | } 10 | 11 | fetch(id) { 12 | return this.get(`/retrieval_collections/${id}`) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Template for general tasks and improvements 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please review [Contribution.md](https://github.com/nasa/earthdata-search/blob/master/CONTRIBUTING.md) before contributing to this project. 11 | 12 | ### Description 13 | Describe the purpose of the issue 14 | 15 | ### Files 16 | Attach any relevant images or files 17 | 18 | ### Acceptance Criteria 19 | -------------------------------------------------------------------------------- /migrations/1598036919806_add-parent-shapefile-id-selected-features-to-shapefiles.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.addColumns('shapefiles', { 5 | parent_shapefile_id: { 6 | type: 'integer', 7 | references: 'shapefiles' 8 | }, 9 | selected_features: { 10 | type: 'text []' 11 | } 12 | }) 13 | } 14 | 15 | exports.down = (pgm) => { 16 | pgm.dropColumns('shapefiles', ['parent_shapefile_id', 'selected_features']) 17 | } 18 | -------------------------------------------------------------------------------- /portals/soos/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hasStyles": true, 3 | "logo": { 4 | "id": "SOOS-logo", 5 | "link": "http://www.soos.aq", 6 | "title": "SOOS Metadata Search" 7 | }, 8 | "org": "SOOS", 9 | "pageTitle": "Southern Ocean Observing System", 10 | "parentConfig": "edsc", 11 | "query": { 12 | "tagKey": "aq.soos", 13 | "hasGranulesOrCwic": null 14 | }, 15 | "ui": { 16 | "showOnlyGranulesCheckbox": false, 17 | "showNonEosdisCheckbox": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionDetails/RelatedUrlsModal.scss: -------------------------------------------------------------------------------- 1 | .related-urls-modal { 2 | &__group { 3 | margin-bottom: $spacer*1.5; 4 | } 5 | 6 | &__group-title { 7 | padding-bottom: $spacer/2; 8 | font-size: 1rem; 9 | border-bottom: 1px solid $color__black--200; 10 | } 11 | 12 | &__url { 13 | margin: 0; 14 | margin-bottom: $spacer/2; 15 | padding: 0; 16 | list-style: none; 17 | } 18 | 19 | &__link { 20 | word-break: break-all; 21 | } 22 | } -------------------------------------------------------------------------------- /static/src/js/components/SpatialDisplay/SpatialDisplayEntry.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const SpatialDisplayEntry = (props) => { 5 | const { children } = props 6 | 7 | return ( 8 | <> 9 | {children} 10 | 11 | ) 12 | } 13 | 14 | SpatialDisplayEntry.defaultProps = { 15 | children: null 16 | } 17 | 18 | SpatialDisplayEntry.propTypes = { 19 | children: PropTypes.node 20 | } 21 | 22 | export default SpatialDisplayEntry 23 | -------------------------------------------------------------------------------- /static/src/js/containers/MapContainer/MapContainer.scss: -------------------------------------------------------------------------------- 1 | .map { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | bottom: 0; 6 | right: 0; 7 | height: 100%; 8 | width: 100%; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | flex-basis: auto; 13 | background: #FEFEFE; 14 | color: #DDD; 15 | font-weight: bold; 16 | z-index: 0; 17 | background: #000; 18 | 19 | .is-panels-dragging & { 20 | pointer-events: none; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionDetails/CollectionDetailsFeatureGroup.js: -------------------------------------------------------------------------------- 1 | import { 2 | MapLayer, 3 | withLeaflet 4 | } from 'react-leaflet' 5 | 6 | import { buildLayer } from '../../util/map/layers' 7 | 8 | export class FeatureGroup extends MapLayer { 9 | createLeafletElement({ metadata }) { 10 | const featureGroup = buildLayer({ color: '#54F7A3', fillOpacity: 0.4, weight: 1 }, metadata) 11 | return featureGroup 12 | } 13 | } 14 | 15 | export default withLeaflet(FeatureGroup) 16 | -------------------------------------------------------------------------------- /static/src/js/middleware/metrics/constants.js: -------------------------------------------------------------------------------- 1 | export const METRICS_CLICK = 'METRICS_CLICK' 2 | export const METRICS_COLLECTION_SORT_CHANGE = 'METRICS_COLLECTION_SORT_CHANGE' 3 | export const METRICS_DATA_ACCESS = 'METRICS_DATA_ACCESS' 4 | export const METRICS_INITIAL_RENDER = 'METRICS_INITIAL_RENDER' 5 | export const METRICS_MAP = 'METRICS_MAP' 6 | export const METRICS_SPATIAL_EDIT = 'METRICS_SPATIAL_EDIT' 7 | export const METRICS_TIMELINE = 'METRICS_TIMELINE' 8 | export const METRICS_TIMING = 'METRICS_TIMING' 9 | -------------------------------------------------------------------------------- /static/src/js/util/request/shapefileRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | export default class ShapefileRequest extends Request { 5 | constructor() { 6 | super(getEnvironmentConfig().apiHost) 7 | this.lambda = true 8 | } 9 | 10 | fetch(shapefileId) { 11 | return this.get(`shapefiles/${shapefileId}`) 12 | } 13 | 14 | save(params) { 15 | return this.post('shapefiles', params) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /portals/ornldaac/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hasStyles": true, 3 | "hasScripts": true, 4 | "logo": { 5 | "id": "ornl-daac-logo", 6 | "link": "https://daac.ornl.gov", 7 | "title": "ORNL DAAC Home" 8 | }, 9 | "org": "ORNL DAAC", 10 | "pageTitle": "ORNL DAAC", 11 | "parentConfig": "edsc", 12 | "query": { 13 | "dataCenter": "ORNL_DAAC", 14 | "hasGranulesOrCwic": null 15 | }, 16 | "ui": { 17 | "showOnlyGranulesCheckbox": false, 18 | "showNonEosdisCheckbox": false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /static/src/js/util/focusedGranule.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash' 2 | 3 | /** 4 | * Returns the collection object from the metadata store for the provided granuleId 5 | * @param {String} granuleId Focused collection id 6 | * @param {Object} granules granules from the metadata store 7 | */ 8 | export const getFocusedGranuleObject = (granuleId, granules) => { 9 | if (isEmpty(granules)) return undefined 10 | 11 | return granules[granuleId] || {} 12 | } 13 | 14 | export default getFocusedGranuleObject 15 | -------------------------------------------------------------------------------- /portals/cwic/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hasStyles": true, 3 | "logo": { 4 | "id": "ceos-logo", 5 | "link": "http://ceos.org/ourwork/workinggroups/wgiss/access/cwic/", 6 | "title": "CEOS Home" 7 | }, 8 | "org": "CWIC", 9 | "pageTitle": "CWIC", 10 | "parentConfig": "edsc", 11 | "query": { 12 | "tagKey": "org.ceos.wgiss.cwic.granules.prod", 13 | "hasGranulesOrCwic": null 14 | }, 15 | "ui": { 16 | "showOnlyGranulesCheckbox": false, 17 | "showNonEosdisCheckbox": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /static/src/js/util/isLinkType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true the current collection link matches. 3 | * This expects the the link follows this format: http://www.somelink.com/example/{link type}# 4 | * @param {string} link - The link to be checked. 5 | * @param {string} linkType - The link type to be checked. 6 | * @return {boolean} 7 | */ 8 | export const isLinkType = (link = '', linkType) => { 9 | if (link.indexOf(`${linkType}#`) > -1) return true 10 | return false 11 | } 12 | 13 | export default isLinkType 14 | -------------------------------------------------------------------------------- /static/src/js/util/query.js: -------------------------------------------------------------------------------- 1 | import history from './history' 2 | 3 | export const addQueryParam = (query) => { 4 | const location = Object.assign({}, history.getCurrentLocation()) 5 | 6 | Object.assign(location.query, query) 7 | 8 | history.push(location) 9 | } 10 | 11 | export const removeQueryParam = (...queryKeys) => { 12 | const location = Object.assign({}, history.getCurrentLocation()) 13 | 14 | queryKeys.forEach(queryKey => delete location.query[queryKey]) 15 | 16 | history.push(location) 17 | } 18 | -------------------------------------------------------------------------------- /static/src/js/util/request/logoutRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | /** 5 | * Request object for concept specific requests 6 | */ 7 | export default class LogoutRequest extends Request { 8 | constructor(authToken) { 9 | super(getEnvironmentConfig().apiHost) 10 | 11 | this.authenticated = true 12 | this.authToken = authToken 13 | } 14 | 15 | logout() { 16 | return this.delete('logout') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /static/src/js/util/collectionMetadata/granuleLimit.js: -------------------------------------------------------------------------------- 1 | import { limitedCollectionSize } from '../../../../../sharedUtils/limitedCollectionSize' 2 | import { getApplicationConfig } from '../../../../../sharedUtils/config' 3 | 4 | /** 5 | * Returns the value of the edsc.limited_collections tag or the defaultMaxOrderSize 6 | * @param {Object} metadata Collection metadata 7 | */ 8 | export const getGranuleLimit = (metadata = {}) => ( 9 | limitedCollectionSize(metadata) || getApplicationConfig().defaultMaxOrderSize 10 | ) 11 | -------------------------------------------------------------------------------- /static/src/js/util/map/actions/panFeatureGroupToCenter.js: -------------------------------------------------------------------------------- 1 | import { panBoundsToCenter } from './panBoundsToCenter' 2 | 3 | /** 4 | * Pans the Leaflet map to the center with padding. 5 | * @param {Object} map - An instance of the Leaflet map. 6 | * @param {Object} featureGroup - The feature group to be centered. 7 | */ 8 | export const panFeatureGroupToCenter = (map, featureGroup) => { 9 | if (!featureGroup.getBounds) return 10 | const bounds = featureGroup.getBounds() 11 | panBoundsToCenter(map, bounds) 12 | } 13 | -------------------------------------------------------------------------------- /serverless/src/util/cmr/__tests__/prepareExposeHeaders.test.js: -------------------------------------------------------------------------------- 1 | import { prepareExposeHeaders } from '../prepareExposeHeaders' 2 | 3 | describe('util#prepareExposeHeaders', () => { 4 | test('appends jwt-token to expose headers when they exist', () => { 5 | expect(prepareExposeHeaders({ 'access-control-expose-headers': 'header-1' })).toEqual('header-1, jwt-token') 6 | }) 7 | 8 | test('returns jwt-token when no expose headers exist', () => { 9 | expect(prepareExposeHeaders({})).toEqual('jwt-token') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/__tests__/getNameValuePairsForResample.test.js: -------------------------------------------------------------------------------- 1 | import { loadedEchoFormXml } from './mocks' 2 | import { getNameValuePairsForResample } from '../getNameValuePairsForResample' 3 | 4 | describe('util#getNameValuePairsForResample', () => { 5 | test('correctly discovers the correct fields from the provided xml', () => { 6 | const projectPairs = getNameValuePairsForResample(loadedEchoFormXml) 7 | 8 | expect(projectPairs).toEqual({ 9 | RESAMPLE: 'PERCENT:100' 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /serverless/src/util/getVerifiedJwtToken.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | 3 | import { getSecretEarthdataConfig } from '../../../sharedUtils/config' 4 | import { cmrEnv } from '../../../sharedUtils/cmrEnv' 5 | 6 | /** 7 | * Verifies the JWT Token and returns the contents 8 | * @param {String} jwtToken 9 | */ 10 | export const getVerifiedJwtToken = (jwtToken) => { 11 | const { secret } = getSecretEarthdataConfig(cmrEnv()) 12 | const verifiedJwtToken = jwt.verify(jwtToken, secret) 13 | return verifiedJwtToken 14 | } 15 | -------------------------------------------------------------------------------- /static/src/js/components/Facets/FacetsSectionHeading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import './FacetsSectionHeading.scss' 5 | 6 | export const FacetsSectionHeading = ({ id, letter }) => ( 7 |

11 | {letter} 12 |

13 | ) 14 | 15 | FacetsSectionHeading.propTypes = { 16 | id: PropTypes.string.isRequired, 17 | letter: PropTypes.string.isRequired 18 | } 19 | 20 | export default FacetsSectionHeading 21 | -------------------------------------------------------------------------------- /portals/carve/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hasStyles": true, 3 | "hasScripts": true, 4 | "logo": { 5 | "id": "ornl-daac-logo", 6 | "link": "https://carve.ornl.gov", 7 | "title": "ORNL DAAC CARVE Home" 8 | }, 9 | "org": "ORNL DAAC", 10 | "pageTitle": "CARVE", 11 | "parentConfig": "edsc", 12 | "query": { 13 | "project": "CARVE", 14 | "hasGranulesOrCwic": null 15 | }, 16 | "title": "CARVE", 17 | "ui": { 18 | "showOnlyGranulesCheckbox": false, 19 | "showNonEosdisCheckbox": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /serverless-infrastructure.yml: -------------------------------------------------------------------------------- 1 | service: earthdata-search-infrastructure 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs10.x 6 | stage: ${opt:stage, 'lab'} 7 | region: us-east-1 8 | vpc: 9 | securityGroupIds: 10 | - Ref: LambdaSecurityGroup 11 | subnetIds: 12 | - ${env:SUBNET_ID_A} 13 | - ${env:SUBNET_ID_B} 14 | 15 | role: EDSCServerlessAppRole 16 | 17 | # 18 | # AWS Infrastructure Resources 19 | # 20 | resources: ${file(./serverless-configs/${self:provider.name}-infrastructure-resources.yml)} -------------------------------------------------------------------------------- /static/src/js/util/datepicker.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | const isCustomTime = (momentObj) => { 4 | if (!moment.isMoment(momentObj)) return true 5 | 6 | const selectedDate = moment(momentObj) 7 | const startOfSelectedDate = moment(selectedDate).clone().startOf('day') 8 | const endOfSelectedDate = moment(selectedDate).clone().endOf('day') 9 | 10 | return !selectedDate.isSame(startOfSelectedDate, 'second') 11 | && !selectedDate.isSame(endOfSelectedDate, 'second') 12 | } 13 | 14 | export default isCustomTime 15 | -------------------------------------------------------------------------------- /serverless/src/decodeId/handler.js: -------------------------------------------------------------------------------- 1 | import { deobfuscateId } from '../util/obfuscation/deobfuscateId' 2 | 3 | const decodeId = async (event) => { 4 | const { queryStringParameters } = event 5 | const { 6 | obfuscated_id: obfuscatedId, 7 | spin = process.env.obfuscationSpin 8 | } = queryStringParameters 9 | 10 | return { 11 | isBase64Encoded: false, 12 | statusCode: 200, 13 | body: JSON.stringify({ 14 | id: deobfuscateId(obfuscatedId, spin) 15 | }) 16 | } 17 | } 18 | 19 | export default decodeId 20 | -------------------------------------------------------------------------------- /serverless/src/util/__tests__/getVerifiedJwtToken.test.js: -------------------------------------------------------------------------------- 1 | import { getVerifiedJwtToken } from '../getVerifiedJwtToken' 2 | import * as getEarthdataConfig from '../../../../sharedUtils/config' 3 | 4 | describe('getVerifiedJwtToken', () => { 5 | test('returns the contents of the jwtToken', () => { 6 | const { jwtToken } = getEarthdataConfig.getEnvironmentConfig('test') 7 | 8 | expect(getVerifiedJwtToken(jwtToken)).toEqual({ 9 | id: 1, 10 | username: 'testuser', 11 | iat: 1578433476 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /static/src/js/components/AdvancedSearchDisplay/AdvancedSearchDisplayEntry.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { pure } from 'recompose' 3 | import PropTypes from 'prop-types' 4 | 5 | 6 | const AdvancedSearchDisplayEntry = pure(({ 7 | children 8 | }) => ( 9 | <> 10 | { children } 11 | 12 | )) 13 | 14 | AdvancedSearchDisplayEntry.defaultProps = { 15 | children: null 16 | } 17 | 18 | AdvancedSearchDisplayEntry.propTypes = { 19 | children: PropTypes.node 20 | } 21 | 22 | export default AdvancedSearchDisplayEntry 23 | -------------------------------------------------------------------------------- /static/src/js/components/DownloadHistory/DownloadHistory.scss: -------------------------------------------------------------------------------- 1 | .download-history { 2 | &__spinner { 3 | margin: 0 auto; 4 | } 5 | } 6 | 7 | .order-status-table { 8 | a { 9 | color: $color__blue; 10 | 11 | &:hover, 12 | &:active { 13 | color: $color__blue--dark; 14 | } 15 | } 16 | 17 | &__ago { 18 | white-space: nowrap; 19 | } 20 | 21 | &__remove { 22 | color: $color__black--300; 23 | 24 | &:hover, 25 | &:active { 26 | color: $color__black--600; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /static/src/js/components/Panels/PanelGroup.scss: -------------------------------------------------------------------------------- 1 | .panel-group { 2 | position: absolute; 3 | top: 0; 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | width: 100%; 8 | background-color: $color__white; 9 | z-index: 0; 10 | opacity: 0; 11 | 12 | &--is-active { 13 | z-index: 1; 14 | } 15 | 16 | &--is-open { 17 | opacity: 1; 18 | 19 | .panels--will-minimize & { 20 | opacity: 0.25; 21 | } 22 | 23 | .panels--is-minimized & { 24 | opacity: 0; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /portals/airmoss/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hasStyles": true, 3 | "hasScripts": true, 4 | "logo": { 5 | "id": "ornl-daac-logo", 6 | "link": "https://airmoss.ornl.gov", 7 | "title": "ORNL DAAC AirMOSS Home" 8 | }, 9 | "org": "ORNL DAAC", 10 | "pageTitle": "AirMOSS", 11 | "parentConfig": "edsc", 12 | "query": { 13 | "project": "AirMOSS", 14 | "hasGranulesOrCwic": null 15 | }, 16 | "title": "AirMOSS", 17 | "ui": { 18 | "showOnlyGranulesCheckbox": false, 19 | "showNonEosdisCheckbox": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionDownloadDisplay/CollectionDownloadDisplay.scss: -------------------------------------------------------------------------------- 1 | .collection-download-display { 2 | &__header { 3 | margin: $spacer; 4 | } 5 | 6 | &__intro { 7 | align-items: center; 8 | } 9 | 10 | &__button { 11 | margin-left: $spacer/2; 12 | } 13 | 14 | &__body { 15 | margin: $spacer; 16 | } 17 | 18 | &__list { 19 | margin: 0; 20 | padding-left: $spacer; 21 | } 22 | 23 | pre { 24 | font-size: .7em; 25 | white-space: pre-wrap; 26 | word-wrap: break-word; 27 | } 28 | } -------------------------------------------------------------------------------- /cypress/support/getJwtToken.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | 3 | import { getSecretEarthdataConfig, getSecretCypressConfig } from '../../sharedUtils/config' 4 | import { cmrEnv } from '../../sharedUtils/cmrEnv' 5 | 6 | /** 7 | * Creates a jwtToken based on the Cypress user config in secret.config.json 8 | */ 9 | export const getJwtToken = () => { 10 | const { secret } = getSecretEarthdataConfig(cmrEnv()) 11 | const { user } = getSecretCypressConfig() 12 | const jwtToken = jwt.sign({ ...user }, secret) 13 | 14 | return jwtToken 15 | } 16 | -------------------------------------------------------------------------------- /serverless/src/util/__tests__/getJwtToken.test.js: -------------------------------------------------------------------------------- 1 | import { getJwtToken } from '../getJwtToken' 2 | 3 | beforeEach(() => { 4 | jest.clearAllMocks() 5 | }) 6 | 7 | describe('util#getJwtToken', () => { 8 | test('correctly returns the JWT token when one is already set', () => { 9 | const token = '123.456.789' 10 | 11 | const event = { 12 | requestContext: { 13 | authorizer: { 14 | jwtToken: token 15 | } 16 | } 17 | } 18 | 19 | expect(getJwtToken(event)).toEqual('123.456.789') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /serverless/src/util/requestTimeout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Determine the length of the time to allow an http request to perform 3 | * @param {Integer} workloadThreshold The time to allow the lambda to do its processing beyond the request time 4 | */ 5 | export const requestTimeout = (workloadThreshold = 10) => { 6 | const lambdaTimeout = parseInt(process.env.LAMBDA_TIMEOUT, 10) 7 | 8 | // Return the difference between our lambda timeout and the 9 | // time required to perform the lambda work 10 | return (lambdaTimeout - workloadThreshold) * 1000 11 | } 12 | -------------------------------------------------------------------------------- /portals/amd/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hasStyles": true, 3 | "logo": { 4 | "id": "amd-logo", 5 | "link": "https://www.scar.org/data-products/antarctic-master-directory/", 6 | "title": "Antarctic Master Directory" 7 | }, 8 | "org": "AMD", 9 | "pageTitle": "Antarctic Master Directory", 10 | "parentConfig": "edsc", 11 | "query": { 12 | "tagKey": "org.scar.amd", 13 | "hasGranulesOrCwic": null 14 | }, 15 | "ui": { 16 | "showOnlyGranulesCheckbox": false, 17 | "showNonEosdisCheckbox": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /static/src/js/util/request/contactInfoRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | export default class ContactInfoRequest extends Request { 5 | constructor(authToken) { 6 | super(getEnvironmentConfig().apiHost) 7 | this.authenticated = true 8 | this.authToken = authToken 9 | } 10 | 11 | fetch() { 12 | return this.get('contact_info') 13 | } 14 | 15 | updateNotificationLevel(data) { 16 | return this.post('contact_info', data) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/__tests__/getNameValuePairsForProjections.test.js: -------------------------------------------------------------------------------- 1 | import { loadedEchoFormXml } from './mocks' 2 | import { getNameValuePairsForProjections } from '../getNameValuePairsForProjections' 3 | 4 | describe('util#getNameValuePairsForProjections', () => { 5 | test('correctly discovers the correct fields from the provided xml', () => { 6 | const projectPairs = getNameValuePairsForProjections(loadedEchoFormXml) 7 | 8 | expect(projectPairs).toEqual({ 9 | PROJECTION_PARAMETERS: 'Sphere:45,FE:54' 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /static/src/js/util/regions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prepare parameters used in getRegions() based on current Redux State 3 | * @param {object} state Current Redux State 4 | * @returns Parameters used in buildRegionSearchParams 5 | */ 6 | export const prepareRegionParams = (state) => { 7 | const { 8 | query 9 | } = state 10 | 11 | const { region: regionQuery } = query 12 | 13 | const { 14 | endpoint, 15 | exact, 16 | keyword 17 | } = regionQuery 18 | 19 | return { 20 | endpoint, 21 | exact, 22 | query: keyword 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /static/src/js/util/url/arrayEncoders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Decodes an array (as object) parameter (returns the values of the object) 3 | * @param {Object} object 4 | */ 5 | export const decodeArray = (object) => { 6 | if (!object) return undefined 7 | 8 | // If the object is a string, return that 9 | if (typeof object === 'string') return [object] 10 | 11 | return Object.values(object) 12 | } 13 | 14 | /** 15 | * Encodes an array parameter (returns the same value) 16 | * @param {Array} array 17 | */ 18 | export const encodeArray = array => array || '' 19 | -------------------------------------------------------------------------------- /bin/cypress-prepare-travis: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # read in static.config.json 4 | config="`cat static.config.json`" 5 | 6 | # update keys for test Travis CI 7 | config="`jq '.application.ciMode = $newValue' --arg newValue true <<< $config`" 8 | 9 | # overwrite static.config.json with new values 10 | echo $config > tmp.$$.json && mv tmp.$$.json static.config.json 11 | 12 | # create empty overrideStatic.config.json if it doesn't exist 13 | overrideFile=overrideStatic.config.json 14 | if [ ! -f $overrideFile ]; then 15 | echo {} > overrideStatic.config.json 16 | fi 17 | -------------------------------------------------------------------------------- /portals/suborbital/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hasStyles": true, 3 | "logo": { 4 | "id": "suborbital-logo", 5 | "link": "https://earthdata.nasa.gov/esds/impact/admg/the-airborne-inventory", 6 | "title": "Sub-Orbital Catalog" 7 | }, 8 | "org": "Sub-Orbital Catalog", 9 | "pageTitle": "NASA Sub-Orbital Catalog", 10 | "parentConfig": "edsc", 11 | "query": { 12 | "tagKey": ["gov.nasa.impact.*"], 13 | "hasGranulesOrCwic": null 14 | }, 15 | "ui": { 16 | "showOnlyGranulesCheckbox": false, 17 | "showNonEosdisCheckbox": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', { 5 | targets: { 6 | node: '8.10', 7 | esmodules: true, 8 | ie: '10' 9 | } 10 | } 11 | ], 12 | '@babel/preset-react' 13 | ], 14 | sourceType: 'unambiguous', 15 | plugins: [ 16 | 'react-hot-loader/babel', 17 | '@babel/plugin-proposal-object-rest-spread', 18 | '@babel/plugin-proposal-class-properties', 19 | '@babel/plugin-transform-runtime', 20 | '@babel/plugin-syntax-dynamic-import' 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /static/src/js/components/Well/Well.scss: -------------------------------------------------------------------------------- 1 | .well { 2 | width: 100%; 3 | overflow: hidden; 4 | color: $color__black--800; 5 | 6 | &__main { 7 | padding: $spacer*1.5; 8 | } 9 | 10 | &__heading { 11 | font-weight: 700; 12 | font-size: 1.5rem; 13 | } 14 | 15 | &__introduction { 16 | margin-bottom: 1.5rem; 17 | 18 | a { 19 | color: inherit; 20 | text-decoration: underline; 21 | } 22 | } 23 | 24 | &__section { 25 | margin-bottom: $spacer*1.5; 26 | } 27 | 28 | &__footer { 29 | padding: $spacer*1.5; 30 | } 31 | } -------------------------------------------------------------------------------- /static/src/js/util/request/loggerRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | /** 5 | * Request object for logging errors to lambda 6 | */ 7 | export default class LoggerRequest extends Request { 8 | constructor() { 9 | super(getEnvironmentConfig().apiHost) 10 | this.lambda = true 11 | } 12 | 13 | log(params) { 14 | return this.post('error_logger', params) 15 | } 16 | 17 | logRelevancy(params) { 18 | return this.post('relevancy_logger', params) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /static/src/js/util/pluralize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a pluralized string if a value is greater than zero. It will return a pluralized value for zero as well. 3 | * @param {string} string The string to pluralize. 4 | * @param {number} value The number to use for pluralization. 5 | * @returns {string} A pluralized string. 6 | */ 7 | export const pluralize = (string, value) => { 8 | if (typeof value !== 'number') return `${string}` 9 | if (value > 1 || value === 0) { 10 | return `${string}s` 11 | } 12 | return `${string}` 13 | } 14 | 15 | export default pluralize 16 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/__tests__/getTopLevelFields.test.js: -------------------------------------------------------------------------------- 1 | import { echoFormXml } from './mocks' 2 | import { getTopLevelFields } from '../getTopLevelFields' 3 | 4 | describe('util#getTopLevelFields', () => { 5 | test('correctly discovers the correct fields from the provided xml', () => { 6 | const topLevelFields = getTopLevelFields(echoFormXml) 7 | 8 | expect(topLevelFields).toEqual({ 9 | CLIENT: 'ESI', 10 | FORMAT: 'VRT', 11 | INCLUDE_META: 'false', 12 | REQUEST_MODE: 'async', 13 | SUBAGENT_ID: 'GDAL' 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /static/src/js/components/EDSCModal/EDSCModalOverlay.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import './EDSCModalOverlay.scss' 5 | 6 | export const EDSCModalOverlay = ({ 7 | children 8 | }) => { 9 | if (!children) return null 10 | 11 | return ( 12 |
13 | {children} 14 |
15 | ) 16 | } 17 | 18 | EDSCModalOverlay.defaultProps = { 19 | children: null 20 | } 21 | 22 | EDSCModalOverlay.propTypes = { 23 | children: PropTypes.node 24 | } 25 | 26 | export default EDSCModalOverlay 27 | -------------------------------------------------------------------------------- /static/src/js/util/findProvider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get full details for a provider from legacy services based on the provided id 3 | * @param {Ojbect} state Current Redux State 4 | * @param {String} id ID of the provider to find (e.g. LPDAAC_ECS) 5 | */ 6 | export const findProvider = (state, id) => { 7 | const { 8 | providers 9 | } = state 10 | 11 | return providers.find((element) => { 12 | const { provider } = element 13 | const { provider_id: providerId } = provider 14 | 15 | return providerId === id 16 | }) 17 | } 18 | 19 | export default findProvider 20 | -------------------------------------------------------------------------------- /static/src/js/reducers/focusedGranule.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_FOCUSED_GRANULE, RESTORE_FROM_URL } from '../constants/actionTypes' 2 | 3 | const initialState = '' 4 | 5 | const focusedGranuleReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case UPDATE_FOCUSED_GRANULE: { 8 | return action.payload 9 | } 10 | case RESTORE_FROM_URL: { 11 | const { focusedGranule = '' } = action.payload 12 | 13 | return focusedGranule 14 | } 15 | default: 16 | return state 17 | } 18 | } 19 | 20 | export default focusedGranuleReducer 21 | -------------------------------------------------------------------------------- /serverless/src/util/urs/getEchoToken.js: -------------------------------------------------------------------------------- 1 | import { getAccessTokenFromJwtToken } from './getAccessTokenFromJwtToken' 2 | import { getEdlConfig } from '../configUtil' 3 | 4 | /** 5 | * Returns the Echo-Token header for requests to CMR 6 | * @param {String} jwtToken 7 | */ 8 | export const getEchoToken = async (jwtToken) => { 9 | const { access_token: accessToken } = await getAccessTokenFromJwtToken(jwtToken) 10 | 11 | const edlConfig = await getEdlConfig() 12 | const { client } = edlConfig 13 | const { id: clientId } = client 14 | 15 | return `${accessToken}:${clientId}` 16 | } 17 | -------------------------------------------------------------------------------- /static/src/js/components/SplitBadge/SplitBadge.scss: -------------------------------------------------------------------------------- 1 | .split-badge { 2 | // If the .split-badge__inner is empty, we want to use the normal padding. Otherwise, 3 | // the height of the .split-badge__inner will provide the padding 4 | &:not(&--empty) { 5 | padding: 0; 6 | padding-left: 0.5rem; 7 | } 8 | 9 | &__inner { 10 | display: inline-block; 11 | margin-left: 0.5rem; 12 | padding: 0.3125rem 0.5rem; 13 | background-color: rgba(0,0,0,0.4); 14 | border-top-right-radius: $border-radius; 15 | border-bottom-right-radius: $border-radius; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /static/src/js/events/events.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events' 2 | import $ from 'jquery' 3 | 4 | const emitter = new EventEmitter() 5 | 6 | // Map granules can consume a ton of listeners, so prevent any console errors. 7 | emitter.setMaxListeners(0) 8 | 9 | emitter.on('error.global', (error = true) => { 10 | const { body } = document 11 | if (error) { 12 | $(body).addClass('body--has-error') 13 | } else { 14 | $(body).removeClass('body--has-error') 15 | } 16 | }) 17 | 18 | // eslint-disable-next-line import/prefer-default-export 19 | export const eventEmitter = emitter 20 | -------------------------------------------------------------------------------- /static/src/js/util/collectionMetadata/scienceKeywords.js: -------------------------------------------------------------------------------- 1 | import { 2 | capitalize, 3 | isEqual, 4 | startCase, 5 | uniqWith 6 | } from 'lodash' 7 | 8 | const fixCase = keyword => startCase(capitalize(keyword)) 9 | 10 | export const buildScienceKeywords = (json) => { 11 | const { scienceKeywords } = json 12 | 13 | if (!scienceKeywords) return [] 14 | 15 | return uniqWith(scienceKeywords.map(keyword => ([ 16 | fixCase(keyword.category), 17 | fixCase(keyword.topic), 18 | fixCase(keyword.term) 19 | ])), isEqual) 20 | } 21 | 22 | export default buildScienceKeywords 23 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/__tests__/getSubsetDataLayers.test.js: -------------------------------------------------------------------------------- 1 | import { loadedEchoFormXml } from './mocks' 2 | import { getSubsetDataLayers } from '../getSubsetDataLayers' 3 | 4 | describe('util#getSubsetDataLayers', () => { 5 | test('correctly discovers the correct fields from the provided xml', () => { 6 | const subsetDataLayers = getSubsetDataLayers(loadedEchoFormXml) 7 | 8 | expect(subsetDataLayers).toEqual({ 9 | SUBSET_DATA_LAYERS: '/MI1B2E/BlueBand,/MI1B2E/BRF Conversion Factors,/MI1B2E/GeometricParameters,/MI1B2E/NIRBand,/MI1B2E/RedBand' 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /static/src/js/components/Panels/PanelSection.scss: -------------------------------------------------------------------------------- 1 | .panel-section { 2 | position: absolute; 3 | top: 0; 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | width: 100%; 8 | background-color: $color__white; 9 | z-index: 0; 10 | overflow: hidden; 11 | opacity: 0; 12 | 13 | &--is-active { 14 | z-index: 1; 15 | opacity: 1; 16 | } 17 | 18 | > * { 19 | will-change: opacity; 20 | transition: opacity 0.1s ease-out; 21 | opacity: 0; 22 | } 23 | 24 | .is-panels-dragging & { 25 | pointer-events: none; 26 | user-select: none; 27 | } 28 | } -------------------------------------------------------------------------------- /static/src/js/containers/AppLogoContainer/AppLogoContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | 5 | import AppLogo from '../../components/AppLogo/AppLogo' 6 | 7 | const mapStateToProps = state => ({ 8 | portal: state.portal 9 | }) 10 | 11 | export const AppLogoContainer = ({ 12 | portal 13 | }) => ( 14 | 17 | ) 18 | 19 | AppLogoContainer.propTypes = { 20 | portal: PropTypes.shape({}).isRequired 21 | } 22 | 23 | export default connect(mapStateToProps)(AppLogoContainer) 24 | -------------------------------------------------------------------------------- /migrations/1564489093225_remove-environment-default-from-users.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | // We don't want the value provided as the default to be an actual default but we 5 | // need to provide a value for any records that existed before adding this column 6 | pgm.db.query('ALTER TABLE users ALTER COLUMN environment DROP DEFAULT;') 7 | } 8 | 9 | exports.down = (pgm) => { 10 | // Add the default back to prevent issues with node pg migrate matching the column 11 | pgm.db.query('ALTER TABLE users ALTER COLUMN environment SET DEFAULT \'prod\';') 12 | } 13 | -------------------------------------------------------------------------------- /serverless/src/util/aws/getLambdaConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns an environment specific configuration object for Lambda 3 | * @return {Object} A configuration object for Lambda 4 | */ 5 | export const getLambdaConfig = () => { 6 | const productionConfig = { 7 | apiVersion: '2015-03-31' 8 | } 9 | 10 | if (process.env.IS_OFFLINE) { 11 | // The endpoint should be point to the serverless offline host:port 12 | return { 13 | ...productionConfig, 14 | region: 'us-east-1', 15 | endpoint: 'http://localhost:3001' 16 | } 17 | } 18 | 19 | return productionConfig 20 | } 21 | -------------------------------------------------------------------------------- /static/src/js/actions/__tests__/advancedSearch.test.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_ADVANCED_SEARCH } from '../../constants/actionTypes' 2 | import { updateAdvancedSearch } from '../advancedSearch' 3 | 4 | beforeEach(() => { 5 | jest.clearAllMocks() 6 | }) 7 | 8 | describe('updateAdvancedSearch', () => { 9 | test('should create an action to update the advancedSearch state', () => { 10 | const payload = 'authToken-token' 11 | const expectedAction = { 12 | type: UPDATE_ADVANCED_SEARCH, 13 | payload 14 | } 15 | expect(updateAdvancedSearch(payload)).toEqual(expectedAction) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /static/src/js/reducers/dataQualitySummaries.js: -------------------------------------------------------------------------------- 1 | import { SET_DATA_QUALITY_SUMMARIES } from '../constants/actionTypes' 2 | 3 | const initialState = {} 4 | 5 | const dataQualitySummariesReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case SET_DATA_QUALITY_SUMMARIES: { 8 | const { catalogItemId, dataQualitySummaries } = action.payload 9 | 10 | return { 11 | ...state, 12 | [catalogItemId]: dataQualitySummaries 13 | } 14 | } 15 | default: 16 | return state 17 | } 18 | } 19 | 20 | export default dataQualitySummariesReducer 21 | -------------------------------------------------------------------------------- /static/src/js/reducers/focusedCollection.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_FOCUSED_COLLECTION, RESTORE_FROM_URL } from '../constants/actionTypes' 2 | 3 | const initialState = '' 4 | 5 | const focusedCollectionReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case UPDATE_FOCUSED_COLLECTION: { 8 | return action.payload 9 | } 10 | case RESTORE_FROM_URL: { 11 | const { focusedCollection = '' } = action.payload 12 | 13 | return focusedCollection 14 | } 15 | default: 16 | return state 17 | } 18 | } 19 | 20 | export default focusedCollectionReducer 21 | -------------------------------------------------------------------------------- /static/src/js/util/collectionMetadata/dataCenters.js: -------------------------------------------------------------------------------- 1 | export const buildDataCenters = (json) => { 2 | const { dataCenters } = json 3 | 4 | if (!dataCenters || !dataCenters.length) return undefined 5 | 6 | return dataCenters.map(dataCenter => ({ 7 | shortname: `${dataCenter.shortName.toLowerCase() === 'not provided' ? 'Name Not Provided' : dataCenter.shortName}`, 8 | longname: dataCenter.longName, 9 | roles: dataCenter.roles, 10 | contactInformation: dataCenter.contactInformation ? dataCenter.contactInformation : undefined 11 | })) 12 | } 13 | 14 | export default buildDataCenters 15 | -------------------------------------------------------------------------------- /static/src/js/util/commafy.js: -------------------------------------------------------------------------------- 1 | const commaRegex = new RegExp(/\B(?=(\d{3})+(?!\d))/g) 2 | 3 | /** 4 | * Returns a commafied number. 5 | * @param {number} number The number to commafy. 6 | * @returns {string} A commafied number string. 7 | */ 8 | export const commafy = (number) => { 9 | // https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript 10 | const num = `${number}` 11 | const parts = num.toString().split('.') 12 | parts[0] = parts[0].replace(commaRegex, ',') 13 | return parts.join('.') 14 | } 15 | 16 | export default commafy 17 | -------------------------------------------------------------------------------- /static/src/js/components/TemporalDisplay/TemporalSelectionDropdownMenu.scss: -------------------------------------------------------------------------------- 1 | .temporal-selection-dropdown-menu { 2 | width: 27.25rem; 3 | margin-top: -0.5rem; 4 | padding: $spacer; 5 | border: 0; 6 | border-top-left-radius: 0; 7 | border-top-right-radius: 0; 8 | box-shadow: $shadow__panel-dark; 9 | 10 | @include media-breakpoint-up(xl) { 11 | width: 29.25rem; 12 | } 13 | 14 | &__button { 15 | margin-right: 0.5rem; 16 | 17 | &--cancel { 18 | color: $color__black--400; 19 | 20 | &:hover { 21 | color: $color__black--600; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /static/src/js/reducers/errors.js: -------------------------------------------------------------------------------- 1 | import { ADD_ERROR, REMOVE_ERROR } from '../constants/actionTypes' 2 | 3 | const initialState = [] 4 | 5 | const errorsReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case ADD_ERROR: { 8 | return [ 9 | ...state, 10 | action.payload 11 | ] 12 | } 13 | case REMOVE_ERROR: { 14 | const id = action.payload 15 | const errors = state.filter(error => error.id !== id) 16 | 17 | return errors 18 | } 19 | default: 20 | return state 21 | } 22 | } 23 | 24 | export default errorsReducer 25 | -------------------------------------------------------------------------------- /migrations/1568146028532_change-users-not-null.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.db.query('ALTER TABLE users ALTER COLUMN echo_id DROP NOT NULL;') 5 | pgm.db.query('ALTER TABLE users ALTER COLUMN urs_id SET NOT NULL;') 6 | pgm.db.query('ALTER TABLE users ALTER COLUMN environment SET NOT NULL;') 7 | } 8 | 9 | exports.down = (pgm) => { 10 | pgm.db.query('ALTER TABLE users ALTER COLUMN echo_id SET NOT NULL;') 11 | pgm.db.query('ALTER TABLE users ALTER COLUMN urs_id DROP NOT NULL;') 12 | pgm.db.query('ALTER TABLE users ALTER COLUMN environment DROP NOT NULL;') 13 | } 14 | -------------------------------------------------------------------------------- /serverless/src/adminIsAuthorized/handler.js: -------------------------------------------------------------------------------- 1 | import { getApplicationConfig } from '../../../sharedUtils/config' 2 | /** 3 | * This lambda basically does nothing. The edlAuthorizer is setup on this lambda to determine if the user 4 | * is authorized as an admin. If not, the authorizer will return a 401 5 | */ 6 | export default async function adminIsAuthorized() { 7 | const { defaultResponseHeaders } = getApplicationConfig() 8 | 9 | return { 10 | isBase64Encoded: false, 11 | statusCode: 200, 12 | headers: defaultResponseHeaders, 13 | body: JSON.stringify({ authorized: true }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /serverless/src/submitHarmonyOrder/bboxToPolygon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert a bounding box (2 points) to a polygon (4 points) 3 | * @param {Array} bbox Array of 2 [lng, lat] points 4 | */ 5 | export const bboxToPolygon = (bbox) => { 6 | const [swPoint, nePoint] = bbox 7 | const [swLng, swLat] = swPoint 8 | const [neLng, neLat] = nePoint 9 | 10 | // Polygons points need to be in CCW direction (ne, nw, sw, se) 11 | return [[ 12 | [neLng, neLat], 13 | [swLng, neLat], 14 | [swLng, swLat], 15 | [neLng, swLat], 16 | [neLng, neLat] // Close the polygon by repeating the first point 17 | ]] 18 | } 19 | -------------------------------------------------------------------------------- /serverless/src/util/database/getDbConnection.js: -------------------------------------------------------------------------------- 1 | import 'pg' 2 | import knex from 'knex' 3 | import { getDbConnectionConfig } from '../database' 4 | 5 | // Initalize a variable to be set once 6 | let dbConnection 7 | 8 | /** 9 | * Returns a Knex database connection object to the EDSC RDS database 10 | */ 11 | export const getDbConnection = async () => { 12 | if (dbConnection == null) { 13 | const dbConnectionConfig = await getDbConnectionConfig() 14 | 15 | dbConnection = knex({ 16 | client: 'pg', 17 | connection: dbConnectionConfig 18 | }) 19 | } 20 | 21 | return dbConnection 22 | } 23 | -------------------------------------------------------------------------------- /serverless/src/util/__tests__/createJwtToken.test.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | 3 | import { createJwtToken } from '../createJwtToken' 4 | 5 | describe('util#createJwtToken', () => { 6 | test('correctly returns the JWT token', () => { 7 | const user = { 8 | id: 1, 9 | urs_id: 'testuser', 10 | site_preferences: {} 11 | } 12 | 13 | const result = createJwtToken(user) 14 | const decoded = jwt.decode(result) 15 | 16 | expect(decoded).toEqual(expect.objectContaining({ 17 | id: 1, 18 | preferences: {}, 19 | username: 'testuser' 20 | })) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /bin/ecc-sync: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo 'deploying' 6 | openssl aes-256-cbc -K $encrypted_461274fed2c2_key -iv $encrypted_461274fed2c2_iv -in .travis/travis.enc -out travis -d 7 | 8 | chmod 600 travis 9 | eval `ssh-agent -s` 10 | ssh-add travis 11 | ssh-keyscan -t rsa -p 7999 git.earthdata.nasa.gov >> ~/.ssh/known_hosts 12 | git remote add ecc ssh://git@git.earthdata.nasa.gov:7999/edsc/earthdata-search-client_repo.git 13 | git fetch ecc "+refs/heads/*:refs/remotes/origin/*" 14 | 15 | git fetch --unshallow || true 16 | echo "TRAVIS_COMMIT=$TRAVIS_COMMIT" 17 | git push ecc $TRAVIS_COMMIT:refs/heads/serverless 18 | -------------------------------------------------------------------------------- /serverless/src/util/createLimitedShapefile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a limited shapefile that only contains the shapes that match the provided selectedFeatures 3 | * @param {Object} file Shapefile 4 | * @param {Array} selectedFeatures Array of the selected features of the shapefile 5 | */ 6 | export const createLimitedShapefile = (file, selectedFeatures) => { 7 | const newFile = file 8 | const newFeatures = newFile.features.filter((feature) => { 9 | const { edscId } = feature 10 | 11 | return selectedFeatures.includes(edscId) 12 | }) 13 | 14 | newFile.features = newFeatures 15 | 16 | return newFile 17 | } 18 | -------------------------------------------------------------------------------- /static/src/js/components/Sidebar/SidebarSection.scss: -------------------------------------------------------------------------------- 1 | .sidebar-section { 2 | min-height: 0; 3 | margin: 0.5*$spacer; 4 | background-color: $color__black--900; 5 | 6 | &__header { 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | padding: 0.75rem 0.75rem 0.75rem 1.125rem; 11 | color: $color__black--300; 12 | border-bottom: 1px solid $color__black--700; 13 | } 14 | 15 | &__title { 16 | margin: 0; 17 | font-size: 0.875rem; 18 | color: $color__black--400; 19 | } 20 | 21 | &__icon { 22 | color: rgba(255,255,255,0.3) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /static/src/js/util/alphabetic-list.js: -------------------------------------------------------------------------------- 1 | export const alphabet = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] 2 | 3 | /** 4 | * Returns an object where the keys are the alphbetic characters and the values are empty arrays. 5 | * ie: { #: [], A: [], B: [], ... Z: []} 6 | * @return {object} The formatted object. 7 | */ 8 | export const createEmptyAlphabeticListObj = () => { 9 | const alphabeticList = {} 10 | 11 | alphabet.forEach((letter) => { 12 | alphabeticList[letter] = [] 13 | }) 14 | 15 | return alphabeticList 16 | } 17 | -------------------------------------------------------------------------------- /static/src/js/components/AccessMethod/AccessMethod.scss: -------------------------------------------------------------------------------- 1 | .access-method { 2 | &__radio-tooltip.fa { 3 | margin-left: $spacer/4; 4 | font-size: 0.875rem; 5 | color: $color__black--400; 6 | } 7 | 8 | &__section-intro, 9 | p.access-method__section-intro { 10 | margin-bottom: $spacer/2 11 | } 12 | 13 | &__section-status { 14 | font-weight: 700; 15 | font-style: italic; 16 | color: $color__black--400; 17 | } 18 | 19 | &__echoform-loading { 20 | height: 10rem; 21 | width: 100%; 22 | display: flex; 23 | align-items: flex-start; 24 | justify-content: flex-start; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/findFieldElement.js: -------------------------------------------------------------------------------- 1 | import xpath from 'xpath' 2 | import { DOMParser } from 'xmldom' 3 | import { namespaces } from './namespaces' 4 | 5 | /** 6 | * Find a specific tag in the provided XML string 7 | * @param {String} xmlDocument ECHO Form xml as a string 8 | * @param {String} fieldName Name of the tag to look for 9 | * @param {String} dataType XML namespace 10 | */ 11 | export const findFieldElement = (xmlDocument, fieldName, dataType = 'ecs') => { 12 | const doc = new DOMParser().parseFromString(xmlDocument) 13 | 14 | return xpath.useNamespaces(namespaces)(`//${dataType}:${fieldName}`, doc) 15 | } 16 | -------------------------------------------------------------------------------- /sharedUtils/prepareGranuleAccessParams.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Takes granule params and returns the params to be used in a CMR request 3 | * @param {Object} params Queue messages from SQS 4 | * @returns {Object} The granule params 5 | */ 6 | export const prepareGranuleAccessParams = (params = {}) => { 7 | const { concept_id: conceptIdsFromParams = [] } = params 8 | 9 | // If there are added granules, return only the added granules. Otherwise, send 10 | // all of the granules params. 11 | if (conceptIdsFromParams.length) { 12 | return { 13 | concept_id: conceptIdsFromParams 14 | } 15 | } 16 | 17 | return params 18 | } 19 | -------------------------------------------------------------------------------- /static/src/js/util/scrollParent.js: -------------------------------------------------------------------------------- 1 | // more minimal version of https://github.com/olahol/scrollParent.js/blob/master/scrollParent.js 2 | const regex = /(auto|scroll)/ 3 | 4 | const style = (node, prop) => getComputedStyle(node, null).getPropertyValue(prop) 5 | 6 | const scroll = node => regex.test( 7 | style(node, 'overflow') 8 | + style(node, 'overflow-y') 9 | + style(node, 'overflow-x') 10 | ) 11 | 12 | const scrollParent = (node) => { 13 | if (!node || node === document.body) { 14 | return document.body 15 | } 16 | return scroll(node) ? node : scrollParent(node.parentNode) 17 | } 18 | 19 | export default scrollParent 20 | -------------------------------------------------------------------------------- /serverless/src/getAccessMethods/supportsVariableSubsetting.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash' 2 | 3 | /** 4 | * Determine whether or not the provided UMM S record supports variable subsetting 5 | * @param {Object} service UMM S record to parse 6 | */ 7 | export const supportsVariableSubsetting = (service) => { 8 | const { serviceOptions = {} } = service 9 | 10 | // If there are no service options the record can not support variable subsetting 11 | if (serviceOptions == null) return false 12 | 13 | const { subset = {} } = serviceOptions 14 | const { variableSubset = {} } = subset 15 | 16 | return !isEmpty(variableSubset) 17 | } 18 | -------------------------------------------------------------------------------- /serverless/src/util/urs/getUserAccessToken.js: -------------------------------------------------------------------------------- 1 | import { getDbConnection } from '../database/getDbConnection' 2 | 3 | /** 4 | * Returns the decrypted urs system credentials from Secrets Manager 5 | */ 6 | export const getUserAccessToken = async (id) => { 7 | // Retrieve a connection to the database 8 | const dbConnection = await getDbConnection() 9 | 10 | const existingUserTokens = await dbConnection('user_tokens') 11 | .first([ 12 | 'access_token', 13 | 'refresh_token', 14 | 'expires_at' 15 | ]) 16 | .where({ user_id: id }) 17 | .orderBy('created_at', 'DESC') 18 | 19 | return existingUserTokens 20 | } 21 | -------------------------------------------------------------------------------- /static/src/js/components/GranuleFilters/GranuleFiltersBody.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * Renders GranuleFiltersBody. 6 | * @param {Object} props - The props passed into the component. 7 | * @param {Object} props.granuleFiltersForm - The granule filters form. 8 | */ 9 | export const GranuleFiltersBody = ({ 10 | granuleFiltersForm 11 | }) => ( 12 |
13 | {granuleFiltersForm} 14 |
15 | ) 16 | 17 | GranuleFiltersBody.propTypes = { 18 | granuleFiltersForm: PropTypes.node.isRequired 19 | } 20 | 21 | export default GranuleFiltersBody 22 | -------------------------------------------------------------------------------- /static/src/js/components/GranuleFilters/GranuleFiltersList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import './GranuleFiltersList.scss' 5 | 6 | /** 7 | * Renders GranuleFiltersList. 8 | * @param {Object} props - The props passed into the component. 9 | * @param {Node} props.children - The granule filter items. 10 | */ 11 | export const GranuleFiltersList = ({ 12 | children 13 | }) => ( 14 | 17 | ) 18 | 19 | GranuleFiltersList.propTypes = { 20 | children: PropTypes.node.isRequired 21 | } 22 | 23 | export default GranuleFiltersList 24 | -------------------------------------------------------------------------------- /static/src/js/util/request/admin/retrievalRequest.js: -------------------------------------------------------------------------------- 1 | import Request from '../request' 2 | import { getEnvironmentConfig } from '../../../../../../sharedUtils/config' 3 | 4 | export default class RetrievalRequest extends Request { 5 | constructor(authToken) { 6 | super(getEnvironmentConfig().apiHost) 7 | this.authenticated = true 8 | this.authToken = authToken 9 | } 10 | 11 | all(params) { 12 | return this.get('admin/retrievals', params) 13 | } 14 | 15 | fetch(id) { 16 | return this.get(`admin/retrievals/${id}`) 17 | } 18 | 19 | isAuthorized() { 20 | return this.get('/admin/is_authorized') 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /serverless/src/util/cmr/prepareExposeHeaders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a list of headers to expose to the client response 3 | * @param {Object} headers The headers that were capable of exposing 4 | * @return {Array} The headers to expose, separated by a comma 5 | */ 6 | export const prepareExposeHeaders = (headers) => { 7 | // Add 'jwt-token' to access-control-expose-headers, so the client app can read the JWT 8 | const { 'access-control-expose-headers': exposeHeaders = '' } = headers 9 | const exposeHeadersList = exposeHeaders.split(',').filter(Boolean) 10 | exposeHeadersList.push('jwt-token') 11 | return exposeHeadersList.join(', ') 12 | } 13 | -------------------------------------------------------------------------------- /serverless/src/util/createJwtToken.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | import cmrEnv from '../../../sharedUtils/cmrEnv' 3 | import { getSecretEarthdataConfig } from '../../../sharedUtils/config' 4 | 5 | /** 6 | * Create a signed JWT Token with user information 7 | * @param {Object} user User object from database 8 | */ 9 | export const createJwtToken = (user) => { 10 | const { 11 | id, 12 | urs_id: username, 13 | site_preferences: preferences 14 | } = user 15 | 16 | const { secret } = getSecretEarthdataConfig(cmrEnv()) 17 | 18 | return jwt.sign({ 19 | id, 20 | username, 21 | preferences 22 | }, secret) 23 | } 24 | -------------------------------------------------------------------------------- /static/src/js/components/Facets/FacetsModalNav.scss: -------------------------------------------------------------------------------- 1 | .facets-modal-nav { 2 | display: flex; 3 | flex-grow: 0; 4 | flex-shrink: 0; 5 | padding: $spacer; 6 | border-bottom: 1px solid #dee2e6; 7 | 8 | &__heading { 9 | margin-right: $spacer/2; 10 | color: $color__black--400; 11 | } 12 | 13 | &__list { 14 | display: flex; 15 | margin: 0; 16 | padding: 0; 17 | list-style: none; 18 | } 19 | 20 | &__entry { 21 | padding: 0 0.125rem; 22 | font-weight: 700; 23 | 24 | &:active { 25 | color: #14669c; 26 | } 27 | 28 | &--inactive { 29 | color: $color__black--400; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | collectCoverageFrom: [ 4 | 'serverless/src/**/*.js', 5 | 'static/src/**/*.js', 6 | 'sharedUtils/**/*.js' 7 | ], 8 | moduleNameMapper: { 9 | '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/static/src/js/util/mocks/fileMock.js', 10 | '^.+\\.(css|less|scss)$': 'babel-jest' 11 | }, 12 | coveragePathIgnorePatterns: [ 13 | 'package.json', 14 | 'package-lock.json' 15 | ], 16 | setupFiles: [ 17 | '/test-env.js' 18 | ], 19 | testPathIgnorePatterns: [ 20 | 'mocks.js' 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer') 2 | const precss = require('precss') 3 | const cssnano = require('cssnano') 4 | 5 | let plugins = [] 6 | 7 | const autoprefix = autoprefixer({ 8 | overrideBrowserslist: ['> 1%', 'last 2 versions'] 9 | }) 10 | 11 | 12 | // In development, we only want autoprefixer. In any other environment, 13 | // we run precss and postcss as well. The order is important here. 14 | if (process.env.NODE_ENV !== 'development') { 15 | plugins = [ 16 | precss, 17 | autoprefix, 18 | cssnano 19 | ] 20 | } else { 21 | plugins = [autoprefix] 22 | } 23 | 24 | module.exports = { 25 | plugins 26 | } 27 | -------------------------------------------------------------------------------- /serverless/src/submitHarmonyOrder/__tests__/ccwShapefile.test.js: -------------------------------------------------------------------------------- 1 | import { ccwShapefile } from '../ccwShapefile' 2 | import { 3 | mockCcwShapefile, 4 | mockCwShapefile, 5 | mockCwShapefileConverted 6 | } from './mocks' 7 | 8 | describe('ccwShapefile', () => { 9 | describe('with a CW shapefile', () => { 10 | test('returns a CCW shapefile', () => { 11 | expect(ccwShapefile(mockCwShapefile)).toEqual(mockCwShapefileConverted) 12 | }) 13 | }) 14 | 15 | describe('with a CCW shapefile', () => { 16 | test('returns a CCW shapefile', () => { 17 | expect(ccwShapefile(mockCcwShapefile)).toEqual(mockCcwShapefile) 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /static/src/js/actions/panels.js: -------------------------------------------------------------------------------- 1 | import { 2 | PANELS_TOGGLE, 3 | PANELS_SET_PANEL, 4 | PANELS_SET_PANEL_GROUP, 5 | PANELS_SET_PANEL_SECTION 6 | } from '../constants/actionTypes' 7 | 8 | 9 | export const togglePanels = value => ({ 10 | type: PANELS_TOGGLE, 11 | payload: value 12 | }) 13 | 14 | export const setActivePanel = panelId => ({ 15 | type: PANELS_SET_PANEL, 16 | payload: panelId 17 | }) 18 | 19 | export const setActivePanelGroup = panelId => ({ 20 | type: PANELS_SET_PANEL_GROUP, 21 | payload: panelId 22 | }) 23 | 24 | export const setActivePanelSection = panelId => ({ 25 | type: PANELS_SET_PANEL_SECTION, 26 | payload: panelId 27 | }) 28 | -------------------------------------------------------------------------------- /static/src/js/components/Panels/PanelGroupHeaderMeta.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * Renders PanelGroupHeaderMeta. 6 | * @param {Object} props - The props passed into the component. 7 | * @param {String} props.children - Children components. 8 | */ 9 | export const PanelGroupHeaderMeta = ({ 10 | children 11 | }) => ( 12 |
13 | {children} 14 |
15 | ) 16 | 17 | PanelGroupHeaderMeta.defaultProps = { 18 | children: null 19 | } 20 | 21 | PanelGroupHeaderMeta.propTypes = { 22 | children: PropTypes.node 23 | } 24 | 25 | export default PanelGroupHeaderMeta 26 | -------------------------------------------------------------------------------- /static/src/js/components/EDSCTable/EDSCTableCell.scss: -------------------------------------------------------------------------------- 1 | .edsc-table-cell { 2 | flex: 1; 3 | width: 100%; 4 | padding: 0.25rem 0.5rem; 5 | font-size: 0.7rem; 6 | border-top: 1px solid $color__white; 7 | border-bottom: 1px solid $color__black--200; 8 | 9 | .edsc-table__tr--odd & { 10 | border-top: 1px solid darken($color__black--100, 1); 11 | } 12 | 13 | .edsc-table__td--centered & { 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | text-align: center; 18 | } 19 | 20 | &__content { 21 | white-space: nowrap; 22 | text-overflow: ellipsis; 23 | overflow: hidden; 24 | cursor: text; 25 | } 26 | } -------------------------------------------------------------------------------- /static/src/js/util/request/__tests__/accessMethodsRequest.test.js: -------------------------------------------------------------------------------- 1 | import AccessMethodsRequest from '../accessMethodsRequest' 2 | 3 | beforeEach(() => { 4 | jest.restoreAllMocks() 5 | jest.clearAllMocks() 6 | }) 7 | 8 | describe('AccessMethodsRequest#constructor', () => { 9 | test('sets the default values when authenticated', () => { 10 | const token = '123' 11 | const request = new AccessMethodsRequest(token) 12 | 13 | expect(request.authenticated).toBeTruthy() 14 | expect(request.authToken).toEqual(token) 15 | expect(request.baseUrl).toEqual('http://localhost:3000') 16 | expect(request.searchPath).toEqual('access_methods') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /serverless/src/util/chunkArray.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Split an array into an array of smaller arrays 3 | * @param {Array} myArray The array to be split up into chunks 4 | * @param {Number} chunkSize The size of the chunks to split the array into 5 | * @return {Array} An array of arrays split up into the requested sizes 6 | */ 7 | export const chunkArray = (myArray, chunkSize) => { 8 | let index = 0 9 | const arrayLength = myArray.length 10 | const tempArray = [] 11 | 12 | for (index = 0; index < arrayLength; index += chunkSize) { 13 | const myChunk = myArray.slice(index, index + chunkSize) 14 | 15 | tempArray.push(myChunk) 16 | } 17 | 18 | return tempArray 19 | } 20 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/getFieldElementValue.js: -------------------------------------------------------------------------------- 1 | import xpath from 'xpath' 2 | import { DOMParser } from 'xmldom' 3 | import { namespaces } from './namespaces' 4 | 5 | /** 6 | * Find a specific tag in the provided XML string and return its text value 7 | * @param {String} xmlDocument ECHO Form xml as a string 8 | * @param {String} fieldName Name of the tag to look for 9 | * @param {String} dataType XML namespace 10 | */ 11 | export const getFieldElementValue = (xmlDocument, fieldName, dataType = 'ecs') => { 12 | const doc = new DOMParser().parseFromString(xmlDocument) 13 | 14 | return xpath.useNamespaces(namespaces)(`string(//${dataType}:${fieldName})`, doc) 15 | } 16 | -------------------------------------------------------------------------------- /static/src/js/components/ProjectCollections/ProjectCollectionsList.scss: -------------------------------------------------------------------------------- 1 | .project-collections-list { 2 | display: flex; 3 | flex-direction: column; 4 | flex-grow: 1; 5 | 6 | &__list { 7 | overflow: visible; 8 | overflow-y: visible; 9 | margin: 0; 10 | padding: 0; 11 | list-style: none; 12 | } 13 | 14 | &__notice { 15 | margin: 0 auto; 16 | padding: $spacer; 17 | font-size: 0.825rem; 18 | font-style: italic; 19 | text-align: center; 20 | color: $color__black--400; 21 | border-right: 1px solid $border-color; 22 | } 23 | 24 | &__filler { 25 | flex-grow: 1; 26 | border-right: 1px solid $border-color; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /static/src/js/util/isProjectCollectionValid.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash' 2 | 3 | /** 4 | * Returns true if the collection meets all validation requirements 5 | * @param {object} method - The collection access method information. 6 | * @return {boolean} 7 | */ 8 | export const isProjectCollectionValid = (method) => { 9 | if (!method) return false 10 | 11 | // Here is where we can check the method to see if its valid 12 | const [methodKey] = Object.keys(method) 13 | 14 | if (methodKey === 'download' && !isEmpty(method)) return true 15 | 16 | const accessMethod = method[methodKey] 17 | const { isValid = false } = accessMethod || {} 18 | 19 | return isValid 20 | } 21 | -------------------------------------------------------------------------------- /static/src/js/util/request/retrievalRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | export default class RetrievalRequest extends Request { 5 | constructor(authToken) { 6 | super(getEnvironmentConfig().apiHost) 7 | this.authenticated = true 8 | this.authToken = authToken 9 | } 10 | 11 | all() { 12 | return this.get('retrievals') 13 | } 14 | 15 | remove(id) { 16 | return this.delete(`retrievals/${id}`) 17 | } 18 | 19 | fetch(id) { 20 | return this.get(`retrievals/${id}`) 21 | } 22 | 23 | submit(params) { 24 | return this.post('retrievals', params) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /static/src/js/components/EDSCTable/EDSCTableCell.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import './EDSCTableCell.scss' 5 | 6 | /** 7 | * Renders EDSCTableCell. 8 | * @param {Object} props - The props passed into the component from react-table. 9 | */ 10 | const EDSCTableCell = ({ cell }) => { 11 | const { value } = cell 12 | 13 | return ( 14 |
15 | 16 | {value} 17 | 18 |
19 | ) 20 | } 21 | 22 | EDSCTableCell.propTypes = { 23 | cell: PropTypes.shape({}).isRequired 24 | } 25 | 26 | export default EDSCTableCell 27 | -------------------------------------------------------------------------------- /static/src/js/components/OrderDropdownList/OrderDropdownItem.scss: -------------------------------------------------------------------------------- 1 | .order-dropdown-item { 2 | &:not(:last-child) { 3 | margin-bottom: $spacer/2; 4 | } 5 | 6 | &__title { 7 | margin-bottom: $spacer/4; 8 | font-size: 0.75rem; 9 | font-weight: 700; 10 | 11 | span { 12 | font-weight: 400; 13 | } 14 | } 15 | 16 | &__list { 17 | list-style: none; 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | &__list-item { 23 | margin: 0; 24 | line-height: 1.15em; 25 | 26 | &:not(:last-child) { 27 | margin-bottom: $spacer/4; 28 | } 29 | } 30 | 31 | &__link { 32 | font-size: 0.75rem; 33 | white-space: nowrap; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /static/src/js/util/url/__tests__/url.test.js: -------------------------------------------------------------------------------- 1 | import { decodeUrlParams, encodeUrlQuery } from '../url' 2 | import { emptyDecodedResult } from './url.mocks' 3 | 4 | describe('url#decodeUrlParams', () => { 5 | test('given no string it returns no object', () => { 6 | const expectedResult = { 7 | ...emptyDecodedResult 8 | } 9 | expect(decodeUrlParams('')).toEqual(expectedResult) 10 | }) 11 | }) 12 | 13 | describe('url#encodeUrlQuery', () => { 14 | test('given no query it returns no params', () => { 15 | const props = { 16 | hasGranulesOrCwic: true, 17 | pathname: '/path/here' 18 | } 19 | expect(encodeUrlQuery(props)).toEqual('/path/here') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /static/src/js/components/TemporalDisplay/TemporalSelectionDropdownToggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Dropdown from 'react-bootstrap/Dropdown' 4 | 5 | const TemporalSelectionDropdownToggle = ({ onToggleClick }) => ( 6 | 12 | 13 | 14 | ) 15 | 16 | TemporalSelectionDropdownToggle.propTypes = { 17 | onToggleClick: PropTypes.func.isRequired 18 | } 19 | 20 | export default TemporalSelectionDropdownToggle 21 | -------------------------------------------------------------------------------- /serverless/src/getAccessMethods/supportsShapefileSubsetting.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Determine whether or not the provided UMM S record supports shapefile subsetting 3 | * @param {Object} service UMM S record to parse 4 | */ 5 | export const supportsShapefileSubsetting = (service) => { 6 | const { serviceOptions = {} } = service 7 | 8 | // If there are no service options the record can not support variable subsetting 9 | if (serviceOptions == null) return false 10 | 11 | const { subset = {} } = serviceOptions 12 | const { spatialSubset = {} } = subset 13 | const { 14 | shapefile = [] 15 | } = spatialSubset 16 | 17 | return shapefile.find(shapefile => shapefile.format === 'GeoJSON') != null 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /static/src/js/components/EDSCTabs/EDSCTabs.scss: -------------------------------------------------------------------------------- 1 | 2 | .edsc-tabs { 3 | &__tabs { 4 | border-bottom: 1px solid $color__black--300; 5 | 6 | .nav-link { 7 | border-radius: 0; 8 | background-color: transparent; 9 | border: 0; 10 | border-bottom: 4px solid transparent; 11 | color: $color__black--500; 12 | 13 | &:hover { 14 | border-bottom: 4px solid $color__black--300; 15 | } 16 | } 17 | 18 | > .nav-link.active { 19 | border: 0; 20 | border-bottom: 4px solid $color__blue--dark; 21 | background-color: transparent; 22 | color: $color__black--700; 23 | } 24 | } 25 | 26 | .tab-content { 27 | padding: $spacer; 28 | } 29 | } -------------------------------------------------------------------------------- /static/src/js/components/GranuleResults/GranuleResultsBrowseImageCell.scss: -------------------------------------------------------------------------------- 1 | .granule-results-browse-image-cell { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | align-self: flex-end; 6 | padding: 0.25rem; 7 | height: 60px; 8 | width: 60px; 9 | overflow: hidden; 10 | font-size: 0.5rem; 11 | border-bottom: 1px solid $color__black--200; 12 | 13 | &--image { 14 | background-color: #1a1a1a; 15 | border-bottom: 1px solid $color__black--700; 16 | } 17 | 18 | &__thumb { 19 | flex: 1; 20 | height: 100%; 21 | width: 100%; 22 | } 23 | 24 | &__thumb-image { 25 | width: 100%; 26 | height: 100%; 27 | object-fit: contain; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /static/src/js/util/collectionMetadata/doi.js: -------------------------------------------------------------------------------- 1 | export const buildDoi = (json) => { 2 | const { doi } = json 3 | 4 | if (!doi) return undefined 5 | 6 | const doiText = doi.doi 7 | if (!doiText) return { doiLink: undefined, doiText: undefined } 8 | 9 | // This link varies. Clean it up so all links start from the same place. 10 | let doiLink = doiText.replace(/^https?:\/\//g, '') 11 | doiLink = doiLink.replace('doi:', '') 12 | doiLink = doiLink.replace('dx.doi.org/', '') 13 | doiLink = doiLink.replace('doi.org/', '') 14 | 15 | if (doiLink !== '') return { doiLink: `https://dx.doi.org/${doiLink}`, doiText } 16 | return { doiLink: undefined, doiText } 17 | } 18 | 19 | export default buildDoi 20 | -------------------------------------------------------------------------------- /serverless/src/getAccessMethods/supportsBoundingBoxSubsetting.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash' 2 | 3 | /** 4 | * Determine whether or not the provided UMM S record supports bounding box subsetting 5 | * @param {Object} service UMM S record to parse 6 | */ 7 | export const supportsBoundingBoxSubsetting = (service) => { 8 | const { serviceOptions = {} } = service 9 | 10 | // If there are no service options the record can not support variable subsetting 11 | if (serviceOptions == null) return false 12 | 13 | const { subset = {} } = serviceOptions 14 | const { spatialSubset = {} } = subset 15 | const { 16 | boundingBox = {} 17 | } = spatialSubset 18 | 19 | return !isEmpty(boundingBox) 20 | } 21 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/__tests__/getFieldElementValue.test.js: -------------------------------------------------------------------------------- 1 | import { echoFormXml } from './mocks' 2 | import { getFieldElementValue } from '../getFieldElementValue' 3 | 4 | describe('util#getFieldElementValue', () => { 5 | test('correctly discovers the correct fields from the provided xml', () => { 6 | expect(getFieldElementValue(echoFormXml, 'CLIENT')).toEqual('ESI') 7 | expect(getFieldElementValue(echoFormXml, 'FORMAT')).toEqual('VRT') 8 | expect(getFieldElementValue(echoFormXml, 'INCLUDE_META')).toEqual('false') 9 | expect(getFieldElementValue(echoFormXml, 'REQUEST_MODE')).toEqual('async') 10 | expect(getFieldElementValue(echoFormXml, 'SUBAGENT_ID')).toEqual('GDAL') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /static/src/css/modules/_links.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | color: $color__blue--dark; 3 | text-decoration: underline; 4 | 5 | &--separated { 6 | &:after { 7 | display: inline-block; 8 | margin-left: $spacer/2; 9 | content: '|'; 10 | color: transparent; 11 | border-left: 1px solid $color__black--300; 12 | } 13 | } 14 | 15 | &--external { 16 | &:after { 17 | display: inline-block; 18 | padding-left: 0.3rem; 19 | content: "\f111"; 20 | font-size: 0.6rem; 21 | font-family: "EUI-Icon-Library"; 22 | color: $color__black--300; 23 | } 24 | 25 | &:hover { 26 | &:after { 27 | color: $color__black--400; 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /static/src/js/util/url/gridEncoders.js: -------------------------------------------------------------------------------- 1 | export const encodeGridCoords = (gridCoords) => { 2 | if (!gridCoords) return '' 3 | 4 | const encodedCoords = gridCoords 5 | .trim() 6 | .replace(/,/g, ':') 7 | .replace(/\s+/g, ',') 8 | .replace(/(^|,)(\d+)($|:)/g, '$1$2-$2$3') 9 | .replace(/(^|:)(\d+)($|,)/g, '$1$2-$2$3') 10 | 11 | return encodedCoords 12 | } 13 | 14 | export const decodeGridCoords = (string) => { 15 | if (!string) return undefined 16 | 17 | const decodedString = string 18 | .replace(/,/g, ' ') 19 | .replace(/:/g, ',') 20 | .replace(/(\d+)-(\d+)/g, (m, m0, m1) => { 21 | if (m0 === m1) return m0 22 | return m 23 | }) 24 | 25 | return decodedString 26 | } 27 | -------------------------------------------------------------------------------- /portals/default/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "advancedSearch": false, 4 | "authentication": false, 5 | "featureFacets": { 6 | "showMapImagery": false, 7 | "showNearRealTime": true, 8 | "showCustomizable": false 9 | } 10 | }, 11 | "footer": { 12 | "displayVersion": false, 13 | "attributionText": "", 14 | "primaryLinks": [], 15 | "secondaryLinks": [] 16 | }, 17 | "hasScripts": false, 18 | "hasStyles": false, 19 | "logo": {}, 20 | "org": "Site Name", 21 | "pageTitle": "", 22 | "query": {}, 23 | "title": "Search", 24 | "ui": { 25 | "showOnlyGranulesCheckbox": false, 26 | "showNonEosdisCheckbox": false, 27 | "showTophat": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionDetailsHighlights/skeleton.js: -------------------------------------------------------------------------------- 1 | export const collectionDetailsRow = [ 2 | { 3 | shape: 'rectangle', 4 | left: 0, 5 | top: 3, 6 | height: 12, 7 | width: 225, 8 | radius: 2 9 | } 10 | ] 11 | 12 | export const collectionDetailsParagraph = [ 13 | { 14 | shape: 'rectangle', 15 | left: 0, 16 | top: 3, 17 | height: 12, 18 | width: 225, 19 | radius: 2 20 | }, 21 | { 22 | shape: 'rectangle', 23 | left: 0, 24 | top: 23, 25 | height: 12, 26 | width: 225, 27 | radius: 2 28 | }, 29 | { 30 | shape: 'rectangle', 31 | left: 0, 32 | top: 43, 33 | height: 12, 34 | width: 225, 35 | radius: 2 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /static/src/js/components/ProjectCollections/ProjectCollections.scss: -------------------------------------------------------------------------------- 1 | .project-collections { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | display: flex; 8 | flex-direction: column; 9 | flex-grow: 0; 10 | background-color: $color__white; 11 | 12 | &__footer { 13 | flex-shrink: 0; 14 | padding: $spacer/2; 15 | border-top: 1px solid $border-color; 16 | border-right: 1px solid $border-color; 17 | } 18 | 19 | &__footer-message { 20 | margin-bottom: $spacer/2; 21 | font-size: 0.825rem; 22 | color: $color__black--400; 23 | } 24 | 25 | .simplebar-content { 26 | height: 100%; 27 | display: flex; 28 | flex-direction: column; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /static/src/js/reducers/contactInfo.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_CONTACT_INFO } from '../constants/actionTypes' 2 | 3 | const initialState = {} 4 | 5 | const contactInfoReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case UPDATE_CONTACT_INFO: { 8 | const { echoPreferences, ursProfile } = action.payload 9 | 10 | return { 11 | ...state, 12 | echoPreferences: { 13 | ...state.echoPreferences, 14 | ...echoPreferences 15 | }, 16 | ursProfile: { 17 | ...state.ursProfile, 18 | ...ursProfile 19 | } 20 | } 21 | } 22 | default: 23 | return state 24 | } 25 | } 26 | 27 | export default contactInfoReducer 28 | -------------------------------------------------------------------------------- /static/src/js/components/Panels/PanelGroupFooter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import './PanelGroupFooter.scss' 5 | 6 | 7 | /** 8 | * Renders PanelGroupFooter. 9 | * @param {object} props - The props passed into the component. 10 | * @param {node} props.footer - The element to be used as the footer. Can be overridden on an individual PanelItem. 11 | */ 12 | export const PanelGroupFooter = ({ footer }) => ( 13 |
14 | {footer} 15 |
16 | ) 17 | 18 | PanelGroupFooter.defaultProps = { 19 | footer: null 20 | } 21 | 22 | PanelGroupFooter.propTypes = { 23 | footer: PropTypes.node 24 | } 25 | 26 | export default PanelGroupFooter 27 | -------------------------------------------------------------------------------- /sharedUtils/portalPath.js: -------------------------------------------------------------------------------- 1 | import { isDefaultPortal } from '../static/src/js/util/portals' 2 | 3 | /** 4 | * Provides a prefix for links that takes the active portal into account 5 | * @param {Object} portal Object with a portalId key 6 | */ 7 | export const portalPath = (portal) => { 8 | if (!portal) return '' 9 | const { portalId = '' } = portal 10 | let portalPath = '' 11 | if (!isDefaultPortal(portalId)) portalPath = `/portal/${portalId}` 12 | 13 | return portalPath 14 | } 15 | 16 | /** 17 | * Wrapper for portalPath() that takes the full Redux state 18 | * @param {Object} state Redux state 19 | */ 20 | export const portalPathFromState = (state) => { 21 | const { portal } = state 22 | return portalPath(portal) 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12.16.3' 4 | addons: 5 | apt: 6 | packages: 7 | # Ubuntu 16+ does not install this dependency by default, so we need to install it ourselves 8 | - libgconf-2-4 9 | cache: 10 | npm: true 11 | directories: 12 | # we also need to cache folder with Cypress binary 13 | - ~/.cache 14 | script: 15 | - npm install 16 | - npm run copy-secrets 17 | - npm run silent-test 18 | - npm run cypress:prepare-travis 19 | - npm run cypress:ci 20 | branches: 21 | only: # Only build master. All pull requests also get built 22 | - master 23 | deploy: 24 | provider: script 25 | script: bin/ecc-sync 26 | on: 27 | branch: master 28 | repo: nasa/earthdata-search 29 | -------------------------------------------------------------------------------- /migrations/1559966487087_projects.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.createTable('projects', { 5 | id: 'id', 6 | name: { 7 | type: 'varchar(1000)' 8 | }, 9 | path: { 10 | type: 'text', 11 | notNull: true 12 | }, 13 | user_id: { 14 | type: 'integer', 15 | references: 'users' 16 | }, 17 | updated_at: { 18 | type: 'timestamp', 19 | notNull: true, 20 | default: pgm.func('current_timestamp') 21 | }, 22 | created_at: { 23 | type: 'timestamp', 24 | notNull: true, 25 | default: pgm.func('current_timestamp') 26 | } 27 | }) 28 | } 29 | 30 | exports.down = (pgm) => { 31 | pgm.dropTable('projects') 32 | } 33 | -------------------------------------------------------------------------------- /serverless/src/util/pick.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Select only desired keys from a provided object. 3 | * @param {object} providedObj - An object containing any keys. 4 | * @param {array} keys - An array of strings that represent the keys to be picked. 5 | * @return {obj} An object containing only the desired keys. 6 | */ 7 | export const pick = (providedObj = {}, keys) => { 8 | let obj = null 9 | 10 | // if `null` is provided the default parameter will not be 11 | // set so we'll handle it manually 12 | if (providedObj == null) { 13 | obj = {} 14 | } else { 15 | obj = providedObj 16 | } 17 | 18 | Object.keys(obj).forEach((k) => { 19 | if (!keys.includes(k)) { 20 | delete obj[k] 21 | } 22 | }) 23 | 24 | return obj 25 | } 26 | -------------------------------------------------------------------------------- /static/src/js/util/__tests__/isLinkType.test.js: -------------------------------------------------------------------------------- 1 | import { isLinkType } from '../isLinkType' 2 | 3 | describe('isLinkType', () => { 4 | describe('when passed an link that matches', () => { 5 | test('returns true', () => { 6 | expect(isLinkType('http://www.link.com/link/testvalue#', 'testvalue')).toEqual(true) 7 | }) 8 | }) 9 | 10 | describe('when passed an link does not match', () => { 11 | test('returns false', () => { 12 | expect(isLinkType('http://www.link.com/link/anothertest#', 'testvalue')).toEqual(false) 13 | }) 14 | }) 15 | 16 | describe('when passed an undefined link', () => { 17 | test('returns false', () => { 18 | expect(isLinkType(undefined, 'testvalue')).toEqual(false) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /static/src/js/actions/admin/isAuthorized.js: -------------------------------------------------------------------------------- 1 | import RetrievalRequest from '../../util/request/admin/retrievalRequest' 2 | import { SET_ADMIN_IS_AUTHORIZED } from '../../constants/actionTypes' 3 | 4 | export const updateIsAuthorized = isAuthorized => ({ 5 | type: SET_ADMIN_IS_AUTHORIZED, 6 | payload: isAuthorized 7 | }) 8 | 9 | export const adminIsAuthorized = () => (dispatch, getState) => { 10 | const { authToken } = getState() 11 | 12 | const requestObject = new RetrievalRequest(authToken) 13 | const response = requestObject.isAuthorized() 14 | .then(() => { 15 | // If the user is not authorized, the 401 will be caught before getting to this code. 16 | dispatch(updateIsAuthorized(true)) 17 | }) 18 | 19 | return response 20 | } 21 | -------------------------------------------------------------------------------- /static/src/js/components/FormFields/Radio/Radio.scss: -------------------------------------------------------------------------------- 1 | .radio { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-start; 5 | padding: $spacer/2 $spacer; 6 | width: 100%; 7 | border: 1px solid $color__black--200; 8 | border-radius: 0.125rem; 9 | vertical-align: middle; 10 | 11 | &--is-selected { 12 | background-color: $color__black--100; 13 | } 14 | 15 | &__input { 16 | display: inline-block; 17 | vertical-align: middle; 18 | } 19 | 20 | &__content { 21 | display: inline-block; 22 | margin-left: $spacer/2; 23 | font-weight: 700; 24 | color: $color__black--500; 25 | vertical-align: middle; 26 | 27 | .radio--is-selected & { 28 | color: $color__black--600; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /sharedUtils/outputFormatMaps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * OUS expects that output formats be provided in the form of file 3 | * extensions, this is a map between UMM-S values and their respective extensions 4 | */ 5 | export const ousFormatMapping = { 6 | 'NETCDF-3': 'nc', 7 | 'NETCDF-4': 'nc4', 8 | BINARY: 'dods', 9 | ASCII: 'ascii' 10 | } 11 | 12 | /** 13 | * Harmony expects that output formats be provided in the form of mimetypes, 14 | * this is a map between UMM-S values and their respective mimetypes 15 | */ 16 | export const harmonyFormatMapping = { 17 | 'NETCDF-4': 'application/x-netcdf4', 18 | GEOTIFF: 'image/tiff', 19 | GIF: 'image/gif', 20 | PNG: 'image/png', 21 | 'Shapefile+zip': 'application/shapefile+zip', 22 | ZARR: 'application/x-zarr' 23 | } 24 | -------------------------------------------------------------------------------- /static/src/css/vendor/bootstrap/_components.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Bootstrap components 3 | */ 4 | 5 | @import "~bootstrap/scss/_alert"; 6 | @import "~bootstrap/scss/_badge"; 7 | @import "~bootstrap/scss/_buttons"; 8 | @import "~bootstrap/scss/_breadcrumb"; 9 | @import "~bootstrap/scss/_card"; 10 | @import "~bootstrap/scss/_close"; 11 | @import "~bootstrap/scss/_code"; 12 | @import "~bootstrap/scss/_dropdown"; 13 | @import "~bootstrap/scss/_input-group"; 14 | @import "~bootstrap/scss/_modal"; 15 | @import "~bootstrap/scss/_nav"; 16 | @import "~bootstrap/scss/_popover"; 17 | @import "~bootstrap/scss/_progress"; 18 | @import "~bootstrap/scss/_spinners"; 19 | @import "~bootstrap/scss/_tables"; 20 | @import "~bootstrap/scss/_tooltip"; 21 | @import "~bootstrap/scss/_transitions"; 22 | -------------------------------------------------------------------------------- /static/src/js/reducers/__tests__/authToken.test.js: -------------------------------------------------------------------------------- 1 | import authTokenReducer from '../authToken' 2 | import { UPDATE_AUTH } from '../../constants/actionTypes' 3 | 4 | describe('INITIAL_STATE', () => { 5 | test('is correct', () => { 6 | const action = { type: 'dummy_action' } 7 | const initialState = '' 8 | 9 | expect(authTokenReducer(undefined, action)).toEqual(initialState) 10 | }) 11 | }) 12 | 13 | describe('UPDATE_AUTH', () => { 14 | test('returns the correct state', () => { 15 | const token = 'authToken-token' 16 | const action = { 17 | type: UPDATE_AUTH, 18 | payload: token 19 | } 20 | 21 | const expectedState = token 22 | 23 | expect(authTokenReducer(undefined, action)).toEqual(expectedState) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /static/src/js/components/Sidebar/Sidebar.scss: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | position: relative; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | flex-shrink: 0; 7 | flex-grow: 0; 8 | width: 17.5rem; 9 | z-index: 3; 10 | 11 | &__inner { 12 | position: relative; 13 | display: flex; 14 | flex-direction: column; 15 | flex-grow: 1; 16 | height: 100%; 17 | z-index: 1; 18 | background: $color__black--800; 19 | } 20 | 21 | &__section-title { 22 | color: $color__white; 23 | } 24 | 25 | &--hidden { 26 | display: none; 27 | } 28 | 29 | &__content { 30 | min-height: 0; 31 | flex: 0 1 100%; 32 | } 33 | 34 | .simplebar-scrollbar:before { 35 | background-color: $color__black--300; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /static/src/js/util/__tests__/portals.test.js: -------------------------------------------------------------------------------- 1 | import { isDefaultPortal } from '../portals' 2 | import * as getApplicationConfig from '../../../../../sharedUtils/config' 3 | 4 | beforeEach(() => { 5 | jest.clearAllMocks() 6 | jest.restoreAllMocks() 7 | }) 8 | 9 | describe('isDefaultPortal', () => { 10 | beforeEach(() => { 11 | jest.spyOn(getApplicationConfig, 'getApplicationConfig').mockImplementation(() => ({ 12 | defaultPortal: 'edsc' 13 | })) 14 | }) 15 | 16 | test('returns true if the portalId matches the defaultPortal', () => { 17 | expect(isDefaultPortal('edsc')).toBeTruthy() 18 | }) 19 | 20 | test('returns false if the portalId does not match the defaultPortal', () => { 21 | expect(isDefaultPortal('simple')).toBeFalsy() 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /secret.config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "environment": { 3 | "development": { 4 | "dbUsername": "", 5 | "dbPassword": "" 6 | }, 7 | "production": {} 8 | }, 9 | "earthdata": { 10 | "sit": { 11 | "clientId": "URS_CLIENT_ID", 12 | "password": "URS_PASSWORD", 13 | "secret": "JWT_SIGNING_SECRET_KEY" 14 | }, 15 | "uat": { 16 | "clientId": "URS_CLIENT_ID", 17 | "password": "URS_PASSWORD", 18 | "secret": "JWT_SIGNING_SECRET_KEY" 19 | }, 20 | "prod": { 21 | "clientId": "URS_CLIENT_ID", 22 | "password": "URS_PASSWORD", 23 | "secret": "JWT_SIGNING_SECRET_KEY" 24 | } 25 | }, 26 | "cypress": { 27 | "user": { 28 | "id": 1, 29 | "username": "your username here" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /static/src/js/components/FilterStack/FilterStack.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | 5 | import './FilterStack.scss' 6 | 7 | const FilterStack = (props) => { 8 | const { 9 | children, 10 | isOpen 11 | } = props 12 | 13 | if (!children) return null 14 | 15 | const className = classNames({ 16 | 'filter-stack': true, 17 | 'filter-stack--is-open': isOpen 18 | }) 19 | 20 | return ( 21 |
    22 | {children} 23 |
24 | ) 25 | } 26 | 27 | FilterStack.defaultProps = { 28 | children: null 29 | } 30 | 31 | FilterStack.propTypes = { 32 | children: PropTypes.node, 33 | isOpen: PropTypes.bool.isRequired 34 | } 35 | 36 | export default FilterStack 37 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionResults/CollectionResultsBody.scss: -------------------------------------------------------------------------------- 1 | .collection-results-body { 2 | height: 100%; 3 | width: 100%; 4 | position: relative; 5 | 6 | &__view-enter { 7 | display: block; 8 | visibility: visible; 9 | opacity: 0; 10 | } 11 | 12 | &__view-enter-active { 13 | opacity: 1; 14 | visibility: visible; 15 | transition: opacity 200ms; 16 | } 17 | 18 | &__view-enter-done { 19 | display: block; 20 | visibility: visible; 21 | opacity: 1; 22 | } 23 | 24 | &__view-exit { 25 | opacity: 1; 26 | visibility: visible; 27 | } 28 | 29 | &__view-exit-active { 30 | opacity: 0; 31 | transition: opacity 200ms; 32 | } 33 | 34 | &__view-exit-done { 35 | opacity: 0; 36 | visibility: hidden; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /static/src/js/util/url/__tests__/buildAuthenticatedRedirectUrl.test.js: -------------------------------------------------------------------------------- 1 | import { buildAuthenticatedRedirectUrl } from '../buildAuthenticatedRedirectUrl' 2 | 3 | describe('url#buildAuthenticatedRedirectUrl', () => { 4 | test('builds a url when no query params are provided', () => { 5 | expect(buildAuthenticatedRedirectUrl('http://edsc-test.com', 'abc.123.def')) 6 | .toEqual('http://localhost:3000/concepts/metadata?url=http://edsc-test.com&token=abc.123.def') 7 | }) 8 | 9 | test('builds a url when provided query params', () => { 10 | expect(buildAuthenticatedRedirectUrl('http://edsc-test.com?some_param=string_value', 'abc.123.def')) 11 | .toEqual('http://localhost:3000/concepts/metadata?url=http://edsc-test.com?some_param=string_value&token=abc.123.def') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/getSwitchFields.js: -------------------------------------------------------------------------------- 1 | import { getFieldElementValue } from './getFieldElementValue' 2 | 3 | const booleanTranslations = { 4 | true: 'Y', 5 | True: 'Y', 6 | TRUE: 'Y', 7 | y: 'Y', 8 | Y: 'Y', 9 | false: 'N', 10 | False: 'N', 11 | FALSE: 'N', 12 | n: 'N', 13 | N: 'N' 14 | } 15 | 16 | /** 17 | * Get boolean data from the provided XML Document 18 | * @param {String} xmlDocument ECHO Form xml as a string 19 | */ 20 | export const getSwitchFields = (xmlDocument) => { 21 | const switchFields = ['INCLUDE_META'] 22 | 23 | const switchFieldValues = {} 24 | 25 | switchFields.forEach((field) => { 26 | switchFieldValues[field] = booleanTranslations[getFieldElementValue(xmlDocument, field).trim()] 27 | }) 28 | 29 | return switchFieldValues 30 | } 31 | -------------------------------------------------------------------------------- /static/src/js/selectors/__tests__/granuleMetadata.test.js: -------------------------------------------------------------------------------- 1 | import { getFocusedGranuleMetadata } from '../granuleMetadata' 2 | 3 | describe('getFocusedGranuleMetadata selector', () => { 4 | test('returns the granule metadata', () => { 5 | const state = { 6 | focusedGranule: 'granuleId', 7 | metadata: { 8 | granules: { 9 | granuleId: { 10 | mock: 'data' 11 | } 12 | } 13 | } 14 | } 15 | 16 | expect(getFocusedGranuleMetadata(state)).toEqual({ mock: 'data' }) 17 | }) 18 | 19 | test('returns an empty object when there is no focusedGranule', () => { 20 | const state = { 21 | focusedGranule: '', 22 | metadata: {} 23 | } 24 | 25 | expect(getFocusedGranuleMetadata(state)).toEqual({}) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /migrations/1556745445952_colormaps.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.createTable('colormaps', { 5 | id: 'id', 6 | product: { 7 | type: 'varchar(1000)', 8 | notNull: true 9 | }, 10 | url: { 11 | type: 'varchar(1000)', 12 | notNull: true 13 | }, 14 | jsondata: { 15 | type: 'jsonb', 16 | notNull: true, 17 | default: '{}' 18 | }, 19 | updated_at: { 20 | type: 'timestamp', 21 | notNull: true, 22 | default: pgm.func('current_timestamp') 23 | }, 24 | created_at: { 25 | type: 'timestamp', 26 | notNull: true, 27 | default: pgm.func('current_timestamp') 28 | } 29 | }) 30 | } 31 | 32 | exports.down = (pgm) => { 33 | pgm.dropTable('colormaps') 34 | } 35 | -------------------------------------------------------------------------------- /serverless/src/relevancyLogger/handler.js: -------------------------------------------------------------------------------- 1 | import { getApplicationConfig } from '../../../sharedUtils/config' 2 | /** 3 | * Logs an error reported by a client 4 | * @param {Object} event Details about the HTTP request that it received 5 | */ 6 | const relevancyLogger = async (event) => { 7 | const { defaultResponseHeaders } = getApplicationConfig() 8 | 9 | const { body } = event 10 | const { params = {} } = JSON.parse(body) 11 | const { data = {} } = params 12 | 13 | const eventData = { 14 | event: 'collection_relevancy', 15 | ...data, 16 | timestamp: Date.now() 17 | } 18 | 19 | console.log(`[metrics] ${JSON.stringify(eventData)}`) 20 | 21 | return { 22 | statusCode: 200, 23 | headers: defaultResponseHeaders 24 | } 25 | } 26 | 27 | export default relevancyLogger 28 | -------------------------------------------------------------------------------- /static/src/js/reducers/admin/__tests__/isAuthorized.test.js: -------------------------------------------------------------------------------- 1 | import { SET_ADMIN_IS_AUTHORIZED } from '../../../constants/actionTypes' 2 | import adminIsAuthorizedReducer from '../isAuthorized' 3 | 4 | describe('INITIAL_STATE', () => { 5 | test('is correct', () => { 6 | const action = { type: 'dummy_action' } 7 | const expectedState = false 8 | 9 | expect(adminIsAuthorizedReducer(undefined, action)).toEqual(expectedState) 10 | }) 11 | }) 12 | 13 | describe('SET_ADMIN_IS_AUTHORIZED', () => { 14 | test('returns the correct state', () => { 15 | const action = { 16 | type: SET_ADMIN_IS_AUTHORIZED, 17 | payload: true 18 | } 19 | 20 | const expectedState = true 21 | 22 | expect(adminIsAuthorizedReducer(undefined, action)).toEqual(expectedState) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /static/src/js/util/getPanelSizeMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} PanelSizeMap 3 | * @property {Boolean} xs - Boolean flag for size 'xs' 4 | * @property {Boolean} sm - Boolean flag for size 'sm' 5 | * @property {Boolean} md - Boolean flag for size 'md' 6 | * @property {Boolean} lg - Boolean flag for size 'lg' 7 | * @property {Boolean} xl - Boolean flag for size 'xl' 8 | */ 9 | 10 | /** 11 | * Given the width of the resizable overlay panel in pixels, returns a 12 | * map to determine the current size of the panel 13 | * @param {Number} width The width of the panel. 14 | * @returns {PanelSizeMap} A map of panel sizes. 15 | */ 16 | export const getPanelSizeMap = width => ({ 17 | xs: true, 18 | sm: width >= 500, 19 | md: width >= 700, 20 | lg: width >= 900, 21 | xl: width >= 1100 22 | }) 23 | -------------------------------------------------------------------------------- /serverless/src/util/urs/getAccessTokenFromJwtToken.js: -------------------------------------------------------------------------------- 1 | import { getDbConnection } from '../database/getDbConnection' 2 | import { getVerifiedJwtToken } from '../getVerifiedJwtToken' 3 | 4 | /** 5 | * Returns the decrypted urs system credentials from Secrets Manager 6 | */ 7 | export const getAccessTokenFromJwtToken = async (jwtToken) => { 8 | // Retrieve a connection to the database 9 | const dbConnection = await getDbConnection() 10 | 11 | const { id } = getVerifiedJwtToken(jwtToken) 12 | 13 | const existingUserTokens = await dbConnection('user_tokens') 14 | .first([ 15 | 'access_token', 16 | 'refresh_token', 17 | 'expires_at', 18 | 'user_id' 19 | ]) 20 | .where({ user_id: id }) 21 | .orderBy('created_at', 'DESC') 22 | 23 | return existingUserTokens 24 | } 25 | -------------------------------------------------------------------------------- /static/src/js/util/__tests__/limitDecimalPoints.test.js: -------------------------------------------------------------------------------- 1 | import { limitDecimalPoints, limitLatLngDecimalPoints } from '../limitDecimalPoints' 2 | 3 | describe('limitDecimalPoints', () => { 4 | test('limits points to 5 decimal places', () => { 5 | const latLng = ['12.123456789', '34.123456789'] 6 | const expectedResult = [12.12346, 34.12346] 7 | 8 | expect(limitDecimalPoints(latLng)).toEqual(expectedResult) 9 | }) 10 | }) 11 | 12 | describe('limitLatLngDecimalPoints', () => { 13 | test('limits array of points to 5 decimal places', () => { 14 | const latLngs = ['12.123456789,34.123456789', '56.123456789,78.123456789'] 15 | const expectedResult = ['12.12346,34.12346', '56.12346,78.12346'] 16 | 17 | expect(limitLatLngDecimalPoints(latLngs)).toEqual(expectedResult) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /static/src/js/util/url/__tests__/focusedGranuleUrl.test.js: -------------------------------------------------------------------------------- 1 | import { decodeUrlParams, encodeUrlQuery } from '../url' 2 | 3 | import { emptyDecodedResult } from './url.mocks' 4 | 5 | describe('url#decodeUrlParams', () => { 6 | test('decodes focusedGranule correctly', () => { 7 | const expectedResult = { 8 | ...emptyDecodedResult, 9 | focusedGranule: 'granuleId' 10 | } 11 | expect(decodeUrlParams('?g=granuleId')).toEqual(expectedResult) 12 | }) 13 | }) 14 | 15 | describe('url#encodeUrlQuery', () => { 16 | test('encodes focusedGranule correctly', () => { 17 | const props = { 18 | hasGranulesOrCwic: true, 19 | pathname: '/path/here', 20 | focusedGranule: 'granuleId' 21 | } 22 | expect(encodeUrlQuery(props)).toEqual('/path/here?g=granuleId') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /serverless/src/util/cmr/cmrUrl.js: -------------------------------------------------------------------------------- 1 | import { stringify } from 'qs' 2 | import { getEarthdataConfig } from '../../../../sharedUtils/config' 3 | import { cmrEnv } from '../../../../sharedUtils/cmrEnv' 4 | 5 | /** 6 | * Construct a CMR url provided a path and query params 7 | * @param {String} path The path component of the url to be generated 8 | * @param {Object} queryParams An object that will represent the query parameters in the url 9 | * @return {String} A completed and valid url to CMR 10 | */ 11 | export const cmrUrl = (path, queryParams = {}) => { 12 | const baseUrl = `${getEarthdataConfig(cmrEnv()).cmrHost}/${path}` 13 | 14 | if (Object.keys(queryParams).length) { 15 | return `${baseUrl}?${stringify(queryParams, { indices: false, arrayFormat: 'brackets' })}` 16 | } 17 | 18 | return baseUrl 19 | } 20 | -------------------------------------------------------------------------------- /static/src/js/selectors/granuleMetadata.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | import { getFocusedGranuleId } from './focusedGranule' 4 | 5 | /** 6 | * Retrieve all granule metadata from Redux 7 | * @param {Object} state Current state of Redux 8 | */ 9 | export const getGranulesMetadata = (state) => { 10 | const { metadata = {} } = state 11 | const { granules = {} } = metadata 12 | 13 | return granules 14 | } 15 | 16 | /** 17 | * Retrieve metadata from Redux pertaining to the focused granule id 18 | */ 19 | export const getFocusedGranuleMetadata = createSelector( 20 | [getFocusedGranuleId, getGranulesMetadata], 21 | (focusedGranuleId, granulesMetadata) => { 22 | const { [focusedGranuleId]: granuleMetadata = {} } = granulesMetadata 23 | 24 | return granuleMetadata 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /static/src/js/util/url/facetEncoders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Encodes a Facet object into a string 3 | * @param {object} facets Facet object 4 | * @return {string} A `!` delimited string of the facet values 5 | */ 6 | export const encodeFacets = (facets) => { 7 | if (!facets) return '' 8 | 9 | const encoded = [] 10 | 11 | facets.forEach((facet) => { 12 | encoded.push(facet) 13 | }) 14 | 15 | return encoded.join('!') 16 | } 17 | 18 | 19 | /** 20 | * Decodes a Facet parameter string into an object 21 | * @param {string} string A `!` delimited string of the facet values 22 | * @return {object} Facet object 23 | */ 24 | export const decodeFacets = (string) => { 25 | if (!string) { 26 | return undefined 27 | } 28 | 29 | const decodedValues = string.split('!') 30 | 31 | return decodedValues 32 | } 33 | -------------------------------------------------------------------------------- /static/src/js/util/getActivePanelSize.js: -------------------------------------------------------------------------------- 1 | import { getPanelSizeMap } from './getPanelSizeMap' 2 | 3 | /** 4 | * Given the width of the resizable overlay panel in pixels, returns a 5 | * a string with the current size. 6 | * @param {Number} width The width of the panel. 7 | * @returns {(String|Null)} A string representing the current size, or null. 8 | */ 9 | export const getActivePanelSize = (width) => { 10 | const panelSizeObj = getPanelSizeMap(width) 11 | 12 | switch (true) { 13 | case panelSizeObj.xl: 14 | return 'xl' 15 | case panelSizeObj.lg: 16 | return 'lg' 17 | case panelSizeObj.md: 18 | return 'md' 19 | case panelSizeObj.sm: 20 | return 'sm' 21 | case panelSizeObj.xs: 22 | return 'xs' 23 | default: 24 | break 25 | } 26 | 27 | return null 28 | } 29 | -------------------------------------------------------------------------------- /static/src/js/selectors/__tests__/collectionMetadata.test.js: -------------------------------------------------------------------------------- 1 | import { getFocusedCollectionMetadata } from '../collectionMetadata' 2 | 3 | describe('getFocusedCollectionMetadata selector', () => { 4 | test('returns the collection metadata', () => { 5 | const state = { 6 | focusedCollection: 'collectionId', 7 | metadata: { 8 | collections: { 9 | collectionId: { 10 | mock: 'data' 11 | } 12 | } 13 | } 14 | } 15 | expect(getFocusedCollectionMetadata(state)).toEqual({ mock: 'data' }) 16 | }) 17 | 18 | test('returns an empty object when there is no focusedCollection', () => { 19 | const state = { 20 | focusedCollection: '', 21 | metadata: {} 22 | } 23 | 24 | expect(getFocusedCollectionMetadata(state)).toEqual({}) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /static/src/js/util/collectionMetadata/__tests__/granuleLimit.test.js: -------------------------------------------------------------------------------- 1 | import { getGranuleLimit } from '../granuleLimit' 2 | 3 | describe('granuleLimit', () => { 4 | test('returns the granule limit if it exists', () => { 5 | const metadata = { 6 | tags: { 7 | 'edsc.limited_collections': { 8 | data: { 9 | limit: 100 10 | } 11 | } 12 | } 13 | } 14 | expect(getGranuleLimit(metadata)).toEqual(100) 15 | }) 16 | 17 | test('returns the app default if the tag does not exist', () => { 18 | const metadata = { 19 | tags: {} 20 | } 21 | expect(getGranuleLimit(metadata)).toEqual(1000000) 22 | }) 23 | 24 | test('returns the app default if the metadata is empty', () => { 25 | expect(getGranuleLimit(undefined)).toEqual(1000000) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /static.webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const WebpackBar = require('webpackbar') 3 | 4 | const StaticCommonConfig = require('./static.webpack.config.common') 5 | 6 | const Config = merge.smartStrategy( 7 | { 8 | devtool: 'replace', 9 | 'module.rules.use': 'prepend' 10 | } 11 | )(StaticCommonConfig, { 12 | mode: 'development', 13 | devtool: 'inline-cheap-module-source-map', 14 | devServer: { 15 | historyApiFallback: true 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(css|scss)$/, 21 | exclude: /portals/i, 22 | use: [ 23 | { 24 | loader: 'style-loader' 25 | } 26 | ] 27 | } 28 | ] 29 | }, 30 | plugins: [ 31 | new WebpackBar() 32 | ] 33 | }) 34 | 35 | module.exports = Config 36 | -------------------------------------------------------------------------------- /static/src/js/components/ProjectPanels/ProjectPanels.scss: -------------------------------------------------------------------------------- 1 | .project-panels { 2 | &__footer { 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: flex-end; 6 | align-items: center; 7 | vertical-align: middle; 8 | padding: $spacer/2 $spacer; 9 | } 10 | 11 | &__action { 12 | margin-left: $spacer/2; 13 | } 14 | 15 | &__collection-status { 16 | display: inline-block; 17 | margin-right: $spacer/2; 18 | font-size: 1.125rem; 19 | vertical-align: middle; 20 | 21 | &--valid { 22 | color: $color__green; 23 | } 24 | 25 | &--invalid { 26 | color: $color__red; 27 | } 28 | } 29 | 30 | &__collection-count { 31 | display: inline-block; 32 | margin-right: $spacer/2; 33 | color: $color__black--400; 34 | vertical-align: middle; 35 | } 36 | } -------------------------------------------------------------------------------- /static/src/js/util/pathStartsWith.js: -------------------------------------------------------------------------------- 1 | import { castArray } from 'lodash' 2 | 3 | /** 4 | * Returns true if the pathname starts with one of the paths 5 | * @param {string} pathname - The pathname to check. 6 | * @param {string|array} paths - The path(s) to check against. 7 | * @return {boolean} 8 | */ 9 | export const pathStartsWith = (pathname, paths) => castArray(paths).some((path = '') => { 10 | const pathnameWithoutTrailingSlash = pathname.replace(/\/+$/, '') 11 | const pathWithoutTrailingSlash = path.replace(/\/+$/, '') 12 | 13 | const pathnameWithoutPortal = pathnameWithoutTrailingSlash.replace(/portal\/[^/]*\//, '') 14 | 15 | return pathnameWithoutTrailingSlash.startsWith(pathWithoutTrailingSlash) 16 | || pathnameWithoutPortal.startsWith(pathWithoutTrailingSlash) 17 | }) 18 | 19 | export default pathStartsWith 20 | -------------------------------------------------------------------------------- /static/src/js/util/request/projectRequest.js: -------------------------------------------------------------------------------- 1 | import Request from './request' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | export default class ProjectRequest extends Request { 5 | constructor(authToken) { 6 | super(getEnvironmentConfig().apiHost) 7 | this.lambda = true 8 | 9 | if (authToken && authToken.length > 0) { 10 | this.authenticated = true 11 | this.authToken = authToken 12 | } 13 | } 14 | 15 | all() { 16 | this.authenticated = true 17 | return this.get('projects') 18 | } 19 | 20 | save(params) { 21 | return this.post('projects', params) 22 | } 23 | 24 | fetch(projectId) { 25 | return this.get(`projects/${projectId}`) 26 | } 27 | 28 | remove(projectId) { 29 | return this.delete(`projects/${projectId}`) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /static/src/js/components/AdminRetrievals/__tests__/AdminRetrievalForm.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Enzyme, { shallow } from 'enzyme' 3 | import Adapter from 'enzyme-adapter-react-16' 4 | 5 | import { Form } from 'react-bootstrap' 6 | import { AdminRetrievalsForm } from '../AdminRetrievalsForm' 7 | 8 | Enzyme.configure({ adapter: new Adapter() }) 9 | 10 | function setup() { 11 | const props = { 12 | onAdminViewRetrieval: jest.fn() 13 | } 14 | 15 | const enzymeWrapper = shallow() 16 | 17 | return { 18 | enzymeWrapper, 19 | props 20 | } 21 | } 22 | 23 | describe('AdminRetrievals component', () => { 24 | test('renders itself correctly', () => { 25 | const { enzymeWrapper } = setup() 26 | 27 | expect(enzymeWrapper.find(Form).length).toBe(1) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /static/src/css/modules/_dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown-menu { 2 | padding: .25rem 0; 3 | 4 | &--carat-right { 5 | @include carat('topright'); 6 | left: 0.45rem !important; 7 | } 8 | } 9 | 10 | .dropdown-header { 11 | font-weight: 700; 12 | color: $color__black--500; 13 | 14 | .dropdown-menu--condensed & { 15 | padding: .25rem .75rem; 16 | padding-bottom: 0; 17 | font-size: 0.625rem; 18 | } 19 | } 20 | 21 | .dropdown-item { 22 | color: $color__black--800; 23 | font-size: 0.875rem; 24 | 25 | &:hover { 26 | background-color: lighten($color__black--200, 5%); 27 | color: $color__black--700; 28 | } 29 | 30 | .dropdown-menu--condensed & { 31 | padding: .25rem .75rem; 32 | font-size: 0.75rem; 33 | } 34 | } 35 | 36 | .dropdown-toggle { 37 | &:after { 38 | vertical-align: middle; 39 | } 40 | } -------------------------------------------------------------------------------- /static/src/js/components/GranuleFilters/__tests__/GranuleFiltersHeader.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Enzyme, { shallow } from 'enzyme' 3 | import Adapter from 'enzyme-adapter-react-16' 4 | 5 | import GranuleFiltersHeader from '../GranuleFiltersHeader' 6 | 7 | Enzyme.configure({ adapter: new Adapter() }) 8 | 9 | function setup() { 10 | const props = { 11 | title: 'Collection ID' 12 | } 13 | 14 | const enzymeWrapper = shallow() 15 | 16 | return { 17 | enzymeWrapper, 18 | props 19 | } 20 | } 21 | 22 | describe('GranuleFiltersHeader component', () => { 23 | test('sets the dataset ID correctly', () => { 24 | const { enzymeWrapper } = setup() 25 | 26 | expect(enzymeWrapper.find('.granule-filters-header__secondary').text()).toEqual('Collection ID') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /static/src/js/components/Panels/PanelSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | 5 | import './PanelSection.scss' 6 | 7 | export const PanelSection = ({ children, isActive, isOpen }) => { 8 | const className = classNames([ 9 | 'panel-section', 10 | { 11 | 'panel-section--is-open': isOpen, 12 | 'panel-section--is-active': isActive 13 | } 14 | ]) 15 | return ( 16 |
17 | {children} 18 |
19 | ) 20 | } 21 | 22 | PanelSection.defaultProps = { 23 | isActive: false, 24 | isOpen: false 25 | } 26 | 27 | 28 | PanelSection.propTypes = { 29 | children: PropTypes.node.isRequired, 30 | isOpen: PropTypes.bool, 31 | isActive: PropTypes.bool 32 | } 33 | 34 | export default PanelSection 35 | -------------------------------------------------------------------------------- /static/src/js/util/__tests__/pluralize.test.js: -------------------------------------------------------------------------------- 1 | import { pluralize } from '../pluralize' 2 | 3 | describe('util#pluralize', () => { 4 | describe('when given no value', () => { 5 | test('returns the correct string', () => { 6 | expect(pluralize('test')).toEqual('test') 7 | }) 8 | }) 9 | describe('when given value of 0', () => { 10 | test('returns the correct string', () => { 11 | expect(pluralize('test', 0)).toEqual('tests') 12 | }) 13 | }) 14 | describe('when given value of 1', () => { 15 | test('returns the correct string', () => { 16 | expect(pluralize('test', 1)).toEqual('test') 17 | }) 18 | }) 19 | describe('when given value greater than 1', () => { 20 | test('returns the correct string', () => { 21 | expect(pluralize('test', 2)).toEqual('tests') 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /static/src/js/util/url/buildAuthenticatedRedirectUrl.js: -------------------------------------------------------------------------------- 1 | import { stringify } from 'qs' 2 | import { getEnvironmentConfig } from '../../../../../sharedUtils/config' 3 | 4 | /** 5 | * Construct a url to a lambda that is responsible for redirecting the user with an appropriate cmr token 6 | * @param {String} url The url to be redirected to after a token has been retrieved 7 | * @param {Object} params An object containing all the query parameters 8 | * @param {String} jwtToken JWT Token that the lambda will use to lookup a user token 9 | */ 10 | export const buildAuthenticatedRedirectUrl = (url, jwtToken) => { 11 | const { apiHost } = getEnvironmentConfig() 12 | 13 | const queryString = stringify({ 14 | url, 15 | token: jwtToken 16 | }, { encode: false }) 17 | 18 | return `${apiHost}/concepts/metadata?${queryString}` 19 | } 20 | -------------------------------------------------------------------------------- /migrations/1563477325342_access-configurations.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined 2 | 3 | exports.up = (pgm) => { 4 | pgm.createTable('access_configurations', { 5 | id: 'id', 6 | user_id: { 7 | type: 'integer', 8 | references: 'users' 9 | }, 10 | collection_id: { 11 | type: 'varchar(1000)', 12 | notNull: true 13 | }, 14 | access_method: { 15 | type: 'jsonb', 16 | notNull: true, 17 | default: '{}' 18 | }, 19 | updated_at: { 20 | type: 'timestamp', 21 | notNull: true, 22 | default: pgm.func('current_timestamp') 23 | }, 24 | created_at: { 25 | type: 'timestamp', 26 | notNull: true, 27 | default: pgm.func('current_timestamp') 28 | } 29 | }) 30 | } 31 | 32 | exports.down = (pgm) => { 33 | pgm.dropTable('access_configurations') 34 | } 35 | -------------------------------------------------------------------------------- /static/src/js/reducers/__tests__/browser.test.js: -------------------------------------------------------------------------------- 1 | import browserReducer from '../browser' 2 | import { 3 | UPDATE_BROWSER_VERSION 4 | } from '../../constants/actionTypes' 5 | 6 | const initialState = {} 7 | 8 | describe('INITIAL_STATE', () => { 9 | test('is correct', () => { 10 | const action = { type: 'dummy_action' } 11 | 12 | expect(browserReducer(undefined, action)).toEqual(initialState) 13 | }) 14 | }) 15 | 16 | describe('UPDATE_BROWSER_VERSION', () => { 17 | test('returns the correct state', () => { 18 | const action = { 19 | type: UPDATE_BROWSER_VERSION, 20 | payload: { 21 | name: 'some browser name' 22 | } 23 | } 24 | 25 | const expectedState = { 26 | name: 'some browser name' 27 | } 28 | 29 | expect(browserReducer(undefined, action)).toEqual(expectedState) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /static/src/js/util/__tests__/getFilenameFromPath.test.js: -------------------------------------------------------------------------------- 1 | import { getFilenameFromPath } from '../getFilenameFromPath' 2 | 3 | describe('#getFilenameFromPath', () => { 4 | describe('when passed no path', () => { 5 | test('should return an empty string', () => { 6 | const result = getFilenameFromPath() 7 | expect(result).toEqual('') 8 | }) 9 | }) 10 | 11 | describe('when passed an empty string', () => { 12 | test('should return an empty string', () => { 13 | const result = getFilenameFromPath('') 14 | expect(result).toEqual('') 15 | }) 16 | }) 17 | 18 | describe('when passed an valid filepath', () => { 19 | test('should return the filepath', () => { 20 | const result = getFilenameFromPath('http://www.testing.com/this/file.txt') 21 | expect(result).toEqual('file.txt') 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /static/src/js/util/url/__tests__/shapefileId.test.js: -------------------------------------------------------------------------------- 1 | import { decodeUrlParams, encodeUrlQuery } from '../url' 2 | 3 | import { emptyDecodedResult } from './url.mocks' 4 | 5 | describe('url#decodeUrlParams', () => { 6 | test('decodes shapefileId correctly', () => { 7 | const expectedResult = { 8 | ...emptyDecodedResult, 9 | shapefile: { 10 | ...emptyDecodedResult.shapefile, 11 | shapefileId: '123' 12 | } 13 | } 14 | expect(decodeUrlParams('?sf=123')).toEqual(expectedResult) 15 | }) 16 | }) 17 | 18 | describe('url#encodeUrlQuery', () => { 19 | test('encodes shapefileId correctly', () => { 20 | const props = { 21 | hasGranulesOrCwic: true, 22 | pathname: '/path/here', 23 | shapefileId: '123' 24 | } 25 | expect(encodeUrlQuery(props)).toEqual('/path/here?sf=123') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /static/src/js/components/Sidebar/SidebarSection.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import React from 'react' 3 | 4 | import './SidebarSection.scss' 5 | 6 | const SidebarSection = (props) => { 7 | const { 8 | children, 9 | sectionTitle 10 | } = props 11 | 12 | return ( 13 |
14 | { 15 | sectionTitle && ( 16 |
17 |

{sectionTitle}

18 |
19 | ) 20 | } 21 | {children} 22 |
23 | ) 24 | } 25 | 26 | SidebarSection.defaultProps = { 27 | sectionTitle: null 28 | } 29 | 30 | SidebarSection.propTypes = { 31 | sectionTitle: PropTypes.string, 32 | children: PropTypes.node.isRequired 33 | } 34 | 35 | export default SidebarSection 36 | -------------------------------------------------------------------------------- /static/src/js/selectors/collectionMetadata.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | import { getFocusedCollectionId } from './focusedCollection' 4 | 5 | /** 6 | * Retrieve all collection metadata from Redux 7 | * @param {Object} state Current state of Redux 8 | */ 9 | export const getCollectionsMetadata = (state) => { 10 | const { metadata = {} } = state 11 | const { collections = {} } = metadata 12 | 13 | return collections 14 | } 15 | 16 | /** 17 | * Retrieve metadata from Redux pertaining to the focused collection id 18 | */ 19 | export const getFocusedCollectionMetadata = createSelector( 20 | [getFocusedCollectionId, getCollectionsMetadata], 21 | (focusedCollectionId, collectionsMetadata) => { 22 | const { [focusedCollectionId]: collectionMetadata = {} } = collectionsMetadata 23 | 24 | return collectionMetadata 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /static/src/js/util/limitDecimalPoints.js: -------------------------------------------------------------------------------- 1 | import { getApplicationConfig } from '../../../../sharedUtils/config' 2 | 3 | /** 4 | * Limits a Latitude/Longitude point to a configured number of decimal places 5 | * @param {Array} latLng A Lat/Lng point as an array 6 | */ 7 | export const limitDecimalPoints = (latLng) => { 8 | const { defaultSpatialDecimalSize } = getApplicationConfig() 9 | return latLng.map(point => Number(parseFloat(point).toFixed(defaultSpatialDecimalSize))) 10 | } 11 | 12 | /** 13 | * Limits an array of Latitude/Longitude points to a configured number of decimal places 14 | * @param {Array} latLngs Array of Lat/Lng points (each point a comma delimited string) 15 | */ 16 | export const limitLatLngDecimalPoints = latLngs => latLngs.map((latLng) => { 17 | const points = latLng.split(',') 18 | return limitDecimalPoints(points).join(',') 19 | }) 20 | -------------------------------------------------------------------------------- /static/src/partials/analytics.html: -------------------------------------------------------------------------------- 1 | <% if (gtmPropertyId.length > 0) { %> 2 | 21 | <% } %> 22 | -------------------------------------------------------------------------------- /static/src/js/components/CollectionDetails/CollectionDetailsHeader.scss: -------------------------------------------------------------------------------- 1 | .collection-details-header { 2 | &__primary { 3 | padding: 0.5rem 1rem; 4 | } 5 | 6 | &__title { 7 | display: inline; 8 | margin-right: 0.75rem; 9 | font-weight: 700; 10 | font-size: 1rem; 11 | vertical-align: middle; 12 | } 13 | 14 | &__title-link { 15 | position: relative; 16 | display: inline; 17 | white-space: nowrap; 18 | font-size: 0.875rem; 19 | vertical-align: middle; 20 | } 21 | 22 | &__short-name { 23 | font-size: 0.625rem; 24 | margin-right: $spacer/2; 25 | 26 | .panels--sm & { 27 | font-size: 0.6875rem; 28 | } 29 | } 30 | 31 | &__version-id { 32 | font-size: 0.625rem; 33 | background-color: $color__purple; 34 | 35 | .panels--sm & { 36 | font-size: 0.6875rem; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /static/src/js/components/OrderProgressList/OrderProgressList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import OrderProgressItem from './OrderProgressItem' 5 | 6 | import './OrderProgressList.scss' 7 | 8 | export const OrderProgressList = ({ 9 | orders 10 | }) => ( 11 |
    12 | { 13 | orders.map((order) => { 14 | const { order_number: orderNumber } = order 15 | 16 | if (orderNumber == null) return null 17 | 18 | return ( 19 | 23 | ) 24 | }) 25 | } 26 |
27 | ) 28 | 29 | OrderProgressList.propTypes = { 30 | orders: PropTypes.arrayOf( 31 | PropTypes.shape({}) 32 | ).isRequired 33 | } 34 | 35 | export default OrderProgressList 36 | -------------------------------------------------------------------------------- /static/src/js/components/Dropzone/ShapefileDropzone.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { PropTypes } from 'prop-types' 3 | 4 | import classNames from 'classnames' 5 | 6 | import withDropzone from './withDropzone' 7 | import './ShapefileDropzone.scss' 8 | 9 | export class ShapefileDropzone extends Component { 10 | constructor(props) { 11 | super(props) 12 | this.ref = React.createRef() 13 | } 14 | 15 | render() { 16 | const { className } = this.props 17 | const classes = classNames([ 18 | 'shapefile-dropzone', 19 | className 20 | ]) 21 | 22 | return ( 23 |
27 | ) 28 | } 29 | } 30 | 31 | ShapefileDropzone.propTypes = { 32 | className: PropTypes.string.isRequired 33 | } 34 | 35 | export default withDropzone(ShapefileDropzone) 36 | -------------------------------------------------------------------------------- /serverless/src/util/echoForms/getNameValuePairsForResample.js: -------------------------------------------------------------------------------- 1 | import { getFieldElementValue } from './getFieldElementValue' 2 | 3 | /** 4 | * Get resampling parameters from the provided XML Document 5 | * @param {String} xmlDocument ECHO Form xml as a string 6 | */ 7 | export const getNameValuePairsForResample = (xmlDocument) => { 8 | const resampleFields = ['RESAMPLE'] 9 | 10 | const resampleFieldValues = {} 11 | 12 | resampleFields.forEach((fieldName) => { 13 | const valueField = getFieldElementValue(xmlDocument, `${fieldName}/*[contains(local-name(),'value')]`) 14 | const dimensionField = getFieldElementValue(xmlDocument, `${fieldName}/*[contains(local-name(),'dimension')]`) 15 | 16 | if (dimensionField && valueField) { 17 | resampleFieldValues[fieldName] = `${dimensionField}:${valueField}` 18 | } 19 | }) 20 | 21 | return resampleFieldValues 22 | } 23 | -------------------------------------------------------------------------------- /sharedUtils/prepKeysForCmr.js: -------------------------------------------------------------------------------- 1 | import { stringify } from 'qs' 2 | 3 | /** 4 | * Create a query string containing both indexed and non-indexed keys. 5 | * @param {object} queryParams - An object containing all queryParams. 6 | * @param {array} nonIndexedKeys - An array of strings that represent the keys which should not be indexed. 7 | * @return {string} A query string containing both indexed and non-indexed keys. 8 | */ 9 | export const prepKeysForCmr = (queryParams, nonIndexedKeys = []) => { 10 | const nonIndexedAttrs = {} 11 | const indexedAttrs = { ...queryParams } 12 | 13 | nonIndexedKeys.forEach((key) => { 14 | nonIndexedAttrs[key] = indexedAttrs[key] 15 | delete indexedAttrs[key] 16 | }) 17 | 18 | return [ 19 | stringify(indexedAttrs), 20 | stringify(nonIndexedAttrs, { indices: false, arrayFormat: 'brackets' }) 21 | ].filter(Boolean).join('&') 22 | } 23 | -------------------------------------------------------------------------------- /static/src/js/components/SpatialDisplay/__tests__/SpatialDisplayEntry.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Enzyme, { shallow } from 'enzyme' 3 | import Adapter from 'enzyme-adapter-react-16' 4 | import SpatialDisplayEntry from '../SpatialDisplayEntry' 5 | 6 | Enzyme.configure({ adapter: new Adapter() }) 7 | 8 | function setup() { 9 | const props = { 10 | type: 'start' 11 | } 12 | 13 | const enzymeWrapper = shallow( 14 | 15 | 38.79165, -77.11976 16 | 17 | ) 18 | 19 | return { 20 | enzymeWrapper, 21 | props 22 | } 23 | } 24 | 25 | describe('SpatialDisplayEntry component', () => { 26 | test('with valid type and children should render correctly', () => { 27 | const { enzymeWrapper } = setup() 28 | 29 | expect(enzymeWrapper.text()).toEqual('38.79165, -77.11976') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /static/src/js/util/url/__tests__/granuleDataFormatFacetUrl.test.js: -------------------------------------------------------------------------------- 1 | import { decodeUrlParams, encodeUrlQuery } from '../url' 2 | 3 | import { emptyDecodedResult } from './url.mocks' 4 | 5 | describe('url#decodeUrlParams', () => { 6 | test('decodes granuleDataFormatFacets correctly', () => { 7 | const expectedResult = { 8 | ...emptyDecodedResult, 9 | cmrFacets: { 10 | granule_data_format_h: ['Binary'] 11 | } 12 | } 13 | expect(decodeUrlParams('?gdf=Binary')).toEqual(expectedResult) 14 | }) 15 | }) 16 | 17 | describe('url#encodeUrlQuery', () => { 18 | test('encodes granuleDataFormatFacets correctly', () => { 19 | const props = { 20 | hasGranulesOrCwic: true, 21 | pathname: '/path/here', 22 | granuleDataFormatFacets: ['Binary'] 23 | } 24 | expect(encodeUrlQuery(props)).toEqual('/path/here?gdf=Binary') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /static/src/js/util/url/__tests__/projectFacetUrl.test.js: -------------------------------------------------------------------------------- 1 | import { decodeUrlParams, encodeUrlQuery } from '../url' 2 | 3 | import { emptyDecodedResult } from './url.mocks' 4 | 5 | describe('url#decodeUrlParams', () => { 6 | test('decodes projectFacets correctly', () => { 7 | const expectedResult = { 8 | ...emptyDecodedResult, 9 | cmrFacets: { 10 | project_h: ['facet 1', 'facet 2'] 11 | } 12 | } 13 | expect(decodeUrlParams('?fpj=facet%201!facet%202')).toEqual(expectedResult) 14 | }) 15 | }) 16 | 17 | describe('url#encodeUrlQuery', () => { 18 | test('encodes projectFacets correctly', () => { 19 | const props = { 20 | hasGranulesOrCwic: true, 21 | pathname: '/path/here', 22 | projectFacets: ['facet 1', 'facet 2'] 23 | } 24 | expect(encodeUrlQuery(props)).toEqual('/path/here?fpj=facet%201!facet%202') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /serverless/src/util/__tests__/pick.test.js: -------------------------------------------------------------------------------- 1 | import { pick } from '../pick' 2 | 3 | beforeEach(() => { 4 | jest.clearAllMocks() 5 | }) 6 | 7 | describe('util#pick', () => { 8 | test('correctly returns when null is provided', () => { 9 | const data = pick(null, ['a']) 10 | 11 | expect(Object.keys(data).sort()).toEqual([]) 12 | }) 13 | 14 | test('correctly returns when undefined is provided', () => { 15 | const data = pick(undefined, ['a']) 16 | 17 | expect(Object.keys(data).sort()).toEqual([]) 18 | }) 19 | 20 | test('correctly picks whitelisted keys', () => { 21 | const object = { 22 | a: 1, 23 | b: 2, 24 | array: [1, 2, 3], 25 | obj: { c: 3 } 26 | } 27 | const desiredKeys = ['array', 'b'] 28 | 29 | const data = pick(object, desiredKeys) 30 | 31 | expect(Object.keys(data).sort()).toEqual(['array', 'b']) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /static/src/js/util/itemToRowColumnIndicies.js: -------------------------------------------------------------------------------- 1 | import { isNumber } from 'lodash' 2 | 3 | /** 4 | * @typedef {Object} Indicies 5 | * @property {Number} rowIndex - The X Coordinate 6 | * @property {Number} columnIndex - The Y Coordinate 7 | */ 8 | 9 | /** 10 | * Given the zero-based index in a list and a number of columns, returns an object 11 | * containing the zero-based row and column indicies of the item. 12 | * @param {Number} index The number to commafy. 13 | * @param {Number} numColumns The number to commafy. 14 | * @returns {Indicies} The item location. 15 | */ 16 | export const itemToRowColumnIndicies = (index, numColumns) => { 17 | if (!isNumber(index) || !isNumber(numColumns)) return undefined 18 | 19 | const rowIndex = Math.floor(index / numColumns) 20 | const columnIndex = index % numColumns 21 | 22 | return { 23 | rowIndex, 24 | columnIndex 25 | } 26 | } 27 | --------------------------------------------------------------------------------