diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ae8aae3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.env +.git +node_modules +dist +.yarn diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9853df7 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# Copy to .env to override compose defaults. Every value here has a working default, +# so the stack runs with no .env at all. + +# Ports published on the host. Change these if something already holds them. +TRAEFIK_HTTP_PORT=88 +TRAEFIK_DASHBOARD_PORT=8088 +API_EXTERNAL_PORT=3005 +POSTGRES_EXTERNAL_PORT=5433 +TEST_POSTGRES_EXTERNAL_PORT=5444 +KEYCLOAK_EXTERNAL_PORT=8280 + +POSTGRES_USER=postgres +POSTGRES_PASSWORD=psql +POSTGRES_DB=postgres + +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin + +DEPLOYMENT=dev +LOG_LEVEL=info,deepreefmap_api=debug + +# Off by default locally so repeated enrolment attempts while testing the desktop +# sync path do not trip the per-IP limiter. +DISABLE_RATE_LIMITING=true diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index ed68028..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "prettier" - ], - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "env": { - "browser": true, - "es2021": true - }, - "settings": { - "react": { - "version": "detect" - } - } -} diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index b730ce5..2c66b75 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v7 # Install the cosign tool # https://github.com/sigstore/cosign-installer diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..81aa3cd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,119 @@ +name: CI + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # Mirrors node:22.23.2-alpine in both Dockerfiles. + NODE_VERSION: '22.23.2' + +jobs: + checks: + name: type-check, lint, format, build + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out ui repository + uses: actions/checkout@v7 + + - name: Set up Node + uses: actions/setup-node@v7 + with: + node-version: ${{ env.NODE_VERSION }} + + # After setup-node, so the shims land on the Node this job runs. + - name: Enable corepack + run: corepack enable + + # Yarn 4 caches globally under ~/.yarn/berry, so ask rather than assume a path. + - name: Resolve Yarn cache folder + id: yarn-cache + run: echo "dir=$(yarn config get cacheFolder)" >> "$GITHUB_OUTPUT" + + - name: Restore Yarn cache + uses: actions/cache@v6 + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: ${{ runner.os }}-yarn- + + # Fails when yarn.lock does not already satisfy package.json. + - name: Install dependencies + run: yarn install --immutable + + - name: Type check + run: yarn type-check + + # Not `yarn lint`, which carries --fix. CI must not rewrite the tree. + - name: Lint + run: yarn eslint ./src + + - name: Format check + run: yarn prettier --check ./src + + - name: Build + run: yarn build + + contract: + # A stale api.d.ts means the console's types disagree with the server it calls. + name: contract types are current + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out ui repository + uses: actions/checkout@v7 + + # Public repo, so the default token suffices. Only contract/ is needed. + - name: Check out api contract + uses: actions/checkout@v7 + with: + repository: eceo-epfl/deepreefmap-api + ref: main + path: api + sparse-checkout: contract + sparse-checkout-cone-mode: false + + - name: Set up Node + uses: actions/setup-node@v7 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Enable corepack + run: corepack enable + + - name: Resolve Yarn cache folder + id: yarn-cache + run: echo "dir=$(yarn config get cacheFolder)" >> "$GITHUB_OUTPUT" + + - name: Restore Yarn cache + uses: actions/cache@v6 + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: ${{ runner.os }}-yarn- + + - name: Install dependencies + run: yarn install --immutable + + # The committed file is generator output passed through prettier, so do both. + - name: Regenerate contract types + env: + DRM_API_DIR: ${{ github.workspace }}/api + run: | + yarn contract-types + yarn prettier --write src/contract/api.d.ts + + - name: Check the checked-in types are current + run: | + if ! git diff --exit-code -- src/contract/api.d.ts; then + echo "::error::src/contract/api.d.ts is stale. Run 'yarn contract-types && yarn format' and commit the result." + exit 1 + fi diff --git a/.gitignore b/.gitignore index 7ceb59f..3602499 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,11 @@ dist dist-ssr *.local +# Yarn keeps its install state here. Only patches and plugins are ours to commit. +.yarn/* +!.yarn/patches +!.yarn/plugins + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..87cba18 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,14 @@ +# Vite resolves from a real node_modules tree, so PnP is not used. +nodeLinker: node-modules + +# Rolldown and lightningcss ship per-platform binaries. The build image is alpine, +# so musl builds must be in the lockfile beside the host's glibc ones. +supportedArchitectures: + os: + - linux + cpu: + - x64 + - arm64 + libc: + - glibc + - musl diff --git a/Dockerfile b/Dockerfile index fee1299..ce466fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,20 @@ -FROM node:20.8.1-alpine as builder +FROM node:22.23.2-alpine AS builder # Set the working directory in the container WORKDIR /app -COPY package.json yarn.lock ./ -RUN yarn install +RUN corepack enable + +COPY package.json yarn.lock .yarnrc.yml ./ +RUN yarn install --immutable # Copy the rest of your application source code to the container COPY . . RUN yarn build -FROM nginx:1.12-alpine +FROM nginx:1.31-alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf COPY --from=builder /app/dist /usr/share/nginx/html # Expose the port your application will listen on (if applicable) EXPOSE 80 diff --git a/Dockerfile.dev b/Dockerfile.dev index 114f77c..420343f 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,9 +1,14 @@ -FROM node:20.8.1-alpine as builder +FROM node:22.23.2-alpine AS builder # Set the working directory in the container WORKDIR /app -COPY package.json yarn.lock ./ +RUN corepack enable + +# Install before copying the source, so a source edit does not re-run yarn. +COPY package.json yarn.lock .yarnrc.yml ./ +RUN yarn install --immutable + COPY . . # Start your Yarn application diff --git a/README.md b/README.md index fd6c30f..a2f77c2 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,47 @@ -# deepreefmap-frontend +# deepreefmap-ui -## Installation +**The web console for the DeepReefMap metadata registry.** -Install the application dependencies by running: +Administrators define the sites, campaigns and transects that field laptops download, +enrol those laptops, and browse the survey metadata they upload. -```sh -yarn +## Quick Start + +The console needs the registry and a Keycloak realm. The compose stack in this repository +brings up all three: + +```bash +docker compose up -d ``` -## Development +The console is then at `http://localhost:88`, and everything binds to loopback only. -Start the application in development mode by running: +For a local dev server against a registry that is already running: -```sh +```bash +yarn install yarn dev ``` -## Production +## Types from the contract -Build the application in production mode by running: +Every entity type is generated from the registry's published `OpenAPI` document, so a +field the server renamed becomes a compile error here: -```sh -yarn build +```bash +yarn contract-types ``` -## DataProvider +It reads `../deepreefmap-api/contract/openapi.json`, or `$DRM_API_DIR/contract` when the +registry lives elsewhere. -The included data provider use [FakeREST](https://github.com/marmelab/fakerest) to simulate a backend. -You'll find a `data.json` file in the `src` directory that includes some fake data for testing purposes. +## Checks -It includes two resources, posts and comments. -Posts have the following properties: `id`, `title` and `content`. -Comments have the following properties: `id`, `post_id` and `content`. - -## Authentication +```bash +yarn type-check +yarn build +``` -The included auth provider should only be used for development and test purposes. -You'll find a `users.json` file in the `src` directory that includes the users you can use. +## Licence -You can sign in to the application with the following usernames and password: -- janedoe / password -- johndoe / password +MIT diff --git a/docker-compose.yaml b/docker-compose.yaml index 4c980e4..eeb55cf 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,159 +1,277 @@ +# Local development stack: Postgres, Keycloak, the Rust API with hot reload, and the +# web interface, behind one traefik so everything shares an origin. +# +# docker compose up -d +# open http://localhost:88 the interface +# open http://localhost:88/docs the API reference +# open http://localhost:8280 Keycloak (admin / admin) +# +# Sign in as admin/admin or user/user. The `norole` user exists to check that an +# authenticated person without a project role is refused. +# +# Integration tests run in their own containers against a throwaway database: +# docker compose up -d deepreefmap-test-watcher +# docker compose logs -f deepreefmap-test-watcher + services: traefik: - image: traefik:v2.9.6 + image: traefik:v3.7 command: - "--api.insecure=true" - "--providers.docker=true" + - "--providers.docker.exposedByDefault=false" + # Declared rather than inherited, so the published port below is unambiguous. + - "--entryPoints.web.address=:80" ports: - - "88:80" - - "8088:8080" + - "127.0.0.1:${TRAEFIK_HTTP_PORT:-88}:80" + - "127.0.0.1:${TRAEFIK_DASHBOARD_PORT:-8088}:8080" volumes: - - /var/run/docker.sock:/var/run/docker.sock + - /var/run/docker.sock:/var/run/docker.sock:ro deepreefmap-api: build: context: ../deepreefmap-api + dockerfile: Dockerfile.dev environment: + - DB_USER=${POSTGRES_USER:-postgres} + - DB_PASSWORD=${POSTGRES_PASSWORD:-psql} - DB_HOST=deepreefmap-db - DB_PORT=5432 - - DB_USER=postgres - - DB_PASSWORD=psql - - DB_NAME=postgres - - DB_PREFIX=postgresql+asyncpg - - S3_URL=${S3_URL} - - S3_BUCKET_ID=${S3_BUCKET_ID} - - S3_ACCESS_KEY=${S3_ACCESS_KEY} - - S3_SECRET_KEY=${S3_SECRET_KEY} - - S3_PREFIX=${S3_PREFIX} - - KUBERNETES_SERVICE_HOST=caas-test.rcp.epfl.ch - - KUBERNETES_SERVICE_PORT=443 - - KUBECONFIG=/root/.kube/config.yaml - - INCOMPLETE_OBJECT_CHECK_INTERVAL=${INCOMPLETE_OBJECT_CHECK_INTERVAL} - - INCOMPLETE_OBJECT_TIMEOUT_SECONDS=${INCOMPLETE_OBJECT_TIMEOUT_SECONDS} - - INCOMPLETE_OBJECT_CONSIDER_ABANDONED=${INCOMPLETE_OBJECT_CONSIDER_ABANDONED} - - DEEPREEFMAP_IMAGE_TAG=${DEEPREEFMAP_IMAGE_TAG} - - DEEPREEFMAP_IMAGE=${DEEPREEFMAP_IMAGE} - - NAMESPACE=${NAMESPACE} - - PROJECT=${PROJECT} - - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID} - - KEYCLOAK_API_ID=${KEYCLOAK_API_ID} - - KEYCLOAK_API_SECRET=${KEYCLOAK_API_SECRET} - - KEYCLOAK_REALM=${KEYCLOAK_REALM} - - KEYCLOAK_URL=${KEYCLOAK_URL} - - DEEPREEFMAP_API_URL=http://deepreefmap-api:8000 - - SERIALIZER_SECRET_KEY=${SERIALIZER_SECRET_KEY} - - DEPLOYMENT=local - - CACHE_URL=deepreefmap-cache - - CACHE_PORT=6379 - - CACHE_DB=0 - - CACHE_SECRET=${CACHE_SECRET} - - CACHE_PASSWORD=${REDIS_PASSWORD} + - DB_NAME=${POSTGRES_DB:-postgres} + - API_HOST=0.0.0.0 + - API_PORT=${API_PORT:-3000} + - DEPLOYMENT=${DEPLOYMENT:-dev} + - RUST_LOG=${LOG_LEVEL:-info,deepreefmap_api=debug} + # Container-internal URL: this is where the API validates tokens. The browser + # uses the host-published URL instead, which is why the two differ. + - KEYCLOAK_URL=http://deepreefmap-keycloak:8080/ + # What /api/config/keycloak advertises: the browser is on the host and cannot + # resolve the container name above. + - KEYCLOAK_BROWSER_URL=http://localhost:${KEYCLOAK_EXTERNAL_PORT:-8280}/ + - KEYCLOAK_REALM=deepreefmap + - KEYCLOAK_CLIENT_ID=deepreefmap-ui-local + # Embedded in every connect code, so it must be an address the desktop + # application can reach from the host, not a container name. + - PUBLIC_BASE_URL=http://localhost:${TRAEFIK_HTTP_PORT:-88}/api + - CORS_ALLOWED_ORIGINS=http://localhost:${TRAEFIK_HTTP_PORT:-88},http://localhost:5173 + - DISABLE_RATE_LIMITING=${DISABLE_RATE_LIMITING:-true} + # The blob archive. In-network endpoint for the API's own bucket operations. + # Internal network only, matching the production constraint: the store is + # never reachable by a client, every byte flows through the API. + - S3_URL=http://minio:9000 + # What presigned URLs are signed against: the browser is on the host and + # cannot resolve the container name above. MinIO publishes this port. + - S3_BUCKET_ID=deepreefmap + - S3_ACCESS_KEY=${MINIO_ROOT_USER:-minioadmin} + - S3_SECRET_KEY=${MINIO_ROOT_PASSWORD:-minioadmin} + - S3_PREFIX=dev + ports: + - "127.0.0.1:${API_EXTERNAL_PORT:-3005}:${API_PORT:-3000}" depends_on: - - deepreefmap-db + deepreefmap-db: + condition: service_healthy + deepreefmap-keycloak: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:${API_PORT:-3000}/healthz || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + # Generous: the first run compiles the whole dependency tree. + start_period: 300s volumes: - - ../deepreefmap-api/app:/app/app - - ../config.yaml:/root/.kube/config.yaml - ports: - - 5001:8000 + - ../deepreefmap-api/src:/app/src + - ../deepreefmap-api/tests:/app/tests + - ../deepreefmap-api/migration:/app/migration + # The exporter writes here and the contract test diffs it. + - ../deepreefmap-api/contract:/app/contract + - ../deepreefmap-api/Cargo.toml:/app/Cargo.toml + - ../deepreefmap-api/Cargo.lock:/app/Cargo.lock + - ../deepreefmap-api/bacon.toml:/app/bacon.toml + # Named volume, so cargo's build cache survives a container rebuild. + - deepreefmap-rust-data:/app/target labels: - - "traefik.http.routers.deepreefmap-bff.rule=Host(`deepreefmap`) && PathPrefix(`/api`)" - - "traefik.http.services.deepreefmap-bff.loadbalancer.server.port=8000" + - "traefik.enable=true" + - "traefik.http.routers.deepreefmap-api.rule=PathPrefix(`/api`) || PathPrefix(`/docs`)" + - "traefik.http.routers.deepreefmap-api.priority=100" + - "traefik.http.services.deepreefmap-api.loadbalancer.server.port=${API_PORT:-3000}" deepreefmap-ui: build: context: . - dockerfile: Dockerfile.dev # Uses the dockerfile containing yarn dev + dockerfile: Dockerfile.dev + depends_on: + deepreefmap-api: + condition: service_started labels: - - "traefik.http.routers.deepreefmap-ui.rule=Host(`deepreefmap`)" + - "traefik.enable=true" + - "traefik.http.routers.deepreefmap-ui.rule=PathPrefix(`/`)" + - "traefik.http.routers.deepreefmap-ui.priority=10" - "traefik.http.services.deepreefmap-ui.loadbalancer.server.port=5173" volumes: - - ../deepreefmap-ui:/app + - ./src:/app/src + - ./index.html:/app/index.html + - ./vite.config.mts:/app/vite.config.mts + - ./public:/app/public deepreefmap-db: - image: postgis/postgis:16-master + image: postgres:18-alpine environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=psql - - POSTGRES_DB=postgres + - POSTGRES_USER=${POSTGRES_USER:-postgres} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-psql} + - POSTGRES_DB=${POSTGRES_DB:-postgres} + ports: + - "127.0.0.1:${POSTGRES_EXTERNAL_PORT:-5433}:5432" + volumes: + # Postgres 18 puts the cluster in /var/lib/postgresql/18/docker and declares the + # volume a level up, so mounting .../data loses it. + - deepreefmap-db-data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] + interval: 5s + timeout: 5s + retries: 5 + + deepreefmap-keycloak: + image: quay.io/keycloak/keycloak:26.7 + command: start-dev --import-realm + environment: + KC_DB: dev-file + KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN:-admin} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} + KC_HOSTNAME_STRICT: false + ports: + - "127.0.0.1:${KEYCLOAK_EXTERNAL_PORT:-8280}:8080" + volumes: + - ./keycloak-realm-dev.json:/opt/keycloak/data/import/realm.json:ro + healthcheck: + test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080"] + interval: 10s + timeout: 5s + retries: 15 + start_period: 30s + + # ── Blob archive ───────────────────────────────────────────────────────── + minio: + image: minio/minio + command: server /data --console-address ":9001" + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER:-minioadmin} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-minioadmin} ports: - - 5433:5432 + # Loopback only, for the API's gated S3 tests running on the host. No client + # in the stack reaches MinIO: the dev topology matches the intranet-only + # store in production. + - "127.0.0.1:${MINIO_EXTERNAL_PORT:-9000}:9000" volumes: - - deepreefmap-db-data:/var/lib/postgresql/data + - deepreefmap-minio-data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 10 + # One-shot: the API expects its bucket to exist rather than creating it, so a typo + # in S3_BUCKET_ID fails loudly instead of silently making a second bucket. + minio-seed: + image: minio/mc + depends_on: + minio: + condition: service_healthy + entrypoint: ["sh", "-c"] + command: + - | + mc alias set local http://minio:9000 ${MINIO_ROOT_USER:-minioadmin} ${MINIO_ROOT_PASSWORD:-minioadmin} + mc mb --ignore-existing local/deepreefmap + restart: "no" + + # ── Dev connect-code seeder (one-shot) ────────────────────────────────── + # Minting a connect code normally needs an interactive login, which makes + # testing the desktop sync path a click-through. This seeds a fixed code so + # `POST /api/enrol` can be exercised straight away: + # + # curl -X POST http://localhost:88/api/enrol \ + # -H 'Content-Type: application/json' \ + # -d '{"code":"'$(printf 'ab%.0s' {1..32})'"}' + # + # Single use, like any connect code: re-run this service to get another. + dev-seed-connect-code: + image: postgres:18-alpine + # Opt in with `--profile seed`: the secret below is public, so an unattended stack + # must not carry a live enrolment credential. + profiles: ["seed"] + depends_on: + deepreefmap-api: + condition: service_healthy + environment: + - PGPASSWORD=${POSTGRES_PASSWORD:-psql} + entrypoint: ["sh", "-c"] + command: + - | + until pg_isready -h deepreefmap-db -U ${POSTGRES_USER:-postgres}; do sleep 1; done + psql -h deepreefmap-db -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres} <<'SQL' + CREATE EXTENSION IF NOT EXISTS pgcrypto; + -- sha256 of the 64-hex secret 'abab...ab', matching the curl above. The + -- server stores only this digest, so the seeded row is indistinguishable + -- from one the mint endpoint wrote. + INSERT INTO connect_code (id, code_hash, created_by, device_name, expires_at, created_at) + VALUES ( + gen_random_uuid(), + encode(digest(repeat('ab', 32), 'sha256'), 'hex'), + 'dev-local-admin', + 'Dev laptop', + now() + interval '15 minutes', + now() + ) + ON CONFLICT (code_hash) DO NOTHING; + SQL + echo "Seeded dev connect code: $(printf 'ab%.0s' $(seq 32))" + restart: "no" + + # ── Test infrastructure ───────────────────────────────────────────────── deepreefmap-test-db: - image: postgis/postgis:16-master + image: postgres:18-alpine environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=psql - - POSTGRES_DB=postgres + - POSTGRES_DB=deepreefmap_test ports: - - 5444:5432 - - deepreefmap-test-cache: - image: redis:7.4.0 - command: ["redis-server", "--requirepass", "test"] + - "127.0.0.1:${TEST_POSTGRES_EXTERNAL_PORT:-5444}:5432" + # In memory: the suite recreates the schema, so nothing here needs to survive. + tmpfs: + - /var/lib/postgresql + # Quiet, so a failing test is not buried under statement logs in the watcher. + command: ["postgres", "-c", "log_min_messages=fatal", "-c", "log_statement=none"] + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 - deepreefmap-test-api: + deepreefmap-test-watcher: build: context: ../deepreefmap-api - dockerfile: Dockerfile.dev # Uses the dockerfile just for pytest + dockerfile: Dockerfile.dev environment: - - DB_HOST=deepreefmap-test-db - - DB_PORT=5432 - - DB_USER=postgres - - DB_PASSWORD=psql - - DB_NAME=postgres - - DB_PREFIX=postgresql+asyncpg - - S3_URL=https://s3.example.com - - S3_BUCKET_ID=1234 - - S3_ACCESS_KEY=1234 - - S3_SECRET_KEY=1234 - - S3_PREFIX=test-only - - KUBERNETES_SERVICE_HOST=test-only - - KUBERNETES_SERVICE_PORT=443 - - KUBECONFIG=/dev/null - - INCOMPLETE_OBJECT_CHECK_INTERVAL=${INCOMPLETE_OBJECT_CHECK_INTERVAL} - - INCOMPLETE_OBJECT_TIMEOUT_SECONDS=${INCOMPLETE_OBJECT_TIMEOUT_SECONDS} - - DEEPREEFMAP_IMAGE_TAG=${DEEPREEFMAP_IMAGE_TAG} - - DEEPREEFMAP_IMAGE=${DEEPREEFMAP_IMAGE} - - NAMESPACE=test - - PROJECT=test - - KEYCLOAK_CLIENT_ID=test - - KEYCLOAK_API_ID=test - - KEYCLOAK_API_SECRET=test - - KEYCLOAK_REALM=test - - KEYCLOAK_URL=test - - SERIALIZER_SECRET_KEY=test + - DATABASE_URL=postgresql://postgres:psql@deepreefmap-test-db:5432/deepreefmap_test + - RUST_LOG=${LOG_LEVEL:-warn} - DEPLOYMENT=local - - CACHE_URL=deepreefmap-test-cache - - CACHE_PORT=6379 - - CACHE_DB=1 - - CACHE_SECRET=test - - CACHE_PASSWORD=test depends_on: - - deepreefmap-test-db - - deepreefmap-test-cache + deepreefmap-test-db: + condition: service_healthy + entrypoint: ["bacon", "--headless", "test-integration"] volumes: - - ../deepreefmap-api:/app - ports: - - 5445:8000 - - deepreefmap-cache: - image: redis:7.4.0 - ports: - - 6379:6379 - command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] - - deepreefmap-tus: - image: tusproject/tusd:latest - environment: - - AWS_ACCESS_KEY_ID=${S3_ACCESS_KEY} - - AWS_SECRET_ACCESS_KEY=${S3_SECRET_KEY} - - AWS_REGION=us-east-1 - command: -s3-bucket ${S3_BUCKET_ID} -s3-endpoint https://${S3_URL} -s3-object-prefix deepreefmap-local -behind-proxy -port 8080 -hooks-http http://deepreefmap-api:8000/tus_hooks - labels: - - "traefik.http.routers.deepreefmap-tus.rule=Host(`deepreefmap`) && PathPrefix(`/files`)" - - "traefik.http.services.deepreefmap-tus.loadbalancer.server.port=8080" + - ../deepreefmap-api/src:/app/src + - ../deepreefmap-api/tests:/app/tests + - ../deepreefmap-api/migration:/app/migration + - ../deepreefmap-api/contract:/app/contract + - ../deepreefmap-api/Cargo.toml:/app/Cargo.toml + - ../deepreefmap-api/Cargo.lock:/app/Cargo.lock + - ../deepreefmap-api/bacon.toml:/app/bacon.toml + - deepreefmap-test-rust-data:/app/target volumes: deepreefmap-db-data: + deepreefmap-minio-data: + deepreefmap-rust-data: + deepreefmap-test-rust-data: diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..9609792 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,46 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import prettier from 'eslint-config-prettier/flat'; +import globals from 'globals'; + +export default tseslint.config( + { + // node_modules and .git are ignored already. These two are ours. + ignores: ['dist/**', 'src/contract/api.d.ts'], + }, + { + // A bare directory on the CLI expands to .js/.mjs/.cjs without this. + files: ['**/*.{js,jsx,ts,tsx}'], + extends: [ + js.configs.recommended, + // Turns off the base rules that misread TypeScript type syntax, `no-unused-vars` + // on an interface signature and `no-redeclare` on a type import among them. + tseslint.configs.recommended, + react.configs.flat.recommended, + react.configs.flat['jsx-runtime'], + // Last, so formatting rules lose to prettier. + prettier, + ], + languageOptions: { + globals: globals.browser, + parserOptions: { + ecmaFeatures: { jsx: true }, + }, + }, + settings: { + // Pinned: 'detect' crashes, eslint-plugin-react probes for a context API + // that eslint 10 removed. + react: { version: '19.2' }, + }, + plugins: { + 'react-hooks': reactHooks, + }, + rules: { + // The v7 flat preset also runs the React Compiler lints at error. + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, + } +); diff --git a/index.html b/index.html index 9543582..95e370c 100644 --- a/index.html +++ b/index.html @@ -7,149 +7,19 @@ - - - - -