Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
set -eu

envsubst \
'${ANNOTATIONS_FETCH_API_URL} \
${MEDIA_ASSET_FETCH_API_URL} \
${DEMO_MEDIA_URL} \
${BASE_PATH} \
${APP_MODE}' \
< /etc/nginx/runtime-env.template.js \
> /var/www/runtime-env.js

exec "$@"
22 changes: 22 additions & 0 deletions .docker/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
worker_processes 1;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;

server {
listen 80;
server_name _;

root /var/www;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}
}
}
7 changes: 7 additions & 0 deletions .docker/runtime-env.template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
window.__RUNTIME_CONFIG__ = {
ANNOTATIONS_FETCH_API_URL: '${ANNOTATIONS_FETCH_API_URL}',
MEDIA_ASSET_FETCH_API_URL: '${MEDIA_ASSET_FETCH_API_URL}',
DEMO_MEDIA_URL: '${DEMO_MEDIA_URL}',
BASE_PATH: '${BASE_PATH}',
APP_MODE: '${APP_MODE}',
};
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
.git
.gitignore
Dockerfile
.env*
npm-debug.log
7 changes: 4 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
VITE_ANNOTATIONS_FETCH_API_URL=https://api.example.com/annotations
VITE_MEDIA_ASSET_FETCH_API_URL=https://api.example.com/media-assets
VITE_DEMO_MEDIA_URL=https://cdn.pixabay.com/video/2023/09/15/180693-864967735_large.mp4
ANNOTATOR_ANNOTATIONS_FETCH_API_URL=https://api.example.com/annotations
ANNOTATOR_MEDIA_ASSET_FETCH_API_URL=https://api.example.com/media-assets
ANNOTATOR_DEMO_MEDIA_URL=https://cdn.pixabay.com/video/2023/09/15/180693-864967735_large.mp4
ANNOTATOR_BASE_PATH=/annotator
48 changes: 48 additions & 0 deletions .github/workflows/build-and-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Build & Push Frontend

on:
push:
branches:
- main
- dev/release-test
tags:
- 'v*'

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USER }}/media-asset-annotator
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=dev,enable=${{ startsWith(github.ref, 'refs/heads/dev') }}
type=ref,event=tag

- name: Build and push image
uses: docker/build-push-action@v6
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
44 changes: 44 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
########################################
# 1. Dependencies
########################################
FROM node:20-alpine AS deps

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund


########################################
# 2. Build
########################################
FROM node:20-alpine AS build

WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build


########################################
# 3. Runtime (nginx)
########################################
FROM nginx:1.25-alpine

# Copy build output
COPY --from=build /app/dist /var/www

# Copy custom JS config
COPY .docker/runtime-env.template.js /etc/nginx/runtime-env.template.js

# Copy entrypoint
COPY .docker/entrypoint.sh /entrypoint.sh

# Copy nginx configuration
COPY .docker/nginx.conf /etc/nginx/nginx.conf

RUN chmod +x /entrypoint.sh

EXPOSE 80

ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,13 @@ npm run dev

Access the application using one of the following URLs:

- http://localhost:5173/ (default demo)
- http://localhost:5173/annotator?id=<id>
- http://localhost:5173/annotator?url=<url>
- http://localhost:5173 (default demo)
- http://localhost:5173/asset?id=<id>
- http://localhost:5173/asset?url=<url>

### Run Production Server

In order to run the application in production mode refer to the [deployment instructions](deploy/README.md).

### Build for Production

Expand All @@ -96,6 +100,6 @@ npm run preview

Access the application using one of the following URLs:

- http://localhost:4173/ (default demo)
- http://localhost:4173/annotator?id=<id>
- http://localhost:4173/annotator?url=<url>
- http://localhost:4173 (default demo)
- http://localhost:4173/asset?id=<id>
- http://localhost:4173/asset?url=<url>
36 changes: 36 additions & 0 deletions deploy/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Container names
# Name of the UI container running the Media Asset Annotator application.
# This name is used by the proxy nginx to route requests via proxy_pass.
ANNOTATOR_CONTAINER_NAME=media-asset-annotator

# Name of the proxy nginx container that exposes the application to the host.
PROXY_CONTAINER_NAME=media-asset-annotator-proxy


# External port
# Host machine port on which the application will be accessible.
# Example: http://localhost:2030/<basePath>/
ANNOTATOR_HOST_PORT=2030

# Application mode
# Controls application runtime behavior (e.g. dev, demo, prod).
APP_MODE=prod


# Base path
# Base URL path under which the application is served.
# Must start with "/" and must be accessed with a trailing slash.
# Example: http://localhost:2030/annotator/
ANNOTATOR_BASE_PATH=/annotator


# Runtime config for the app
# Default media URL used for initial redirect or demo mode
# when no media URL is explicitly provided.
ANNOTATOR_DEMO_MEDIA_URL=https://cdn.pixabay.com/video/2020/08/03/46320-447422988_large.mp4

# Backend API endpoint used to fetch annotations.
ANNOTATIONS_FETCH_API_URL=https://api.example.com/annotations

# Backend API endpoint used to fetch media assets.
MEDIA_ASSET_FETCH_API_URL=https://api.example.com/media
48 changes: 48 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Production Deployment (Docker Compose)

This document describes how to deploy **Media Asset Annotator** in **production** using **Docker Compose** with environment-based configuration.

