diff --git a/README.md b/README.md index 9f028f56dd..99554e8a00 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,11 @@ All of them availables with the `@globalfishingwatch/` prefix: | [api-client](libs/api-client) | JS library to simplify GFW API login and resources fetch | | [api-types](libs/api-types) | API typescript schema definitions | | [data-transforms](libs/data-transforms) | Set ot shared tools for data transformations | +| [datasets-client](libs/datasets-client) | A set of utils for handling api datasets | +| [dataviews-client](libs/dataviews-client) | A set of utils for merge, combine and consume api dataviews into the apps | | [deck-layer-composer](libs/deck-layer-composer) | Map integration of the deck-layers | | [deck-layers](libs/deck-layers) | Deck classes for GFW layers | | [deck-loaders](libs/deck-loaders) | Deck loaders for GFW layers | -| [dataviews-client](libs/dataviews-client) | Api-client wrapper to fetch and edit dataviews and associated datasets/data | | [fourwings-aggregate](libs/fourwings-aggregate) | Logic to turn fourwings tiles or cells into meaningful values for the frontend | | [i18n-labels](libs/i18n-labels) | GFW shared translations | | [layer-composer](libs/layer-composer) | Orchestrates various Layer Generators to generate a Mapbox GL Style document | @@ -27,8 +28,10 @@ All of them availables with the `@globalfishingwatch/` prefix: | | | | --------------------------------------------------- | -------------------------------------- | | [api-portal](apps/api-portal) | Api documentation portal | +| [data-download-portal](apps/data-download-portal) | The place to download datasets | | [fishing-map-e2e](apps/fishing-map-e2e) | Cypress e2e testing for the map | | [fishing-map](apps/fishing-map) | Version 3.0 of the fishing map project | +| [image-labeler](apps/image-labeler) | Labeling tool for satellite images | | [port-labeler](apps/port-labeler) | Labeling tool for ports | | [user-groups-admin](applications/user-groups-admin) | Tool to manage user groups with ease | | [vessel-history](apps/vessel-history) | Vessel history app | diff --git a/apps/api-portal/components/header/header.tsx b/apps/api-portal/components/header/header.tsx index 118c1bf303..43c40f1909 100644 --- a/apps/api-portal/components/header/header.tsx +++ b/apps/api-portal/components/header/header.tsx @@ -1,8 +1,12 @@ /* eslint-disable jsx-a11y/anchor-is-valid */ import React, { Fragment } from 'react' import { UserData } from '@globalfishingwatch/api-types' -import { Header as UIHeader, HeaderMenuItem, IconButton } from '@globalfishingwatch/ui-components' -import { MenuItem } from '@globalfishingwatch/ui-components/header/Header.links' +import { + Header as UIHeader, + HeaderMenuItem, + IconButton, + MenuItem, +} from '@globalfishingwatch/ui-components' import styles from './header.module.css' interface HeaderProps { diff --git a/apps/data-download-portal/.env.sample b/apps/data-download-portal/.env.sample new file mode 100644 index 0000000000..e364da68d7 --- /dev/null +++ b/apps/data-download-portal/.env.sample @@ -0,0 +1 @@ +NEXT_PUBLIC_API_GATEWAY=https://gateway.api.dev.globalfishingwatch.org diff --git a/apps/data-download-portal/.eslintrc.json b/apps/data-download-portal/.eslintrc.json new file mode 100644 index 0000000000..d3e61a2ea8 --- /dev/null +++ b/apps/data-download-portal/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.js"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/data-download-portal/.nxignore b/apps/data-download-portal/.nxignore new file mode 100644 index 0000000000..75882da749 --- /dev/null +++ b/apps/data-download-portal/.nxignore @@ -0,0 +1,4 @@ +## https://github.com/nrwl/nx/issues/8796#issuecomment-1775515199 + +docker-compose.yaml +Dockerfile diff --git a/apps/data-download-portal/.stylelintrc.js b/apps/data-download-portal/.stylelintrc.js new file mode 100644 index 0000000000..b9461d4925 --- /dev/null +++ b/apps/data-download-portal/.stylelintrc.js @@ -0,0 +1 @@ +module.exports = require('@globalfishingwatch/linting/stylelint') diff --git a/apps/data-download-portal/Dockerfile b/apps/data-download-portal/Dockerfile new file mode 100644 index 0000000000..b3874469f3 --- /dev/null +++ b/apps/data-download-portal/Dockerfile @@ -0,0 +1,24 @@ +################################################################################# +# Generate htpasswd file +################################################################################# +# FROM appsoa/docker-alpine-htpasswd as htpasswd +# ARG BASIC_AUTH_USER=gfw +# ARG BASIC_AUTH_PASS=default +# ENV BASIC_AUTH_USER $BASIC_AUTH_USER +# ENV BASIC_AUTH_PASS $BASIC_AUTH_PASS +# RUN htpasswd -Bbn "$BASIC_AUTH_USER" "$BASIC_AUTH_PASS" > /home/.htpasswd + +################################################################################# +# Actual application to run +################################################################################# +# Using stable version that uses alpine 3.14 to fix build errors +# https://github.com/alpinelinux/docker-alpine/issues/182 +FROM nginx:stable-alpine as production + +COPY ./nginx.conf /etc/nginx/nginx.template +# COPY --from=htpasswd /home/.htpasswd /home/.htpasswd +# RUN cat /home/.htpasswd >> /etc/nginx/.htpasswd +COPY ./entrypoint.sh entrypoint.sh +COPY ./ /usr/share/nginx/www/ +ENTRYPOINT ["./entrypoint.sh"] + diff --git a/apps/data-download-portal/cloudbuild.yaml b/apps/data-download-portal/cloudbuild.yaml new file mode 100644 index 0000000000..78ae28386f --- /dev/null +++ b/apps/data-download-portal/cloudbuild.yaml @@ -0,0 +1,104 @@ +steps: + - name: 'gcr.io/$PROJECT_ID/restore_cache' + id: restore_cache + waitFor: ['-'] + script: | + #!/usr/bin/env bash + restore_cache \ + --bucket=gs://frontend-cache-dependencies \ + --key=node_modules-$( checksum yarn.lock ) + restore_cache \ + --bucket=gs://frontend-cache-dependencies \ + --key=yarn-$( checksum yarn.lock ) + restore_cache \ + --bucket=gs://frontend-cache-dependencies \ + --key=yarn-install-state-$( checksum yarn.lock ) + + - id: 'install-yarn' + waitFor: ['restore_cache'] + name: node:21 + script: | + yarn set version 4.4.0 + yarn -v + yarn install --immutable + + - id: 'save_cache' + waitFor: ['install-yarn'] + name: 'gcr.io/$PROJECT_ID/restore_cache' + script: | + #!/usr/bin/env bash + save_cache \ + --bucket=gs://frontend-cache-dependencies \ + --key=node_modules-$( checksum yarn.lock ) \ + --path=./node_modules \ + --no-clobber + save_cache \ + --bucket=gs://frontend-cache-dependencies \ + --key=yarn-$( checksum yarn.lock ) \ + --path=.yarn/cache \ + --no-clobber + save_cache \ + --bucket=gs://frontend-cache-dependencies \ + --key=yarn-install-state-$( checksum yarn.lock ) \ + --path=.yarn/install-state.gz \ + --no-clobber + + - id: 'build-app' + waitFor: ['install-yarn'] + name: node:21 + entrypoint: yarn + args: ['nx', 'build', 'data-download-portal', '--parallel'] + env: + - 'NX_BRANCH=$_NX_BRANCH' + - 'NX_CLOUD_AUTH_TOKEN=$_NX_CLOUD_AUTH_TOKEN' + - 'API_GATEWAY=$_API_GATEWAY' + + - name: 'gcr.io/kaniko-project/executor:latest' + id: 'build-image' + waitFor: ['build-app'] + args: + [ + '--destination=gcr.io/world-fishing-827/github.com/globalfishingwatch/data-download-portal:$SHORT_SHA', + '--cache=true', + '--build-arg', + 'BASIC_AUTH_USER=$_BASIC_AUTH_USER', + '--build-arg', + 'BASIC_AUTH_PASS=$_BASIC_AUTH_PASS', + '--target', + 'production', + '-f', + './apps/data-download-portal/Dockerfile', + '-c', + './dist/apps/data-download-portal', + ] + + # Deploy to the appropriate environment + - name: 'gcr.io/cloud-builders/gcloud' + waitFor: ['build-image'] + id: 'deploy-cloud-run' + entrypoint: 'bash' + env: + - '_RUN_SERVICE_NAME=$_RUN_SERVICE_NAME' + args: + - '-eEuo' + - 'pipefail' + - '-c' + - |- + branch_service_name=`echo data-download-portal-$BRANCH_NAME | sed -r 's,[/\.],-,g' | awk '{print substr(tolower($0),0,62)}'` + service_name=${_RUN_SERVICE_NAME:-${branch_service_name}} + gcloud beta run deploy \ + $service_name \ + --project \ + $_RUN_PROJECT \ + --image \ + gcr.io/world-fishing-827/github.com/globalfishingwatch/data-download-portal:$SHORT_SHA \ + --region \ + $_RUN_REGION \ + --platform managed \ + --set-env-vars \ + BASIC_AUTH=$_BASIC_AUTH \ + --allow-unauthenticated + +timeout: 1800s +options: + machineType: 'E2_HIGHCPU_8' diff --git a/apps/data-download-portal/docker-compose.yaml b/apps/data-download-portal/docker-compose.yaml new file mode 100644 index 0000000000..100e88856a --- /dev/null +++ b/apps/data-download-portal/docker-compose.yaml @@ -0,0 +1,14 @@ +services: + data-download-portal: + build: + context: ../../dist/apps/data-download-portal + dockerfile: ../../../apps/data-download-portal/Dockerfile + target: production + container_name: data-download-portal + ports: + - 3000:80 + env_file: + - .env + environment: + - PORT=80 + - API_GATEWAY=https://gateway.api.dev.globalfishingwatch.org diff --git a/apps/data-download-portal/index.html b/apps/data-download-portal/index.html new file mode 100644 index 0000000000..003909684b --- /dev/null +++ b/apps/data-download-portal/index.html @@ -0,0 +1,22 @@ + + + + + Data Download Portal + + + + + + + + + + +
+ + + diff --git a/apps/data-download-portal/nginx.conf b/apps/data-download-portal/nginx.conf new file mode 100644 index 0000000000..2fa5e5fc9b --- /dev/null +++ b/apps/data-download-portal/nginx.conf @@ -0,0 +1,48 @@ +events { + worker_connections 1024; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + include /etc/nginx/mime.types; + default_type application/octet-stream; + client_header_buffer_size 64k; + large_client_header_buffers 4 64k; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log warn; + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + # Specify the minimum length of the response to compress (default 20) + gzip_min_length 100; + + server { + listen ${PORT}; + root /usr/share/nginx/www/; + + location ~* ^/image-labeler/(.+\..+)$ { + alias /usr/share/nginx/www/$1; + } + + location ~* ^/(.+\..+)$ { + alias /usr/share/nginx/www/$1; + } + + location / { + # auth_basic ${BASIC_AUTH}; + # auth_basic_user_file /etc/nginx/.htpasswd; + try_files /index.html =404; + } + } +} diff --git a/apps/data-download-portal/project.json b/apps/data-download-portal/project.json new file mode 100644 index 0000000000..d3d32170ad --- /dev/null +++ b/apps/data-download-portal/project.json @@ -0,0 +1,58 @@ +{ + "name": "data-download-portal", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/data-download-portal/src", + "projectType": "application", + "tags": [], + "targets": { + "build": { + "executor": "@nx/vite:build", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "outputPath": "dist/apps/data-download-portal" + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production" + } + } + }, + "start": { + "executor": "@nx/vite:dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "data-download-portal:build" + }, + "configurations": { + "development": { + "buildTarget": "data-download-portal:build:development", + "hmr": true + }, + "production": { + "buildTarget": "data-download-portal:build:production", + "hmr": false + } + } + }, + "preview": { + "executor": "@nx/vite:preview-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "data-download-portal:build" + }, + "configurations": { + "development": { + "buildTarget": "data-download-portal:build:development" + }, + "production": { + "buildTarget": "data-download-portal:build:production" + } + }, + "dependsOn": ["build"] + } + } +} diff --git a/apps/data-download-portal/public/favicon.ico b/apps/data-download-portal/public/favicon.ico new file mode 100644 index 0000000000..cbd0899c03 Binary files /dev/null and b/apps/data-download-portal/public/favicon.ico differ diff --git a/apps/data-download-portal/public/robots.txt b/apps/data-download-portal/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/apps/data-download-portal/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/apps/data-download-portal/src/app.jsx b/apps/data-download-portal/src/app.jsx new file mode 100755 index 0000000000..2fc7425b0a --- /dev/null +++ b/apps/data-download-portal/src/app.jsx @@ -0,0 +1,76 @@ +import React, { Suspense, lazy, Fragment, useEffect, useState } from 'react' +import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom' +import { GFWAPI } from '@globalfishingwatch/api-client' +import { Footer } from '@globalfishingwatch/ui-components' +import { useGFWLogin } from './components/login/use-login' +import Login from './pages/login/login.jsx' +import HeaderHtml from './components/header/header.jsx' +import Loader from './components/loader/loader.jsx' +import styles from './app.module.css' +import '../../../libs/ui-components/src/base.css' + +const Home = lazy(() => import('./pages/home/home')) +const Dataset = lazy(() => import('./pages/dataset/dataset')) +const Report = lazy(() => import('./pages/report/report')) +const basename = process.env.NODE_ENV === 'production' ? '/data-download' : '' + +const App = () => { + const { loading, logged, user } = useGFWLogin(GFWAPI) + const [trackLogin, setTrackLogin] = useState(true) + + // Set to track login only when the user has logged out + useEffect(() => { + !logged && setTrackLogin(true) + }, [logged]) + + useEffect(() => { + if (user && trackLogin) { + const dataLayer = window['dataLayer'] || [] + dataLayer.push({ + event: 'set', + user_country: user.country ?? '', + user_group: user.groups ?? '', + user_org_type: user.organizationType ?? '', + user_organization: user.organization ?? '', + user_language: user.language ?? '', + }) + dataLayer.push({ + event: 'login', + eventModel: { + category: 'user', + }, + }) + setTrackLogin(false) + } + }, [user, trackLogin]) + + return logged || loading ? ( + + + +
+ {loading ? ( + + ) : ( +
+ + }> + + + + + + + + +
+ )} +
+