Skip to content
Open
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
36 changes: 36 additions & 0 deletions infra/docker/compose.dev-frontend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Dev overlay: serve a locally-built frontend via nginx instead of rebuilding the image.
#
# Usage (from infra/docker/):
# 1. Build locally:
# cd ../../src/frontend && yarn build:fast
# 2. Start with overlay:
# docker compose -f compose.yml -f compose.hobby-dotnet.yml -f compose.dev-frontend.yml up -d frontend
#
# After any code change: re-run `yarn build:fast`, then refresh the browser.
# No Docker rebuild needed — turnaround goes from ~10 min to ~60s.
#
# Note: frontend-entrypoint.py writes env var substitutions into the mounted
# build directory at container start. Re-run `yarn build:fast` to reset it.

services:
frontend:
image: nginx:stable-alpine
build: !reset null
restart: on-failure
ports:
- '0.0.0.0:3000:3000'
- '0.0.0.0:8080:8080'
volumes:
- ../../src/frontend/build:/build/frontend/build
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ../../src/backend/localhostssl/server.key:/etc/ssl/private/ssl-cert.key:ro
- ../../src/backend/localhostssl/server.pem:/etc/ssl/certs/ssl-cert.pem:ro
- ./frontend-entrypoint.py:/frontend-entrypoint.py:ro
environment:
- SSL=false
- REACT_APP_AUTH_MODE=${REACT_APP_AUTH_MODE:-Password}
- REACT_APP_FRONTEND_URI=${REACT_APP_FRONTEND_URI:-http://localhost:3000}
- REACT_APP_PRIVATE_GRAPH_URI=${REACT_APP_PRIVATE_GRAPH_URI:-http://localhost:8082/private}
- REACT_APP_PUBLIC_GRAPH_URI=${REACT_APP_PUBLIC_GRAPH_URI:-http://localhost:8082/public}
- REACT_APP_OTLP_ENDPOINT=${REACT_APP_OTLP_ENDPOINT:-http://localhost:4318}
command: ["python3", "/frontend-entrypoint.py"]
68 changes: 49 additions & 19 deletions infra/docker/frontend.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,32 +1,60 @@
FROM --platform=$BUILDPLATFORM node:lts-alpine AS pruner

RUN apk add --no-cache libc6-compat && npm install -g turbo@^2

WORKDIR /app
COPY . .
RUN turbo prune @holdfast-io/frontend --docker

# ── Dependency installation layer ────────────────────────────────────────────
# Uses the pruned package manifest (12 packages vs 87 in the full monorepo)
# to drastically reduce yarn install time.
FROM --platform=$BUILDPLATFORM node:lts-alpine AS frontend-build

RUN apk update && apk add --no-cache build-base python3

WORKDIR /highlight
WORKDIR /app

# ── Dependency installation layer ────────────────────────────────────────────
# Copy workspace manifests and full workspace dirs so `yarn install --immutable`
# resolves all workspace members. Full dirs are needed because the workspace
# globs (packages/*, tests/e2e/*, rrweb/packages/*) reference many subdirs.
COPY .yarnrc.yml package.json yarn.lock turbo.json tsconfig.json graphql.config.js ./
COPY .yarnrc.yml .
COPY .yarn/patches ./.yarn/patches
COPY .yarn/releases ./.yarn/releases
COPY packages ./packages
COPY sdk ./sdk
COPY rrweb ./rrweb
COPY tests/e2e ./tests/e2e
COPY tools/scripts/package.json ./tools/scripts/package.json
COPY src/frontend/package.json ./src/frontend/package.json

# Install only pruned workspace deps.
# rrweb/package.json (@rrweb/_monorepo) is not included by turbo prune because
# no pruned package explicitly depends on it, but its devDeps
# (esbuild-plugin-umd-wrapper, rollup-plugin-visualizer, vite-plugin-dts) are
# imported by rrweb/vite.config.default.ts which every rrweb package uses.
COPY --from=pruner /app/out/json/ .
COPY rrweb/package.json ./rrweb/package.json
# Use the full yarn.lock (not the pruned subset) so --immutable works when
# rrweb/package.json brings in deps not covered by the pruned lockfile.
COPY yarn.lock ./yarn.lock

RUN --mount=type=cache,target=/root/.yarn/berry/cache,sharing=locked \
yarn install --immutable
yarn install

# ── Source copy ───────────────────────────────────────────────────────────────
COPY --from=pruner /app/out/full/ .

# turbo prune omits rrweb root files that all rrweb/packages/*/ depend on.
# Copy them explicitly from the build context.
# tsconfig.base.json — extended by every rrweb package tsconfig.json
# tsconfig.json — rrweb composite project references root
# vite.config.default.ts — imported by every rrweb package vite.config
# turbo.json — defines the prepublish task (with ^prepublish), extends //
COPY rrweb/tsconfig.base.json ./rrweb/tsconfig.base.json
COPY rrweb/tsconfig.json ./rrweb/tsconfig.json
COPY rrweb/vite.config.default.ts ./rrweb/vite.config.default.ts
COPY rrweb/turbo.json ./rrweb/turbo.json

# Root config files not included in turbo prune full/ output
COPY tsconfig.json ./tsconfig.json
COPY graphql.config.js ./graphql.config.js

# ── Source copy ──────────────────────────────────────────────────────────────
# GraphQL schemas are outside the frontend workspace; copy them for codegen/typegen
COPY src/backend/localhostssl ./src/backend/localhostssl
COPY src/backend/private-graph ./src/backend/private-graph
COPY src/backend/public-graph ./src/backend/public-graph
COPY src/frontend ./src/frontend
COPY tools/scripts ./tools/scripts

# ── Build ────────────────────────────────────────────────────────────────────
# Bake in the same placeholder URLs the entrypoint knows to replace at runtime.
Expand All @@ -47,15 +75,17 @@ ENV REACT_APP_PUBLIC_GRAPH_URI=$REACT_APP_PUBLIC_GRAPH_URI
ENV REACT_APP_OTLP_ENDPOINT=$REACT_APP_OTLP_ENDPOINT
ENV REACT_APP_IN_DOCKER=$REACT_APP_IN_DOCKER

# build:fast skips tsc for the frontend; workspace deps still build normally
# via turbo's ^build dependency. Type checking is enforced in CI via `build`.
RUN --mount=type=cache,target=/root/.turbo,sharing=locked \
TURBO_CACHE_DIR=/root/.turbo yarn build:frontend
TURBO_CACHE_DIR=/root/.turbo npx turbo run build:fast --filter=@holdfast-io/frontend...

# ── Runtime image ─────────────────────────────────────────────────────────────
FROM nginx:stable-alpine AS frontend-prod

RUN apk update && apk add --no-cache python3

LABEL org.opencontainers.image.source=https://github.com/holdfast-io/holdfast
LABEL org.opencontainers.image.source=https://github.com/BrewingCoder/holdfast
LABEL org.opencontainers.image.description="HoldFast Frontend Image"
LABEL org.opencontainers.image.licenses="AGPL-3.0"

Expand All @@ -65,7 +95,7 @@ COPY src/backend/localhostssl/server.pem /etc/ssl/certs/ssl-cert.pem
COPY infra/docker/frontend-entrypoint.py /frontend-entrypoint.py

WORKDIR /build
COPY --from=frontend-build /highlight/src/frontend/build ./frontend/build
COPY --from=frontend-build /app/src/frontend/build ./frontend/build

# Runtime env vars — replaced in constants.js by entrypoint.py at startup
ENV REACT_APP_AUTH_MODE=firebase
Expand Down
1 change: 1 addition & 0 deletions src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"build": "yarn types:check && NODE_OPTIONS='--max-old-space-size=32768' vite build",
"build:fast": "NODE_OPTIONS='--max-old-space-size=32768' vite build",
"codegen": "graphql-codegen --config codegen.yml",
"dev": "run-p --print-label --race 'dev:**'",
"dev:vite": "vite",
Expand Down
135 changes: 81 additions & 54 deletions src/frontend/src/routers/ProjectRouter/ApplicationRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
import { useAuthContext } from '@authentication/AuthContext'
import AlertsRouter from '@pages/Alerts/AlertsRouter'
import LogAlertsRouter from '@pages/Alerts/LogAlert/LogAlertRouter'
import ErrorsV2 from '@pages/ErrorsV2/ErrorsV2'
import IntegrationsPage from '@pages/IntegrationsPage/IntegrationsPage'
import LogsPage from '@pages/LogsPage/LogsPage'
import { PlayerPage } from '@pages/Player/PlayerPage'
import { ConnectRouter } from '@/pages/Connect/ConnectRouter'
import React from 'react'
import React, { lazy, Suspense } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'

import { RelatedResourcePanel } from '@/components/RelatedResources/RelatedResourcePanel'
import { SignInRedirect } from '@/pages/Auth/SignInRedirect'
import DashboardRouter from '@/pages/Graphing/DashboardRouter'
import { SettingsRouter } from '@/pages/SettingsRouter/SettingsRouter'
import { TracesPage } from '@/pages/Traces/TracesPage'

// Lazy-load all core route components so Vite splits them into separate chunks.
// Named exports need the .then(m => ({ default: m.X })) wrapper for React.lazy.
const PlayerPage = lazy(() =>
import('@pages/Player/PlayerPage').then((m) => ({ default: m.PlayerPage })),
)
const ErrorsV2 = lazy(() => import('@pages/ErrorsV2/ErrorsV2'))
const TracesPage = lazy(() =>
import('@/pages/Traces/TracesPage').then((m) => ({ default: m.TracesPage })),
)
const LogsPage = lazy(() => import('@pages/LogsPage/LogsPage'))
const SettingsRouter = lazy(() =>
import('@/pages/SettingsRouter/SettingsRouter').then((m) => ({
default: m.SettingsRouter,
})),
)
const AlertsRouter = lazy(() => import('@pages/Alerts/AlertsRouter'))
const LogAlertsRouter = lazy(
() => import('@pages/Alerts/LogAlert/LogAlertRouter'),
)
const ConnectRouter = lazy(() =>
import('@/pages/Connect/ConnectRouter').then((m) => ({
default: m.ConnectRouter,
})),
)
const IntegrationsPage = lazy(
() => import('@pages/IntegrationsPage/IntegrationsPage'),
)
const DashboardRouter = lazy(() => import('@/pages/Graphing/DashboardRouter'))

const BASE_PATH = 'sessions'

Expand All @@ -22,51 +41,59 @@ const ApplicationRouter: React.FC = () => {

return (
<>
<Routes>
<Route
path="sessions/:session_secure_id?"
element={<PlayerPage />}
/>
<Suspense fallback={null}>
<Routes>
<Route
path="sessions/:session_secure_id?"
element={<PlayerPage />}
/>

<Route
path="errors/:error_secure_id?/:error_tab_key?/:error_object_id?"
element={<ErrorsV2 />}
/>
<Route
path="errors/:error_secure_id?/:error_tab_key?/:error_object_id?"
element={<ErrorsV2 />}
/>

{isLoggedIn ? (
<>
<Route
path="traces/:trace_id?/:span_id?"
element={<TracesPage />}
/>
<Route
path="logs/:log_cursor?"
element={<LogsPage />}
/>
<Route path="settings/*" element={<SettingsRouter />} />
<Route path="alerts/*" element={<AlertsRouter />} />
<Route
path="alerts/logs/*"
element={<LogAlertsRouter />}
/>
<Route path="connect/*" element={<ConnectRouter />} />
<Route
path="integrations/*"
element={<IntegrationsPage />}
/>
<Route
path="dashboards/*"
element={<DashboardRouter />}
/>
<Route
path="*"
element={<Navigate to={BASE_PATH} replace />}
/>
</>
) : (
<Route path="*" element={<SignInRedirect />} />
)}
</Routes>
{isLoggedIn ? (
<>
<Route
path="traces/:trace_id?/:span_id?"
element={<TracesPage />}
/>
<Route
path="logs/:log_cursor?"
element={<LogsPage />}
/>
<Route
path="settings/*"
element={<SettingsRouter />}
/>
<Route path="alerts/*" element={<AlertsRouter />} />
<Route
path="alerts/logs/*"
element={<LogAlertsRouter />}
/>
<Route
path="connect/*"
element={<ConnectRouter />}
/>
<Route
path="integrations/*"
element={<IntegrationsPage />}
/>
<Route
path="dashboards/*"
element={<DashboardRouter />}
/>
<Route
path="*"
element={<Navigate to={BASE_PATH} replace />}
/>
</>
) : (
<Route path="*" element={<SignInRedirect />} />
)}
</Routes>
</Suspense>
<RelatedResourcePanel />
</>
)
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@lottie/*": ["./lottie/*"]
}
},
"exclude": ["**/node_modules/**", "**/build/**"],
"exclude": ["**/node_modules/**", "**/build/**", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],
"references": [
{ "path": "../../sdk/highlight-run"},
{ "path": "../../rrweb/packages/rrweb" },
Expand Down
20 changes: 20 additions & 0 deletions src/frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,26 @@ export default defineConfig(({ mode }) => {
exclude: ['src/**/*.test.*', 'src/**/*.spec.*', 'src/graph/generated/**', 'src/__generated/**'],
},
},
optimizeDeps: {
include: [
'antd',
'@ant-design/icons',
'react',
'react-dom',
'react-router-dom',
'@apollo/client',
'graphql',
'recharts',
'framer-motion',
'firebase/app',
'firebase/auth',
'@codemirror/view',
'@codemirror/state',
'react-virtuoso',
'@tanstack/react-table',
'lottie-react',
],
},
css: {
transformer: 'postcss',
devSourcemap: true,
Expand Down
Loading
Loading