The setup uses:

- one **UI container** (React + nginx, SPA fallback)
- one **proxy nginx container** (base-path routing via `proxy_pass`)
- runtime configuration injected via `.env`

No rebuild is required when changing base paths or API URLs.

---

## Prerequisites

- Docker 20+
- Docker Compose v2+
- Ports you plan to use are free on the host

---

## Deployment Steps

### 1️⃣ Create `.env` file

Copy the example file and adjust values for your environment:

```bash
cp .env.example .env
```

Edit .env and fill in all required variables. Descriptions are provided in the file as comments.

### 2️⃣ Start the stack

Run Docker Compose with the env file:

```bash
docker compose --env-file .env up -d
```

### 3️⃣ Access the application

Open the application in your browser:

- `http://<host>:<port>/<basePath>/`
⚠️ Trailing slash is mandatory
27 changes: 27 additions & 0 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
version: '3.9'

services:
media-asset-annotator:
build: ../
container_name: ${ANNOTATOR_CONTAINER_NAME:-media-asset-annotator}
expose:
- '80'
environment:
BASE_PATH: '${ANNOTATOR_BASE_PATH:-/annotator}'
DEMO_MEDIA_URL: '${ANNOTATOR_DEMO_MEDIA_URL:-https://cdn.pixabay.com/video/2023/09/15/180693-864967735_large.mp4}'
ANNOTATIONS_FETCH_API_URL: '${ANNOTATIONS_FETCH_API_URL:-}'
MEDIA_ASSET_FETCH_API_URL: '${MEDIA_ASSET_FETCH_API_URL:-}'

nginx:
image: nginx:alpine
container_name: ${PROXY_CONTAINER_NAME:-media-asset-annotator-proxy}
ports:
- '${ANNOTATOR_HOST_PORT:-2030}:80'
environment:
BASE_PATH: '${ANNOTATOR_BASE_PATH:-/annotator}'
ANNOTATOR_CONTAINER_NAME: '${ANNOTATOR_CONTAINER_NAME:-media-asset-annotator}'
volumes:
- ./nginx/nginx.conf.template:/etc/nginx/templates/nginx.conf.template:ro
- /dev/null:/etc/nginx/conf.d/default.conf
depends_on:
- media-asset-annotator
21 changes: 21 additions & 0 deletions deploy/nginx/nginx.conf.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
server {
listen 80;
server_name _;

# redirect /annotator -> /annotator/
location = ${BASE_PATH} {
return 302 ${BASE_PATH}/;
}

# proxy UI
location ${BASE_PATH}/ {
proxy_pass http://${ANNOTATOR_CONTAINER_NAME}/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location /health {
return 200;
}
}
5 changes: 2 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />

<!-- Favicon -->
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
<script type="module" src="/runtime-env.js"></script>
<title>Media Asset Annotator</title>
</head>
<body>
Expand Down
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
"name": "media-asset-annotator",
"private": true,
"version": "0.0.0",
"type": "module",
"keywords": [
"react",
"media",
"annotation"
],
"author": {
"name": "Yevgeniy Ulchenkov",
"email": "ulcheyev@fel.cvut.cz"
},
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
Expand Down
2 changes: 2 additions & 0 deletions public/runtime-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Replace placeholders with actual runtime configuration values
window.__RUNTIME_CONFIG__ = {};
11 changes: 3 additions & 8 deletions src/api/fetchAnnotations.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import type { AnnotationData } from '../types/extern/annotationData.ts';
import { mockAnnotations } from './mocks/annotatios';

const ANNOTATIONS_FETCH_API_URL = import.meta.env.VITE_ANNOTATIONS_FETCH_API_URL as string;
import { runtimeConfig } from '../utils/runtimeConfig.ts';

export const fetchAnnotations = async (mediaAssetId: string): Promise<AnnotationData[]> => {
if (import.meta.env.DEV) {
if (runtimeConfig.USE_MOCK_DATA) {
console.warn('[fetchAnnotations] DEV mode – returning mock data');
return mockAnnotations;
}

try {
if (!ANNOTATIONS_FETCH_API_URL) {
throw new Error('Missing VITE_ANNOTATIONS_FETCH_API_URL');
}

const response = await fetch(`${ANNOTATIONS_FETCH_API_URL}/${mediaAssetId}`, {
const response = await fetch(`${runtimeConfig.ANNOTATIONS_FETCH_API_URL}/${mediaAssetId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Expand Down
6 changes: 4 additions & 2 deletions src/api/fetchMediaAsset.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { MediaAsset } from '../types/intern/media';
import { mockVideoMediaAsset } from './mocks/mediaAsset.ts';
import { runtimeConfig } from '../utils/runtimeConfig.ts';

export const fetchMediaAsset = async (mediaAssetId: string): Promise<MediaAsset> => {
if (import.meta.env.DEV) {
if (runtimeConfig.USE_MOCK_DATA) {
console.warn('[fetchAnnotations] DEV mode – returning mock asset data');
return mockVideoMediaAsset;
}

const response = await fetch(`${import.meta.env.VITE_MEDIA_ASSET_FETCH_API_URL}/${mediaAssetId}`);
const response = await fetch(`${runtimeConfig.MEDIA_ASSET_FETCH_API_URL}/${mediaAssetId}`);

if (!response.ok) {
throw new Error('Failed to fetch media asset');
Expand Down
Loading