├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ ├── checks.yml │ ├── deploy.yml │ └── trigger-workflow.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .stylelintrc.json ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── app ├── assets │ ├── fonts │ │ └── TitilliumWeb.woff2 │ ├── graphics │ │ ├── content │ │ │ ├── .gitkeep │ │ │ ├── app-logo.svg │ │ │ ├── guide-analysis.png │ │ │ ├── guide-aoi.png │ │ │ ├── guide-checkpoint.png │ │ │ ├── guide-compare.png │ │ │ ├── guide-export.png │ │ │ ├── guide-home.png │ │ │ ├── guide-imagery.png │ │ │ ├── guide-models.png │ │ │ ├── guide-mosaic.png │ │ │ ├── guide-naip.png │ │ │ ├── guide-predictions.png │ │ │ ├── guide-refine.png │ │ │ ├── guide-retrain.png │ │ │ ├── home-bg--largeUp.jpg │ │ │ ├── logo_devseed.svg │ │ │ └── pearl_prediction_op.gif │ │ ├── layout │ │ │ └── .gitkeep │ │ └── meta │ │ │ ├── .gitkeep │ │ │ ├── android-chrome.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── default-meta-image.png │ │ │ └── favicon.png │ ├── icons │ │ ├── brand-development-seed.svg │ │ └── collecticons │ │ │ ├── arrow-loop.svg │ │ │ ├── arrow-semi-spin-ccw.svg │ │ │ ├── brand-osm.svg │ │ │ ├── chevron-down--small.svg │ │ │ ├── chevron-left--small.svg │ │ │ ├── chevron-right--small.svg │ │ │ ├── circle-question.svg │ │ │ ├── clipboard.svg │ │ │ ├── download.svg │ │ │ ├── eraser.svg │ │ │ ├── expand-from-left.svg │ │ │ ├── expand-from-right.svg │ │ │ ├── expand-top-right.svg │ │ │ ├── house.svg │ │ │ ├── polygon.svg │ │ │ ├── resize-center-horizontal.svg │ │ │ ├── save-disk.svg │ │ │ ├── shrink-to-left.svg │ │ │ ├── shrink-to-right.svg │ │ │ ├── upload.svg │ │ │ └── user.svg │ ├── scripts │ │ ├── components │ │ │ ├── about │ │ │ │ └── index.js │ │ │ ├── admin │ │ │ │ ├── models │ │ │ │ │ ├── index.js │ │ │ │ │ ├── new.js │ │ │ │ │ ├── upload.js │ │ │ │ │ └── view.js │ │ │ │ └── users │ │ │ │ │ └── index.js │ │ │ ├── common │ │ │ │ ├── abort-batch-button.js │ │ │ │ ├── app.js │ │ │ │ ├── auto-focus-form-input.js │ │ │ │ ├── card-list.js │ │ │ │ ├── custom-modal │ │ │ │ │ ├── index.js │ │ │ │ │ └── styled.js │ │ │ │ ├── details-list.js │ │ │ │ ├── faux-file-dialog.js │ │ │ │ ├── forms │ │ │ │ │ ├── form-group-structure.js │ │ │ │ │ ├── input-color.js │ │ │ │ │ ├── input-file.js │ │ │ │ │ ├── input-select.js │ │ │ │ │ ├── input-switch.js │ │ │ │ │ └── input-text.js │ │ │ │ ├── global-loading │ │ │ │ │ ├── index.js │ │ │ │ │ └── styles.js │ │ │ │ ├── info-button.js │ │ │ │ ├── map │ │ │ │ │ ├── base-map-layer.js │ │ │ │ │ ├── center-map.js │ │ │ │ │ ├── constants.js │ │ │ │ │ ├── generic-control.js │ │ │ │ │ ├── geocoder.js │ │ │ │ │ ├── geojson-layer.js │ │ │ │ │ ├── osm-qa-layer.js │ │ │ │ │ └── tile-layer.js │ │ │ │ ├── meta-tags.js │ │ │ │ ├── modal-wrapper.js │ │ │ │ ├── page-header.js │ │ │ │ ├── paginator.js │ │ │ │ ├── panel-block.js │ │ │ │ ├── panel.js │ │ │ │ ├── shadow-scrollbar.js │ │ │ │ ├── size-aware-element.js │ │ │ │ ├── tabbed-block-body.js │ │ │ │ ├── table.js │ │ │ │ ├── toasts.js │ │ │ │ ├── tooltip.js │ │ │ │ ├── tour.js │ │ │ │ └── user-dropdown.js │ │ │ ├── compare-map │ │ │ │ ├── SideBySideTileLayer.js │ │ │ │ ├── index.js │ │ │ │ └── leaflet-side-by-side.js │ │ │ ├── home │ │ │ │ └── index.js │ │ │ ├── profile │ │ │ │ ├── project │ │ │ │ │ ├── batch-list.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── project-card.js │ │ │ │ │ ├── project-map.js │ │ │ │ │ └── project.js │ │ │ │ └── projects │ │ │ │ │ ├── index.js │ │ │ │ │ └── projects.js │ │ │ ├── project │ │ │ │ ├── aoi-modal-dialog.js │ │ │ │ ├── header │ │ │ │ │ ├── index.js │ │ │ │ │ └── shortcut-help.js │ │ │ │ ├── index.js │ │ │ │ ├── layers-panel.js │ │ │ │ ├── main.js │ │ │ │ ├── map │ │ │ │ │ ├── freehand-draw-control.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── polygon-draw-control.js │ │ │ │ ├── prime-panel │ │ │ │ │ ├── footer │ │ │ │ │ │ ├── batch-prediction-panel.js │ │ │ │ │ │ ├── prime-button.js │ │ │ │ │ │ └── save-checkpoint-button.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── retrain-refine-styles.js │ │ │ │ │ ├── selection-styles.js │ │ │ │ │ ├── tabs │ │ │ │ │ │ ├── predict │ │ │ │ │ │ │ ├── aoi-selector │ │ │ │ │ │ │ │ ├── action-buttons.js │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ └── modals │ │ │ │ │ │ │ │ │ ├── confirm-aoi-change.js │ │ │ │ │ │ │ │ │ └── confirm-aoi-delete.js │ │ │ │ │ │ │ ├── checkpoint-selector.js │ │ │ │ │ │ │ ├── imagery-source-selector │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ └── modal.js │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── model-selector │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ ├── modal.js │ │ │ │ │ │ │ │ └── model-card.js │ │ │ │ │ │ │ └── mosaic-selector │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ └── modal │ │ │ │ │ │ │ │ ├── content.js │ │ │ │ │ │ │ │ ├── form.js │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ └── map-preview.js │ │ │ │ │ │ └── retrain │ │ │ │ │ │ │ ├── add-class.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ └── upload-aoi-modal.js │ │ │ │ └── sec-panel │ │ │ │ │ ├── class-analytics-chart.js │ │ │ │ │ └── index.js │ │ │ ├── public-maps │ │ │ │ └── index.js │ │ │ ├── share-map │ │ │ │ ├── index.js │ │ │ │ └── layers-control.js │ │ │ └── uhoh │ │ │ │ └── index.js │ │ ├── config.js │ │ ├── config │ │ │ ├── base.js │ │ │ ├── cypress.js │ │ │ ├── production.js │ │ │ ├── reforestamos-prod.js │ │ │ ├── reforestamos.js │ │ │ ├── staging.js │ │ │ └── testing.js │ │ ├── context │ │ │ └── auth.js │ │ ├── fatal-error-boundary.js │ │ ├── fsm │ │ │ └── project │ │ │ │ ├── actions.js │ │ │ │ ├── constants.js │ │ │ │ ├── guards.js │ │ │ │ ├── index.js │ │ │ │ ├── machine.js │ │ │ │ ├── selectors.js │ │ │ │ ├── services.js │ │ │ │ └── websocket-client.js │ │ ├── history.js │ │ ├── main.js │ │ ├── styles │ │ │ ├── animation.js │ │ │ ├── button.js │ │ │ ├── collecticons │ │ │ │ └── index.js │ │ │ ├── dropdown.js │ │ │ ├── global.js │ │ │ ├── inpage.js │ │ │ ├── links.js │ │ │ ├── local-button.js │ │ │ ├── page.js │ │ │ ├── panel.js │ │ │ ├── placeholder.js │ │ │ ├── skins.js │ │ │ ├── theme.js │ │ │ ├── type │ │ │ │ ├── definition-list.js │ │ │ │ ├── heading.js │ │ │ │ └── prose.js │ │ │ ├── utils │ │ │ │ └── general.js │ │ │ └── vendor │ │ │ │ ├── joyridestyles.js │ │ │ │ ├── leaflet-side-by-side.js │ │ │ │ ├── leaflet.js │ │ │ │ ├── react-input-range.js │ │ │ │ └── react-toastify.js │ │ └── utils │ │ │ ├── api-health.js │ │ │ ├── azure-app-insights.js │ │ │ ├── compose-components.js │ │ │ ├── copy-text-to-clipboard.js │ │ │ ├── dates.js │ │ │ ├── fancy-styled.js │ │ │ ├── format.js │ │ │ ├── is-rectangle.js │ │ │ ├── local-storage.js │ │ │ ├── logger.js │ │ │ ├── machine-state-logger.js │ │ │ ├── map.js │ │ │ ├── mosaics.js │ │ │ ├── pagination-options.js │ │ │ ├── rest-api-client.js │ │ │ ├── reverse-geocode.js │ │ │ ├── share-link.js │ │ │ ├── use-fetch.js │ │ │ ├── use-focus.js │ │ │ ├── use-pc-collection.js │ │ │ ├── use-previous.js │ │ │ └── utils.js │ └── types.js ├── humans.txt ├── index.html ├── manifest.json └── robots.txt ├── cypress-stress.json ├── cypress.config.js ├── cypress ├── e2e │ ├── about.cy.js │ ├── auth.cy.js │ ├── home.cy.js │ ├── project │ │ ├── aois.cy.js │ │ ├── batch.cy.js │ │ ├── delete.cy.js │ │ ├── gpu.cy.js │ │ ├── keyboard-shortcuts.cy.js │ │ ├── layers-panel.cy.js │ │ ├── new.cy.js │ │ ├── panel.cy.js │ │ ├── retrain.cy.js │ │ └── sec-panel.cy.js │ ├── stress-live-api.cy.js │ └── users.cy.js ├── fixtures │ ├── aoi-2088.json │ ├── aoi-upload │ │ ├── aoi-empty-collection.geojson │ │ ├── aoi-outside-usa.geojson │ │ ├── aoi-zero-area.geojson │ │ ├── live-inferencing.geojson │ │ ├── no-live-inferencing.geojson │ │ └── really-large-area.geojson │ ├── aois-with-batch.json │ ├── aois.0.json │ ├── aois.1.json │ ├── aois.2.json │ ├── geocoder │ │ ├── dc.json │ │ ├── ocean.json │ │ └── rural.json │ ├── planetary-computer │ │ ├── data │ │ │ └── mosaic │ │ │ │ └── register.json │ │ └── stac │ │ │ └── collections │ │ │ └── sentinel-2-l2a.json │ ├── samples.geojson │ ├── tiles │ │ ├── imagery-tile.png │ │ ├── jpeg-tile.jpeg │ │ ├── osm-tile.png │ │ └── png-tile.png │ └── websocket-workflow │ │ ├── base-model-prediction.json │ │ ├── load-aoi.json │ │ ├── retrain-one-sample-aborted.json │ │ ├── retrain.json │ │ └── run-prediction-aborted.json ├── plugins │ └── index.js └── support │ ├── commands │ ├── fake-login.js │ └── mock-api-routes │ │ ├── common.js │ │ ├── fixtures │ │ ├── health.json │ │ ├── imagery.json │ │ ├── index.json │ │ ├── model │ │ │ ├── 1.json │ │ │ ├── 2.json │ │ │ └── index.json │ │ ├── mosaic │ │ │ ├── index.json │ │ │ ├── naip.latest.json │ │ │ └── post-response.json │ │ └── project │ │ │ ├── 1 │ │ │ ├── aoi │ │ │ │ ├── 1 │ │ │ │ │ ├── get.json │ │ │ │ │ ├── post.json │ │ │ │ │ └── timeframe │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ ├── get.json │ │ │ │ │ │ └── tiles │ │ │ │ │ │ │ └── get.json │ │ │ │ │ │ └── 2.json │ │ │ │ ├── 2 │ │ │ │ │ └── get.json │ │ │ │ ├── 3 │ │ │ │ │ └── get.json │ │ │ │ └── index.json │ │ │ ├── checkpoint │ │ │ │ ├── 1.json │ │ │ │ ├── 2.json │ │ │ │ ├── 3.json │ │ │ │ └── index.json │ │ │ ├── get.json │ │ │ ├── instance │ │ │ │ ├── 1.json │ │ │ │ └── index.json │ │ │ ├── patch.json │ │ │ └── post.json │ │ │ └── index.json │ │ ├── models.js │ │ ├── projects.js │ │ └── utils.js │ └── e2e.js ├── gulpfile.js ├── package.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js}] 14 | charset = utf-8 15 | 16 | # Indentation override for all JS under lib directory 17 | [lib/**.js] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | # Matches the exact files either package.json or .travis.yml 22 | [{package.json,.travis.yml}] 23 | indent_style = space 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", // Specifies the ESLint parser 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:react/recommended", 6 | "plugin:prettier/recommended", 7 | "plugin:cypress/recommended" 8 | ], 9 | "settings": { 10 | "react": { 11 | "version": "detect" 12 | } 13 | }, 14 | "env": { 15 | "browser": true, 16 | "node": true, 17 | "es6": true 18 | }, 19 | "plugins": ["react", "react-hooks", "inclusive-language"], 20 | "parserOptions": { 21 | "ecmaFeatures": { 22 | "jsx": true 23 | }, 24 | "ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features 25 | "sourceType": "module" // Allows for the use of imports 26 | }, 27 | "rules": { 28 | "prettier/prettier": ["error", {"semi": true, "singleQuote": true, "jsxSingleQuote": true, "parser": "flow"}], 29 | "comma-dangle": 0, 30 | "inclusive-language/use-inclusive-words": "error", 31 | "semi": [2, "always"], 32 | "jsx-quotes": [2, "prefer-single"], 33 | "no-console": 2, 34 | "no-extra-semi": 2, 35 | "semi-spacing": [2, { "before": false, "after": true }], 36 | "no-dupe-else-if": 0, 37 | "no-setter-return": 0, 38 | "prefer-promise-reject-errors": 0, 39 | "react/button-has-type": 2, 40 | "react/default-props-match-prop-types": 2, 41 | "react/jsx-closing-bracket-location": 2, 42 | "react/jsx-closing-tag-location": 2, 43 | "react/jsx-curly-spacing": 2, 44 | "react/jsx-curly-newline": 0, 45 | "react/jsx-equals-spacing": 2, 46 | "react/jsx-max-props-per-line": [2, { "maximum": 1, "when": "multiline"}], 47 | "react/jsx-first-prop-new-line": 2, 48 | "react/jsx-curly-brace-presence": [2, { "props": "never", "children": "never" }], 49 | "react/jsx-pascal-case": 2, 50 | "react/jsx-props-no-multi-spaces": 2, 51 | "react/jsx-tag-spacing": [2, { "beforeClosing": "never" }], 52 | "react/jsx-wrap-multilines": 2, 53 | "react/no-array-index-key": 2, 54 | "react/no-typos": 2, 55 | "react/no-unsafe": 2, 56 | "react/no-unused-prop-types": 2, 57 | "react/no-unused-state": 2, 58 | "react/self-closing-comp": 2, 59 | "react/sort-comp": 2, 60 | "react/style-prop-object": 2, 61 | "react/void-dom-elements-no-children": 2, 62 | "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks 63 | "react-hooks/exhaustive-deps": 0 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.github/workflows/trigger-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Invoke workflow in another repo with inputs 2 | on: 3 | push: 4 | branches: 5 | - 'develop' 6 | - 'v2' 7 | jobs: 8 | trigger-deploy: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 60 11 | steps: 12 | - name: Trigger deploy workflow 13 | uses: benc-uk/workflow-dispatch@v1 14 | with: 15 | workflow: frontend-staging.yml 16 | repo: developmentseed/pearl-reforestamos-deploy 17 | inputs: '{ "commit": "${{ github.event.after }}", "branch": "${{ github.ref }}" }' 18 | ref: main 19 | token: ${{ secrets.PAT }} 20 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "jsxSingleQuote": true, 5 | "printWidth": 80 6 | } -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended" 4 | ], 5 | "rules": { 6 | "font-family-no-missing-generic-family-keyword": null 7 | } 8 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Launch Chrome against localhost", 8 | "url": "http://localhost:9000", 9 | "webRoot": "${workspaceFolder}", 10 | "sourceMapPathOverrides": { 11 | "*": "${workspaceFolder}/*" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.eslintIntegration": true, 3 | "prettier.stylelintIntegration": true, 4 | "css.validate": false, 5 | "scss.validate": false, 6 | "stylelint.enable": true 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Development Seed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/assets/fonts/TitilliumWeb.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/fonts/TitilliumWeb.woff2 -------------------------------------------------------------------------------- /app/assets/graphics/content/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/.gitkeep -------------------------------------------------------------------------------- /app/assets/graphics/content/app-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-analysis.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-aoi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-aoi.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-checkpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-checkpoint.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-compare.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-export.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-home.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-imagery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-imagery.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-models.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-models.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-mosaic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-mosaic.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-naip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-naip.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-predictions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-predictions.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-refine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-refine.png -------------------------------------------------------------------------------- /app/assets/graphics/content/guide-retrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/guide-retrain.png -------------------------------------------------------------------------------- /app/assets/graphics/content/home-bg--largeUp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/home-bg--largeUp.jpg -------------------------------------------------------------------------------- /app/assets/graphics/content/pearl_prediction_op.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/content/pearl_prediction_op.gif -------------------------------------------------------------------------------- /app/assets/graphics/layout/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/layout/.gitkeep -------------------------------------------------------------------------------- /app/assets/graphics/meta/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/meta/.gitkeep -------------------------------------------------------------------------------- /app/assets/graphics/meta/android-chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/meta/android-chrome.png -------------------------------------------------------------------------------- /app/assets/graphics/meta/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/meta/apple-touch-icon.png -------------------------------------------------------------------------------- /app/assets/graphics/meta/default-meta-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/meta/default-meta-image.png -------------------------------------------------------------------------------- /app/assets/graphics/meta/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/app/assets/graphics/meta/favicon.png -------------------------------------------------------------------------------- /app/assets/icons/brand-development-seed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/arrow-loop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/arrow-semi-spin-ccw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/chevron-down--small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/chevron-left--small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/chevron-right--small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/circle-question.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | eraser 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/expand-from-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/expand-from-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/expand-top-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/house.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/resize-center-horizontal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/save-disk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/shrink-to-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/shrink-to-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/icons/collecticons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/abort-batch-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import T from 'prop-types'; 3 | import { Button } from '@devseed-ui/button'; 4 | 5 | import { hideGlobalLoading, showGlobalLoadingMessage } from './global-loading'; 6 | import toasts from './toasts'; 7 | import { useAuth } from '../../context/auth'; 8 | import logger from '../../utils/logger'; 9 | 10 | export const AbortBatchJobButton = ({ 11 | projectId, 12 | batchId, 13 | compact = false, 14 | disabled = false, 15 | afterOnClickFn = () => {}, 16 | }) => { 17 | const { restApiClient } = useAuth(); 18 | 19 | return ( 20 | 47 | ); 48 | }; 49 | 50 | AbortBatchJobButton.propTypes = { 51 | projectId: T.number, 52 | batchId: T.number, 53 | compact: T.bool, 54 | disabled: T.bool, 55 | afterOnClickFn: T.func, 56 | }; 57 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/app.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import T from 'prop-types'; 3 | import { withRouter } from 'react-router'; 4 | import MetaTags from './meta-tags'; 5 | import SizeAwareElement from './size-aware-element'; 6 | import { Page } from '../../styles/page'; 7 | import config from '../../config'; 8 | import checkApiHealth from '../../utils/api-health'; 9 | 10 | const { appTitle, appDescription, environment } = config; 11 | 12 | const App = (props) => { 13 | const { location, pageTitle, children, hideFooter } = props; 14 | const title = pageTitle ? `${pageTitle} — ` : ''; 15 | 16 | // Handle cases where the page is updated without changing 17 | useEffect(() => { 18 | window.scrollTo(0, 0); 19 | }, [location]); 20 | 21 | // Check API health and fetch API meta on page mount 22 | useEffect(() => { 23 | checkApiHealth(); 24 | }, []); 25 | 26 | return ( 27 | 28 | 29 | {children} 30 | 31 | ); 32 | }; 33 | 34 | App.propTypes = { 35 | children: T.node, 36 | location: T.object, 37 | hideFooter: T.bool, 38 | pageTitle: T.string, 39 | }; 40 | 41 | const AppWrapper = (InnerApp) => { 42 | // Avoid importing Application Insights in development 43 | if (environment === 'production' || environment === 'staging') { 44 | const withAITracking = require('@microsoft/applicationinsights-react-js') 45 | .withAITracking; 46 | const reactPlugin = require('../../utils/azure-app-insights').reactPlugin; 47 | return withRouter(withAITracking(reactPlugin, InnerApp)); 48 | } 49 | 50 | return withRouter(InnerApp); 51 | }; 52 | 53 | export default AppWrapper(App); 54 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/auto-focus-form-input.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import T from 'prop-types'; 3 | 4 | import { FormInput } from '@devseed-ui/form'; 5 | 6 | export const AutoFocusFormInput = ({ 7 | value, 8 | setValue, 9 | placeholder, 10 | inputId, 11 | }) => { 12 | const inputRef = useRef(); 13 | 14 | useEffect(() => { 15 | inputRef.current.focus(); 16 | }, []); 17 | 18 | return ( 19 | { 25 | e.stopPropagation(); 26 | }} 27 | onChange={(e) => { 28 | setValue(e.target.value); 29 | }} 30 | /> 31 | ); 32 | }; 33 | AutoFocusFormInput.propTypes = { 34 | value: T.string, 35 | setValue: T.func, 36 | placeholder: T.string, 37 | inputId: T.string, 38 | }; 39 | 40 | export default AutoFocusFormInput; 41 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/details-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import T from 'prop-types'; 4 | import { Heading } from '@devseed-ui/typography'; 5 | import { toTitleCase } from '../../utils/format'; 6 | import { Subheading } from '../../styles/type/heading'; 7 | const List = styled.ol` 8 | display: grid; 9 | grid-gap: 0.25rem; 10 | li { 11 | display: grid; 12 | grid-gap: 1rem; 13 | grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); 14 | align-items: center; 15 | h1 { 16 | margin: 0; 17 | } 18 | } 19 | ${Heading} { 20 | letter-spacing: ${({ useAlt }) => useAlt && '0.5px'}; 21 | } 22 | dd { 23 | overflow: hidden; 24 | text-overflow: ellipsis; 25 | } 26 | `; 27 | 28 | function DetailsList(props) { 29 | const { details, styles } = props; 30 | return ( 31 | 32 | {Object.entries(details).map(([key, value]) => ( 33 |
  • 34 | <> 35 | {toTitleCase(key)} 36 | {React.isValidElement(value) ? value :
    {value}
    } 37 | 38 |
  • 39 | ))} 40 |
    41 | ); 42 | } 43 | 44 | DetailsList.propTypes = { 45 | details: T.object, 46 | styles: T.object, 47 | }; 48 | 49 | export default DetailsList; 50 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/faux-file-dialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a file input triggered by the element returned by the children 3 | * 4 | * @prop {string} id Id for the file input 5 | * @prop {string} name Name for the file input 6 | * @prop {func} onFileSelect Callback for when the file is selected. Called with the selected file. 7 | * @prop {func} children Render function for the trigger element 8 | */ 9 | import React, { useCallback, useRef } from 'react'; 10 | import T from 'prop-types'; 11 | import styled from 'styled-components'; 12 | import { visuallyHidden } from '@devseed-ui/theme-provider'; 13 | 14 | const FileInput = styled.input` 15 | ${visuallyHidden()} 16 | `; 17 | 18 | export function FauxFileDialog({ 19 | name, 20 | id, 21 | onFileSelect, 22 | children, 23 | 'data-cy': dataCy, 24 | }) { 25 | const fileInputRef = useRef(null); 26 | 27 | const onUploadClick = useCallback(() => fileInputRef.current.click(), []); 28 | const onChangeFile = useCallback( 29 | (e) => { 30 | const file = e.target.files[0]; 31 | e.target.value = ''; 32 | onFileSelect(file); 33 | }, 34 | [onFileSelect] 35 | ); 36 | 37 | if (typeof children !== 'function') { 38 | throw new Error(' expects a single function child'); 39 | } 40 | 41 | return ( 42 | 43 | {children({ onClick: onUploadClick })} 44 | 52 | 53 | ); 54 | } 55 | 56 | FauxFileDialog.propTypes = { 57 | name: T.string, 58 | id: T.string, 59 | onFileSelect: T.func, 60 | children: T.func, 61 | 'data-cy': T.string, 62 | }; 63 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/forms/input-file.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PropTypes as T } from 'prop-types'; 3 | import { FormInput } from '@devseed-ui/form'; 4 | 5 | import FormGroupStructure from './form-group-structure'; 6 | 7 | /** 8 | * Text input with form group structure. 9 | * 10 | * @prop {string} id Input field id 11 | * @prop {string} name Input field name 12 | * @prop {string} label Label for the input 13 | * @prop {function|string} labelHint Hint for the label. Setting it to true 14 | * shows (optional) 15 | * @prop {mixed} value Input value 16 | * @prop {string} inputSize Styled input size option 17 | * @prop {string} inputVariation Styled input variation option 18 | * @prop {boolean} invalid If value is invalid or not 19 | * @prop {function} onChange On change event handler 20 | * @prop {string} placeholder Input placeholder value. 21 | * @prop {string} description Field description shown in a tooltip 22 | * @prop {node} helper Helper message shown below input. 23 | */ 24 | export function InputFile(props) { 25 | const { 26 | id, 27 | label, 28 | labelHint, 29 | className, 30 | inputSize, 31 | inputVariation, 32 | description, 33 | helper, 34 | inputRef, 35 | invalid, 36 | name, 37 | value, 38 | onChange, 39 | onBlur, 40 | accept, 41 | } = props; 42 | 43 | return ( 44 | 52 | 65 | 66 | ); 67 | } 68 | 69 | InputFile.propTypes = { 70 | className: T.string, 71 | description: T.string, 72 | helper: T.node, 73 | id: T.string, 74 | inputRef: T.object, 75 | inputSize: T.string, 76 | inputVariation: T.string, 77 | invalid: T.bool, 78 | label: T.string, 79 | labelHint: T.oneOfType([T.bool, T.func, T.string]), 80 | name: T.string, 81 | onBlur: T.func, 82 | onChange: T.func, 83 | value: T.oneOfType([T.string, T.number]), 84 | accept: T.string, 85 | }; 86 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/global-loading/styles.js: -------------------------------------------------------------------------------- 1 | import styled, { createGlobalStyle, keyframes } from 'styled-components'; 2 | import { rgba } from 'polished'; 3 | 4 | import { 5 | themeVal, 6 | antialiased, 7 | glsp, 8 | stylizeFunction, 9 | } from '@devseed-ui/theme-provider'; 10 | import collecticon from '@devseed-ui/collecticons'; 11 | 12 | const _rgba = stylizeFunction(rgba); 13 | // Block the body scroll when the loading is visible 14 | export const BodyUnscrollable = createGlobalStyle` 15 | body { 16 | overflow-y: hidden; 17 | } 18 | `; 19 | 20 | const rotate360 = keyframes` 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | `; 28 | 29 | export const Spinner = styled.div` 30 | display: flex; 31 | margin: 0.5rem; 32 | &::before { 33 | ${collecticon('arrow-spin-cw')} 34 | animation: ${rotate360} 1s linear infinite; 35 | transform: translateZ(0); 36 | font-size: 1.5rem; 37 | } 38 | `; 39 | 40 | export const GlobalLoadingWrapper = styled.div` 41 | position: fixed; 42 | padding-top: 3.5rem; 43 | top: 0; 44 | left: 0; 45 | height: 100%; 46 | width: 100%; 47 | z-index: 9997; 48 | display: flex; 49 | justify-content: center; 50 | align-items: flex-start; 51 | flex-flow: row; 52 | cursor: not-allowed; 53 | background: linear-gradient( 54 | to bottom, 55 | transparent 0%, 56 | transparent 3.49rem, 57 | ${themeVal('color.baseDark')} 3.5rem, 58 | ${_rgba(themeVal('color.baseDark'), 0.8)} 16%, 59 | ${_rgba(themeVal('color.baseDark'), 0.32)} 32% 60 | ); 61 | 62 | &.overlay-loader-enter { 63 | transform: translate3d(0, 0, 0); 64 | transition: opacity 0.32s ease 0s, visibility 0.32s linear 0s; 65 | opacity: 0; 66 | visibility: hidden; 67 | 68 | &.overlay-loader-enter-active { 69 | opacity: 1; 70 | visibility: visible; 71 | } 72 | } 73 | 74 | &.overlay-loader-exit { 75 | transition: opacity 0.32s ease 0s, visibility 0.32s linear 0s; 76 | opacity: 1; 77 | visibility: visible; 78 | 79 | &.overlay-loader-exit-active { 80 | opacity: 0; 81 | visibility: hidden; 82 | } 83 | } 84 | `; 85 | 86 | export const Message = styled.p` 87 | ${antialiased()} 88 | margin-top: ${glsp()}; 89 | color: ${themeVal('color.base')}; 90 | `; 91 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/info-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import T from 'prop-types'; 3 | import { Button } from '@devseed-ui/button'; 4 | import { LocalButton } from '../../styles/local-button'; 5 | import { StyledTooltip } from './tooltip'; 6 | 7 | export const InfoButton = React.forwardRef((props, ref) => { 8 | const { info, id, useIcon, width, useLocalButton } = props; 9 | const ButtonType = useLocalButton ? LocalButton : Button; 10 | return ( 11 | <> 12 | 22 | {props.children} 23 | 24 | {info && ( 25 | 26 | {info} 27 | 28 | )} 29 | 30 | ); 31 | }); 32 | 33 | InfoButton.displayName = 'InfoButton'; 34 | InfoButton.propTypes = { 35 | info: T.oneOfType([T.string, T.bool]), 36 | id: T.string, 37 | children: T.node, 38 | useIcon: T.oneOfType([T.string, T.array]), 39 | useLocalButton: T.bool, 40 | width: T.string, 41 | 'data-cy': T.string, 42 | }; 43 | 44 | export default InfoButton; 45 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/map/base-map-layer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TileLayer } from 'react-leaflet'; 3 | import config from '../../../config'; 4 | 5 | export const MAX_BASE_MAP_ZOOM_LEVEL = 19; 6 | 7 | const { mapboxAccessToken } = config; 8 | 9 | export const BaseMapLayer = () => ( 10 | 15 | ); 16 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/map/center-map.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useMap } from 'react-leaflet'; 3 | import L from 'leaflet'; 4 | import { BOUNDS_PADDING } from './constants'; 5 | 6 | function CenterMap({ aoiRef }) { 7 | const map = useMap(); 8 | const CenterControl = L.Control.extend({ 9 | onAdd: function () { 10 | const container = L.DomUtil.create('div', 'leaflet-control leaflet-bar'); 11 | 12 | const button = L.DomUtil.create('a', 'centerMap', container); 13 | button.setAttribute('role', 'button'); 14 | button.setAttribute('href', '#'); 15 | button.setAttribute('title', 'Center Map'); 16 | button.onclick = () => 17 | map.fitBounds(aoiRef.getBounds(), { padding: BOUNDS_PADDING }); 18 | 19 | button.ondblclick = (e) => { 20 | e.preventDefault(); 21 | e.stopPropagation(); 22 | }; 23 | return container; 24 | }, 25 | // Don't need to do anything on remove 26 | onRemove: () => {}, 27 | }); 28 | useEffect(() => { 29 | const center = new CenterControl({ position: 'topleft' }); 30 | center.addTo(map); 31 | return () => center.remove(map); 32 | }, [map]); // eslint-disable-line react-hooks/exhaustive-deps 33 | return null; 34 | } 35 | 36 | export default CenterMap; 37 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/map/constants.js: -------------------------------------------------------------------------------- 1 | export const BOUNDS_PADDING = [25, 25]; 2 | 3 | export const MOSAIC_LAYER_OPACITY = 0.8; 4 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/map/generic-control.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useMap } from 'react-leaflet'; 3 | import L from 'leaflet'; 4 | 5 | function GenericControl({ position, onClick, id }) { 6 | const map = useMap(); 7 | const [cntrl, setCntrl] = useState(); 8 | 9 | const GenericControl = L.Control.extend({ 10 | options: { 11 | position: position || 'topleft', 12 | }, 13 | 14 | onAdd: function () { 15 | const container = L.DomUtil.create('div'); 16 | 17 | container.style.backgroundSize = '30px 30px'; 18 | container.style.width = '30px'; 19 | container.style.height = '30px'; 20 | 21 | container.className = 'generic-leaflet-control'; 22 | 23 | container.id = id; 24 | 25 | container.onclick = onClick; 26 | container.ondblclick = (e) => { 27 | e.preventDefault(); 28 | e.stopPropagation(); 29 | }; 30 | setCntrl(container); 31 | 32 | return container; 33 | }, 34 | }); 35 | 36 | useEffect(() => { 37 | map.addControl(new GenericControl()); 38 | }, [map]); 39 | 40 | useEffect(() => { 41 | if (cntrl) { 42 | cntrl.onclick = onClick; 43 | } 44 | }, [cntrl, onClick]); 45 | return null; 46 | } 47 | 48 | export default GenericControl; 49 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/map/geocoder.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import * as GeoSearch from 'leaflet-geosearch'; 3 | import config from '../../../config'; 4 | import { useMap } from 'react-leaflet'; 5 | 6 | const { bingApiKey } = config; 7 | 8 | function GeoCoder() { 9 | const map = useMap(); 10 | 11 | useEffect(() => { 12 | const search = new GeoSearch.GeoSearchControl({ 13 | showMarker: false, 14 | autoClose: true, 15 | resetButton: null, 16 | provider: new GeoSearch.BingProvider({ 17 | params: { 18 | key: bingApiKey, 19 | }, 20 | }), 21 | }); 22 | search.searchElement.input.onkeydown = (e) => e.stopPropagation(); 23 | map.addControl(search); 24 | }, [map]); 25 | return null; 26 | } 27 | 28 | export default GeoCoder; 29 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/map/geojson-layer.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useMap } from 'react-leaflet'; 3 | import L from 'leaflet'; 4 | import {} from 'leaflet.vectorgrid'; 5 | 6 | function GeoJSONLayer(props) { 7 | const { data, style, opacity, pointToLayer, pane } = props; 8 | const map = useMap(); 9 | const [layer, setLayer] = useState(null); 10 | 11 | useEffect(() => { 12 | const geolayer = L.geoJSON(data, { 13 | pointToLayer, 14 | pane: pane || 'overlayPane', 15 | }); 16 | geolayer.on('add', () => { 17 | setLayer(geolayer); 18 | geolayer.setStyle(style); 19 | }); 20 | 21 | geolayer.addTo(map); 22 | return () => { 23 | geolayer.clearLayers(); 24 | geolayer.remove(); 25 | setLayer(null); 26 | }; 27 | }, []); 28 | 29 | useEffect(() => { 30 | if (layer) { 31 | layer.clearLayers(); 32 | const geolayer = L.geoJSON(data, { 33 | pointToLayer, 34 | pane: pane || 'overlayPane', 35 | }); 36 | 37 | geolayer.on('add', () => { 38 | setLayer(geolayer); 39 | geolayer.setStyle(style); 40 | }); 41 | geolayer.addTo(map); 42 | return () => { 43 | geolayer.clearLayers(); 44 | geolayer.remove(); 45 | setLayer(null); 46 | }; 47 | } 48 | }, [data]); 49 | 50 | useEffect(() => { 51 | if (layer) { 52 | layer.setStyle({ 53 | fillOpacity: opacity, 54 | }); 55 | } 56 | }, [opacity, layer]); 57 | 58 | return null; 59 | } 60 | 61 | export default GeoJSONLayer; 62 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/meta-tags.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PropTypes as T } from 'prop-types'; 3 | import { Helmet } from 'react-helmet'; 4 | 5 | import theme from '../../styles/theme'; 6 | 7 | import config from '../../config'; 8 | const { environment, baseUrl, appTitle, twitterHandle } = config; 9 | 10 | const MetaTags = ({ title, description, children }) => { 11 | return ( 12 | 13 | {title} 14 | {description ? : null} 15 | 16 | {/* Theme color */} 17 | 18 | 19 | {/* Twitter */} 20 | 21 | 22 | 23 | {description ? ( 24 | 25 | ) : null} 26 | 30 | 31 | {/* Open Graph */} 32 | 33 | 34 | 35 | 36 | 40 | {description ? ( 41 | 42 | ) : null} 43 | 44 | {/* Additional children */} 45 | {children} 46 | 47 | ); 48 | }; 49 | 50 | if (environment !== 'production') { 51 | MetaTags.propTypes = { 52 | title: T.string, 53 | description: T.string, 54 | children: T.node, 55 | }; 56 | } 57 | 58 | export default MetaTags; 59 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/modal-wrapper.js: -------------------------------------------------------------------------------- 1 | import { glsp } from '@devseed-ui/theme-provider'; 2 | import styled from 'styled-components'; 3 | 4 | export const ModalWrapper = styled.div` 5 | display: grid; 6 | grid-template-areas: 7 | 'a a' 8 | 'b c'; 9 | grid-gap: ${glsp(1)}; 10 | padding: ${glsp()}; 11 | div { 12 | grid-area: a; 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/panel-block.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { rgba } from 'polished'; 3 | 4 | import { themeVal, stylizeFunction, glsp } from '@devseed-ui/theme-provider'; 5 | import { Heading } from '@devseed-ui/typography'; 6 | import ShadowScrollbar from '../common/shadow-scrollbar'; 7 | 8 | const _rgba = stylizeFunction(rgba); 9 | 10 | export const PanelBlock = styled.section` 11 | flex: 1; 12 | display: flex; 13 | flex-flow: column nowrap; 14 | 15 | position: relative; 16 | z-index: 10; 17 | box-shadow: 0 -1px 0 0 ${themeVal('color.baseAlphaB')}; 18 | `; 19 | 20 | export const PanelBlockHeader = styled.header` 21 | padding: 0 ${glsp(1.5)}; 22 | background: ${_rgba(themeVal('color.surface'), 0.64)}; 23 | position: relative; 24 | z-index: 10; 25 | `; 26 | 27 | export const PanelBlockFooter = styled.footer` 28 | padding: ${glsp()} ${glsp(1.5)}; 29 | box-shadow: 0px -1px 1px -1px ${themeVal('color.baseAlphaD')}; 30 | position: relative; 31 | z-index: 10; 32 | `; 33 | 34 | export const PanelBlockTitle = styled(Heading).attrs({ size: 'medium' })` 35 | margin: 0; 36 | color: ${themeVal('color.base')}; 37 | `; 38 | 39 | export const PanelBlockBody = styled.div` 40 | display: flex; 41 | flex-flow: column nowrap; 42 | justify-content: center; 43 | flex: 1; 44 | `; 45 | 46 | export const PanelBlockScroll = styled(ShadowScrollbar)` 47 | flex: 1; 48 | z-index: 1; 49 | background: ${_rgba(themeVal('color.surface'), 0.64)}; 50 | `; 51 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/size-aware-element.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import React from 'react'; 3 | import { PropTypes as T } from 'prop-types'; 4 | import elementResizeEvent from 'element-resize-event'; 5 | 6 | class SizeAwareElement extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.elRef = React.createRef(); 10 | 11 | this.width = null; 12 | this.height = null; 13 | 14 | this.resizeListener = this.resizeListener.bind(this); 15 | } 16 | 17 | componentDidMount() { 18 | elementResizeEvent(this.elRef.current, this.resizeListener); 19 | this.resizeListener(); 20 | } 21 | 22 | getSize() { 23 | if (!this.elRef.current) return; 24 | 25 | const { clientHeight, clientWidth } = this.elRef.current; 26 | const { 27 | paddingTop, 28 | paddingBottom, 29 | paddingLeft, 30 | paddingRight, 31 | } = getComputedStyle(this.elRef.current); 32 | return { 33 | width: clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight), 34 | height: clientHeight - parseFloat(paddingTop) - parseFloat(paddingBottom), 35 | }; 36 | } 37 | 38 | resizeListener() { 39 | const { width, height } = this.getSize(); 40 | if (width !== this.width || height !== this.height) { 41 | this.width = width; 42 | this.height = height; 43 | this.props.onChange({ width, height }); 44 | } 45 | } 46 | 47 | render() { 48 | const { element: Element, children, ...rest } = this.props; 49 | return ( 50 | 51 | {children} 52 | 53 | ); 54 | } 55 | } 56 | 57 | SizeAwareElement.propTypes = { 58 | onChange: T.func, 59 | element: T.elementType, 60 | children: T.node, 61 | }; 62 | 63 | SizeAwareElement.defaultProps = { 64 | element: 'div', 65 | onChange: () => {}, 66 | }; 67 | 68 | export default SizeAwareElement; 69 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/toasts.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { ToastContainer, toast } from 'react-toastify'; 5 | 6 | import { Button } from '@devseed-ui/button'; 7 | 8 | const ButtonIconXmark = styled(Button)` 9 | transform: translate(0.25rem, -0.25rem) !important; 10 | `; 11 | 12 | // The close button is set globally in the toast container. 13 | export const CloseButton = ({ closeToast }) => ( 14 | 23 | Dismiss notification 24 | 25 | ); 26 | 27 | CloseButton.propTypes = { 28 | closeToast: PropTypes.func, 29 | }; 30 | 31 | export const ToastContainerCustom = () => ( 32 | } 36 | /> 37 | ); 38 | 39 | const defaultOptions = { 40 | autoClose: 5000, 41 | hideProgressBar: true, 42 | }; 43 | 44 | const toasts = { 45 | error: (content, opts) => 46 | toast.error(content, { ...defaultOptions, ...opts }), 47 | success: (content, opts) => 48 | toast.success(content, { ...defaultOptions, ...opts }), 49 | info: (content, opts) => toast.info(content, { ...defaultOptions, ...opts }), 50 | warn: (content, opts) => toast.warn(content, { ...defaultOptions, ...opts }), 51 | }; 52 | 53 | export default toasts; 54 | -------------------------------------------------------------------------------- /app/assets/scripts/components/common/tooltip.js: -------------------------------------------------------------------------------- 1 | import ReactTooltip from 'react-tooltip'; 2 | import styled from 'styled-components'; 3 | 4 | export const StyledTooltip = styled(ReactTooltip)` 5 | &.__react_component_tooltip { 6 | width: ${({ width }) => width || 'auto'}; 7 | /* Z index set to 1000 to go over shadow scroll bar 8 | * which has z-index 1000 */ 9 | z-index: 1001; 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /app/assets/scripts/components/compare-map/SideBySideTileLayer.js: -------------------------------------------------------------------------------- 1 | import { useMap } from 'react-leaflet'; 2 | import { memo, useEffect, useRef } from 'react'; 3 | import T from 'prop-types'; 4 | import L from 'leaflet'; 5 | import React from 'react'; 6 | import './leaflet-side-by-side'; 7 | 8 | function SideBySideTileLayer({ 9 | leftTile, 10 | rightTile, 11 | minZoom, 12 | maxZoom, 13 | zIndex, 14 | }) { 15 | const mapRef = useMap(); 16 | const leftPredictionLayerRef = useRef(null); 17 | const rightPredictionLayerRef = useRef(null); 18 | 19 | function sideBySideControl() { 20 | // Create Layer group for the left panel 21 | leftPredictionLayerRef.current = new L.TileLayer(leftTile.url, { 22 | attribution: leftTile.attr, 23 | minZoom: minZoom, 24 | maxZoom: maxZoom, 25 | zIndex: zIndex, 26 | opacity: leftTile.opacity, 27 | }); 28 | 29 | const leftMosaicLayer = new L.TileLayer(leftTile.mosaicUrl, { 30 | attribution: leftTile.mosaicAttr, 31 | minZoom: minZoom, 32 | maxZoom: maxZoom, 33 | zIndex: zIndex, 34 | }); 35 | 36 | const leftLayerGroup = new L.layerGroup([ 37 | leftMosaicLayer, 38 | leftPredictionLayerRef.current, 39 | ]).addTo(mapRef); 40 | 41 | // Create Layer group for the right panel 42 | rightPredictionLayerRef.current = new L.TileLayer(rightTile.url, { 43 | attribution: rightTile.attr, 44 | minZoom: minZoom, 45 | maxZoom: maxZoom, 46 | zIndex: zIndex, 47 | opacity: rightTile.opacity, 48 | }); 49 | 50 | const rightMosaicLayer = new L.TileLayer(rightTile.mosaicUrl, { 51 | attribution: rightTile.mosaicAttr, 52 | minZoom: minZoom, 53 | maxZoom: maxZoom, 54 | zIndex: zIndex, 55 | }); 56 | 57 | const rightLayerGroup = new L.layerGroup([ 58 | rightMosaicLayer, 59 | rightPredictionLayerRef.current, 60 | ]).addTo(mapRef); 61 | 62 | const ctrl = L.control.sideBySide(leftLayerGroup, rightLayerGroup); 63 | return ctrl; 64 | } 65 | 66 | useEffect(() => { 67 | if (mapRef === null) { 68 | return; 69 | } 70 | const ctrl = sideBySideControl(); 71 | ctrl.addTo(mapRef); 72 | L.DomEvent.disableClickPropagation(mapRef._container); 73 | return () => { 74 | ctrl.remove(); 75 | }; 76 | }, []); 77 | 78 | useEffect(() => { 79 | if (leftPredictionLayerRef.current && rightPredictionLayerRef.current) { 80 | leftPredictionLayerRef.current.setOpacity(leftTile.opacity); 81 | rightPredictionLayerRef.current.setOpacity(rightTile.opacity); 82 | } 83 | }, [leftTile.opacity, rightTile.opacity]); 84 | 85 | return
    ; 86 | } 87 | 88 | SideBySideTileLayer.propTypes = { 89 | leftTile: T.object, 90 | rightTile: T.object, 91 | minZoom: T.number, 92 | maxZoom: T.number, 93 | zIndex: T.number, 94 | opacity: T.number, 95 | }; 96 | 97 | export default memo(SideBySideTileLayer); 98 | -------------------------------------------------------------------------------- /app/assets/scripts/components/profile/project/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from '../../common/app'; 3 | 4 | import PageHeader from '../../common/page-header'; 5 | import { PageBody } from '../../../styles/page'; 6 | import ProjectComponent from './project'; 7 | 8 | function Project() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default Project; 20 | -------------------------------------------------------------------------------- /app/assets/scripts/components/profile/project/project-card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import DetailsList from '../../common/details-list'; 4 | import ProjectMap from './project-map'; 5 | import { media } from '@devseed-ui/theme-provider'; 6 | import { formatDateTime } from '../../../utils/format'; 7 | import T from 'prop-types'; 8 | import get from 'lodash.get'; 9 | 10 | const ProjectContainer = styled.div` 11 | display: grid; 12 | grid-gap: 2rem; 13 | grid-template-columns: 1fr; 14 | justify-items: center; 15 | grid-auto-rows: auto 1fr; 16 | ${media.smallUp` 17 | grid-template-columns: auto 1fr; 18 | align-items: flex-start; 19 | justify-items: flex-start; 20 | `} 21 | `; 22 | 23 | function ProjectCard({ project, shares }) { 24 | const bounds = get(shares, '[0].aoi.bounds'); 25 | 26 | let details = {}; 27 | 28 | if (project) { 29 | details = { 30 | Created: formatDateTime(project.created), 31 | Model: project.model_name, 32 | Checkpoints: project.checkpoints.length, 33 | 'Last Checkpoint': 34 | project.checkpoints.length > 0 35 | ? project.checkpoints[0].name 36 | : 'No checkpoints', 37 | 'Exported Maps': shares.length || 'None', 38 | }; 39 | } 40 | return ( 41 | 42 | {bounds ? : null} 43 | 44 | 45 | ); 46 | } 47 | 48 | ProjectCard.propTypes = { 49 | project: T.object, 50 | shares: T.array, 51 | }; 52 | 53 | export default ProjectCard; 54 | -------------------------------------------------------------------------------- /app/assets/scripts/components/profile/project/project-map.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import T from 'prop-types'; 3 | import tBbox from '@turf/bbox'; 4 | import config from '../../../config'; 5 | function ProjectMap({ bounds }) { 6 | let bbox = tBbox(bounds); 7 | // lat,lng 8 | bbox = [bbox[1], bbox[0], bbox[3], bbox[2]]; 9 | const src = `https://dev.virtualearth.net/REST/v1/Imagery/Map/AerialWithLabels?mapArea=${bbox.join( 10 | ',' 11 | )}&key=${config.bingApiKey}`; 12 | return ( 13 |
    14 | 15 |
    16 | ); 17 | } 18 | 19 | ProjectMap.propTypes = { 20 | bounds: T.object, 21 | }; 22 | 23 | export default ProjectMap; 24 | -------------------------------------------------------------------------------- /app/assets/scripts/components/profile/projects/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from '../../common/app'; 3 | 4 | import PageHeader from '../../common/page-header'; 5 | import { PageBody } from '../../../styles/page'; 6 | import ProjectsComponent from './projects'; 7 | 8 | function Projects() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default Projects; 20 | -------------------------------------------------------------------------------- /app/assets/scripts/components/project/header/shortcut-help.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button } from '@devseed-ui/button'; 3 | import { Modal } from '../../common/custom-modal'; 4 | 5 | import styled from 'styled-components'; 6 | 7 | import { themeVal, glsp } from '@devseed-ui/theme-provider'; 8 | 9 | const ShortcutsWrapper = styled.dl` 10 | display: grid; 11 | grid-template-columns: min-content 1fr; 12 | align-items: baseline; 13 | justify-content: space-between; 14 | grid-gap: ${glsp()}; 15 | `; 16 | const Shortcut = styled.dt` 17 | background: ${themeVal('color.background')}; 18 | border: 1px solid ${themeVal('color.primaryAlphaB')}; 19 | font-weight: ${themeVal('type.heading.weight')}; 20 | text-align: center; 21 | min-width: ${glsp(1.75)}; 22 | justify-self: flex-start; 23 | padding: ${glsp(0.125)} ${glsp(0.5)}; 24 | `; 25 | 26 | // TODO Reinstate commented shortcuts 27 | 28 | export const ShortcutHelp = () => { 29 | const [displayShortcutsModal, setDisplayShortcutsModal] = useState(false); 30 | 31 | return ( 32 | <> 33 | 39 | 54 | 55 | } 56 | /> 57 | ); 58 | } 59 | 60 | ConfirmAoiChangeModal.propTypes = { 61 | setAoiIdToSwitch: PropTypes.func, 62 | aoiId: PropTypes.number, 63 | }; 64 | -------------------------------------------------------------------------------- /app/assets/scripts/components/project/prime-panel/tabs/predict/aoi-selector/modals/confirm-aoi-delete.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Button } from '@devseed-ui/button'; 4 | import { Modal } from '@devseed-ui/modal'; 5 | import { ProjectMachineContext } from '../../../../../../../fsm/project'; 6 | import { ModalWrapper } from '../../../../../../common/modal-wrapper'; 7 | 8 | export function ConfirmAoiDeleteModal({ aoiId, setAoiIdToDelete }) { 9 | const actorRef = ProjectMachineContext.useActorRef(); 10 | 11 | const revealed = aoiId !== null; 12 | 13 | return ( 14 | setAoiIdToDelete(null)} 19 | onCloseClick={() => setAoiIdToDelete(null)} 20 | title='Delete AOI' 21 | size='small' 22 | content={ 23 | 24 |
    Are you sure you want to delete this AOI?
    25 | 34 | 46 |
    47 | } 48 | /> 49 | ); 50 | } 51 | 52 | ConfirmAoiDeleteModal.propTypes = { 53 | setAoiIdToDelete: PropTypes.func, 54 | aoiId: PropTypes.number, 55 | }; 56 | -------------------------------------------------------------------------------- /app/assets/scripts/components/project/prime-panel/tabs/predict/imagery-source-selector/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ProjectMachineContext } from '../../../../../../fsm/project'; 3 | import { EditButton } from '../../../../../../styles/button'; 4 | import { 5 | HeadOption, 6 | HeadOptionHeadline, 7 | HeadOptionToolbar, 8 | } from '../../../../../../styles/panel'; 9 | import { 10 | Subheading, 11 | SubheadingStrong, 12 | } from '../../../../../../styles/type/heading'; 13 | import { ImagerySourceSelectorModal } from './modal'; 14 | import selectors from '../../../../../../fsm/project/selectors'; 15 | import * as guards from '../../../../../../fsm/project/guards'; 16 | import { SESSION_MODES } from '../../../../../../fsm/project/constants'; 17 | 18 | export function ImagerySourceSelector() { 19 | const [showModal, setShowModal] = useState(false); 20 | const sessionMode = ProjectMachineContext.useSelector(selectors.sessionMode); 21 | const currentAoi = ProjectMachineContext.useSelector(selectors.currentAoi); 22 | const currentImagerySource = ProjectMachineContext.useSelector( 23 | selectors.currentImagerySource 24 | ); 25 | const isProjectNew = ProjectMachineContext.useSelector((s) => 26 | guards.isProjectNew(s.context) 27 | ); 28 | 29 | let label; 30 | let disabled = true; 31 | if (sessionMode === SESSION_MODES.LOADING) { 32 | label = 'Loading...'; 33 | disabled = true; 34 | } else if (isProjectNew) { 35 | if (!currentAoi) { 36 | label = 'Define first AOI'; 37 | disabled = true; 38 | } else { 39 | label = !currentImagerySource 40 | ? 'Select Imagery Source' 41 | : currentImagerySource.name; 42 | disabled = false; 43 | } 44 | } else { 45 | label = currentImagerySource?.name || ''; 46 | disabled = true; 47 | } 48 | 49 | return ( 50 | <> 51 | 55 | 56 | 57 | Imagery Source 58 | 59 | !disabled && setShowModal(true)} 62 | title={label} 63 | disabled={disabled} 64 | > 65 | {label} 66 | 67 | {!disabled && ( 68 | 69 | setShowModal(true)} 73 | title='Select Imagery ImagerySource' 74 | > 75 | Edit Imagery Source Selection 76 | 77 | 78 | )} 79 | 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /app/assets/scripts/components/project/prime-panel/tabs/predict/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import T from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { glsp } from '@devseed-ui/theme-provider'; 5 | import { PlaceholderMessage } from '../../../../../styles/placeholder'; 6 | import { PanelBlockHeader as BasePanelBlockHeader } from '../../../../common/panel-block'; 7 | import { AoiSelector } from './aoi-selector'; 8 | import { ImagerySourceSelector } from './imagery-source-selector'; 9 | import { MosaicSelector } from './mosaic-selector'; 10 | import { ModelSelector } from './model-selector'; 11 | import CheckpointSelector from './checkpoint-selector'; 12 | 13 | export const ToolsWrapper = styled.div` 14 | display: grid; 15 | grid-gap: ${glsp()}; 16 | 17 | ${PlaceholderMessage} { 18 | padding: 2rem; 19 | } 20 | `; 21 | 22 | function Predict({ className }) { 23 | return ( 24 | 25 |
    26 | 27 | ); 28 | } 29 | 30 | Predict.propTypes = { 31 | className: T.string, 32 | }; 33 | export default Predict; 34 | 35 | const PanelBlockHeader = styled(BasePanelBlockHeader)` 36 | display: grid; 37 | grid-gap: ${glsp(0.75)}; 38 | padding: 0; 39 | margin: unset; 40 | background: none; 41 | `; 42 | 43 | function Header() { 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | } 54 | 55 | Header.propTypes = {}; 56 | -------------------------------------------------------------------------------- /app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import T from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | import { Modal } from '@devseed-ui/modal'; 6 | import { glsp } from '@devseed-ui/theme-provider'; 7 | import { Button } from '@devseed-ui/button'; 8 | import { Heading } from '@devseed-ui/typography'; 9 | 10 | import { MosaicContentInner } from './content'; 11 | import { ProjectMachineContext } from '../../../../../../../fsm/project'; 12 | 13 | const ModalHeader = styled.header` 14 | padding: ${glsp(2)} ${glsp(2)} 0; 15 | `; 16 | 17 | const Headline = styled.div` 18 | display: flex; 19 | flex-direction: row; 20 | justify-content: space-between; 21 | padding-bottom: ${glsp(1)}; 22 | 23 | h1 { 24 | margin: 0; 25 | } 26 | 27 | ${Button} { 28 | height: min-content; 29 | align-self: center; 30 | } 31 | `; 32 | 33 | export function MosaicSelectorModal({ showModal, setShowModal }) { 34 | const mapRef = ProjectMachineContext.useSelector( 35 | ({ context }) => context.mapRef 36 | ); 37 | 38 | // Get the current map zoom and center on modal open 39 | const [mapZoom, mapCenter] = useMemo(() => { 40 | if (!showModal || !mapRef) return [null, null]; 41 | const { lng, lat } = mapRef.getCenter(); 42 | return [mapRef.getZoom(), [lat, lng]]; 43 | }, [mapRef, showModal]); 44 | 45 | return ( 46 | setShowModal(false)} 52 | closeButton={false} 53 | renderHeader={() => ( 54 | 55 | 56 | Set Mosaic Date Range 57 | 68 | 69 | 70 | )} 71 | content={ 72 | { 77 | setShowModal(false); 78 | }} 79 | /> 80 | } 81 | /> 82 | ); 83 | } 84 | 85 | MosaicSelectorModal.propTypes = { 86 | showModal: T.bool, 87 | setShowModal: T.func.isRequired, 88 | imagerySource: T.shape({ 89 | name: T.string, 90 | }), 91 | }; 92 | -------------------------------------------------------------------------------- /app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal/map-preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | import { MapContainer, TileLayer } from 'react-leaflet'; 6 | 7 | import { themeVal, rgba } from '@devseed-ui/theme-provider'; 8 | import { MOSAIC_LAYER_OPACITY } from '../../../../../../../fsm/project/constants'; 9 | 10 | const MapPreviewWrapper = styled.div` 11 | width: 100%; 12 | `; 13 | 14 | const MapPreviewPlaceholder = styled.div` 15 | height: 100%; 16 | display: flex; 17 | align-items: center; 18 | border: ${themeVal('layout.border')} solid 19 | ${rgba(themeVal('color.base'), 0.16)}; 20 | border-radius: ${themeVal('shape.rounded')}; 21 | justify-content: center; 22 | `; 23 | 24 | export const MosaicMapPreview = ({ 25 | mosaicTileUrl, 26 | initialMapCenter, 27 | initialMapZoom, 28 | }) => { 29 | return ( 30 | 31 | {mosaicTileUrl && initialMapZoom && initialMapCenter ? ( 32 | 37 | 43 | 44 | ) : ( 45 | 46 | Set date to display map preview 47 | 48 | )} 49 | 50 | ); 51 | }; 52 | 53 | MosaicMapPreview.propTypes = { 54 | mosaicTileUrl: PropTypes.string, 55 | initialMapZoom: PropTypes.number, 56 | initialMapCenter: PropTypes.array, 57 | }; 58 | -------------------------------------------------------------------------------- /app/assets/scripts/components/project/prime-panel/tabs/retrain/add-class.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button } from '@devseed-ui/button'; 3 | import { ChromePicker } from 'react-color'; 4 | 5 | import { DropdownHeader, DropdownItem } from '../../../../../styles/dropdown'; 6 | import { 7 | PickerStyles, 8 | PickerDropdownBody, 9 | PickerDropdownItem, 10 | PickerDropdownFooter, 11 | } from '../../retrain-refine-styles'; 12 | import AutoFocusFormInput from '../../../../common/auto-focus-form-input'; 13 | import { ProjectMachineContext } from '../../../../../fsm/project'; 14 | 15 | const AddClass = () => { 16 | const actorRef = ProjectMachineContext.useActorRef(); 17 | const [addClassColor, setAddClassColor] = useState('#ffffff'); 18 | const [addClassName, setAddClassName] = useState(''); 19 | 20 | const saveClass = () => { 21 | actorRef.send({ 22 | type: 'Add retrain class', 23 | data: { 24 | retrainClass: { 25 | name: addClassName, 26 | color: addClassColor, 27 | }, 28 | }, 29 | }); 30 | }; 31 | 32 | return ( 33 | <> 34 | 35 |

    New class

    36 |
    37 | 38 | 39 | 47 | 48 | 49 | 64 | 65 | 66 | 67 | 68 | Cancel 69 | 70 | 74 | 87 | 88 | 89 | 90 | ); 91 | }; 92 | 93 | AddClass.propTypes = {}; 94 | 95 | export default AddClass; 96 | -------------------------------------------------------------------------------- /app/assets/scripts/components/uhoh/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from '../common/app'; 3 | 4 | export default class UhOh extends React.Component { 5 | render() { 6 | return ( 7 | 8 |

    Page not found.

    9 |
    10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/assets/scripts/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import defaultsDeep from 'lodash.defaultsdeep'; 3 | /* 4 | * App configuration. 5 | * 6 | * Uses settings in config/production.js, with any properties set by 7 | * config/staging.js or config/local.js overriding them depending upon the 8 | * environment. 9 | * 10 | * This file should not be modified. Instead, modify one of: 11 | * 12 | * - config/production.js 13 | * Production settings (base). 14 | * - config/staging.js 15 | * Overrides to production if ENV is staging. 16 | * - config/local.js 17 | * Overrides if local.js exists. 18 | * This last file is gitignored, so you can safely change it without 19 | * polluting the repo. 20 | */ 21 | 22 | // Initialize with base config 23 | var config = require('./config/base.js'); 24 | 25 | // Load environment-specific configs 26 | var envConfig = { 27 | cypress: require('./config/cypress.js'), 28 | production: require('./config/production.js'), 29 | reforestamos: require('./config/reforestamos.js'), 30 | reforestamosProd: require('./config/reforestamos-prod.js'), 31 | staging: require('./config/staging.js'), 32 | testing: require('./config/testing.js'), 33 | }; 34 | 35 | if (process.env.NODE_ENV === 'production') { 36 | config = defaultsDeep(envConfig.production || {}, config); 37 | } else if (process.env.NODE_ENV === 'reforestamos') { 38 | config = defaultsDeep(envConfig.reforestamos || {}, config); 39 | } else if (process.env.NODE_ENV === 'reforestamos-prod') { 40 | config = defaultsDeep(envConfig.reforestamosProd || {}, config); 41 | } else if (process.env.NODE_ENV === 'cypress') { 42 | config = defaultsDeep(envConfig.cypress || {}, config); 43 | } else if (process.env.NODE_ENV === 'testing') { 44 | config = defaultsDeep(envConfig.testing || {}, config); 45 | } else if (process.env.NODE_ENV === 'staging') { 46 | config = defaultsDeep(envConfig.staging, config); 47 | } 48 | 49 | // Apply local config 50 | config = defaultsDeep(require('./config/local.js') || {}, config); 51 | 52 | // The require doesn't play super well with es6 imports. It creates an internal 53 | // 'default' property. Export that. 54 | export default config.default; 55 | -------------------------------------------------------------------------------- /app/assets/scripts/config/base.js: -------------------------------------------------------------------------------- 1 | // module exports is required to be able to load from gulpfile. 2 | module.exports = { 3 | default: { 4 | reduxeedLogs: false, 5 | appTitle: 'PEARL', 6 | appLongTitle: 'Planetary Computer Land Cover Mapping', 7 | appDescription: 8 | 'Microsoft Planetary Computer Land Use/Land Classification Mapping tool', 9 | restApiEndpoint: 'https://api.lulc-staging.ds.io', 10 | websocketEndpoint: 'wss://socket.lulc-staging.ds.io', 11 | websocketPingPongInterval: 3000, 12 | planetaryComputerStacApi: 13 | 'https://planetarycomputer.microsoft.com/api/stac/v1', 14 | auth0Domain: 'pearl-landcover-staging.us.auth0.com', 15 | auth0ClientId: 'BTwFngSNG0pz1nNXsOFtfN0TUAtPENLu', 16 | minSampleCount: 1, 17 | bingApiKey: 18 | 'ArLmu8JG2PHK_-_zo7yS1WbvDz7PgsoVEgcqFTg8uaH-lsXLcjADCAtnyQB054uq', 19 | bingSearchUrl: 'https://dev.virtualearth.net/REST/v1', 20 | reverseGeocodeRadius: 1, 21 | tileUrlTemplate: 22 | 'https://planetarycomputer.microsoft.com/api/data/v1/mosaic/tiles/87b72c66331e136e088004fba817e3e8/{z}/{x}/{y}?asset_bidx=image|1%2C2%2C3&assets=image&collection=naip', 23 | appInsightsKey: '07b5adb4-0447-4c6f-881a-a23e108bc861', 24 | instanceCreationTimeout: 10 * 60 * 60 * 1000, 25 | instanceCreationCheckInterval: 5000, 26 | minimumAoiArea: 1000000, 27 | mapboxAccessToken: 28 | 'pk.eyJ1IjoiZGV2c2VlZCIsImEiOiJjbGdtajZjMXgwNjczM3JvZTg4bm42bjNtIn0.MJclBE_ARI264ZaotMoEjw', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /app/assets/scripts/config/cypress.js: -------------------------------------------------------------------------------- 1 | // module exports is required to be able to load from gulpfile. 2 | module.exports = { 3 | default: { 4 | environment: 'cypress', 5 | restApiEndpoint: 'http://localhost:2000', 6 | websocketEndpoint: 'ws://localhost:1999', 7 | instanceCreationTimeout: 5000, 8 | instanceCreationCheckInterval: 500, 9 | minimumAoiArea: 40000, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /app/assets/scripts/config/production.js: -------------------------------------------------------------------------------- 1 | // module exports is required to be able to load from gulpfile. 2 | module.exports = { 3 | default: { 4 | environment: 'production', 5 | restApiEndpoint: 'https://api.lulc.ds.io', 6 | websocketEndpoint: 'wss://socket.lulc.ds.io', 7 | auth0Domain: 'pearl-landcover.us.auth0.com', 8 | auth0ClientId: 'OQtYR72fGdgrogeokjr9CBl4vg1P6SYP', 9 | tileUrlTemplate: 10 | 'https://planetarycomputer.microsoft.com/api/data/v1/mosaic/tiles/87b72c66331e136e088004fba817e3e8/{z}/{x}/{y}?asset_bidx=image|1%2C2%2C3&assets=image&collection=naip', 11 | appInsightsKey: '0291f153-9634-463e-8aa0-34700141d37c', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /app/assets/scripts/config/reforestamos-prod.js: -------------------------------------------------------------------------------- 1 | // module exports is required to be able to load from gulpfile. 2 | module.exports = { 3 | default: { 4 | environment: 'production', 5 | restApiEndpoint: 'https://api.reforestamos.ds.io', 6 | websocketEndpoint: 'wss://socket.reforestamos.ds.io', 7 | auth0Domain: 'pearl-landcover.us.auth0.com', 8 | auth0ClientId: 'mXokXsHo1eabFlWuJyQPpzErqOX3wZ87', 9 | tileUrlTemplate: 10 | 'https://planetarycomputer.microsoft.com/api/data/v1/mosaic/tiles/87b72c66331e136e088004fba817e3e8/{z}/{x}/{y}?asset_bidx=image|1%2C2%2C3&assets=image&collection=naip', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /app/assets/scripts/config/reforestamos.js: -------------------------------------------------------------------------------- 1 | // module exports is required to be able to load from gulpfile. 2 | module.exports = { 3 | default: { 4 | environment: 'production', 5 | restApiEndpoint: 'https://api.lulc-staging.ds.io', 6 | websocketEndpoint: 'wss://socket.lulc-staging.ds.io', 7 | auth0Domain: 'pearl-landcover.us.auth0.com', 8 | auth0ClientId: 'mXokXsHo1eabFlWuJyQPpzErqOX3wZ87', 9 | tileUrlTemplate: 10 | 'https://planetarycomputer.microsoft.com/api/data/v1/mosaic/tiles/87b72c66331e136e088004fba817e3e8/{z}/{x}/{y}?asset_bidx=image|1%2C2%2C3&assets=image&collection=naip', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /app/assets/scripts/config/staging.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: { 3 | restApiEndpoint: 'https://api.lulc-staging.ds.io', 4 | websocketEndpoint: 'wss://socket.lulc-staging.ds.io', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/assets/scripts/config/testing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // module exports is required to be able to load from gulpfile. 4 | module.exports = { 5 | default: { 6 | restApiEndpoint: 'https://api.lulc-test.ds.io', 7 | websocketEndpoint: 'wss://socket.lulc-test.ds.io', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /app/assets/scripts/fatal-error-boundary.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const FatalError = styled.div` 5 | text-align: center; 6 | 7 | span { 8 | font-style: italic; 9 | } 10 | `; 11 | 12 | export default class ErrorBoundary extends React.Component { 13 | static getDerivedStateFromError(error) { 14 | return { error: error }; 15 | } 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = { error: null }; 20 | } 21 | 22 | render() { 23 | return this.state.error ? ( 24 | 25 |

    Oh snap :'( Something went wrong

    26 |

    This on us, and we'll fix it as soon as possible

    27 |

    28 | Error: {this.state.error.message} 29 |

    30 |
    31 | ) : ( 32 | // eslint-disable-next-line react/prop-types 33 | this.props.children 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/assets/scripts/fsm/project/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Session modes 3 | */ 4 | export const SESSION_MODES = { 5 | LOADING: 'LOADING', 6 | PREDICT: 'PREDICT', 7 | RETRAIN: 'RETRAIN', 8 | }; 9 | 10 | /** 11 | * Retrain map modes 12 | */ 13 | export const RETRAIN_MAP_MODES = { 14 | BROWSE: 'BROWSE', 15 | ADD_POINT: 'ADD_POINT', 16 | ADD_POLYGON: 'ADD_POLYGON', 17 | ADD_FREEHAND: 'ADD_FREEHAND', 18 | REMOVE_SAMPLE: 'REMOVE_SAMPLE', 19 | DELETE_SAMPLES: 'DELETE_SAMPLES', 20 | }; 21 | -------------------------------------------------------------------------------- /app/assets/scripts/fsm/project/guards.js: -------------------------------------------------------------------------------- 1 | import config from '../../config'; 2 | 3 | export const isLargeAoi = (context) => { 4 | const { currentAoi, apiLimits } = context; 5 | return currentAoi?.area > apiLimits?.live_inference; 6 | }; 7 | 8 | export const retrainModeEnabled = (context) => { 9 | return !isLargeAoi(context) && context.currentTimeframe; 10 | }; 11 | 12 | export const isRetrainReady = (context) => { 13 | return ( 14 | context.retrainClasses?.length > 0 && context.retrainSamples?.length > 0 15 | ); 16 | }; 17 | 18 | export const isPredictionReady = (context) => { 19 | const { 20 | currentImagerySource, 21 | currentMosaic, 22 | currentModel, 23 | currentAoi, 24 | } = context; 25 | 26 | if (context.project.id === 'new') { 27 | return ( 28 | !!currentAoi && 29 | !!currentImagerySource && 30 | !!currentMosaic && 31 | !!currentModel 32 | ); 33 | } else { 34 | return !!currentAoi && !!currentMosaic; 35 | } 36 | }; 37 | 38 | export const isProjectNew = (c) => c.project.id === 'new'; 39 | export const isFirstAoi = (c) => c.aoisList?.length === 0; 40 | export const isAoiNew = ({ currentAoi }) => !currentAoi || !currentAoi.id; 41 | export const isAuthenticated = (c) => c.isAuthenticated; 42 | 43 | export const isLivePredictionAreaSize = ({ currentAoi, apiLimits }) => 44 | currentAoi && 45 | currentAoi.area > config.minimumAoiArea && 46 | currentAoi.area < apiLimits.live_inference; 47 | 48 | export const isBatchRunning = (c) => !!c.currentBatchPrediction; 49 | 50 | export const hasAois = (c) => c.aoisList?.length > 0; 51 | 52 | export const hasTimeframe = (c) => c.currentTimeframe; 53 | -------------------------------------------------------------------------------- /app/assets/scripts/fsm/project/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import T from 'prop-types'; 3 | import { createActorContext } from '@xstate/react'; 4 | import { projectMachine } from './machine'; 5 | import { machineStateLogger } from '../../utils/machine-state-logger'; 6 | 7 | import config from '../../config'; 8 | const { environment } = config; 9 | 10 | export const ProjectMachineContext = createActorContext(projectMachine); 11 | 12 | export function ProjectMachineProvider(props) { 13 | return ( 14 | 15 | {environment !== 'production' && } 16 | {props.children} 17 | 18 | ); 19 | } 20 | 21 | ProjectMachineProvider.propTypes = { 22 | children: T.node, 23 | }; 24 | 25 | /** 26 | * This component logs state changes 27 | */ 28 | function MachineStateLogger() { 29 | const actor = ProjectMachineContext.useActorRef(); 30 | useEffect(() => { 31 | actor.onTransition(machineStateLogger); 32 | }, []); 33 | return <>; 34 | } 35 | -------------------------------------------------------------------------------- /app/assets/scripts/fsm/project/websocket-client.js: -------------------------------------------------------------------------------- 1 | import config from '../../config'; 2 | import ReconnectingWebSocket from 'reconnecting-websocket'; 3 | 4 | export class WebsocketClient extends ReconnectingWebSocket { 5 | constructor(token) { 6 | super(config.websocketEndpoint + `?token=${token}`); 7 | } 8 | 9 | sendMessage(message) { 10 | this.send(JSON.stringify(message)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/assets/scripts/history.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-disable inclusive-language/use-inclusive-words */ 3 | // We will need to access history from outside components. 4 | // The only way to do this is create our own history and pass it to the router. 5 | // https://github.com/ReactTraining/react-router/blob/master/FAQ.md#how-do-i-access-the-history-object-outside-of-components 6 | import { createBrowserHistory } from 'history'; 7 | import config from './config'; 8 | 9 | export default createBrowserHistory({ 10 | basename: config.baseUrl ? new URL(config.baseUrl).pathname : '/', 11 | }); 12 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/animation.js: -------------------------------------------------------------------------------- 1 | import { keyframes } from 'styled-components'; 2 | 3 | export const shake = keyframes` 4 | 8%, 41% { 5 | transform: translateX(-0.5rem); 6 | } 7 | 8 | 25%, 58% { 9 | transform: translateX(0.5rem); 10 | } 11 | 12 | 75% { 13 | transform: translateX(-0.25rem); 14 | } 15 | 16 | 92% { 17 | transform: translateX(0.25rem); 18 | } 19 | 20 | 0%, 100% { 21 | transform: translateX(0); 22 | } 23 | `; 24 | 25 | export const reveal = keyframes` 26 | 0% { 27 | opacity: 0; 28 | } 29 | 30 | 100% { 31 | opacity: 1; 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/button.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { InfoButton } from '../components/common/info-button'; 4 | 5 | // TODO This button name is not used on edit only and should be replaced by 6 | // the Action Button 7 | export const EditButton = styled(InfoButton).attrs({ 8 | variation: 'base-plain', 9 | size: 'small', 10 | hideText: true, 11 | })` 12 | opacity: 80%; 13 | width: min-content; 14 | `; 15 | 16 | export const ActionButton = styled(InfoButton).attrs({ 17 | variation: 'base-plain', 18 | size: 'small', 19 | hideText: true, 20 | })` 21 | opacity: 80%; 22 | width: min-content; 23 | `; 24 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/collecticons/index.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | const catalog = require('./catalog.json'); 4 | 5 | export const collecticonsFont = () => css` 6 | @font-face { 7 | font-family: '${catalog.name}'; 8 | src: url(data:application/font-woff2;charset=utf-8;base64,${catalog.fonts 9 | .woff2}) 10 | format('woff2'); 11 | font-weight: normal; 12 | font-style: normal; 13 | } 14 | `; 15 | 16 | /** 17 | * Includes a collecticons icon by name. 18 | * @param {string} name Icon name 19 | */ 20 | export default function collecticon(name) { 21 | name = `${catalog.className}-${name}`; 22 | const icon = catalog.icons.find((i) => i.icon === name); 23 | const content = icon ? `\\${icon.charCode}` : 'n/a'; 24 | 25 | return css` 26 | speak: none; 27 | font-family: '${catalog.name}'; 28 | font-style: normal; 29 | font-weight: normal; 30 | font-variant: normal; 31 | text-transform: none; 32 | 33 | /* Better font rendering */ 34 | text-rendering: auto; 35 | -webkit-font-smoothing: antialiased; 36 | -moz-osx-font-smoothing: grayscale; 37 | content: '${content}'; 38 | `; 39 | } 40 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/dropdown.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { glsp, themeVal, multiply } from '@devseed-ui/theme-provider'; 3 | import collecticon from '@devseed-ui/collecticons'; 4 | import BaseDropdown from '@devseed-ui/dropdown'; 5 | import InfoButton from '../components/common/info-button'; 6 | 7 | export const DropdownHeader = styled.header` 8 | background: ${({ unshaded }) => 9 | unshaded ? 'none' : themeVal('color.baseDarkAlphaE')}; 10 | display: grid; 11 | padding: ${glsp()}; 12 | p { 13 | text-transform: uppercase; 14 | font-size: 0.875rem; 15 | } 16 | h1 { 17 | margin: 0; 18 | overflow-wrap: break-word; 19 | overflow-wrap: anywhere; 20 | } 21 | `; 22 | 23 | export const DropdownBody = styled.ul` 24 | display: grid; 25 | grid-gap: ${glsp(0.5)}; 26 | padding: ${glsp(0.5)} 0 ${glsp(1)}; 27 | overflow: auto; 28 | `; 29 | export const DropdownItem = styled.a` 30 | display: grid; 31 | grid-template-columns: 1fr auto; 32 | justify-items: start; 33 | padding: ${glsp(0.25)} ${glsp()}; 34 | grid-gap: ${glsp()}; 35 | font-weight: ${themeVal('type.heading.weight')}; 36 | color: ${themeVal('color.base')}; 37 | transition: all 0.16s ease-in-out; 38 | overflow-wrap: break-word; 39 | overflow-wrap: anywhere; 40 | 41 | ${({ useIcon }) => 42 | useIcon && 43 | css` 44 | grid-template-columns: max-content 1fr auto; 45 | ::before { 46 | ${collecticon(useIcon)} 47 | } 48 | `} 49 | 50 | /* Add a tick if checked, may conflict with useIcon */ 51 | ${({ checked }) => 52 | checked && 53 | css` 54 | grid-template-columns: max-content 1fr auto; 55 | ::before { 56 | ${collecticon('tick')} 57 | } 58 | `} 59 | 60 | :visited { 61 | color: ${themeVal('color.base')}; 62 | } 63 | :hover { 64 | opacity: 1; 65 | } 66 | ${({ nonhoverable }) => 67 | !nonhoverable && 68 | css` 69 | :hover { 70 | color: ${themeVal('color.primary')}; 71 | background: ${themeVal('color.primaryAlphaA')}; 72 | } 73 | `} 74 | 75 | ${({ muted }) => 76 | muted && 77 | css` 78 | color: ${themeVal('color.baseAlphaD')}; 79 | `} 80 | `; 81 | export const DropdownFooter = styled.footer` 82 | border-top: 1px solid ${themeVal('color.baseAlphaD')}; 83 | padding: ${glsp(1)} 0; 84 | `; 85 | 86 | export const Dropdown = styled(BaseDropdown)` 87 | padding: 0; 88 | background: ${themeVal('color.surface')}; 89 | color: ${themeVal('color.base')}; 90 | max-width: 18rem; 91 | box-shadow: 0 0 0 1px ${themeVal('color.baseAlphaB')}, 92 | 0 0 32px 2px ${themeVal('color.baseDarkAlphaE')}, 93 | 0 16px 48px -16px ${themeVal('color.baseDark')}; 94 | `; 95 | export const DropdownTrigger = styled(InfoButton)` 96 | &::before { 97 | ${({ usePreIcon }) => usePreIcon && collecticon(usePreIcon)} 98 | font-size: ${multiply(themeVal('type.base.size'), 0.85)}; 99 | } 100 | `; 101 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/global.js: -------------------------------------------------------------------------------- 1 | import { css, createGlobalStyle } from 'styled-components'; 2 | import { collecticonsFont } from './collecticons'; 3 | import leafletStyles from './vendor/leaflet'; 4 | import leafletSideBySideStyles from './vendor/leaflet-side-by-side'; 5 | import reactToastStyles from './vendor/react-toastify'; 6 | import inputRangeStyles from './vendor/react-input-range'; 7 | import joyrideStyles from './vendor/joyridestyles'; 8 | import { themeVal, rgba } from '@devseed-ui/theme-provider'; 9 | 10 | const darkStyles = () => css` 11 | .modal { 12 | background: ${rgba(themeVal('color.background'), 0.7)} !important; 13 | } 14 | 15 | .modal__contents { 16 | box-shadow: 0 0 32px 2px ${themeVal('color.baseDarkAlphaB')}, 17 | 0 16px 48px -16px ${themeVal('color.baseDarkAlphaD')} !important; 18 | } 19 | `; 20 | 21 | export default createGlobalStyle` 22 | ${darkStyles()}; 23 | ${collecticonsFont()}; 24 | ${leafletStyles()}; 25 | ${leafletSideBySideStyles()}; 26 | ${reactToastStyles()}; 27 | ${inputRangeStyles()}; 28 | ${joyrideStyles()}; 29 | 30 | .tether-element { 31 | z-index: 9999; 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/inpage.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | import { 4 | visuallyHidden, 5 | truncated, 6 | themeVal, 7 | glsp, 8 | rgba, 9 | media, 10 | } from '@devseed-ui/theme-provider'; 11 | import { headingAlt } from '@devseed-ui/typography'; 12 | 13 | export const Inpage = styled.article` 14 | display: grid; 15 | height: 100%; 16 | grid-template-rows: auto 1fr; 17 | 18 | /** 19 | * Make Inpage map-centric 20 | * 21 | * Vizually hides inpageHeader and sets the grid layout to a single row. 22 | * The latter is needed so that inpageBody can be displayed in full height. 23 | */ 24 | 25 | ${({ isMapCentric }) => 26 | isMapCentric && 27 | css` 28 | grid-template-rows: 1fr; 29 | ${InpageHeader} { 30 | ${visuallyHidden()} 31 | } 32 | `} 33 | `; 34 | 35 | export const InpageHeader = styled.header` 36 | /* Visually hidden */ 37 | ${({ isHidden }) => 38 | isHidden && 39 | css` 40 | ${visuallyHidden()} 41 | `} 42 | `; 43 | 44 | export const InpageHeaderInner = styled.div` 45 | display: flex; 46 | flex-flow: row nowrap; 47 | align-items: flex-end; 48 | padding: ${glsp(2)} ${glsp()}; 49 | max-width: ${themeVal('layout.max')}; 50 | margin: 0 auto; 51 | ${media.mediumUp` 52 | padding: ${glsp(2)}; 53 | `} 54 | `; 55 | 56 | export const InpageHeadline = styled.div` 57 | display: flex; 58 | flex-flow: column; 59 | min-width: 100%; 60 | 61 | > *:last-child { 62 | margin-bottom: 0; 63 | } 64 | `; 65 | 66 | export const InpageToolbar = styled.div` 67 | display: flex; 68 | flex-flow: row nowrap; 69 | align-items: center; 70 | justify-content: space-between; 71 | > * ~ * { 72 | margin-left: ${glsp()}; 73 | } 74 | ${media.mediumUp` 75 | padding-left: ${glsp(2)}; 76 | margin-left: auto; 77 | `} 78 | `; 79 | 80 | export const InpageTitleWrapper = styled.div` 81 | display: flex; 82 | min-width: 0; 83 | flex-flow: column nowrap; 84 | margin-bottom: ${glsp(1.5)}; 85 | ${media.mediumUp` 86 | /* padding: ${glsp(4)}; */ 87 | flex-flow: row nowrap; 88 | `} 89 | `; 90 | 91 | export const InpageTitle = styled.h1` 92 | /* ${truncated()} */ 93 | font-size: 2rem; 94 | line-height: 2.5rem; 95 | margin: 0; 96 | `; 97 | 98 | export const InpageTagline = styled.p` 99 | ${headingAlt()} 100 | order: -1; 101 | font-size: 0.875rem; 102 | line-height: 1rem; 103 | color: ${rgba('#FFFFFF', 0.64)}; 104 | `; 105 | 106 | export const InpageBody = styled.div` 107 | background: transparent; 108 | `; 109 | 110 | export const InpageBodyInner = styled.div` 111 | padding: ${glsp()}; 112 | padding-top: 0; 113 | max-width: ${themeVal('layout.max')}; 114 | margin: 0 auto; 115 | ${media.mediumUp` 116 | padding: ${glsp(2)}; 117 | `} 118 | `; 119 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/links.js: -------------------------------------------------------------------------------- 1 | import { NavLink, Link } from 'react-router-dom'; 2 | import { filterComponentProps } from './utils/general'; 3 | 4 | // Please refer to filterComponentProps to understand why this is needed 5 | const propsToFilter = [ 6 | 'variation', 7 | 'size', 8 | 'hideText', 9 | 'useIcon', 10 | 'active', 11 | 'visuallyDisabled', 12 | ]; 13 | 14 | export const StyledNavLink = filterComponentProps(NavLink, propsToFilter); 15 | export const StyledLink = filterComponentProps(Link, propsToFilter); 16 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/local-button.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { Button } from '@devseed-ui/button'; 3 | import localCollecticon from './collecticons/'; 4 | /* 5 | * Library button will use library collecticons by default 6 | * In order to use a local collection, use this component 7 | * Which overrides the application of useIcon with local collecticon catalog 8 | */ 9 | export const LocalButton = styled(Button)` 10 | ${({ useIcon }) => { 11 | if (!useIcon) return null; 12 | 13 | const [icon, position] = Array.isArray(useIcon) 14 | ? useIcon 15 | : [useIcon, 'before']; 16 | 17 | const selector = `&::${position}`; 18 | 19 | return css` 20 | ${selector} { 21 | ${localCollecticon(icon)} 22 | } 23 | `; 24 | }} 25 | ${({ outlined }) => 26 | outlined && 27 | css` 28 | border: 1px solid; 29 | `} 30 | `; 31 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/page.js: -------------------------------------------------------------------------------- 1 | import { reveal } from './animation'; 2 | 3 | import styled from 'styled-components'; 4 | export const Page = styled.div` 5 | display: grid; 6 | grid-template-rows: minmax(2rem, min-content) 1fr ${({ hideFooter }) => 7 | hideFooter ? 0 : 'auto'}; 8 | min-height: 100vh; 9 | `; 10 | 11 | export const PageBody = styled.main` 12 | padding: 0; 13 | margin: 0; 14 | 15 | /* Animation */ 16 | animation: ${reveal} 0.48s ease 0s 1; 17 | `; 18 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/panel.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { themeVal, glsp } from '@devseed-ui/theme-provider'; 3 | 4 | export const HeadOptionHeadline = styled.div` 5 | grid-column: 1 / -1; 6 | ${({ usePadding }) => 7 | usePadding && 8 | css` 9 | padding: ${glsp(0)} ${glsp(1.5)}; 10 | `} 11 | `; 12 | 13 | export const HeadOptionToolbar = styled.div` 14 | display: grid; 15 | justify-self: end; 16 | grid-template-columns: repeat(auto-fill, minmax(1rem, 1fr)); 17 | gap: 1rem; 18 | grid-auto-flow: column; 19 | justify-items: center; 20 | align-self: flex-start; 21 | padding-right: ${glsp(1.5)}; 22 | `; 23 | 24 | export const HeadOption = styled.div` 25 | display: grid; 26 | grid-template-columns: minmax(0, 1fr) min-content; 27 | gap: 0.5rem; 28 | &:not(:last-of-type) { 29 | box-shadow: 0px 1px 0px 0px ${themeVal('color.baseAlphaC')}; 30 | padding-bottom: 0.75rem; 31 | } 32 | ${({ hasSubtitle }) => 33 | hasSubtitle && 34 | css` 35 | ${HeadOptionToolbar} { 36 | grid-row: 2; 37 | grid-column: 2; 38 | } 39 | 40 | h1.subtitle { 41 | grid-column: 1; 42 | margin: 0; 43 | } 44 | `} 45 | `; 46 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/placeholder.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Prose from './type/prose'; 3 | import { themeVal } from '@devseed-ui/theme-provider'; 4 | 5 | export const PlaceholderMessage = styled(Prose)` 6 | font-weight: 350; 7 | text-align: center; 8 | color: ${themeVal('color.base')}; 9 | `; 10 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/skins.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { themeVal } from '@devseed-ui/theme-provider'; 3 | 4 | export const stackSkin = () => css` 5 | background-color: ${themeVal('color.surface')}; 6 | box-shadow: 0 0 0 1px ${themeVal('color.baseAlphaB')}; 7 | `; 8 | 9 | export const cardSkin = () => css` 10 | border-radius: ${themeVal('shape.rounded')}; 11 | background-color: ${themeVal('color.surface')}; 12 | box-shadow: 0 0 0 1px ${themeVal('color.baseAlphaB')}; 13 | `; 14 | 15 | export const panelSkin = () => css` 16 | background-color: ${themeVal('color.background')}; 17 | box-shadow: 0 0 16px 2px ${themeVal('color.baseDarkAlphaB')}; 18 | `; 19 | 20 | export const surfaceElevatedD = () => css` 21 | background-color: ${themeVal('color.surface')}; 22 | box-shadow: 0 0 0 1px ${themeVal('color.baseAlphaB')}, 23 | 0 0 16px 2px ${themeVal('color.baseDarkAlphaB')}; 24 | `; 25 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/type/definition-list.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { divide } from '../utils/math'; 4 | import { themeVal } from '../utils/general'; 5 | import { headingAlt } from '@devseed-ui/typography'; 6 | 7 | const Dl = styled.dl` 8 | font-feature-settings: 'pnum' 0; /* Use proportional numbers */ 9 | 10 | dt { 11 | ${headingAlt()} 12 | margin: 0 0 ${divide(themeVal('layout.space'), 4)} 0; 13 | } 14 | 15 | dd { 16 | margin: 0 0 ${themeVal('layout.space')} 0; 17 | } 18 | `; 19 | 20 | export default Dl; 21 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/type/heading.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { themeVal, rgba, glsp, truncated } from '@devseed-ui/theme-provider'; 3 | import collecticon from '../collecticons'; 4 | 5 | export const Subheading = styled.h2` 6 | color: ${rgba(themeVal('color.base'), 0.72)}; 7 | font-size: 0.875rem; 8 | line-height: 1.25rem; 9 | letter-spacing: 0.5px; 10 | font-feature-settings: 'pnum' 0; /* Use proportional numbers */ 11 | font-family: ${themeVal('type.base.family')}; 12 | font-weight: ${themeVal('type.heading.regular')}; 13 | text-transform: uppercase; 14 | `; 15 | 16 | export const SubheadingStrong = styled.h3` 17 | color: ${themeVal('color.base')}; 18 | font-weight: ${themeVal('type.heading.weight')}; 19 | font-size: ${themeVal('type.base.size')}; 20 | line-height: 1.5rem; 21 | padding-left: ${glsp(1.5)}; 22 | ${truncated} 23 | 24 | ${({ useIcon }) => 25 | useIcon && 26 | css` 27 | display: grid; 28 | grid-template-columns: max-content max-content; 29 | grid-gap: 1rem; 30 | &::after { 31 | ${collecticon(useIcon)} 32 | } 33 | `} 34 | ${({ onClick, disabled }) => 35 | onClick && 36 | !disabled && 37 | css` 38 | transition: opacity 0.24s ease 0s; 39 | &:hover { 40 | cursor: pointer; 41 | opacity: 0.64; 42 | } 43 | `} 44 | `; 45 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/type/prose.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { themeVal, multiply, glsp } from '@devseed-ui/theme-provider'; 3 | 4 | // Extract fn to avoid complaints by the linter. 5 | const numColumnsFn = ({ numColumns }) => 6 | numColumns && 7 | css` 8 | column-count: ${numColumns}; 9 | column-gap: ${glsp(2)}; 10 | `; 11 | 12 | const Prose = styled.div` 13 | font-size: ${({ size }) => 14 | size === 'small' ? '0.875rem' : themeVal('type.base.size')}; /* 16px */ 15 | line-height: ${({ size }) => 16 | size === 'small' ? '1.25rem' : themeVal('type.base.line')}; /* 16px */ 17 | 18 | ${numColumnsFn} 19 | 20 | ul, ol, dl { 21 | padding: 0; 22 | } 23 | 24 | ul { 25 | list-style-type: disc; 26 | } 27 | 28 | ol { 29 | list-style-type: decimal; 30 | } 31 | 32 | ul, 33 | ol { 34 | list-style-position: outside; 35 | margin-left: ${themeVal('layout.space')}; 36 | } 37 | 38 | ol ol, 39 | ol ul, 40 | ul ol, 41 | ul ul { 42 | margin-bottom: 0; 43 | } 44 | 45 | > * { 46 | margin-bottom: ${({ size }) => 47 | size === 'small' 48 | ? '1rem' 49 | : multiply(themeVal('type.base.size'), themeVal('type.base.line'))}; 50 | } 51 | 52 | > *:last-child { 53 | margin-bottom: 0; 54 | } 55 | `; 56 | 57 | export default Prose; 58 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/utils/general.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Removes given props from the component returning a new one. 5 | * This is used to circumvent a bug with styled-component where unwanted props 6 | * are passed to the dom causing react to display an error: 7 | * 8 | * ``` 9 | * `Warning: React does not recognize the hideText prop on a DOM element. 10 | * If you intentionally want it to appear in the DOM as a custom attribute, 11 | * spell it as lowercase hideText instead. If you accidentally passed it from 12 | * a parent component, remove it from the DOM element.` 13 | * ``` 14 | * 15 | * This commonly happens when an element is impersonating another with the 16 | * `as` prop: 17 | * 18 | * 19 | * 20 | * Because of a bug, all the props passed to `Button` are passed to `Link` 21 | * without being filtered before rendering, causing the aforementioned error. 22 | * 23 | * This utility creates a component that filter out unwanted props and can be 24 | * safely used as an impersonator. 25 | * 26 | * const CleanLink = filterComponentProps(Link, ['hideText']) 27 | * 28 | * 29 | * Issue tracking the bug: https://github.com/styled-components/styled-components/issues/2131 30 | * 31 | * Note: The props to filter out are manually defined to reduce bundle size, 32 | * but it would be possible to automatically filter out all invalid props 33 | * using something like @emotion/is-prop-valid 34 | * 35 | * @param {object} Comp The react component 36 | * @param {array} filterProps Props to filter off of the component 37 | */ 38 | export function filterComponentProps(Comp, filterProps = []) { 39 | const isValidProp = (p) => !filterProps.includes(p); 40 | 41 | return React.forwardRef((rawProps, ref) => { 42 | const props = Object.keys(rawProps).reduce( 43 | (acc, p) => (isValidProp(p) ? { ...acc, [p]: rawProps[p] } : acc), 44 | {} 45 | ); 46 | return ; 47 | }); 48 | } 49 | 50 | /* Transform string into title case */ 51 | export function makeTitleCase(text) { 52 | return text 53 | .split(' ') 54 | .map((word) => `${word[0].toUpperCase()}${word.slice(1)}`) 55 | .join(' '); 56 | } 57 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/vendor/joyridestyles.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { themeVal } from '@devseed-ui/theme-provider'; 3 | 4 | export default () => css` 5 | /* Overrides for joyride styles. */ 6 | .__floater__arrow { 7 | svg polygon { 8 | fill: ${themeVal('color.surface')}; 9 | } 10 | } 11 | .react-joyride__overlay { 12 | background-color: rgba(0, 0, 0, 0.625) !important; 13 | } 14 | .react-joyride__spotlight { 15 | background-color: #8c8c8c; 16 | border: 1px solid ${themeVal('color.secondary')}; 17 | } 18 | .react-joyride__beacon { 19 | display: none !important; 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/vendor/leaflet.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import collecticon from '@devseed-ui/collecticons'; 3 | import { themeVal } from '@devseed-ui/theme-provider'; 4 | 5 | export default () => css` 6 | .leaflet-geosearch-button.active .leaflet-bar-part.leaflet-bar-part-single { 7 | width: 30px; 8 | } 9 | 10 | .leaflet-control-attribution { 11 | a, 12 | a:visited { 13 | color: ${themeVal('color.baseDark')}; 14 | } 15 | } 16 | 17 | .leaflet-control-geosearch { 18 | & form { 19 | border-radius: 4px; 20 | color: ${themeVal('color.base')}; 21 | background: ${themeVal('color.surface')}; 22 | input { 23 | color: ${themeVal('color.base')}; 24 | background: ${themeVal('color.surface')}; 25 | } 26 | } 27 | .results, 28 | .results.active { 29 | color: ${themeVal('color.base')}; 30 | background: ${themeVal('color.surface')}; 31 | > :hover { 32 | background: ${themeVal('color.primaryAlphaA')}; 33 | } 34 | } 35 | & a.reset { 36 | color: ${themeVal('color.base')}; 37 | background: ${themeVal('color.surface')}; 38 | &:hover { 39 | background: inherit; 40 | color: ${themeVal('color.primary')}; 41 | } 42 | } 43 | & a.leaflet-bar-part::before { 44 | content: none; 45 | } 46 | & a.leaflet-bar-part.leaflet-bar-part-single::after { 47 | ${collecticon('magnifier-left')}; 48 | top: unset; 49 | left: unset; 50 | height: 100%; 51 | width: 100%; 52 | border-radius: unset; 53 | border: unset; 54 | } 55 | } 56 | 57 | .leaflet-bar a, 58 | .leaflet-bar a:hover { 59 | color: ${themeVal('color.base')}; 60 | background: ${themeVal('color.surface')}; 61 | opacity: 1; 62 | &.leaflet-disabled { 63 | background: #5d5c66; 64 | } 65 | } 66 | 67 | .leaflet-control.leaflet-bar a.centerMap::after { 68 | ${collecticon('crosshair-2')}; 69 | } 70 | 71 | .leaflet-top.leaflet-left { 72 | .leaflet-control { 73 | box-shadow: 0 -1px 1px 0 rgba(68, 63, 63, 0.08), 74 | 0 2px 6px 0 rgba(68, 63, 63, 0.16); 75 | border: none; 76 | border-radius: 0.25rem; 77 | } 78 | } 79 | 80 | #layer-control.leaflet-control.generic-leaflet-control { 81 | background: ${themeVal('color.surface')}; 82 | line-height: 30px; 83 | text-align: center; 84 | } 85 | #layer-control.leaflet-control.generic-leaflet-control { 86 | cursor: pointer; 87 | } 88 | #layer-control.leaflet-control.generic-leaflet-control::after { 89 | ${collecticon('iso-stack')}; 90 | color: ${themeVal('color.base')}; 91 | top: unset; 92 | left: unset; 93 | height: 100%; 94 | width: 100%; 95 | border-radius: unset; 96 | border: unset; 97 | } 98 | .leaflet-control-scale-line { 99 | background: rgba(255, 255, 255, 0.75); 100 | } 101 | `; 102 | -------------------------------------------------------------------------------- /app/assets/scripts/styles/vendor/react-input-range.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { buttonVariation } from '@devseed-ui/button'; 3 | import { themeVal, rem, disabled } from '@devseed-ui/theme-provider'; 4 | 5 | // Some dependencies include styles that must be included. 6 | // This file overrides to be used the default react-input-range styles. 7 | 8 | const sliderSize = 1; 9 | const trackHeight = 0.5; 10 | 11 | export default () => css` 12 | .input-range__slider { 13 | ${(props) => { 14 | return buttonVariation( 15 | props.theme.color.base, 16 | 'raised', 17 | 'semidark', 18 | props 19 | ); 20 | }} 21 | /* background: none; */ 22 | border: none; 23 | width: ${rem(sliderSize)}; 24 | height: ${rem(sliderSize)}; 25 | margin-left: ${rem(sliderSize / -2)}; 26 | margin-top: ${rem(sliderSize / -2 + trackHeight / -2)}; 27 | 28 | &:active { 29 | transform: none; 30 | } 31 | 32 | &:focus { 33 | box-shadow: none; 34 | } 35 | 36 | .input-range--disabled & { 37 | border: none; 38 | box-shadow: none; 39 | transform: none; 40 | pointer-events: none; 41 | } 42 | } 43 | 44 | .input-range__label--value { 45 | display: none; 46 | } 47 | 48 | .input-range__track { 49 | height: ${rem(trackHeight)}; 50 | background: ${themeVal('color.baseAlphaD')}; 51 | 52 | &--active { 53 | background: ${themeVal('color.primary')}; 54 | } 55 | } 56 | 57 | .input-range--disabled { 58 | &, 59 | & * { 60 | cursor: not-allowed; 61 | } 62 | } 63 | 64 | .input-range--disabled .input-range__track.input-range__track--active { 65 | background: ${themeVal('color.primary')}; 66 | ${disabled()} 67 | } 68 | 69 | .input-range__label-container { 70 | left: -25%; 71 | } 72 | .input-range__label--max .input-range__label-container { 73 | left: 25%; 74 | } 75 | `; 76 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/api-health.js: -------------------------------------------------------------------------------- 1 | import toasts from '../components/common/toasts'; 2 | import config from '../config'; 3 | import { fetchJSON } from './utils'; 4 | import history from '../history'; 5 | import logger from './logger'; 6 | const { restApiEndpoint } = config; 7 | 8 | export default async function checkApiHealth() { 9 | // Bypass home and about page 10 | if (['/', '/about'].includes(history.location.pathname)) { 11 | return; 12 | } 13 | 14 | let isHealthy = false; 15 | try { 16 | const { body } = await fetchJSON(`${restApiEndpoint}/health`); 17 | isHealthy = body && body.healthy; 18 | } catch (error) { 19 | logger(error); 20 | isHealthy = false; 21 | } 22 | 23 | if (!isHealthy) { 24 | history.push('/'); 25 | toasts.error('API is unreachable, please try again later.'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/azure-app-insights.js: -------------------------------------------------------------------------------- 1 | // AppInsights.js 2 | // Based on https://docs.microsoft.com/en-us/azure/azure-monitor/app/javascript-react-plugin#basic-usage 3 | 4 | import { ApplicationInsights } from '@microsoft/applicationinsights-web'; 5 | import { ReactPlugin } from '@microsoft/applicationinsights-react-js'; 6 | import { createBrowserHistory } from 'history'; 7 | import config from '../config'; 8 | 9 | const { appInsightsKey } = config; 10 | 11 | const browserHistory = createBrowserHistory({ basename: '' }); 12 | const reactPlugin = new ReactPlugin(); 13 | const appInsights = new ApplicationInsights({ 14 | config: { 15 | instrumentationKey: appInsightsKey, 16 | extensions: [reactPlugin], 17 | extensionConfig: { 18 | [reactPlugin.identifier]: { history: browserHistory }, 19 | }, 20 | }, 21 | }); 22 | appInsights.loadAppInsights(); 23 | export { reactPlugin, appInsights }; 24 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/compose-components.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Composes components to to avoid deep nesting trees. Useful for contexts. 5 | * 6 | * @param {node} children Component children 7 | * @param {array} components The components to compose. 8 | */ 9 | export default function Composer(props) { 10 | const { children, components } = props; 11 | const itemToCompose = [...components].reverse(); 12 | 13 | return itemToCompose.reduce( 14 | (acc, Component) => {acc}, 15 | children 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/copy-text-to-clipboard.js: -------------------------------------------------------------------------------- 1 | import logger from './logger'; 2 | 3 | export default async function copyTextToClipboard(newClip) { 4 | const success = await navigator.clipboard.writeText(newClip).then( 5 | function () { 6 | /* clipboard successfully set */ 7 | return true; 8 | }, 9 | function (err) { 10 | logger(err); 11 | /* clipboard write failed */ 12 | return false; 13 | } 14 | ); 15 | return success; 16 | } 17 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/dates.js: -------------------------------------------------------------------------------- 1 | import { format, addMonths, isBefore } from 'date-fns'; 2 | 3 | /** 4 | * This function formats a timestamp to a simple UTC string, generally used 5 | * for displaying timestamps for mosaics. 6 | */ 7 | export function formatTimestampToSimpleUTC(timestamp) { 8 | const isoString = new Date(timestamp).toISOString(); 9 | const datePart = isoString.split('T')[0]; 10 | const timePart = isoString.split('T')[1].slice(0, 5); 11 | return `${datePart} ${timePart} UTC`; 12 | } 13 | 14 | export function formatTimestampToSimpleUTCDate(timestamp) { 15 | const isoString = new Date(timestamp).toISOString(); 16 | const datePart = getDatePartFromISOString(isoString); 17 | return `${datePart}`; 18 | } 19 | 20 | export function formatMosaicDateRange(startTimestamp, endTimestamp) { 21 | return `${format(startTimestamp, 'MMM dd, yyyy')} - ${format( 22 | endTimestamp, 23 | 'MMM dd, yyyy' 24 | )}`; 25 | } 26 | 27 | export function getDatePartFromISOString(isoString) { 28 | return isoString.split('T')[0]; 29 | } 30 | 31 | // This function generates an array of quarters between two dates, starting from 32 | // March. 33 | export function generateQuartersInBetweenDates(startDate1, startDate2) { 34 | // Ensure startDate1 is the earlier date 35 | let start = new Date(startDate1 < startDate2 ? startDate1 : startDate2); 36 | const end = new Date(startDate1 < startDate2 ? startDate2 : startDate1); 37 | 38 | const quarters = []; 39 | while (isBefore(start, end)) { 40 | // Calculate the end of the quarter 41 | const endOfQuarter = addMonths(start, 2); 42 | // Create quarter object with label and timestamps 43 | const quarter = { 44 | label: `${format(start, 'MMM')} - ${format(endOfQuarter, 'MMM, yyyy')}`, 45 | startTimestamp: start.toISOString(), 46 | endTimestamp: endOfQuarter.toISOString(), 47 | }; 48 | // Add the quarter object to the list 49 | quarters.push(quarter); 50 | // Move to the next quarter by setting start to the month after endOfQuarter 51 | start = addMonths(start, 3); 52 | } 53 | 54 | return quarters; 55 | } 56 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/is-rectangle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Check if geojson feature is a rectangle. 4 | * 5 | * @param {object} feature A valid GeoJSON feature 6 | * @returns a boolean, true if feature is a rectangle 7 | */ 8 | export function isRectangle(feature) { 9 | try { 10 | // Should be Polygon type 11 | if (feature.type !== 'Polygon') { 12 | return false; 13 | } 14 | 15 | // Polygon contain only one outer ring 16 | if (feature.coordinates.length !== 1) { 17 | return false; 18 | } 19 | 20 | // Get outer ring coordinates 21 | const coords = feature.coordinates[0]; 22 | 23 | // Should have five lonlat pairs 24 | if (coords.length !== 5) { 25 | return false; 26 | } 27 | 28 | // Get pairs 29 | const [p1, p2, p3, p4, p5] = coords; 30 | 31 | // First and last pairs should match 32 | if (p1[0] !== p5[0] || p1[0] !== p5[0]) { 33 | return false; 34 | } 35 | 36 | // Latitude on these corners should match 37 | if (p1[1] !== p2[1] || p3[1] !== p3[1]) { 38 | return false; 39 | } 40 | 41 | // Longitude on these corners should match 42 | if (p1[0] !== p4[0] || p2[0] !== p3[0]) { 43 | return false; 44 | } 45 | 46 | // Feature is rectangle when all conditions are satisfied 47 | return true; 48 | } catch (error) { 49 | // Feature must be malformed, return false 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/local-storage.js: -------------------------------------------------------------------------------- 1 | import logger from './logger'; 2 | 3 | /** 4 | * This files adds helper functions to read/write from localStorage. In some scenarios (e. g. Firefox on Apple M1) 5 | * localStorage get corrupted sometimes and the app crashes. 6 | * 7 | * ref. https://stackoverflow.com/questions/18877643/error-in-local-storage-ns-error-file-corrupted-firefox 8 | */ 9 | 10 | const theLocalStorage = 11 | window.localStorage || window.content.localStorage || null; 12 | 13 | export function getLocalStorageItem(key, format) { 14 | let value; 15 | try { 16 | value = theLocalStorage.getItem(key); 17 | if (format === 'json') { 18 | value = JSON.parse(value); 19 | } 20 | } catch (error) { 21 | logger('Could not load local storage key: ', key); 22 | if (error.name === 'NS_ERROR_FILE_CORRUPTED') { 23 | theLocalStorage.clear(); 24 | } 25 | } 26 | return value; 27 | } 28 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/logger.js: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | const { environment } = config; 3 | 4 | /** 5 | * Log a message to the console if the environment is not production 6 | * @param {...any} message - The message to log 7 | * @example 8 | * logger('Hello world'); 9 | * // => 'Hello world' 10 | * @example 11 | * logger('Hello', 'world'); 12 | * // => 'Hello world' 13 | */ 14 | export default function (...message) { 15 | if (environment !== 'production') { 16 | console.log(...message); // eslint-disable-line no-console 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/machine-state-logger.js: -------------------------------------------------------------------------------- 1 | import get from 'lodash.get'; 2 | 3 | /* eslint-disable no-console */ 4 | 5 | export function machineStateLogger(s) { 6 | console.groupCollapsed( 7 | '%c event', 8 | 'color: gray; font-weight: lighter;', 9 | s.event.type 10 | ); 11 | console.log('%c prev state', 'color: #9E9E9E; font-weight: bold;', s.history); 12 | console.log('%c event', 'color: #03A9F4; font-weight: bold;', s.event); 13 | console.log('%c next state', 'color: #4CAF50; font-weight: bold;', s); 14 | console.groupEnd(); 15 | 16 | const checkpointList = get(s.context, 'checkpointList', []); 17 | console.log(`ckpts ${checkpointList?.map((c) => c.id)}`); 18 | console.log(checkpointList); 19 | 20 | const aoisList = get(s.context, 'aoisList', []); 21 | console.log(`aois ${aoisList?.map((aoi) => aoi.id)}`); 22 | console.log(aoisList); 23 | 24 | const timeframesList = get(s.context, 'timeframesList', []); 25 | console.log(`tmfs ${timeframesList?.map((t) => t.id)}`); 26 | console.log(timeframesList); 27 | 28 | console.log( 29 | [ 30 | ['currentCheckpoint.id', 'ckpt'], 31 | ['currentAoi.id', 'aoi'], 32 | ['currentTimeframe.id', 'tmf'], 33 | ['currentMosaic.id', 'msc'], 34 | ] 35 | .map(([path, label]) => { 36 | const value = get(s.context, path); 37 | return `${label}: ${value}`; 38 | }) 39 | .join(' | ') 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/map.js: -------------------------------------------------------------------------------- 1 | import tArea from '@turf/area'; 2 | import tBboxPolygon from '@turf/bbox-polygon'; 3 | import booleanIntersects from '@turf/boolean-intersects'; 4 | 5 | /** 6 | * Get area from bbox 7 | * 8 | * @param {array} bbox extent in minX, minY, maxX, maxY order 9 | */ 10 | export function areaFromBounds(bbox) { 11 | const poly = tBboxPolygon(bbox); 12 | return tArea(poly); 13 | } 14 | 15 | /** 16 | * Verify if a bbox intersects a mapBounds 17 | * 18 | * @param {array} bbox extent in minX, minY, maxX, maxY order 19 | * @param {function} mapBounds Leaflet map bounds 20 | */ 21 | export function bboxIntersectsMapBounds(bbox, mapBounds) { 22 | if (mapBounds) { 23 | const mapBoundsPolygon = tBboxPolygon( 24 | mapBounds 25 | .toBBoxString() 26 | .split(',') 27 | .map((i) => Number(i)) 28 | ); 29 | return booleanIntersects(tBboxPolygon(bbox), mapBoundsPolygon); 30 | } 31 | return false; 32 | } 33 | 34 | export function aoiBoundsToArray(bounds) { 35 | // Get bbox polygon from AOI 36 | const { 37 | _southWest: { lng: minX, lat: minY }, 38 | _northEast: { lng: maxX, lat: maxY }, 39 | } = bounds; 40 | 41 | return [minX, minY, maxX, maxY]; 42 | } 43 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/pagination-options.js: -------------------------------------------------------------------------------- 1 | export function listPageOptions(page, lastPage) { 2 | let pageOptions = [1]; 3 | if (lastPage === 1) { 4 | return pageOptions; 5 | } 6 | if (page === 0 || page > lastPage) { 7 | return pageOptions.concat([2, '...', lastPage]); 8 | } 9 | if (lastPage > 5) { 10 | if (page < 3) { 11 | return pageOptions.concat([2, 3, '...', lastPage]); 12 | } 13 | if (page === 3) { 14 | return pageOptions.concat([2, 3, 4, '...', lastPage]); 15 | } 16 | if (page === lastPage) { 17 | return pageOptions.concat(['...', page - 2, page - 1, lastPage]); 18 | } 19 | if (page === lastPage - 1) { 20 | return pageOptions.concat(['...', page - 1, page, lastPage]); 21 | } 22 | if (page === lastPage - 2) { 23 | return pageOptions.concat(['...', page - 1, page, page + 1, lastPage]); 24 | } 25 | return pageOptions.concat([ 26 | '...', 27 | page - 1, 28 | page, 29 | page + 1, 30 | '...', 31 | lastPage, 32 | ]); 33 | } else { 34 | let range = []; 35 | for (let i = 1; i <= lastPage; i++) { 36 | range.push(i); 37 | } 38 | return range; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/share-link.js: -------------------------------------------------------------------------------- 1 | import { 2 | hideGlobalLoading, 3 | showGlobalLoadingMessage, 4 | } from '../components/common/global-loading'; 5 | import toasts from '../components/common/toasts'; 6 | import copyTextToClipboard from './copy-text-to-clipboard'; 7 | import logger from './logger'; 8 | import { saveAs } from 'file-saver'; 9 | 10 | export const getShareLink = (share) => 11 | `${window.location.origin}/share/${share.uuid}/map`; 12 | 13 | export const copyShareUrlToClipboard = (share) => { 14 | copyTextToClipboard(getShareLink(share)).then((result) => { 15 | if (result) { 16 | toasts.success('URL copied to clipboard'); 17 | } else { 18 | logger('Failed to copy', result); 19 | toasts.error('Failed to copy URL to clipboard'); 20 | } 21 | }); 22 | }; 23 | 24 | export function downloadGeotiff(arrayBuffer, filename) { 25 | var blob = new Blob([arrayBuffer], { 26 | type: 'application/x-geotiff', 27 | }); 28 | saveAs(blob, filename); 29 | } 30 | 31 | export const downloadShareGeotiff = async (restApiClient, share) => { 32 | showGlobalLoadingMessage('Preparing GeoTIFF for Download'); 33 | try { 34 | const geotiffArrayBuffer = await restApiClient.get( 35 | `/share/${share.uuid}/download/color`, 36 | 'binary' 37 | ); 38 | const filename = `${share.uuid}.tiff`; 39 | downloadGeotiff(geotiffArrayBuffer, filename); 40 | } catch (error) { 41 | logger('Error with geotiff download', error); 42 | toasts.error('Failed to download GeoTIFF'); 43 | } 44 | hideGlobalLoading(); 45 | return; 46 | }; 47 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/use-fetch.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { useEffect, useState } from 'react'; 3 | import { useAuth } from '../context/auth'; 4 | 5 | /** 6 | * Creates a hook to perform data fetching using restApiClient. 7 | * 8 | * @param {string} path URL path to query. 9 | * @param {object} opts Available options. 10 | * @param {func} [opts.authRequired=true] Whether auth is required. 11 | * @param {func} [opts.mutator=(body)=>body] Function to mutate API response. 12 | * 13 | * @returns {object} 14 | * 15 | */ 16 | export default function useFetch(urlPath, options = {}) { 17 | const { authRequired, mutator } = options; 18 | 19 | const request = useRef(); 20 | const { restApiClient, isAuthenticated, logout } = useAuth(); 21 | 22 | const [status, setStatus] = useState('idle'); 23 | const [error, setError] = useState(); 24 | const [data, setData] = useState(null); 25 | 26 | function fetch() { 27 | if (authRequired && !isAuthenticated) { 28 | return; 29 | } 30 | setStatus('loading'); 31 | request.current = setTimeout(() => { 32 | restApiClient 33 | .get(urlPath) 34 | .then((body) => { 35 | setData(mutator ? mutator(body) : body); 36 | setStatus('success'); 37 | }) 38 | .catch(({ message }) => { 39 | if (message === 'Invalid Token') { 40 | logout(); 41 | } 42 | setError(message); 43 | setStatus('error'); 44 | }); 45 | }, 250); 46 | } 47 | 48 | // Fetch request on page load, clear on unmount 49 | useEffect(() => { 50 | fetch(); 51 | return () => clearTimeout(request.current); 52 | }, [urlPath, authRequired, isAuthenticated]); 53 | 54 | return { 55 | fetch, 56 | data, 57 | status, 58 | isReady: status !== 'idle' && status !== 'loading', 59 | hasError: status === 'error', 60 | error, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/use-focus.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | /** 4 | * Helper hook to auto focus inputs in case `autofocus` cannot be applied. 5 | * @param {*} delay time to trigger focus 6 | */ 7 | export function useFocus(delay = 0) { 8 | const htmlElRef = useRef(null); 9 | const setFocus = () => { 10 | setTimeout(() => { 11 | htmlElRef.current && htmlElRef.current.focus(); 12 | }, delay); 13 | }; 14 | 15 | return [htmlElRef, setFocus]; 16 | } 17 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/use-pc-collection.js: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | import config from '../config'; 3 | import get from 'lodash.get'; 4 | import { endOfDay, subDays } from 'date-fns'; 5 | const { planetaryComputerStacApi } = config; 6 | 7 | const imagerySourceCollectionIds = { 8 | NAIP: 'naip', 9 | 'Sentinel-2': 'sentinel-2-l2a', 10 | }; 11 | 12 | export const usePlanetaryComputerCollection = (imagerySourceName) => 13 | useSWR(imagerySourceCollectionIds[imagerySourceName], (collectionId) => 14 | fetch(`${planetaryComputerStacApi}/collections/${collectionId}`) 15 | .then((res) => res.json()) 16 | .then((data) => { 17 | const temporalExtentInterval = get( 18 | data, 19 | 'extent.temporal.interval[0]', 20 | [null, null] 21 | ); 22 | 23 | let acquisitionStart = temporalExtentInterval[0]; 24 | let acquisitionEnd = temporalExtentInterval[1]; 25 | 26 | // If no end date is set, set it to yesterday 27 | if (acquisitionEnd === null) { 28 | acquisitionEnd = endOfDay(subDays(new Date(), 1)).toISOString(); 29 | } 30 | 31 | // If no start date is set, set it to 90 days ago 32 | if (acquisitionStart === null) { 33 | // Subtract 90 days to get 90 days ago 34 | acquisitionStart = endOfDay(subDays(new Date(), 90)).toISOString(); 35 | } 36 | 37 | return { 38 | acquisitionStart, 39 | acquisitionEnd, 40 | }; 41 | }) 42 | ); 43 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/use-previous.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | // From https://usehooks.com/usePrevious 4 | 5 | export default function usePrevious(value) { 6 | // The ref object is a generic container whose current property is mutable ... 7 | // ... and can hold any value, similar to an instance property on a class 8 | const ref = useRef(); 9 | 10 | // Store current value in ref 11 | useEffect(() => { 12 | ref.current = value; 13 | }, [value]); // Only re-run if value changes 14 | 15 | // Return previous value (happens before update in useEffect above) 16 | return ref.current; 17 | } 18 | -------------------------------------------------------------------------------- /app/assets/scripts/utils/utils.js: -------------------------------------------------------------------------------- 1 | export function inRange(value, min, max, exclusive) { 2 | if (!exclusive) { 3 | return min <= value && value <= max; 4 | } else { 5 | return min < value && value < max; 6 | } 7 | } 8 | 9 | /** 10 | * Delays the execution in x milliseconds. 11 | * 12 | * @param {int} millis Milliseconds 13 | */ 14 | export function delay(millis) { 15 | return new Promise((resolve) => setTimeout(resolve, millis)); 16 | } 17 | 18 | /** 19 | * Performs a request to the given url returning the response in json format 20 | * or throwing an error. 21 | * 22 | * @param {string} url Url to query 23 | * @param {object} options Options for fetch 24 | */ 25 | export async function fetchJSON(url, options) { 26 | let response; 27 | options = options || {}; 28 | const format = options.format || 'json'; 29 | let data; 30 | try { 31 | response = await fetch(url, options); 32 | if (format === 'json') { 33 | data = await response.json(); 34 | } else if (format === 'binary') { 35 | data = await response.arrayBuffer(); 36 | } else { 37 | data = await response.text(); 38 | } 39 | 40 | if (response.status >= 400) { 41 | const err = new Error(data.message); 42 | err.statusCode = response.status; 43 | err.data = data; 44 | throw err; 45 | } 46 | 47 | return { body: data, headers: response.headers }; 48 | } catch (error) { 49 | error.statusCode = response ? response.status || null : null; 50 | throw error; 51 | } 52 | } 53 | 54 | export function filterObject(obj, callback) { 55 | return Object.fromEntries( 56 | Object.entries(obj).filter(([key, val]) => callback(key, val)) 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /app/assets/types.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const StacCollectionType = PropTypes.shape({ 4 | acquisitionStart: PropTypes.string.isRequired, 5 | acquisitionEnd: PropTypes.string.isRequired, 6 | }); 7 | -------------------------------------------------------------------------------- /app/humans.txt: -------------------------------------------------------------------------------- 1 | _____ _ _ _____ _ 2 | | _ \ | | | | / ___| | | 3 | | | | |_____ _____| | ___ _ __ _ __ ___ ___ _ __ | |_ \ `--. ___ ___ __| | 4 | | | | / _ \ \ / / _ \ |/ _ \| '_ \| '_ ` _ \ / _ \ '_ \| __| `--. \/ _ \/ _ \/ _` | 5 | | |/ / __/\ V / __/ | (_) | |_) | | | | | | __/ | | | |_ /\__/ / __/ __/ (_| | 6 | |___/ \___| \_/ \___|_|\___/| .__/|_| |_| |_|\___|_| |_|\__| \____/ \___|\___|\__,_| 7 | | | 8 | |_| 9 | ==================================================================================== 10 | 11 | 12 | /* Team */ -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "optional_permissions": ["clipboardWrite"] 3 | } 4 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * 4 | Disallow: 5 | -------------------------------------------------------------------------------- /cypress-stress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:9000/", 3 | "experimentalFetchPolyfill": true, 4 | "chromeWebSecurity": false, 5 | "defaultCommandTimeout": 7000, 6 | "testFiles": "**/stress-live-api.spec.js" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }]], 3 | }); 4 | 5 | const { defineConfig } = require('cypress'); 6 | const appConfig = require('./app/assets/scripts/config').default; 7 | const { startMockWsServer } = require('cypress-websocket-server'); 8 | 9 | module.exports = defineConfig({ 10 | experimentalFetchPolyfill: true, 11 | chromeWebSecurity: false, 12 | defaultCommandTimeout: 7000, 13 | video: false, 14 | viewportHeight: 800, 15 | e2e: { 16 | baseUrl: 'http://localhost:9000/', 17 | excludeSpecPattern: [ 18 | '**/gpu.cy.js', 19 | '**/stress-live-api.cy.js', 20 | '**/keyboard-shortcuts.cy.js', 21 | '**/layers-panel.cy.js', 22 | '**/panel.cy.js', 23 | '**/retrain.cy.js', 24 | '**/sec-panel.cy.js', 25 | '**/aois.cy.js', 26 | '**/new.cy.js', 27 | ], 28 | setupNodeEvents(on, cypressConfig) { 29 | startMockWsServer(cypressConfig); 30 | 31 | // Pass app config to Cypress tests 32 | return { 33 | ...cypressConfig, 34 | restApiEndpoint: appConfig.restApiEndpoint, 35 | apiToken: appConfig.apiToken, 36 | }; 37 | }, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /cypress/e2e/about.cy.js: -------------------------------------------------------------------------------- 1 | describe('The About Page', () => { 2 | it('successfully loads', () => { 3 | cy.mockCommonApiRoutes(); 4 | cy.visit('/about'); 5 | cy.get('body'); 6 | cy.get('header'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /cypress/e2e/auth.cy.js: -------------------------------------------------------------------------------- 1 | const restApiEndpoint = Cypress.config('restApiEndpoint'); 2 | 3 | describe('The app header', () => { 4 | beforeEach(() => { 5 | cy.mockCommonApiRoutes(); 6 | }); 7 | 8 | it('shows the account button when logged in', () => { 9 | cy.fakeLogin(); 10 | cy.visit('/'); 11 | cy.get('body'); 12 | cy.get('[data-cy=account-button]').should('exist'); 13 | cy.get('[data-cy=login-button]').should('not.exist'); 14 | }); 15 | 16 | it('admin user can see link and access Model admin page', () => { 17 | cy.fakeLogin('admin'); 18 | cy.visit('/'); 19 | cy.get('body'); 20 | cy.get('[data-cy=account-button]').should('exist').click(); 21 | cy.get('[data-cy=manage-models-link]').should('exist').click(); 22 | cy.get('body').should('contain', 'Models'); 23 | }); 24 | 25 | it('normal user does not see the Manage models link', () => { 26 | cy.fakeLogin(); 27 | cy.visit('/'); 28 | cy.get('body'); 29 | cy.get('[data-cy=account-button]').should('exist').click(); 30 | cy.get('[data-cy=manage-models-link]').should('not.exist'); 31 | }); 32 | 33 | it('invalid route displays uhoh page', () => { 34 | cy.visit('/invalid-route'); 35 | cy.get('body').should('contain', 'Page not found.'); 36 | }); 37 | 38 | it('/project/new is protected', () => { 39 | cy.visit('/project/new'); 40 | cy.location().should((loc) => { 41 | expect(loc.pathname).to.eq('/'); 42 | }); 43 | cy.get('#a-toast').should('contain', 'Please sign in to view this page.'); 44 | }); 45 | 46 | it('/project/:id is protected', () => { 47 | cy.visit('/project/1'); 48 | cy.location().should((loc) => { 49 | expect(loc.pathname).to.eq('/'); 50 | }); 51 | cy.get('#a-toast').should('contain', 'Please sign in to view this page.'); 52 | }); 53 | 54 | it('/profile/projects is protected', () => { 55 | cy.visit('/profile/projects'); 56 | cy.location().should((loc) => { 57 | expect(loc.pathname).to.eq('/'); 58 | }); 59 | cy.get('#a-toast').should('contain', 'Please sign in to view this page.'); 60 | }); 61 | 62 | it('/profile/projects/:id is protected', () => { 63 | cy.visit('/profile/projects/:1'); 64 | cy.location().should((loc) => { 65 | expect(loc.pathname).to.eq('/'); 66 | }); 67 | cy.get('#a-toast').should('contain', 'Please sign in to view this page.'); 68 | }); 69 | 70 | it('when a generic 401 status happens, redirect to login', () => { 71 | cy.intercept( 72 | { 73 | url: restApiEndpoint + '/api/*', 74 | }, 75 | (req) => { 76 | req.reply(401, { 77 | status: 401, 78 | message: 'Authentication Required', 79 | messages: [], 80 | }); 81 | } 82 | ); 83 | 84 | cy.visit('/project/1'); 85 | 86 | cy.location().should((loc) => { 87 | expect(loc.pathname).to.eq('/'); 88 | }); 89 | cy.get('#a-toast').should('contain', 'Please sign in to view this page.'); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /cypress/e2e/home.cy.js: -------------------------------------------------------------------------------- 1 | // / 2 | describe('The Home Page', () => { 3 | it('successfully loads', () => { 4 | cy.mockCommonApiRoutes(); 5 | cy.visit('/'); 6 | cy.get('body'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /cypress/e2e/project/delete.cy.js: -------------------------------------------------------------------------------- 1 | describe('Delete a project', () => { 2 | beforeEach(() => { 3 | cy.mockCommonApiRoutes(); 4 | cy.mockModelApiRoutes(); 5 | cy.mockProjectEndpoints(); 6 | cy.fakeLogin(); 7 | cy.visit('/profile/projects/1'); 8 | }); 9 | 10 | it('Loads project page', () => { 11 | cy.get('[data-cy=project-name]').should('contain', 'Untitled'); 12 | }); 13 | 14 | it('Can delete project', () => { 15 | cy.get('[data-cy=delete-project-button]').should('exist').click(); 16 | cy.get('[data-cy=confirm-delete-project-modal]').should('exist'); 17 | cy.get('[data-cy=confirm-project-delete]').click(); 18 | cy.get('[data-cy=confirm-delete-project-modal]').should('not.exist'); 19 | cy.wait('@deleteProject'); 20 | cy.location().should((loc) => { 21 | expect(loc.pathname).to.eq('/profile/projects'); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /cypress/e2e/project/layers-panel.cy.js: -------------------------------------------------------------------------------- 1 | describe('Open existing project', () => { 2 | beforeEach(() => { 3 | cy.mockCommonApiRoutes(); 4 | }); 5 | 6 | it('successfully loads', () => { 7 | cy.fakeLogin(); 8 | cy.visit('/project/1'); 9 | 10 | cy.get('#layer-control').click({ force: true }); 11 | cy.get('[data-cy=layers-panel]').should('be.visible'); 12 | cy.get('#layer-control').click({ force: true }); 13 | cy.get('[data-cy=layers-panel]').should('not.be.visible'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /cypress/e2e/project/sec-panel.cy.js: -------------------------------------------------------------------------------- 1 | const restApiEndpoint = Cypress.config('restApiEndpoint'); 2 | 3 | const instance = { 4 | id: 1, 5 | project_id: 1, 6 | aoi_id: 2, 7 | checkpoint_id: 2, 8 | last_update: '2021-07-12T09:59:04.442Z', 9 | created: '2021-07-12T09:58:57.459Z', 10 | active: true, 11 | token: 'app_client', 12 | status: { 13 | phase: 'Running', 14 | }, 15 | }; 16 | const aoiNoStats = { 17 | id: 2, 18 | name: 'Seneca Rocks', 19 | created: '2021-04-30T18:05:26.309Z', 20 | storage: true, 21 | bookmarked: true, 22 | project_id: 1, 23 | checkpoint_id: '1', 24 | patches: [], 25 | classes: [ 26 | { name: 'Water / Wetland', color: '#486DA2' }, 27 | { name: 'Emergent Wetlands', color: '#00A884' }, 28 | { name: 'Tree', color: '#6CA966' }, 29 | { name: 'Shrubland', color: '#ABC964' }, 30 | { name: 'Low Vegetation', color: '#D0F3AB' }, 31 | { name: 'Barren', color: '#D2AD74' }, 32 | { name: 'Structure', color: '#F10100' }, 33 | { name: 'Impervious Surface', color: '#BFB5B5' }, 34 | { name: 'Impervious Road', color: '#320000' }, 35 | ], 36 | bounds: { 37 | type: 'Polygon', 38 | coordinates: [ 39 | [ 40 | [-79.389824867, 38.828040665], 41 | [-79.372229576, 38.828040665], 42 | [-79.372229576, 38.846058444], 43 | [-79.389824867, 38.846058444], 44 | [-79.389824867, 38.828040665], 45 | ], 46 | ], 47 | }, 48 | px_stats: {}, 49 | 50 | shares: [], 51 | }; 52 | 53 | describe('Panel functions', () => { 54 | beforeEach(() => { 55 | cy.mockCommonApiRoutes(); 56 | cy.fakeLogin(); 57 | cy.setWebsocketWorkflow('websocket-workflow/retrain.json'); 58 | 59 | /** 60 | * GET /project/:id/instance/:id 61 | */ 62 | cy.intercept( 63 | { 64 | url: restApiEndpoint + '/api/project/1/instance/1', 65 | }, 66 | instance 67 | ); 68 | }); 69 | it('Renders px stats chart on load', () => { 70 | cy.visit('/project/1'); 71 | cy.wait(['@fetchAoi2', '@fetchCheckpoint2']); 72 | cy.get('[data-cy=checkpoint_class_distro]').should( 73 | 'not.contain', 74 | 'Class distribution metrics are not available' 75 | ); 76 | }); 77 | it('Renders px stats chart on load', () => { 78 | cy.intercept( 79 | { 80 | url: restApiEndpoint + '/api/project/1/aoi/2', 81 | }, 82 | aoiNoStats 83 | ).as('fetchAoiNoStat'); 84 | 85 | cy.visit('/project/1'); 86 | cy.wait(['@fetchAoiNoStat', '@fetchCheckpoint2']); 87 | cy.get('[data-cy=checkpoint_class_distro]').should( 88 | 'contain', 89 | 'Class distribution metrics are not available' 90 | ); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /cypress/fixtures/aoi-2088.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2088, 3 | "name": "Derwood", 4 | "created": 1632766179168, 5 | "storage": true, 6 | "bookmarked": false, 7 | "project_id": 966, 8 | "checkpoint_id": 2, 9 | "patches": [], 10 | "px_stats": { 11 | "0": 0.016779691001297323, 12 | "1": 0.6555917561033141, 13 | "2": 0.14320232338719188, 14 | "3": 0.18442622950819673 15 | }, 16 | "classes": [ 17 | { "name": "Water / Wetland", "color": "#486DA2" }, 18 | { "name": "Tree", "color": "#6CA966" }, 19 | { "name": "Low Vegetation / Barren", "color": "#D0F3AB" }, 20 | { "name": "Built Environment", "color": "#BFB5B5" } 21 | ], 22 | "bounds": { 23 | "type": "Polygon", 24 | "coordinates": [ 25 | [ 26 | [-77.10290329, 39.103477125], 27 | [-77.044200897, 39.103477125], 28 | [-77.044200897, 39.179923694], 29 | [-77.10290329, 39.179923694], 30 | [-77.10290329, 39.103477125] 31 | ] 32 | ] 33 | }, 34 | "shares": [] 35 | } 36 | -------------------------------------------------------------------------------- /cypress/fixtures/aoi-upload/aoi-empty-collection.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [] 4 | } 5 | -------------------------------------------------------------------------------- /cypress/fixtures/aoi-upload/aoi-outside-usa.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [ 12 | -38.3090204000473, 13 | -12.914237363017046 14 | ], 15 | [ 16 | -38.310205936431885, 17 | -12.915879178457336 18 | ], 19 | [ 20 | -38.309454917907715, 21 | -12.916313159716845 22 | ], 23 | [ 24 | -38.30828547477722, 25 | -12.914728863038173 26 | ], 27 | [ 28 | -38.3090204000473, 29 | -12.914237363017046 30 | ] 31 | ] 32 | ] 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /cypress/fixtures/aoi-upload/aoi-zero-area.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Point", 9 | "coordinates": [20.390625, 12.211180191503997] 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /cypress/fixtures/aoi-upload/live-inferencing.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [ 12 | -79.41853523254395, 13 | 38.84993551372082 14 | ], 15 | [ 16 | -79.42557334899902, 17 | 38.840376148661505 18 | ], 19 | [ 20 | -79.41364288330078, 21 | 38.83101608549351 22 | ], 23 | [ 24 | -79.40531730651855, 25 | 38.840509854931064 26 | ], 27 | [ 28 | -79.4032573699951, 29 | 38.849200223547825 30 | ], 31 | [ 32 | -79.41853523254395, 33 | 38.84993551372082 34 | ] 35 | ] 36 | ] 37 | } 38 | }, 39 | { 40 | "type": "Feature", 41 | "properties": {}, 42 | "geometry": { 43 | "type": "Polygon", 44 | "coordinates": [ 45 | [ 46 | [ 47 | -79.40840721130371, 48 | 38.83469339997494 49 | ], 50 | [ 51 | -79.39939498901367, 52 | 38.8284752845139 53 | ], 54 | [ 55 | -79.39390182495117, 56 | 38.83669912779739 57 | ], 58 | [ 59 | -79.40840721130371, 60 | 38.83469339997494 61 | ] 62 | ] 63 | ] 64 | } 65 | } 66 | ] 67 | } -------------------------------------------------------------------------------- /cypress/fixtures/aoi-upload/no-live-inferencing.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { 4 | "type": "name", 5 | "properties": { 6 | "name": "urn:ogc:def:crs:OGC:1.3:CRS84" 7 | } 8 | }, 9 | "features": [ 10 | { 11 | "type": "Feature", 12 | "properties": {}, 13 | "geometry": { 14 | "coordinates": [ 15 | [ 16 | [-109.66554614494017, 40.41269314330078], 17 | [-109.62546894851961, 40.355446964239626], 18 | [-109.53914489833107, 40.38819037930594], 19 | [-109.55051422119139, 40.45426224994321], 20 | [-109.57077026367188, 40.47071851668331], 21 | [-109.66554614494017, 40.41269314330078] 22 | ] 23 | ], 24 | "type": "Polygon" 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /cypress/fixtures/aoi-upload/really-large-area.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [-79.41853523254395, 38.84993551372082], 12 | [-79.42557334899902, 38.840376148661505], 13 | [-79.41364288330078, 38.83101608549351], 14 | [-79.40531730651855, 38.840509854931064], 15 | [-79.4032573699951, 38.849200223547825], 16 | [-79.41853523254395, 38.84993551372082] 17 | ] 18 | ] 19 | } 20 | }, 21 | { 22 | "type": "Feature", 23 | "properties": {}, 24 | "geometry": { 25 | "type": "Polygon", 26 | "coordinates": [ 27 | [ 28 | [-79.40840721130371, 38.83469339997494], 29 | [-79.39939498901367, 38.8284752845139], 30 | [-79.39390182495117, 38.83669912779739], 31 | [-79.40840721130371, 38.83469339997494] 32 | ] 33 | ] 34 | } 35 | }, 36 | { 37 | "type": "Feature", 38 | "properties": {}, 39 | "geometry": { 40 | "type": "Polygon", 41 | "coordinates": [ 42 | [ 43 | [-87.7587890625, 40.97989806962013], 44 | [-90.72509765625, 36.03133177633187], 45 | [-83.21044921875, 35.53222622770337], 46 | [-79.12353515625, 39.67337039176558], 47 | [-85.1220703125, 42.66628070564928], 48 | [-87.7587890625, 40.97989806962013] 49 | ] 50 | ] 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /cypress/fixtures/aois.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "total":0, 3 | "project_id":1, 4 | "aois":[ 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /cypress/fixtures/aois.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "total":1, 3 | "project_id":1, 4 | "aois":[ 5 | { 6 | "id":1, 7 | "name":"Seneca Rocks", 8 | "bookmarked":true, 9 | "bounds":{ 10 | "type":"Polygon", 11 | "coordinates":[ 12 | [ 13 | [ 14 | -79.389824867, 15 | 38.828040665 16 | ], 17 | [ 18 | -79.372229576, 19 | 38.828040665 20 | ], 21 | [ 22 | -79.372229576, 23 | 38.846058444 24 | ], 25 | [ 26 | -79.389824867, 27 | 38.846058444 28 | ], 29 | [ 30 | -79.389824867, 31 | 38.828040665 32 | ] 33 | ] 34 | ] 35 | }, 36 | "created":"2021-04-30T18:11:51.705Z", 37 | "storage":false, 38 | "checkpoint_id":2, 39 | "checkpoint_name":"Seneca Rocks", 40 | "classes":[ 41 | { 42 | "name":"Water / Wetland", 43 | "color":"#486DA2" 44 | }, 45 | { 46 | "name":"Emergent Wetlands", 47 | "color":"#00A884" 48 | }, 49 | { 50 | "name":"Tree", 51 | "color":"#6CA966" 52 | }, 53 | { 54 | "name":"Shrubland", 55 | "color":"#ABC964" 56 | }, 57 | { 58 | "name":"Low Vegetation", 59 | "color":"#D0F3AB" 60 | }, 61 | { 62 | "name":"Barren", 63 | "color":"#D2AD74" 64 | }, 65 | { 66 | "name":"Structure", 67 | "color":"#F10100" 68 | }, 69 | { 70 | "name":"Impervious Surface", 71 | "color":"#BFB5B5" 72 | }, 73 | { 74 | "name":"Impervious Road", 75 | "color":"#320000" 76 | } 77 | ], 78 | "px_stats":{ 79 | "0":0.002208709716796875, 80 | "1":0.0003598531087239583, 81 | "2":0.00007502237955729167, 82 | "3":0 83 | }, 84 | "shares":[ 85 | 86 | ] 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /cypress/fixtures/geocoder/dc.json: -------------------------------------------------------------------------------- 1 | {"authenticationResultCode":"ValidCredentials","brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png","copyright":"Copyright © 2021 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.","resourceSets":[{"estimatedTotal":1,"resources":[{"__type":"Location:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1","bbox":[38.891243382429323,-77.022251725948308,38.898968817570676,-77.0090176740517],"name":"441 Indiana Ave NW, Washington, DC 20001, United States","point":{"type":"Point","coordinates":[38.8951061,-77.0156347]},"address":{"addressLine":"441 Indiana Ave NW","adminDistrict":"DC","adminDistrict2":"City of Washington","countryRegion":"United States","formattedAddress":"441 Indiana Ave NW, Washington, DC 20001, United States","locality":"Judiciary Square","postalCode":"20001"},"confidence":"High","entityType":"Address","geocodePoints":[{"type":"Point","coordinates":[38.8951061,-77.0156347],"calculationMethod":"Rooftop","usageTypes":["Display"]}],"matchCodes":["Good"]}]}],"statusCode":200,"statusDescription":"OK","traceId":"1644fddd46084cd0abeecfeb23828c67|BN000021CF|0.0.0.1|BN01EAP00004D50"} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/geocoder/ocean.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/cypress/fixtures/geocoder/ocean.json -------------------------------------------------------------------------------- /cypress/fixtures/geocoder/rural.json: -------------------------------------------------------------------------------- 1 | {"authenticationResultCode":"ValidCredentials","brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png","copyright":"Copyright © 2021 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.","resourceSets":[{"estimatedTotal":1,"resources":[{"__type":"Location:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1","bbox":[40.061317443847656,-78.25732421875,40.743545532226562,-77.676597595214844],"name":"Huntingdon County","point":{"type":"Point","coordinates":[40.483257293701172,-78.007156372070312]},"address":{"adminDistrict":"PA","adminDistrict2":"Huntingdon Co.","countryRegion":"United States","formattedAddress":"Huntingdon County"},"confidence":"Medium","entityType":"AdminDivision2","geocodePoints":[{"type":"Point","coordinates":[40.483257293701172,-78.007156372070312],"calculationMethod":"Rooftop","usageTypes":["Display"]}],"matchCodes":["Good","UpHierarchy"]}]}],"statusCode":200,"statusDescription":"OK","traceId":"203f730fd7104ddc8782e61142454906|BN000021E2|0.0.0.1|BN01EAP0000099E"} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/planetary-computer/data/mosaic/register.json: -------------------------------------------------------------------------------- 1 | { 2 | "searchid": "7f63c66c920e8a29b69fa7f45bafe038", 3 | "links": [ 4 | { 5 | "rel": "metadata", 6 | "type": "application/json", 7 | "href": "https://planetarycomputer.microsoft.com/api/data/v1/mosaic/7f63c66c920e8a29b69fa7f45bafe038/info" 8 | }, 9 | { 10 | "rel": "tilejson", 11 | "type": "application/json", 12 | "href": "https://planetarycomputer.microsoft.com/api/data/v1/mosaic/7f63c66c920e8a29b69fa7f45bafe038/tilejson.json" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /cypress/fixtures/samples.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Point", 9 | "coordinates": [-79.38308715820314, 38.83957390576818] 10 | } 11 | }, 12 | { 13 | "type": "Feature", 14 | "properties": {}, 15 | "geometry": { 16 | "type": "Point", 17 | "coordinates": [-79.38137054443361, 38.8409109722324] 18 | } 19 | }, 20 | { 21 | "type": "Feature", 22 | "properties": {}, 23 | "geometry": { 24 | "type": "Polygon", 25 | "coordinates": [ 26 | [ 27 | [-79.383087, 38.844254], 28 | [-79.381371, 38.845591], 29 | [-79.379654, 38.844254], 30 | [-79.381371, 38.842917], 31 | [-79.383087, 38.844254] 32 | ] 33 | ] 34 | } 35 | }, 36 | { 37 | "type": "Feature", 38 | "properties": {}, 39 | "geometry": { 40 | "type": "Invalid", 41 | "coordinates": [ 42 | [ 43 | [-80.15625, 34.70549341022544], 44 | [-81.1669921875, 33.247875947924385], 45 | [-77.93701171875, 33.137551192346145], 46 | [-80.15625, 34.70549341022544] 47 | ] 48 | ] 49 | } 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /cypress/fixtures/tiles/imagery-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/cypress/fixtures/tiles/imagery-tile.png -------------------------------------------------------------------------------- /cypress/fixtures/tiles/jpeg-tile.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/cypress/fixtures/tiles/jpeg-tile.jpeg -------------------------------------------------------------------------------- /cypress/fixtures/tiles/osm-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/cypress/fixtures/tiles/osm-tile.png -------------------------------------------------------------------------------- /cypress/fixtures/tiles/png-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/pearl-frontend/07785395044db300c515d5f8f9be7942bd41a68b/cypress/fixtures/tiles/png-tile.png -------------------------------------------------------------------------------- /cypress/fixtures/websocket-workflow/load-aoi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "server", 4 | "payload": { "message": "info#connected" } 5 | }, 6 | { 7 | "type": "client", 8 | "payload": { "action": "model#status" } 9 | }, 10 | { 11 | "type": "server", 12 | "payload": { 13 | "message": "model#status", 14 | "data": { 15 | "is_aborting": false, 16 | "processing": false, 17 | "timeframe": 1, 18 | "checkpoint": 2 19 | } 20 | } 21 | }, 22 | { 23 | "type": "client", 24 | "payload": { "action": "model#timeframe", "data": { "id": 2 } } 25 | }, 26 | { 27 | "type": "server", 28 | "payload": { 29 | "message": "model#timeframe#progress", 30 | "data": { "aoi": 2, "processed": 0, "total": 1 } 31 | } 32 | }, 33 | { 34 | "type": "server", 35 | "payload": { "message": "model#timeframe#complete", "data": { "aoi": 2 } } 36 | }, 37 | { 38 | "type": "client", 39 | "payload": { "action": "model#status" } 40 | }, 41 | { 42 | "type": "server", 43 | "payload": { 44 | "message": "model#status", 45 | "data": { 46 | "is_aborting": false, 47 | "processing": false, 48 | "aoi": 2, 49 | "checkpoint": 2 50 | } 51 | } 52 | } 53 | ] 54 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // Use babel to enable import/export statements, used in config.js 4 | require('@babel/register')({ 5 | presets: [['@babel/preset-env', { targets: { node: 'current' } }]], 6 | }); 7 | 8 | const appConfig = require('../../app/assets/scripts/config').default; 9 | 10 | const { startMockWsServer } = require('cypress-websocket-server'); 11 | 12 | /** 13 | * @type {Cypress.PluginConfig} 14 | */ 15 | module.exports = (on, cypressConfig) => { 16 | startMockWsServer(cypressConfig); 17 | 18 | // Pass app config to Cypress tests 19 | return { 20 | ...cypressConfig, 21 | restApiEndpoint: appConfig.restApiEndpoint, 22 | apiToken: appConfig.apiToken, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /cypress/support/commands/fake-login.js: -------------------------------------------------------------------------------- 1 | const apiToken = Cypress.config('apiToken') || 'FAKE_API_TOKEN'; 2 | 3 | /** 4 | * Make client look like it has authenticated 5 | */ 6 | Cypress.Commands.add('fakeLogin', (access, flags = {}) => { 7 | window.localStorage.setItem('useFakeLogin', true); 8 | window.localStorage.setItem( 9 | 'authState', 10 | JSON.stringify({ 11 | isLoading: false, 12 | error: false, 13 | isAuthenticated: true, 14 | userAccessLevel: access || 'user', 15 | apiToken, 16 | user: { 17 | name: 'Test User', 18 | flags, 19 | }, 20 | }) 21 | ); 22 | window.localStorage.setItem('site-tour', -1); 23 | }); 24 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/common.js: -------------------------------------------------------------------------------- 1 | import { interceptHostname, interceptUrl, interceptApiRoute } from './utils'; 2 | 3 | import apiIndex from '../mock-api-routes/fixtures/index.json'; 4 | import apiHealth from '../mock-api-routes/fixtures/health.json'; 5 | import imageryIndex from '../mock-api-routes/fixtures/imagery.json'; 6 | import mosaicIndex from '../mock-api-routes/fixtures/mosaic/index.json'; 7 | import mosaicNaipLatest from '../mock-api-routes/fixtures/mosaic/naip.latest.json'; 8 | import mosaicPostResponse from '../mock-api-routes/fixtures/mosaic/post-response.json'; 9 | 10 | const restApiEndpoint = Cypress.config('restApiEndpoint'); 11 | 12 | Cypress.Commands.add('mockCommonApiRoutes', () => { 13 | // OSM Tiles 14 | ['a', 'b', 'c'].forEach((subdomain) => { 15 | interceptHostname(`${subdomain}.tile.openstreetmap.org`, { 16 | fixture: 'tiles/osm-tile.png', 17 | }); 18 | }); 19 | 20 | // Fake Imagery Layer 21 | interceptUrl('https://tiles.lulc.ds.io/**', 'GET', { 22 | fixture: 'tiles/imagery-tile.png', 23 | }); 24 | 25 | // API Health 26 | interceptUrl(`${restApiEndpoint}/health`, 'GET', apiHealth); 27 | 28 | // API Limits 29 | interceptApiRoute('', 'GET', apiIndex); 30 | 31 | // Mosaic 32 | interceptApiRoute('mosaic', 'POST', mosaicPostResponse, 'postMosaic'); 33 | interceptApiRoute('mosaic/naip.latest', 'GET', mosaicNaipLatest); 34 | 35 | // Geocoder 36 | interceptUrl( 37 | 'https://dev.virtualearth.net/REST/v1/Locations/*?*', 38 | 'GET', 39 | { fixture: 'geocoder/dc.json' }, 40 | 'reverseGeocodeCity' 41 | ); 42 | interceptUrl( 43 | 'https://dev.virtualearth.net/REST/v1/Locations/40.36315736436661,-77.7938461303711?*', 44 | 'GET', 45 | { fixture: 'geocoder/rural.json' }, 46 | 'reverseGeocodeRural' 47 | ); 48 | 49 | // Planetary Computer 50 | interceptUrl( 51 | 'https://planetarycomputer.microsoft.com/api/data/v1/mosaic/register', 52 | 'POST', 53 | { fixture: 'planetary-computer/data/mosaic/register.json' }, 54 | 'registerPlanetaryComputerMosaic' 55 | ); 56 | interceptUrl( 57 | 'https://planetarycomputer.microsoft.com/api/stac/v1/collections/sentinel-2-l2a', 58 | 'GET', 59 | { fixture: 'planetary-computer/stac/collections/sentinel-2-l2a.json' }, 60 | 'getPlanetaryComputerSentinel2Collection' 61 | ); 62 | 63 | // Imagery and Mosaic 64 | interceptApiRoute('imagery', 'GET', imageryIndex); 65 | interceptApiRoute('mosaic', 'GET', mosaicIndex); 66 | }); 67 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/health.json: -------------------------------------------------------------------------------- 1 | { "healthy": true, "message": "Good to go" } 2 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/imagery.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": { 3 | "total": 2, 4 | "imagery_sources": [ 5 | { 6 | "id": 1, 7 | "created": 1675835884757, 8 | "updated": 1675835884757, 9 | "name": "NAIP", 10 | "bounds": { 11 | "type": "Polygon", 12 | "coordinates": [ 13 | [ 14 | [-180, -85.0511287798066], 15 | [-180, 85.0511287798066], 16 | [180, 85.0511287798066], 17 | [180, -85.0511287798066], 18 | [-180, -85.0511287798066] 19 | ] 20 | ], 21 | "bounds": [-180, -85.0511287798066, 180, 85.0511287798066] 22 | } 23 | }, 24 | { 25 | "id": 2, 26 | "created": 1675866091377, 27 | "updated": 1675866091377, 28 | "name": "Sentinel-2", 29 | "bounds": { 30 | "type": "Polygon", 31 | "coordinates": [ 32 | [ 33 | [-180, -85.0511287798066], 34 | [-180, 85.0511287798066], 35 | [180, 85.0511287798066], 36 | [180, -85.0511287798066], 37 | [-180, -85.0511287798066] 38 | ] 39 | ], 40 | "bounds": [-180, -85.0511287798066, 180, 85.0511287798066] 41 | } 42 | } 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "limits": { 4 | "live_inference": 10000000, 5 | "max_inference": 100000000, 6 | "instance_window": 600, 7 | "total_cpus": 15, 8 | "active_cpus": 5, 9 | "total_gpus": 15, 10 | "active_gpus": 5 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/model/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "created": 1636558577692, 4 | "active": true, 5 | "imagery_source_id": 1, 6 | "uid": 4, 7 | "name": "Midwest 7 Class", 8 | "meta": { 9 | "name": "Midwest 7 Class", 10 | "imagery": "NAIP", 11 | "f1_score": { 12 | "tree": 0.77, 13 | "grass": 0.72, 14 | "roads": 0.8, 15 | "water": 0.63, 16 | "bare soil": 0.16, 17 | "buildings": 0.73, 18 | "other impervious": 0.73 19 | }, 20 | "description": "Midwest Combined multi-year", 21 | "f1_weighted": 0.738, 22 | "label_sources": "uvm", 23 | "training_area": 20802.340261, 24 | "training_data_aoi": "https://mvpmodels.blob.core.windows.net/midwest-multi-year/midwest_aoi.geojson", 25 | "class_distribution": { 26 | "tree": 0.43, 27 | "grass": 0.28, 28 | "roads": 0.06, 29 | "water": 0.01, 30 | "bare soil": 0.01, 31 | "buildings": 0.09, 32 | "other impervious": 0.13 33 | }, 34 | "imagery_resolution": "100 cm" 35 | }, 36 | "classes": [ 37 | { "name": "tree", "color": "#6CA966" }, 38 | { "name": "grass", "color": "#D0F3AB" }, 39 | { "name": "bare soil", "color": "#D2AD74" }, 40 | { "name": "water", "color": "#486DA2" }, 41 | { "name": "buildings", "color": "#F10100" }, 42 | { "name": "roads", "color": "#FFC300" }, 43 | { "name": "other impervious", "color": "#FF5733" } 44 | ], 45 | "bounds": [ 46 | -83.25599304179488, 47 | 41.246646145864226, 48 | -81.37080774185067, 49 | 42.44198353133044 50 | ], 51 | "storage": true 52 | } 53 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/model/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "created": 1675854437985, 4 | "active": true, 5 | "imagery_source_id": 2, 6 | "uid": 41, 7 | "name": "srm-rm-model-02", 8 | "meta": { 9 | "imagery": "sentinel-2", 10 | "f1_score": { 11 | "Agua": 0.5, 12 | "Bosque": 0.5, 13 | "Pastos": 0.5, 14 | "Selvas": 0.5, 15 | "Urbano": 0.5, 16 | "Agricultura": 0.5, 17 | "Suelo desnudo": 0.5, 18 | "Sin vegetación aparente": 0.5 19 | }, 20 | "description": "Sentinel Model test", 21 | "f1_weighted": 0.6, 22 | "label_sources": "RM", 23 | "class_distribution": { 24 | "Agua": 0.5, 25 | "Bosque": 0.5, 26 | "Pastos": 0.5, 27 | "Selvas": 0.5, 28 | "Urbano": 0.5, 29 | "Agricultura": 0.5, 30 | "Suelo desnudo": 0.5, 31 | "Sin vegetación aparente": 0.5 32 | }, 33 | "imagery_resolution": "14" 34 | }, 35 | "classes": [ 36 | { "name": "Bosque", "color": "#14d921" }, 37 | { "name": "Selvas", "color": "#9aec3f" }, 38 | { "name": "Pastos", "color": "#d8ec49" }, 39 | { "name": "Agricultura", "color": "#f3e48b" }, 40 | { "name": "Urbano", "color": "#f3f5f2" }, 41 | { "name": "Sin vegetación aparente", "color": "#54d4d1" }, 42 | { "name": "Agua", "color": "#2237d9" }, 43 | { "name": "Suelo desnudo", "color": "#842ff8" } 44 | ], 45 | "bounds": [-180, -90, 180, 90], 46 | "storage": true 47 | } 48 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/mosaic/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": { 3 | "total": 2, 4 | "mosaics": [ 5 | { 6 | "id": "87b72c66331e136e088004fba817e3e8", 7 | "name": "naip.latest", 8 | "params": { 9 | "assets": "image", 10 | "asset_bidx": "image|1,2,3,4", 11 | "collection": "naip" 12 | }, 13 | "imagery_source_id": 1, 14 | "created": 1677597442954, 15 | "updated": 1677597442954, 16 | "mosaic_ts_start": null, 17 | "mosaic_ts_end": null 18 | }, 19 | { 20 | "id": "2849689f57f1b3b9c1f725abb75aa411", 21 | "name": "Sentinel-2 Dec 2019 - March 2020", 22 | "params": { 23 | "assets": ["B04", "B03", "B02"], 24 | "collection": "sentinel-2-l2a", 25 | "color_formula": "Gamma+RGB+3.2+Saturation+0.8+Sigmoidal+RGB+25+0.35" 26 | }, 27 | "imagery_source_id": 2, 28 | "created": 1677597442954, 29 | "updated": 1677597442954, 30 | "mosaic_ts_start": 1575158400000, 31 | "mosaic_ts_end": 1585612800000 32 | }, 33 | { 34 | "id": "dce67bf58e5c9dbcf9393776f13f9ebd", 35 | "name": "Sentinel-2 Dec 2020 - March 2021", 36 | "params": { 37 | "assets": ["B04", "B03", "B02"], 38 | "collection": "sentinel-2-l2a", 39 | "color_formula": "Gamma+RGB+3.2+Saturation+0.8+Sigmoidal+RGB+25+0.35" 40 | }, 41 | "imagery_source_id": 2, 42 | "created": 1677597442954, 43 | "updated": 1677597442954, 44 | "mosaic_ts_start": 1606780800000, 45 | "mosaic_ts_end": 1617148800000 46 | }, 47 | { 48 | "id": "da05434b9b6a177a6999078221e19481", 49 | "name": "Sentinel-2 Dec 2021 - March 2022", 50 | "params": { 51 | "assets": ["B04", "B03", "B02"], 52 | "collection": "sentinel-2-l2a", 53 | "color_formula": "Gamma+RGB+3.2+Saturation+0.8+Sigmoidal+RGB+25+0.35" 54 | }, 55 | "imagery_source_id": 2, 56 | "created": 1677597442954, 57 | "updated": 1677597442954, 58 | "mosaic_ts_start": 1638316800000, 59 | "mosaic_ts_end": 1648684800000 60 | }, 61 | { 62 | "id": "9406dbfba1d5416dc521857008180079", 63 | "name": "Sentinel-2 Dec 2022 - Feb 2023", 64 | "params": { 65 | "assets": ["B04", "B03", "B02"], 66 | "collection": "sentinel-2-l2a", 67 | "color_formula": "Gamma+RGB+3.2+Saturation+0.8+Sigmoidal+RGB+25+0.35" 68 | }, 69 | "imagery_source_id": 2, 70 | "created": 1677597442954, 71 | "updated": 1677597442954, 72 | "mosaic_ts_start": 1669852800000, 73 | "mosaic_ts_end": 1677542400000 74 | } 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/mosaic/naip.latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "tilejson": "2.2.0", 3 | "name": "mosaic", 4 | "version": "1.0.0", 5 | "scheme": "xyz", 6 | "tiles": ["http://lulc-helm-tiles/mosaic/naip.latest/tiles/{z}/{x}/{y}@1x?"], 7 | "minzoom": 12, 8 | "maxzoom": 18, 9 | "bounds": [ 10 | -124.81903735821528, 11 | 24.49673997373884, 12 | -66.93084562551495, 13 | 49.44192498524237 14 | ], 15 | "center": [-95.87494149186512, 36.9693324794906, 12] 16 | } 17 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/mosaic/post-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "assets": ["B04", "B03", "B02", "B08"], 4 | "rescale": "0,10000", 5 | "collection": "sentinel-2-l2a" 6 | }, 7 | "imagery_source_id": 2, 8 | "ui_params": { 9 | "assets": ["B04", "B03", "B02"], 10 | "collection": "sentinel-2-l2a", 11 | "color_formula": "Gamma+RGB+3.2+Saturation+0.8+Sigmoidal+RGB+25+0.35" 12 | }, 13 | "id": "7f63c66c920e8a29b69fa7f45bafe038", 14 | "name": "Sentinel-2 Level-2A 2023-12-27 11:25 UTC - 2024-02-27 11:25 UTC", 15 | "mosaic_ts_start": 1703676331000, 16 | "mosaic_ts_end": 1709033131000 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/aoi/1/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "name": "Seneca Rocks", 4 | "bookmarked": true, 5 | "area": 3060131, 6 | "bounds": { 7 | "type": "Polygon", 8 | "coordinates": [ 9 | [ 10 | [-79.389824867, 38.828040665], 11 | [-79.372229576, 38.828040665], 12 | [-79.372229576, 38.846058444], 13 | [-79.389824867, 38.846058444], 14 | [-79.389824867, 38.828040665] 15 | ] 16 | ] 17 | }, 18 | "created": "2021-04-30T18:11:51.705Z", 19 | "storage": false, 20 | "checkpoint_id": "1", 21 | "checkpoint_name": "Seneca Rocks", 22 | "classes": [ 23 | { "name": "Water / Wetland", "color": "#486DA2" }, 24 | { "name": "Emergent Wetlands", "color": "#00A884" }, 25 | { "name": "Tree", "color": "#6CA966" }, 26 | { "name": "Shrubland", "color": "#ABC964" }, 27 | { "name": "Low Vegetation", "color": "#D0F3AB" }, 28 | { "name": "Barren", "color": "#D2AD74" }, 29 | { "name": "Structure", "color": "#F10100" }, 30 | { "name": "Impervious Surface", "color": "#BFB5B5" }, 31 | { "name": "Impervious Road", "color": "#320000" } 32 | ], 33 | "px_stats": { 34 | "0": 0.002208709716796875, 35 | "1": 0.0003598531087239583, 36 | "2": 0.00007502237955729167, 37 | "3": 0 38 | }, 39 | "shares": [] 40 | } 41 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/aoi/1/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "name": "Seneca Rocks", 4 | "bookmarked": true, 5 | "area": 10489000, 6 | "bounds": { 7 | "type": "Polygon", 8 | "coordinates": [ 9 | [ 10 | [-79.389824867, 38.828040665], 11 | [-79.372229576, 38.828040665], 12 | [-79.372229576, 38.846058444], 13 | [-79.389824867, 38.846058444], 14 | [-79.389824867, 38.828040665] 15 | ] 16 | ] 17 | }, 18 | "created": "2021-04-30T18:11:51.705Z", 19 | "storage": false, 20 | "checkpoint_id": "1", 21 | "checkpoint_name": "Seneca Rocks", 22 | "classes": [ 23 | { "name": "Water / Wetland", "color": "#486DA2" }, 24 | { "name": "Emergent Wetlands", "color": "#00A884" }, 25 | { "name": "Tree", "color": "#6CA966" }, 26 | { "name": "Shrubland", "color": "#ABC964" }, 27 | { "name": "Low Vegetation", "color": "#D0F3AB" }, 28 | { "name": "Barren", "color": "#D2AD74" }, 29 | { "name": "Structure", "color": "#F10100" }, 30 | { "name": "Impervious Surface", "color": "#BFB5B5" }, 31 | { "name": "Impervious Road", "color": "#320000" } 32 | ], 33 | "px_stats": { 34 | "0": 0.002208709716796875, 35 | "1": 0.0003598531087239583, 36 | "2": 0.00007502237955729167, 37 | "3": 0 38 | }, 39 | "shares": [] 40 | } 41 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/aoi/1/timeframe/1/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "checkpoint_id": 1, 4 | "created": 1678182035141, 5 | "storage": false, 6 | "bookmarked": false, 7 | "patches": [], 8 | "px_stats": {}, 9 | "bookmarked_at": null, 10 | "classes": [ 11 | { "name": "Bosque", "color": "#6CA966" }, 12 | { "name": "Selvas", "color": "#D0F3AB" }, 13 | { "name": "Pastos", "color": "#D2AD74" }, 14 | { "name": "Agricultura", "color": "#486DA2" }, 15 | { "name": "Urbano", "color": "#F10100" }, 16 | { "name": "Sin vegetación aparente", "color": "#FFC300" }, 17 | { "name": "Agua", "color": "#FF5733" }, 18 | { "name": "Suelo desnudo", "color": "#48F374" } 19 | ], 20 | "mosaic": "2849689f57f1b3b9c1f725abb75aa411", 21 | "checkpoint_name": "Villa de Allende", 22 | "shares": [] 23 | } 24 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/aoi/1/timeframe/1/tiles/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "tilejson": "2.2.0", 3 | "name": "aoi-1", 4 | "version": "1.0.0", 5 | "schema": "xyz", 6 | "tiles": [ 7 | "/api/project/1/aoi/1/timeframe/*/tiles/{z}/{x}/{y}?colormap=%7B%220%22%3A%22%23486DA2%22%2C%221%22%3A%22%236CA966%22%2C%222%22%3A%22%23D0F3AB%22%2C%223%22%3A%22%23BFB5B5%22%7D" 8 | ], 9 | "minzoom": 15, 10 | "maxzoom": 17, 11 | "bounds": [ 12 | -77.04711914062497, 13 | 38.89744587262309, 14 | -77.03613281249997, 15 | 38.90385833966774 16 | ], 17 | "center": [-77.04162597656247, 38.900652106145415, 15] 18 | } 19 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/aoi/1/timeframe/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "checkpoint_id": 1, 4 | "created": 1678182006648, 5 | "storage": false, 6 | "bookmarked": false, 7 | "patches": [], 8 | "px_stats": {}, 9 | "bookmarked_at": null, 10 | "classes": [ 11 | { "name": "Bosque", "color": "#6CA966" }, 12 | { "name": "Selvas", "color": "#D0F3AB" }, 13 | { "name": "Pastos", "color": "#D2AD74" }, 14 | { "name": "Agricultura", "color": "#486DA2" }, 15 | { "name": "Urbano", "color": "#F10100" }, 16 | { "name": "Sin vegetación aparente", "color": "#FFC300" }, 17 | { "name": "Agua", "color": "#FF5733" }, 18 | { "name": "Suelo desnudo", "color": "#48F374" } 19 | ], 20 | "mosaic": "2849689f57f1b3b9c1f725abb75aa411", 21 | "checkpoint_name": "Villa de Allende", 22 | "shares": [] 23 | } 24 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/aoi/2/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "name": "Seneca Rocks #1", 4 | "bookmarked": true, 5 | "area": 3060131, 6 | "bounds": { 7 | "type": "Polygon", 8 | "coordinates": [ 9 | [ 10 | [-79.389824867, 38.828040665], 11 | [-79.372229576, 38.828040665], 12 | [-79.372229576, 38.846058444], 13 | [-79.389824867, 38.846058444], 14 | [-79.389824867, 38.828040665] 15 | ] 16 | ] 17 | }, 18 | "created": "2021-04-30T18:05:26.309Z", 19 | "storage": true, 20 | "checkpoint_id": "2", 21 | "checkpoint_name": "Seneca Rocks", 22 | "classes": [ 23 | { "name": "Water / Wetland", "color": "#486DA2" }, 24 | { "name": "Emergent Wetlands", "color": "#00A884" }, 25 | { "name": "Tree", "color": "#6CA966" }, 26 | { "name": "Shrubland", "color": "#ABC964" }, 27 | { "name": "Low Vegetation", "color": "#D0F3AB" }, 28 | { "name": "Barren", "color": "#D2AD74" }, 29 | { "name": "Structure", "color": "#F10100" }, 30 | { "name": "Impervious Surface", "color": "#BFB5B5" }, 31 | { "name": "Impervious Road", "color": "#320000" } 32 | ], 33 | "px_stats": { 34 | "0": 0.002208709716796875, 35 | "1": 0.0003598531087239583, 36 | "2": 0.00007502237955729167, 37 | "3": 0 38 | }, 39 | "shares": [] 40 | } 41 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/aoi/3/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "name": "Seneca Rocks", 4 | "created": "2021-04-30T18:05:26.309Z", 5 | "storage": true, 6 | "bookmarked": true, 7 | "project_id": 1, 8 | "checkpoint_id": "3", 9 | "patches": [], 10 | "classes": [ 11 | { "name": "Water / Wetland", "color": "#486DA2" }, 12 | { "name": "Emergent Wetlands", "color": "#00A884" }, 13 | { "name": "Tree", "color": "#6CA966" }, 14 | { "name": "Shrubland", "color": "#ABC964" }, 15 | { "name": "Low Vegetation", "color": "#D0F3AB" }, 16 | { "name": "Barren", "color": "#D2AD74" }, 17 | { "name": "Structure", "color": "#F10100" }, 18 | { "name": "Impervious Surface", "color": "#BFB5B5" }, 19 | { "name": "Impervious Road", "color": "#320000" } 20 | ], 21 | "bounds": { 22 | "type": "Polygon", 23 | "coordinates": [ 24 | [ 25 | [-79.389824867, 38.828040665], 26 | [-79.372229576, 38.828040665], 27 | [-79.372229576, 38.846058444], 28 | [-79.389824867, 38.846058444], 29 | [-79.389824867, 38.828040665] 30 | ] 31 | ] 32 | }, 33 | "px_stats": { 34 | "0": 0.002208709716796875, 35 | "1": 0.0003598531087239583, 36 | "2": 0.00007502237955729167, 37 | "3": 0 38 | }, 39 | 40 | "shares": [] 41 | } 42 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/aoi/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 2, 3 | "project_id": 1, 4 | "aois": [ 5 | { 6 | "id": 1, 7 | "name": "Seneca Rocks", 8 | "bookmarked": true, 9 | "area": 3060131, 10 | "bounds": { 11 | "type": "Polygon", 12 | "coordinates": [ 13 | [ 14 | [-79.389824867, 38.828040665], 15 | [-79.372229576, 38.828040665], 16 | [-79.372229576, 38.846058444], 17 | [-79.389824867, 38.846058444], 18 | [-79.389824867, 38.828040665] 19 | ] 20 | ] 21 | }, 22 | "created": "2021-04-30T18:11:51.705Z", 23 | "storage": false, 24 | "checkpoint_id": "1", 25 | "checkpoint_name": "Seneca Rocks", 26 | "classes": [ 27 | { "name": "Water / Wetland", "color": "#486DA2" }, 28 | { "name": "Emergent Wetlands", "color": "#00A884" }, 29 | { "name": "Tree", "color": "#6CA966" }, 30 | { "name": "Shrubland", "color": "#ABC964" }, 31 | { "name": "Low Vegetation", "color": "#D0F3AB" }, 32 | { "name": "Barren", "color": "#D2AD74" }, 33 | { "name": "Structure", "color": "#F10100" }, 34 | { "name": "Impervious Surface", "color": "#BFB5B5" }, 35 | { "name": "Impervious Road", "color": "#320000" } 36 | ], 37 | "px_stats": { 38 | "0": 0.002208709716796875, 39 | "1": 0.0003598531087239583, 40 | "2": 0.00007502237955729167, 41 | "3": 0 42 | }, 43 | "shares": [] 44 | }, 45 | { 46 | "id": 2, 47 | "name": "Seneca Rocks #1", 48 | "bookmarked": true, 49 | "area": 3060131, 50 | "bounds": { 51 | "type": "Polygon", 52 | "coordinates": [ 53 | [ 54 | [-79.389824867, 38.828040665], 55 | [-79.372229576, 38.828040665], 56 | [-79.372229576, 38.846058444], 57 | [-79.389824867, 38.846058444], 58 | [-79.389824867, 38.828040665] 59 | ] 60 | ] 61 | }, 62 | "created": "2021-04-30T18:05:26.309Z", 63 | "storage": true, 64 | "checkpoint_id": "2", 65 | "checkpoint_name": "Seneca Rocks", 66 | "classes": [ 67 | { "name": "Water / Wetland", "color": "#486DA2" }, 68 | { "name": "Emergent Wetlands", "color": "#00A884" }, 69 | { "name": "Tree", "color": "#6CA966" }, 70 | { "name": "Shrubland", "color": "#ABC964" }, 71 | { "name": "Low Vegetation", "color": "#D0F3AB" }, 72 | { "name": "Barren", "color": "#D2AD74" }, 73 | { "name": "Structure", "color": "#F10100" }, 74 | { "name": "Impervious Surface", "color": "#BFB5B5" }, 75 | { "name": "Impervious Road", "color": "#320000" } 76 | ], 77 | "px_stats": { 78 | "0": 0.002208709716796875, 79 | "1": 0.0003598531087239583, 80 | "2": 0.00007502237955729167, 81 | "3": 0 82 | }, 83 | "shares": [] 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/checkpoint/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "project_id": 1, 4 | "parent": null, 5 | "name": "Seneca Rocks", 6 | "bookmarked": false, 7 | "classes": [ 8 | { "name": "Water / Wetland", "color": "#486DA2" }, 9 | { "name": "Emergent Wetlands", "color": "#00A884" }, 10 | { "name": "Tree", "color": "#6CA966" }, 11 | { "name": "Shrubland", "color": "#ABC964" }, 12 | { "name": "Low Vegetation", "color": "#D0F3AB" }, 13 | { "name": "Barren", "color": "#D2AD74" }, 14 | { "name": "Structure", "color": "#F10100" }, 15 | { "name": "Impervious Surface", "color": "#BFB5B5" }, 16 | { "name": "Impervious Road", "color": "#320000" } 17 | ], 18 | "created": "2021-04-30T18:05:26.176Z", 19 | "storage": true, 20 | "analytics": [ 21 | { "counts": 0, "f1score": 0, "percent": 0 }, 22 | { "counts": 0, "f1score": 0, "percent": 0 }, 23 | { "counts": 0, "f1score": 0, "percent": 0 }, 24 | { "counts": 0, "f1score": 0, "percent": 0 }, 25 | { "counts": 0, "f1score": 0, "percent": 0 }, 26 | { "counts": 0, "f1score": 0, "percent": 0 }, 27 | { "counts": 0, "f1score": 0, "percent": 0 }, 28 | { "counts": 0, "f1score": 0, "percent": 0 }, 29 | { "counts": 0, "f1score": 0, "percent": 0 } 30 | ], 31 | "retrain_geoms": [ 32 | { "type": "MultiPoint", "coordinates": [] }, 33 | { "type": "MultiPoint", "coordinates": [] }, 34 | { "type": "MultiPoint", "coordinates": [] }, 35 | { "type": "MultiPoint", "coordinates": [] }, 36 | { "type": "MultiPoint", "coordinates": [] }, 37 | { "type": "MultiPoint", "coordinates": [] }, 38 | { "type": "MultiPoint", "coordinates": [] }, 39 | { "type": "MultiPoint", "coordinates": [] }, 40 | { "type": "MultiPoint", "coordinates": [] } 41 | ], 42 | "input_geoms": [ 43 | { "type": "GeometryCollection", "geometries": [] }, 44 | { "type": "GeometryCollection", "geometries": [] }, 45 | { "type": "GeometryCollection", "geometries": [] }, 46 | { "type": "GeometryCollection", "geometries": [] }, 47 | { "type": "GeometryCollection", "geometries": [] }, 48 | { "type": "GeometryCollection", "geometries": [] }, 49 | { "type": "GeometryCollection", "geometries": [] }, 50 | { "type": "GeometryCollection", "geometries": [] }, 51 | { "type": "GeometryCollection", "geometries": [] } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/checkpoint/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "project_id": 1, 4 | "parent": 1, 5 | "name": "Seneca Rocks", 6 | "bookmarked": true, 7 | "classes": [ 8 | { "name": "Water / Wetland", "color": "#486DA2" }, 9 | { "name": "Emergent Wetlands", "color": "#00A884" }, 10 | { "name": "Tree", "color": "#6CA966" }, 11 | { "name": "Shrubland", "color": "#ABC964" }, 12 | { "name": "Low Vegetation", "color": "#D0F3AB" }, 13 | { "name": "Barren", "color": "#D2AD74" }, 14 | { "name": "Structure", "color": "#F10100" }, 15 | { "name": "Impervious Surface", "color": "#BFB5B5" }, 16 | { "name": "Impervious Road", "color": "#320000" } 17 | ], 18 | "created": "2021-04-30T18:05:26.176Z", 19 | "storage": true, 20 | "analytics": [ 21 | { "counts": 50, "f1score": 1, "percent": 0 }, 22 | { "counts": 0, "f1score": 0, "percent": 0 }, 23 | { "counts": 0, "f1score": 0, "percent": 0 }, 24 | { "counts": 0, "f1score": 0, "percent": 0 }, 25 | { "counts": 0, "f1score": 0, "percent": 0 }, 26 | { "counts": 0, "f1score": 0, "percent": 0 }, 27 | { "counts": 0, "f1score": 0, "percent": 0 }, 28 | { "counts": 0, "f1score": 0, "percent": 0 }, 29 | { "counts": 0, "f1score": 0, "percent": 0 } 30 | ], 31 | "retrain_geoms": [ 32 | { "type": "MultiPoint", "coordinates": [] }, 33 | { "type": "MultiPoint", "coordinates": [] }, 34 | { "type": "MultiPoint", "coordinates": [] }, 35 | { "type": "MultiPoint", "coordinates": [] }, 36 | { "type": "MultiPoint", "coordinates": [] }, 37 | { "type": "MultiPoint", "coordinates": [] }, 38 | { "type": "MultiPoint", "coordinates": [] }, 39 | { "type": "MultiPoint", "coordinates": [] }, 40 | { "type": "MultiPoint", "coordinates": [] } 41 | ], 42 | "input_geoms": [ 43 | { "type": "GeometryCollection", "geometries": [] }, 44 | { "type": "GeometryCollection", "geometries": [] }, 45 | { "type": "GeometryCollection", "geometries": [] }, 46 | { "type": "GeometryCollection", "geometries": [] }, 47 | { "type": "GeometryCollection", "geometries": [] }, 48 | { "type": "GeometryCollection", "geometries": [] }, 49 | { "type": "GeometryCollection", "geometries": [] }, 50 | { "type": "GeometryCollection", "geometries": [] }, 51 | { "type": "GeometryCollection", "geometries": [] } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/checkpoint/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "project_id": 1, 4 | "parent": 2, 5 | "name": "Seneca Rocks", 6 | "bookmarked": true, 7 | "classes": [ 8 | { "name": "Water / Wetland", "color": "#486DA2" }, 9 | { "name": "Emergent Wetlands", "color": "#00A884" }, 10 | { "name": "Tree", "color": "#6CA966" }, 11 | { "name": "Shrubland", "color": "#ABC964" }, 12 | { "name": "Low Vegetation", "color": "#D0F3AB" }, 13 | { "name": "Barren", "color": "#D2AD74" }, 14 | { "name": "Structure", "color": "#F10100" }, 15 | { "name": "Impervious Surface", "color": "#BFB5B5" }, 16 | { "name": "Impervious Road", "color": "#320000" } 17 | ], 18 | "created": "2021-04-30T18:05:26.176Z", 19 | "storage": true, 20 | "analytics": [ 21 | { "counts": 50, "f1score": 1, "percent": 0 }, 22 | { "counts": 0, "f1score": 0, "percent": 0 }, 23 | { "counts": 0, "f1score": 0, "percent": 0 }, 24 | { "counts": 0, "f1score": 0, "percent": 0 }, 25 | { "counts": 0, "f1score": 0, "percent": 0 }, 26 | { "counts": 0, "f1score": 0, "percent": 0 }, 27 | { "counts": 0, "f1score": 0, "percent": 0 }, 28 | { "counts": 0, "f1score": 0, "percent": 0 }, 29 | { "counts": 0, "f1score": 0, "percent": 0 } 30 | ], 31 | "retrain_geoms": [ 32 | { "type": "MultiPoint", "coordinates": [] }, 33 | { "type": "MultiPoint", "coordinates": [] }, 34 | { "type": "MultiPoint", "coordinates": [] }, 35 | { "type": "MultiPoint", "coordinates": [] }, 36 | { "type": "MultiPoint", "coordinates": [] }, 37 | { "type": "MultiPoint", "coordinates": [] }, 38 | { "type": "MultiPoint", "coordinates": [] }, 39 | { "type": "MultiPoint", "coordinates": [] }, 40 | { "type": "MultiPoint", "coordinates": [] } 41 | ], 42 | "input_geoms": [ 43 | { "type": "GeometryCollection", "geometries": [] }, 44 | { "type": "GeometryCollection", "geometries": [] }, 45 | { "type": "GeometryCollection", "geometries": [] }, 46 | { "type": "GeometryCollection", "geometries": [] }, 47 | { "type": "GeometryCollection", "geometries": [] }, 48 | { "type": "GeometryCollection", "geometries": [] }, 49 | { "type": "GeometryCollection", "geometries": [] }, 50 | { "type": "GeometryCollection", "geometries": [] }, 51 | { "type": "GeometryCollection", "geometries": [] } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/checkpoint/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 2, 3 | "project_id": 1, 4 | "checkpoints": [ 5 | { 6 | "id": 2, 7 | "parent": 1, 8 | "name": "Seneca Rocks", 9 | "created": "2021-04-30T18:11:51.509Z", 10 | "storage": true, 11 | "bookmarked": true 12 | }, 13 | { 14 | "id": 1, 15 | "parent": null, 16 | "name": "Seneca Rocks", 17 | "created": "2021-04-30T18:05:26.176Z", 18 | "storage": true, 19 | "bookmarked": true 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "name": "Untitled", 4 | "model_id": 1, 5 | "mosaic": "naip.latest", 6 | "created": "2021-03-19T12:47:07.838Z", 7 | "checkpoints": [] 8 | } 9 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/instance/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "project_id": 1, 4 | "timeframe_id": 2, 5 | "checkpoint_id": 2, 6 | "last_update": "2021-07-12T09:59:04.442Z", 7 | "created": "2021-07-12T09:58:57.459Z", 8 | "active": true, 9 | "status": { 10 | "phase": "Running" 11 | }, 12 | "token": "app_client", 13 | "type": "cpu" 14 | } 15 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/instance/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 1, 3 | "instances": [ 4 | { 5 | "id": 1, 6 | "uid": 123, 7 | "active": true, 8 | "created": "2021-03-18T18:42:42.224Z", 9 | "token": "app_client", 10 | "type": "cpu" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "name": "New name", 4 | "model_id": 1, 5 | "mosaic": "naip.latest", 6 | "created": "2021-03-19T12:47:07.838Z" 7 | } 8 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/1/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "uid": 1, 4 | "name": "A test project", 5 | "model_id": 1, 6 | "mosaic": "naip.latest", 7 | "created": "2021-08-12T13:59:25.070Z" 8 | } 9 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/fixtures/project/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 2, 3 | "projects": [ 4 | { 5 | "id": 1, 6 | "name": "Untitled", 7 | "model_id": 1, 8 | "mosaic": "naip.latest", 9 | "created": "2021-03-19T12:47:07.838Z", 10 | "checkpoints": [], 11 | "aois": [] 12 | }, 13 | { 14 | "id": 2, 15 | "name": "Project 2", 16 | "model_id": 1, 17 | "mosaic": "naip.latest", 18 | "created": "2021-03-20T12:47:07.838Z", 19 | "checkpoints": [], 20 | "aois": [] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/models.js: -------------------------------------------------------------------------------- 1 | import { interceptApiRoute } from './utils'; 2 | 3 | import modelIndex from './fixtures/model/index.json'; 4 | import model1 from './fixtures/model/1.json'; 5 | import model2 from './fixtures/model/2.json'; 6 | 7 | Cypress.Commands.add('mockModelApiRoutes', () => { 8 | interceptApiRoute('model', 'GET', modelIndex); 9 | interceptApiRoute('model/1', 'GET', model1); 10 | interceptApiRoute('model/2', 'GET', model2); 11 | }); 12 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/projects.js: -------------------------------------------------------------------------------- 1 | import { interceptApiRoute } from './utils'; 2 | 3 | import projectIndex from './fixtures/project/index.json'; 4 | import project1Get from './fixtures/project/1/get.json'; 5 | import project1Post from './fixtures/project/1/post.json'; 6 | import project1Patch from './fixtures/project/1/patch.json'; 7 | import project1InstanceIndex from './fixtures/project/1/instance/index.json'; 8 | import project1Instance1 from './fixtures/project/1/instance/1.json'; 9 | import project1CheckpointIndex from './fixtures/project/1/checkpoint/index.json'; 10 | import project1Checkpoint1 from './fixtures/project/1/checkpoint/1.json'; 11 | import project1Checkpoint2 from './fixtures/project/1/checkpoint/2.json'; 12 | import project1Checkpoint3 from './fixtures/project/1/checkpoint/3.json'; 13 | import project1AoiIndex from './fixtures/project/1/aoi/index.json'; 14 | import project1Aoi1Get from './fixtures/project/1/aoi/1/get.json'; 15 | import project1Aoi1Post from './fixtures/project/1/aoi/1/post.json'; 16 | import project1Aoi2 from './fixtures/project/1/aoi/2/get.json'; 17 | import project1Aoi3 from './fixtures/project/1/aoi/3/get.json'; 18 | 19 | /** 20 | * Mock a project scenario: an instance is running with checkpoint 2 and AOI 2 applied. 21 | */ 22 | Cypress.Commands.add('mockProjectEndpoints', () => { 23 | interceptApiRoute('project', 'POST', project1Post, 'postProject'); 24 | interceptApiRoute('project/1', 'GET', project1Get, 'getProject'); 25 | interceptApiRoute( 26 | 'project/?page=*&limit=20', 27 | 'GET', 28 | projectIndex, 29 | 'getProjects' 30 | ); 31 | interceptApiRoute('project/1', 'PATCH', project1Patch, 'patchProjectName'); 32 | interceptApiRoute('project/1/aoi', 'GET', project1AoiIndex, 'loadAois'); 33 | interceptApiRoute('project/1/aoi', 'POST', project1Aoi1Post); 34 | interceptApiRoute('project/1/checkpoint', 'GET', project1CheckpointIndex); 35 | interceptApiRoute('project/1/instance', 'GET', project1InstanceIndex); 36 | interceptApiRoute('project/1/instance/1', 'GET', project1Instance1); 37 | interceptApiRoute('project/1/checkpoint/1', 'GET', project1Checkpoint1); 38 | interceptApiRoute( 39 | 'project/1/checkpoint/2', 40 | 'GET', 41 | project1Checkpoint2, 42 | 'fetchCheckpoint2' 43 | ); 44 | interceptApiRoute('project/1/checkpoint/3', 'GET', project1Checkpoint3); 45 | interceptApiRoute('project/1/aoi/1', 'GET', project1Aoi1Get); 46 | interceptApiRoute('project/1/aoi/2', 'GET', project1Aoi2, 'fetchAoi2'); 47 | interceptApiRoute('project/1/aoi/3', 'GET', project1Aoi3); 48 | interceptApiRoute( 49 | 'project/1', 50 | 'DELETE', 51 | { 52 | statusCode: 200, 53 | body: {}, 54 | }, 55 | 'deleteProject' 56 | ); 57 | }); 58 | -------------------------------------------------------------------------------- /cypress/support/commands/mock-api-routes/utils.js: -------------------------------------------------------------------------------- 1 | import sortBy from 'lodash.sortby'; 2 | 3 | const restApiEndpoint = Cypress.config('restApiEndpoint'); 4 | 5 | export const interceptHostname = (hostname, response) => { 6 | cy.intercept({ hostname }, response); 7 | }; 8 | 9 | export const interceptUrl = (url, method, response, alias) => { 10 | if (alias) { 11 | cy.intercept({ url, method }, response).as(alias); 12 | } else { 13 | cy.intercept({ url, method }, response); 14 | } 15 | }; 16 | 17 | export const interceptApiRoute = (path, method, response, alias) => { 18 | const url = `${restApiEndpoint}/api/${path}`; 19 | interceptUrl(url, method, response, alias); 20 | }; 21 | 22 | // Get key/values from query string 23 | export const getQueryElement = (key, url) => { 24 | const tokens = (url.split('?')[1] || '').split('&'); 25 | const keyValPair = tokens.find((t) => t.includes(key)); 26 | if (keyValPair) { 27 | return keyValPair.split('=')[1]; 28 | } else { 29 | return null; 30 | } 31 | }; 32 | 33 | /** 34 | * Helper function to mock paginate request in cy.intercept(); 35 | * 36 | * @param {function} mockItem A function to generate items in the list. 37 | * 38 | */ 39 | export const paginatedList = (itemName, mockItem) => (req) => { 40 | let total = 25; 41 | const page = getQueryElement('page', req.url) || 0; 42 | const limit = parseInt(getQueryElement('limit', req.url) || 10); 43 | 44 | // Create all projects 45 | let all = new Array(total).fill(null).map((_, i) => { 46 | return mockItem(i + 1); 47 | }); 48 | 49 | // Apply sorting 50 | all = sortBy(all, (req.query && req.query.sort) || 'id'); 51 | 52 | // Apply order 53 | if (req.query && req.query.order && req.query.order === 'asc') { 54 | all = all.reverse(); 55 | } 56 | 57 | // Get page 58 | let items = []; 59 | for (let i = page * limit; i < Math.min(page * limit + limit, total); i++) { 60 | items.push(all[i]); 61 | } 62 | 63 | // Return response 64 | req.reply({ 65 | total, 66 | [itemName]: items, 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /cypress/support/e2e.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 'cypress-file-upload'; 17 | 18 | import { addCommand } from 'cypress-websocket-server'; 19 | addCommand(); 20 | 21 | require('./commands/fake-login'); 22 | require('./commands/mock-api-routes/common'); 23 | require('./commands/mock-api-routes/models'); 24 | require('./commands/mock-api-routes/projects'); 25 | 26 | // 27 | // Uncomment next block to stop testing on first failure 28 | // 29 | // afterEach(function () { 30 | // if (this.currentTest.state === 'failed') { 31 | // Cypress.runner.stop(); 32 | // } 33 | // }); 34 | --------------------------------------------------------------------------------