diff --git a/.env.docker.example b/.env.docker.example index f4368e9d..37dc8f99 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -1,18 +1,8 @@ -MB_PORT=4300 CLIENT_PORT=4400 API_PORT=4500 -PREMIUM_EMBEDDING_TOKEN="" -METABASE_JWT_SHARED_SECRET="ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - -METABASE_ADMIN_EMAIL: "admin@example.com" -METABASE_ADMIN_PASSWORD="foobarbaz" - -METABASE_APP_DB_HOST: "shoppy_app_db" -METABASE_APP_DB_PORT: "5432" -METABASE_APP_DB: "shoppy_app" -METABASE_APP_DB_USER: "shoppy_app" -METABASE_APP_DB_PASSWORD: "foobarbaz" +MB_INSTANCE_URL="https://shoppy.metabaseapp.com" +METABASE_JWT_SHARED_SECRET="your_secret_here" SHOPPY_DB_HOST="shoppy_db" SHOPPY_DB_PORT="5432" @@ -21,7 +11,3 @@ SHOPPY_DB_USER="shoppy" SHOPPY_DB_PASSWORD="foobarbaz" WATCH="false" - -# for development -# MB_RUN_MODE="dev" -# METASTORE_DEV_SERVER_URL="" diff --git a/.env.example b/.env.example index dc8dd415..46101689 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ PORT=4173 -VITE_APP_METABASE_INSTANCE_URL=https://shoppy.coredev.metabase.com +VITE_APP_METABASE_INSTANCE_URL=https://shoppy.metabaseapp.com VITE_APP_BACKEND_URL=https://embedded-analytics-sdk-demo.metabase.com diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 9c182f87..da53bb13 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -20,50 +20,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Tailscale - uses: tailscale/github-action@v2 - with: - oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }} - oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} - tags: tag:ci - version: 1.50.1 - sha256sum: d9fe6b480fb5078f0aa57dace686898dda7e2a768884271159faa74846bfb576 - - - name: Create OIDC Token - id: create-oidc-token - shell: bash - run: | - export OIDC_URL_WITH_AUDIENCE="$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${{ secrets.K8S_AUDIENCE }}" - IDTOKEN=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ - -H "Accept: application/json; api-version=2.0" "$OIDC_URL_WITH_AUDIENCE" \ - | jq -r .value) - echo "::add-mask::${IDTOKEN}" - echo "idToken=${IDTOKEN}" >>$GITHUB_OUTPUT - - - name: Setup Kube Context - uses: azure/k8s-set-context@v2 - with: - method: kubeconfig - kubeconfig: | - kind: Config - apiVersion: v1 - current-context: default - clusters: - - name: default - cluster: - certificate-authority-data: ${{ secrets.K8S_CERTIFICATE_AUTHORITY_DATA }} - server: ${{ secrets.K8S_SERVER }} - users: - - name: oidc-token - user: - token: ${{ steps.create-oidc-token.outputs.IDTOKEN }} - contexts: - - name: default - context: - cluster: default - namespace: ${{secrets.K8S_NAMESPACE}} - user: oidc-token - - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -71,53 +27,34 @@ jobs: role-session-name: GitHub_to_AWS_via_FederatedOIDC aws-region: us-east-1 - - name: Setup psql client + - name: Get runner public IP + id: runner-ip + run: echo "ip=$(curl -s https://checkip.amazonaws.com)" >> "$GITHUB_OUTPUT" + + - name: Authorize runner IP on the warehouse security group run: | - sudo apt-get update - sudo apt-get install -y postgresql-client + aws ec2 authorize-security-group-ingress \ + --group-id ${{ secrets.SHOPPY_DB_SECURITY_GROUP_ID }} \ + --protocol tcp --port ${{ vars.SHOPPY_DB_PORT }} \ + --cidr ${{ steps.runner-ip.outputs.ip }}/32 - name: Ensure that Esno executable is ready run: yarn install --cwd api --frozen-lockfile - name: Reinitialize Shoppy's DWH DB env: - DB_URL: postgres://${{ secrets.SHOPPY_DB_USER }}:${{ secrets.SHOPPY_DB_PASSWORD }}@${{ secrets.SHOPPY_DB_HOST }}:${{ vars.SHOPPY_DB_PORT }}/${{ vars.SHOPPY_DB }} + DB_URL: postgres://${{ secrets.SHOPPY_DB_USER }}:${{ secrets.SHOPPY_DB_PASSWORD }}@${{ secrets.SHOPPY_DB_HOST }}:${{ vars.SHOPPY_DB_PORT }}/${{ vars.SHOPPY_DB }}?sslmode=no-verify run: | cd api && yarn db:initialize - - name: Download PostgreSQL dump from S3 - run: | - aws s3 cp s3://${{ secrets.METABASE_APP_DB_S3_BUCKET }}/${{ vars.METABASE_APP_DB_S3_KEY }} dump.sql - - - name: Pause Shoppy's Metabase Instance (set pause.enabled=true) - run: | - kubectl patch metabase hosting-shoppy \ - --type=merge \ - -p '{"spec":{"pause":{"enabled":true}}}' - - - name: Drop Shoppy's Metabase Instance app database and restore - run: | - export PGPASSWORD='${{ secrets.APP_DB_PASSWORD }}' - export PGUSER=${{ secrets.APP_DB_USER }} - export PGDATABASE=${{ secrets.APP_DB_DATABASE }} - export PGHOST=${{ secrets.APP_DB_HOST }} - psql -d ${{ secrets.APP_DB_DATABASE }} \ - -c "DROP DATABASE IF EXISTS ${{ secrets.APP_DB_SHOPPY_DATABASE }} WITH (FORCE);" - psql -d ${{ secrets.APP_DB_DATABASE }} \ - -c "CREATE DATABASE ${{ secrets.APP_DB_SHOPPY_DATABASE }} WITH OWNER ${{ secrets.APP_DB_USER }};" - pg_restore -d ${{ secrets.APP_DB_SHOPPY_DATABASE }} dump.sql - - - name: Update Shoppy's Metabase Instance version and unpause - run: | - kubectl patch metabase hosting-shoppy \ - --type=merge \ - -p '{"spec":{"image":{"repo": "${{ secrets.MB_IMAGE_REPO }}", "name": "${{ vars.MB_IMAGE_NAME }}", "version":"${{ vars.MB_IMAGE_VERSION }}"},"pause":{"enabled":false}}}' - - - name: Verify Shoppy's Metabase Instance deployment + - name: Revoke runner IP from the warehouse security group + if: always() run: | - kubectl get metabase hosting-shoppy -o jsonpath='{.spec.image.version}' - echo "Metabase updated to version ${{ vars.MB_IMAGE_VERSION }} and unpaused" + aws ec2 revoke-security-group-ingress \ + --group-id ${{ secrets.SHOPPY_DB_SECURITY_GROUP_ID }} \ + --protocol tcp --port ${{ vars.SHOPPY_DB_PORT }} \ + --cidr ${{ steps.runner-ip.outputs.ip }}/32 deploy-to-vercel: needs: [ deploy ] diff --git a/.github/workflows/dump.yml b/.github/workflows/dump.yml deleted file mode 100644 index ec201037..00000000 --- a/.github/workflows/dump.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Dump -on: - workflow_dispatch: - -jobs: - dump: - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.IAM_ROLE }} - role-session-name: GitHub_to_AWS_via_FederatedOIDC - aws-region: us-east-1 - - name: Tailscale - uses: tailscale/github-action@v2 - with: - oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }} - oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} - tags: tag:ci - version: 1.50.1 - sha256sum: d9fe6b480fb5078f0aa57dace686898dda7e2a768884271159faa74846bfb576 - - name: Setup psql client - run: | - sudo apt-get update - sudo apt-get install -y postgresql-client - - name: Export PostgreSQL dump to S3 - run: | - export PGPASSWORD='${{ secrets.APP_DB_PASSWORD }}' - export PGUSER=${{ secrets.APP_DB_USER }} - export PGDATABASE=${{ secrets.APP_DB_SHOPPY_DATABASE }} - export PGHOST=${{ secrets.APP_DB_HOST }} - pg_dump -F c -b -v -f dump.sql - aws s3 cp dump.sql s3://${{ secrets.METABASE_APP_DB_S3_BUCKET }}/${{ vars.METABASE_APP_DB_S3_KEY }} \ No newline at end of file diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 5fbb98e5..85def127 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -12,44 +12,22 @@ on: jobs: e2e-tests: runs-on: ubuntu-22.04 - timeout-minutes: 10 + timeout-minutes: 15 name: e2e-tests - env: - PREMIUM_EMBEDDING_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }} - MB_RUN_MODE: dev - METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }} - METABASE_APP_DB_USER: ${{ secrets.APP_DB_USER }} - METABASE_APP_DB: ${{ secrets.APP_DB_SHOPPY_DATABASE }} permissions: - id-token: write contents: read steps: - uses: actions/checkout@v4 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.IAM_ROLE }} - role-session-name: GitHub_to_AWS_via_FederatedOIDC - aws-region: us-east-1 - - - name: Download App DB PostgreSQL dump from S3 + - name: Prepare env run: | - aws s3 cp s3://${{ secrets.METABASE_APP_DB_S3_BUCKET }}/${{ vars.METABASE_APP_DB_S3_KEY }} ./metabase/metabase_dump.sql + cp .env.docker.example .env.docker + # The api signs SSO JWTs that production Metabase must trust, so use the production secret. + sed -i "s|^METABASE_JWT_SHARED_SECRET=.*|METABASE_JWT_SHARED_SECRET=\"${{ secrets.SHOPPY_PROD_JWT_SHARED_SECRET }}\"|" .env.docker - - name: Run Shoppy in Docker - run: | - cp .env.docker.example .env.docker && - yarn docker:e2e:up --wait - - - name: Dump container logs on failure - if: failure() - run: | - echo "=== shoppy-metabase-1 logs ===" && docker logs shoppy-metabase-1 2>&1 || true - echo "=== shoppy-api-1 logs ===" && docker logs shoppy-api-1 2>&1 || true - echo "=== shoppy-client-1 logs ===" && docker logs shoppy-client-1 2>&1 || true - echo "=== Container statuses ===" && docker ps -a 2>&1 || true + - name: Run Shoppy (client + api + warehouse) against production Metabase + run: yarn docker:e2e:up --wait - name: Install Chrome v111 uses: browser-actions/setup-chrome@v1 @@ -65,6 +43,13 @@ jobs: id: run-e2e-tests run: cd e2e && yarn cypress:run + - name: Dump container logs on failure + if: failure() + run: | + echo "=== shoppy-client-1 ===" && docker logs shoppy-client-1 2>&1 || true + echo "=== shoppy-api-1 ===" && docker logs shoppy-api-1 2>&1 || true + echo "=== container statuses ===" && docker ps -a 2>&1 || true + - name: Upload Cypress Artifacts upon failure uses: actions/upload-artifact@v4 if: ${{ steps.run-e2e-tests.outcome != 'success' }} diff --git a/.gitignore b/.gitignore index 34ed61fc..ec1940d1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,6 @@ node_modules .pnp .pnp.js -# local dist -local-dist/* -!local-dist/.gitkeep - # testing coverage cypress @@ -17,11 +13,6 @@ cypress dist build -# db dumps -*.sql -# but allow migration files -!api/drizzle/**/*.sql - # misc .DS_Store .env.development.local diff --git a/Dockerfile b/Dockerfile index 601f6b22..874fdab8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,13 +39,6 @@ RUN yarn --frozen-lockfile # Copy source code last (changes most frequently) COPY --exclude=./api --exclude=./metabase . . -RUN if [ -d "./local-dist/embedding-sdk" ]; then \ - echo "Local embedding-sdk dist is found in ./local-dist/embedding-sdk, installing it..."; \ - yarn add file:./local-dist/embedding-sdk; \ - else \ - echo "Local embedding-sdk dist is not found in ./local-dist/embedding-sdk, skipping copy"; \ - fi - RUN if [ "$WATCH" != "true" ]; then \ echo "WATCH env is not set; running production yarn build..."; \ yarn build; \ diff --git a/README.md b/README.md index c3d9d366..f079554e 100644 --- a/README.md +++ b/README.md @@ -32,24 +32,13 @@ This demo uses the data from the hosted Metabase Cloud instance and provides a h ### Using Docker -- Clone `.env.docker.example` to `.env.docker` and set the proper `PREMIUM_EMBEDDING_TOKEN` value. -- Run Docker via `yarn docker:up` for the `production` build or `WATCH=true yarn docker:up` for the development build with the `watch` support. - - The command launches containers with the local MB instance, Shoppy DWH, Shoppy API and Shoppy Client. +- Clone `.env.docker.example` to `.env.docker`. Set `MB_INSTANCE_URL` to your Metabase instance and `METABASE_JWT_SHARED_SECRET` to that instance's JWT shared secret. +- Run `yarn docker:up` for a production build, or `WATCH=true yarn docker:up` for a dev build with watch. + - The command launches the Shoppy DWH, API and Client, pointing at the configured Metabase instance. - Visit `http://localhost:4400`. - To stop containers run `yarn docker:down`. - To remove containers and images completely run `yarn docker:rm`. -#### Local development (For Metabase developers) - -For a local development the App DB dump of the Shoppy's Metabase Instance must be downloaded. - -See the [Getting the App DB dump](#getting-the-app-db-dump-of-the-shoppys-metabase-instance) section. - -- To run containers with a locally built `metabase.jar`, copy it to the `./local-dist` folder as `./local-dist/metabase.jar`. -- To run containers with a locally built Embedding SDK package, copy it to the `./local-dist` folder as `./local-dist/embedding-sdk`. -- Run `yarn docker:local-dist:up` to start containers and use locally built dist from the `./local-dist` folder. -- To remove containers and images completely run `yarn docker:rm`. - ### Using an existing running MB instance - Place the metabase repository in `../metabase` @@ -76,20 +65,11 @@ If you cannot use the hosted JWT server, you can run the JWT server locally. ### Running e2e tests (For Metabase developers) -To run e2e tests locally, the App DB dump of the Shoppy's Metabase Instance must be downloaded. - -See the [Getting the App DB dump](#getting-the-app-db-dump-of-the-shoppys-metabase-instance) section. - -Then run `yarn docker:e2e:up` to start all required containers. -After containers are up, run `cd e2e && yarn cypress:open` to run Cypress. -To stop containers run `yarn docker:rm`. - - -### Getting the App DB dump of the Shoppy's Metabase Instance +The e2e tests run the client, api and warehouse locally (via Docker) against the production Metabase instance, and drive the local client with Cypress. -For a local development or for running e2e locally an App DB dump of the Shoppy's Metabase Instance must be placed to the `./metabase/metabase_dump.sql` +- In `.env.docker`, set `METABASE_JWT_SHARED_SECRET` to the production instance's JWT shared secret (from 1Password) so the api's SSO tokens are trusted, and `MB_INSTANCE_URL` to that instance. +- Start the stack: `yarn docker:e2e:up --wait`. +- Run the tests: `cd e2e && yarn cypress:run` (headless) or `yarn cypress:open` (Cypress UI). +- Stop the stack: `yarn docker:down`. -You can get it by: -- Enabling the `Tailscale` and logging in using your work email address. -- Running `pg_dump "postgres://{{ username }}:{{ password }}@{{ host }}:{{ port }}/{{ database }}" > ./metabase/metabase_dump.sql` command. - - See the `Shoppy Coredev Appdb` record in `1password` for credentials. +CI runs the same suite in `.github/workflows/e2e-tests.yml`, injecting the production secret from the `SHOPPY_PROD_JWT_SHARED_SECRET` GitHub secret. diff --git a/api/.env.example b/api/.env.example index c6061c9d..20d99180 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,5 +1,5 @@ PORT=3003 FRONTEND_URL=http://localhost:3004 -METABASE_INSTANCE_URL=https://shoppy.coredev.metabase.com +METABASE_INSTANCE_URL=https://shoppy.metabaseapp.com METABASE_JWT_SHARED_SECRET=your_secret_here DB_URL=your_postgres_url_here \ No newline at end of file diff --git a/api/src/constants/users.ts b/api/src/constants/users.ts index f188be10..c41124c9 100644 --- a/api/src/constants/users.ts +++ b/api/src/constants/users.ts @@ -1,32 +1,31 @@ import { User } from "../types/user" -import { METABASE_ADMIN_EMAIL } from "./env" export const users: User[] = [ { firstName: "Rene", lastName: "Mueller", - email: METABASE_ADMIN_EMAIL ?? "rene@example.com", + email: "rene@example.com", group: "Pug & Play", shopId: 1, }, { firstName: "Cecilia", lastName: "Stark", - email: METABASE_ADMIN_EMAIL ?? "cecilia@example.com", + email: "cecilia@example.com", group: "theStitch", shopId: 2, }, { firstName: "Emily", lastName: "Johnson", - email: METABASE_ADMIN_EMAIL ?? "emily@example.com", + email: "emily@example.com", group: "Luminara Beauty", shopId: 3, }, { firstName: "Jennifer", lastName: "Martinez", - email: METABASE_ADMIN_EMAIL ?? "jennifer@example.com", + email: "jennifer@example.com", group: "ProficiencyLabs", shopId: 4, }, diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml deleted file mode 100644 index 6737e904..00000000 --- a/docker-compose.e2e.yml +++ /dev/null @@ -1,10 +0,0 @@ -services: - metabase: - image: metabase/metabase-enterprise:v1.63.x - volumes: - - type: bind - source: ./metabase/metabase_dump.sql - target: /app/metabase_dump.sql - read_only: true - bind: - create_host_path: false \ No newline at end of file diff --git a/docker-compose.local-dist.yml b/docker-compose.local-dist.yml deleted file mode 100644 index 0e0f8d2a..00000000 --- a/docker-compose.local-dist.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - metabase: - volumes: - - type: bind - source: ./local-dist/metabase.jar - target: /app/metabase.jar - read_only: true - bind: - create_host_path: false - - type: bind - source: ./metabase/metabase_dump.sql - target: /app/metabase_dump.sql - read_only: true - bind: - create_host_path: false diff --git a/docker-compose.yml b/docker-compose.yml index ebf4dae0..55143a10 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +# Shoppy stack: client + api + warehouse. Metabase is the production instance (MB_INSTANCE_URL); +# the client talks to it same-origin via `/mb`, which the api proxies onward (see api/src/main.ts). services: shoppy_db: image: postgres:latest @@ -14,25 +16,8 @@ services: expose: - "${SHOPPY_DB_PORT}" - shoppy_app_db: - image: postgres:latest - environment: - POSTGRES_PORT: "${METABASE_APP_DB_PORT}" - POSTGRES_DB: "${METABASE_APP_DB}" - POSTGRES_USER: "${METABASE_APP_DB_USER}" - POSTGRES_PASSWORD: "${METABASE_APP_DB_PASSWORD}" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${METABASE_APP_DB_USER}"] - interval: 10s - timeout: 5s - retries: 5 - expose: - - "${METABASE_APP_DB_PORT}" - api: depends_on: - shoppy_app_db: - condition: service_healthy shoppy_db: condition: service_healthy build: @@ -45,9 +30,8 @@ services: CLIENT_PORT: "${CLIENT_PORT}" IS_LOCAL_DB: "true" DB_URL: "postgres://${SHOPPY_DB_USER}:${SHOPPY_DB_PASSWORD}@${SHOPPY_DB_HOST}:${SHOPPY_DB_PORT}/${SHOPPY_DB}" - METABASE_INSTANCE_URL: "http://metabase:${MB_PORT}" + METABASE_INSTANCE_URL: "${MB_INSTANCE_URL}" METABASE_JWT_SHARED_SECRET: "${METABASE_JWT_SHARED_SECRET}" - METABASE_ADMIN_EMAIL: "${METABASE_ADMIN_EMAIL}" healthcheck: test: curl --fail -X GET -I "http://localhost:${API_PORT}/" || exit 1 interval: 15s @@ -56,63 +40,15 @@ services: ports: - "${API_PORT}:${API_PORT}" - metabase: - depends_on: - shoppy_app_db: - condition: service_healthy - shoppy_db: - condition: service_healthy - api: - condition: service_healthy - image: metabase/metabase-enterprise:v1.63.x - entrypoint: ["sh", "/app/entrypoint.sh"] - environment: - MB_CONFIG_FILE_PATH: "./app/config.yml" - MB_JETTY_PORT: "${MB_PORT}" - MB_EDITION: "ee" - MB_SITE_URL: "http://localhost:${MB_PORT}/" - MB_PREMIUM_EMBEDDING_TOKEN: "${PREMIUM_EMBEDDING_TOKEN}" - MB_RUN_MODE: "${MB_RUN_MODE}" - METASTORE_DEV_SERVER_URL: "${METASTORE_DEV_SERVER_URL}" - MB_JWT_IDENTITY_PROVIDER_URI: "http://localhost:${API_PORT}/sso/metabase" - MB_JWT_SHARED_SECRET: "${METABASE_JWT_SHARED_SECRET}" - MB_SETUP_TOKEN: "${PREMIUM_EMBEDDING_TOKEN}" - MB_DB_TYPE: "postgres" - MB_DB_HOST: "${METABASE_APP_DB_HOST}" - MB_DB_PORT: "${METABASE_APP_DB_PORT}" - MB_DB_USER: "${METABASE_APP_DB_USER}" - MB_DB_PASS: "${METABASE_APP_DB_PASSWORD}" - MB_DB_DBNAME: "${METABASE_APP_DB}" - METABASE_ADMIN_EMAIL: "${METABASE_ADMIN_EMAIL}" - METABASE_ADMIN_PASSWORD: "${METABASE_ADMIN_PASSWORD}" - SHOPPY_DB_HOST: "${SHOPPY_DB_HOST}" - SHOPPY_DB_PORT: "${SHOPPY_DB_PORT}" - SHOPPY_DB: "${SHOPPY_DB}" - SHOPPY_DB_USER: "${SHOPPY_DB_USER}" - SHOPPY_DB_PASSWORD: "${SHOPPY_DB_PASSWORD}" - healthcheck: - test: curl --fail -X GET -I "http://localhost:${MB_PORT}/api/health" || exit 1 - interval: 15s - timeout: 5s - retries: 10 - ports: - - "${MB_PORT}:${MB_PORT}" - volumes: - - ./metabase/entrypoint.sh:/app/entrypoint.sh - - ./metabase/config.yml:/app/config.yml - - ./metabase/metabase_data.tar.gz:/app/metabase_data.tar.gz - client: depends_on: - metabase: - condition: service_healthy api: condition: service_healthy build: context: . dockerfile: Dockerfile args: - VITE_APP_METABASE_INSTANCE_URL: "http://localhost:${MB_PORT}" + VITE_APP_METABASE_INSTANCE_URL: "/mb" VITE_APP_BACKEND_URL: "http://localhost:${API_PORT}" VITE_DATADOG_APPLICATION_ID: "${DATADOG_APPLICATION_ID}" VITE_DATADOG_CLIENT_TOKEN: "${DATADOG_CLIENT_TOKEN}" @@ -122,7 +58,7 @@ services: WATCH: "${WATCH}" environment: CLIENT_PORT: "${CLIENT_PORT}" - VITE_APP_METABASE_INSTANCE_URL: "http://localhost:${MB_PORT}" + VITE_APP_METABASE_INSTANCE_URL: "/mb" VITE_APP_BACKEND_URL: "http://localhost:${API_PORT}" VITE_APP_DOCKER_OVERRIDE_BACKEND_URL: "http://api:${API_PORT}" WATCH: "${WATCH}" diff --git a/e2e/support/helpers/sign-in.js b/e2e/support/helpers/sign-in.js deleted file mode 100644 index e4cb8a67..00000000 --- a/e2e/support/helpers/sign-in.js +++ /dev/null @@ -1,59 +0,0 @@ -// TODO (Kelvin 2026-07-07) bandage, not a fix (EMB-2059) — see compatibility.cy.spec.js. - -export const METABASE_URL = "http://localhost:4300" - -// JWT_SHARED_SECRET also appears in .env.docker.example, so it's not a real secret. -const JWT_SHARED_SECRET = - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" -const ADMIN_EMAIL = "shoppy@metabase.com" - -function base64url(bytes) { - let binary = "" - new Uint8Array(bytes).forEach((byte) => { - binary += String.fromCharCode(byte) - }) - return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") -} - -async function getSignedJwtForAdmin() { - const encoder = new TextEncoder() - const header = { alg: "HS256", typ: "JWT" } - const payload = { - email: ADMIN_EMAIL, - first_name: "Shoppy", - last_name: "Admin", - exp: Math.floor(Date.now() / 1000) + 600, - } - const signingInput = `${base64url(encoder.encode(JSON.stringify(header)))}.${base64url( - encoder.encode(JSON.stringify(payload)), - )}` - - const key = await crypto.subtle.importKey( - "raw", - encoder.encode(JWT_SHARED_SECRET), - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], - ) - const signature = await crypto.subtle.sign( - "HMAC", - key, - encoder.encode(signingInput), - ) - - return `${signingInput}.${base64url(signature)}` -} - -export function signInAsAdmin() { - return cy - .wrap(getSignedJwtForAdmin()) - .then((jwt) => - cy.request({ - method: "GET", - url: `${METABASE_URL}/auth/sso`, - qs: { jwt }, - headers: { "X-Metabase-Client": "embedding-sdk-react" }, - }), - ) - .then(({ body }) => body.id) -} diff --git a/e2e/test/compatibility.cy.spec.js b/e2e/test/compatibility.cy.spec.js index 470538fd..7bf6214d 100644 --- a/e2e/test/compatibility.cy.spec.js +++ b/e2e/test/compatibility.cy.spec.js @@ -1,5 +1,3 @@ -import { METABASE_URL, signInAsAdmin } from "../support/helpers/sign-in" - const TIMEOUT = 20000 describe("Embedding SDK: shoppy compatibility", () => { @@ -171,51 +169,7 @@ describe("Embedding SDK: shoppy compatibility", () => { }) }) - // TODO (Kelvin 2026-07-07) bandage, not a fix. Metabase's appdb search-index reindex has a - // race that can strand a fully-built index as "pending" instead of activating it. Couldn't - // land the real backend fix yet; this forces a reindex and waits for it first. The actual - // backend bug (QUE2-736) hasn't landed yet. - // Scoped to just this test, not the whole suite — it's the only one that needs it. describe("data picker", () => { - before(() => { - const REINDEX_POLL_INTERVAL_MS = 1000 - const REINDEX_POLL_MAX_ATTEMPTS = 60 - - function waitForDatasetIndex(sessionId, attempt = 0) { - cy.request({ - method: "GET", - url: `${METABASE_URL}/api/search?models=dataset`, - headers: { "X-Metabase-Session": sessionId }, - }).then(({ body }) => { - if (body.total > 0) { - return - } - if (attempt >= REINDEX_POLL_MAX_ATTEMPTS) { - // Fail loudly here instead of silently proceeding into a doomed test — a confusing - // "can't find Orders" UI assertion failure downstream is much harder to diagnose than - // this being the actual problem. - throw new Error( - `waitForDatasetIndex: no datasets found after ${attempt} attempts`, - ) - } - - cy.wait(REINDEX_POLL_INTERVAL_MS) - waitForDatasetIndex(sessionId, attempt + 1) - }) - } - - signInAsAdmin().then((sessionId) => { - cy.request({ - method: "POST", - url: `${METABASE_URL}/api/search/force-reindex`, - headers: { "X-Metabase-Session": sessionId }, - failOnStatusCode: false, - }) - - waitForDatasetIndex(sessionId) - }) - }) - it("should not display tables in the data picker", () => { cy.visit({ url: "/admin/analytics/new/from-scratch", diff --git a/local-dist/.gitkeep b/local-dist/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/metabase/config.yml b/metabase/config.yml deleted file mode 100644 index 1dd2c826..00000000 --- a/metabase/config.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: 1 -config: - users: - - first_name: "Admin" - last_name: "Admin" - password: "{{ env METABASE_ADMIN_PASSWORD }}" - email: "{{ env METABASE_ADMIN_EMAIL }}" - is_superuser: true - - first_name: "Rene" - last_name: "Mueller" - password: "{{ env METABASE_ADMIN_PASSWORD }}" - email: "rene@example.com" - - first_name: "Cecilia" - last_name: "Stark" - password: "{{ env METABASE_ADMIN_PASSWORD }}" - email: "cecilia@example.com" - - first_name: "Emily" - last_name: "Johnson" - password: "{{ env METABASE_ADMIN_PASSWORD }}" - email: "emily@example.com" - - first_name: "Jennifer" - last_name: "Martinez" - password: "{{ env METABASE_ADMIN_PASSWORD }}" - email: "jennifer@example.com" - settings: - enable-embedding-sdk: true - jwt-enabled: true - databases: - - name: Customer Zero Database - engine: postgres - details: - host: "{{ env SHOPPY_DB_HOST }}" - port: "{{ env SHOPPY_DB_PORT }}" - dbname: "{{ env SHOPPY_DB }}" - user: "{{ env SHOPPY_DB_USER }}" - password: "{{ env SHOPPY_DB_PASSWORD }}" diff --git a/metabase/entrypoint.sh b/metabase/entrypoint.sh deleted file mode 100644 index c2545849..00000000 --- a/metabase/entrypoint.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -set -e - -DUMP=./app/metabase_dump.sql - -if [ -f $DUMP ]; then - echo "Installing dependencies..." - - apk update \ - && apk add --no-cache postgresql-client file \ - && rm -rf /var/cache/apk/* - - echo "Restoring MB App DB from dump..."; - - # reset `MB_CONFIG_FILE_PATH` value to prevent initialization from config - export MB_CONFIG_FILE_PATH='' - - METABASE_APP_DB_URL="postgres://${MB_DB_USER}:${MB_DB_PASS}@${MB_DB_HOST}:${MB_DB_PORT}/${MB_DB_DBNAME}" - METABASE_APP_DB_DUMP_TYPE=$(file --brief --mime-type "$DUMP") - - case "$METABASE_APP_DB_DUMP_TYPE" in - application/x-tar|application/octet-stream) - echo "Custom or directory‑format archive → pg_restore" - pg_restore -d "$METABASE_APP_DB_URL" "$DUMP" --no-owner > /dev/null - ;; - text/plain) - echo "Plain text SQL → psql" - psql "$METABASE_APP_DB_URL" -f "$DUMP" --quiet - ;; - *) - echo "Unknown dump format..." - ;; - esac -fi - -./app/run_metabase.sh "$@" & -SERVER_PID=$! - -until curl -s http://metabase:${MB_JETTY_PORT}/api/health | grep -q "\"status\":\"ok\""; do - echo "Waiting for the Metabase to be healthy..." - sleep 2 -done - -echo "Metabase is healthy. Running additional configuration..." - -if [ ! -f $DUMP ]; then - echo "Running import..."; - - SESSION_ID=$(curl -s -X POST \ - -H "Content-Type: application/json" \ - -d "{\"username\": \"${METABASE_ADMIN_EMAIL}\", \"password\": \"${METABASE_ADMIN_PASSWORD}\"}" \ - http://metabase:${MB_JETTY_PORT}/api/session | sed -n 's/.*"id":"\([^"]*\)".*/\1/p') - - curl -X POST \ - -H "X-Metabase-Session: $SESSION_ID" \ - -F file=@./app/metabase_data.tar.gz \ - http://metabase:${MB_JETTY_PORT}/api/ee/serialization/import -fi - -wait $SERVER_PID diff --git a/metabase/export_metabase_data.sh b/metabase/export_metabase_data.sh deleted file mode 100755 index d329110f..00000000 --- a/metabase/export_metabase_data.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -if [ -z "$SHOPPY_METABASE_INSTANCE_API_KEY" ]; then - echo "Define SHOPPY_METABASE_INSTANCE_API_KEY value in your environment" - exit 1 -fi - -curl -X POST \ - -H "x-api-key: $SHOPPY_METABASE_INSTANCE_API_KEY" \ - "https://shoppy.coredev.metabase.com/api/ee/serialization/export?data_model=false&dirname=metabase_data" \ - -o metabase_data.tar.gz - -tar -xzf metabase_data.tar.gz - -find metabase_data -type f -name "export.log" -delete - -# Find all directories ending with '_user_generated' and process them -find metabase_data/collections -type d -name "*_user_generated" | while IFS= read -r dir; do - # Recursively delete all nested directories inside the '_user_generated' directory - find "$dir" -mindepth 1 -type d -exec rm -rf {} + - - # Make sure to preserve files ending with '_user_generated.yaml' - find "$dir" -type f ! -name "*_user_generated.yaml" -exec rm -f {} + -done - -tar -czf metabase_data.tar.gz metabase_data -rm -rf metabase_data diff --git a/metabase/import_metabase_data.sh b/metabase/import_metabase_data.sh deleted file mode 100755 index 280a1d9e..00000000 --- a/metabase/import_metabase_data.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -if [ -z "$SHOPPY_METABASE_INSTANCE_API_KEY" ]; then - echo "Define SHOPPY_METABASE_INSTANCE_API_KEY value in your environment" - exit 1 -fi - -curl -X POST \ - -H "x-api-key: $SHOPPY_METABASE_INSTANCE_API_KEY" \ - -F file=@./metabase_data.tar.gz \ - "https://shoppy.coredev.metabase.com/api/ee/serialization/import" \ No newline at end of file diff --git a/metabase/metabase_data.tar.gz b/metabase/metabase_data.tar.gz deleted file mode 100644 index 512c114a..00000000 Binary files a/metabase/metabase_data.tar.gz and /dev/null differ diff --git a/package.json b/package.json index 92f7ff49..9aa39c62 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,7 @@ }, "scripts": { "docker:up": "docker compose --env-file .env.docker up", - "docker:e2e:up": "docker compose -f docker-compose.yml -f docker-compose.e2e.yml --env-file .env.docker up --build", - "docker:local-dist:up": "docker compose -f docker-compose.yml -f docker-compose.local-dist.yml --env-file .env.docker up", + "docker:e2e:up": "docker compose --env-file .env.docker up --build", "docker:down": "docker compose --env-file .env.docker down", "docker:rm": "yarn docker:down --rmi all --volumes", "install-deps": "yarn --force", diff --git a/vercel.json b/vercel.json index a27a7b9a..f2689de3 100644 --- a/vercel.json +++ b/vercel.json @@ -55,7 +55,7 @@ }, { "source": "/mb/:path*", - "destination": "https://shoppy.coredev.metabase.com/:path*" + "destination": "https://shoppy.metabaseapp.com/:path*" }, { "source": "/(.*)", diff --git a/vite.config.ts b/vite.config.ts index cab23ac6..d51d2f7a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,6 +13,17 @@ export default defineConfig(({ mode }) => { const devPort = parseInt(env.CLIENT_PORT, 10) || 3004 const previewPort = parseInt(env.CLIENT_PORT ?? env.PORT, 10) || undefined + // The client calls same-origin `/mb/*`; forward it to the api, which proxies onward to + // METABASE_INSTANCE_URL (see api/src/main.ts). Applied to both dev and preview. + const mbProxy = { + "/mb": { + target: + env.VITE_APP_DOCKER_OVERRIDE_BACKEND_URL || env.VITE_APP_BACKEND_URL, + changeOrigin: true, + secure: false, + }, + } + return { plugins: [ react(), @@ -24,19 +35,11 @@ export default defineConfig(({ mode }) => { server: { open: false, port: devPort, - proxy: { - // Ensure we have the correct backend host set for local development - "/mb": { - target: - env.VITE_APP_DOCKER_OVERRIDE_BACKEND_URL || - env.VITE_APP_BACKEND_URL, - changeOrigin: true, - secure: false, - }, - }, + proxy: mbProxy, }, preview: { port: previewPort, + proxy: mbProxy, }, } })