317 lines
12 KiB
Markdown
317 lines
12 KiB
Markdown
|
|
# Phase 9: Frontend React Shell
|
||
|
|
|
||
|
|
**Date:** 2026-04-04
|
||
|
|
**Status:** Draft
|
||
|
|
**Depends on:** Phase 4 (Observability Pipeline + Inbound Routing)
|
||
|
|
**Gitea issue:** #31
|
||
|
|
|
||
|
|
## Context
|
||
|
|
|
||
|
|
Phases 1-4 built the complete backend: tenants, licensing, environments, app deployment with JAR upload, async deployment pipeline, container logs, agent status, observability status, and inbound HTTP routing. The cameleer3-server observability dashboard is already served at `/dashboard`. But there is no management UI — all operations require curl/API calls.
|
||
|
|
|
||
|
|
Phase 9 adds the SaaS management shell: a React SPA for managing tenants, environments, apps, and deployments. The observability UI is already handled by cameleer3-server — this shell covers everything else.
|
||
|
|
|
||
|
|
## Key Decisions
|
||
|
|
|
||
|
|
| Decision | Choice | Rationale |
|
||
|
|
|----------|--------|-----------|
|
||
|
|
| Location | `ui/` directory in cameleer-saas repo | Matches cameleer3-server pattern. Single build pipeline. Spring Boot serves the SPA. |
|
||
|
|
| Relationship to dashboard | Two separate SPAs, linked via navigation | SaaS shell at `/`, observability at `/dashboard`. Same design system = cohesive feel. No coupling. |
|
||
|
|
| Layout | Sidebar navigation | Consistent with cameleer3-server dashboard. Same AppShell pattern from design system. |
|
||
|
|
| Auth | Shared Logto OIDC session | Same client ID, same localStorage keys. True SSO between SaaS shell and observability dashboard. |
|
||
|
|
| Tech stack | React 19 + Vite + React Router + Zustand + TanStack Query | Identical to cameleer3-server SPA. Same patterns, same libraries, same conventions. |
|
||
|
|
| Design system | `@cameleer/design-system` v0.1.31 | Shared component library. CSS Modules + design tokens. Dark theme. |
|
||
|
|
| RBAC | Frontend role-based visibility | Roles from JWT claims. Hide/disable UI for unauthorized actions. Backend enforces — frontend is UX only. |
|
||
|
|
|
||
|
|
## Tech Stack
|
||
|
|
|
||
|
|
- **React 19** + TypeScript
|
||
|
|
- **Vite 8** (bundler + dev server)
|
||
|
|
- **React Router 7** (client-side routing)
|
||
|
|
- **Zustand** (auth state store)
|
||
|
|
- **TanStack React Query** (data fetching + caching)
|
||
|
|
- **@cameleer/design-system** (UI components)
|
||
|
|
- **Lucide React** (icons)
|
||
|
|
|
||
|
|
## Auth Flow
|
||
|
|
|
||
|
|
1. User navigates to `/` — `ProtectedRoute` checks `useAuthStore.isAuthenticated`
|
||
|
|
2. If not authenticated, redirect to Logto OIDC authorize endpoint
|
||
|
|
3. Logto callback at `/callback` — exchange code for tokens
|
||
|
|
4. Store `accessToken`, `refreshToken`, `username`, `roles` in Zustand + localStorage
|
||
|
|
5. Tokens stored with same keys as cameleer3-server SPA: `cameleer-access-token`, `cameleer-refresh-token`
|
||
|
|
6. API client injects `Authorization: Bearer {token}` on all requests
|
||
|
|
7. On 401, attempt token refresh; on failure, redirect to login
|
||
|
|
|
||
|
|
## RBAC Model
|
||
|
|
|
||
|
|
Roles from JWT or API response:
|
||
|
|
|
||
|
|
| Role | Permissions | UI Access |
|
||
|
|
|------|------------|-----------|
|
||
|
|
| **OWNER** | All | Everything + tenant settings |
|
||
|
|
| **ADMIN** | All except tenant:manage, billing:manage | Environments CRUD, apps CRUD, routing, deploy |
|
||
|
|
| **DEVELOPER** | apps:deploy, secrets:manage, observe:read, observe:debug | Deploy, stop, restart, re-upload JAR, view logs |
|
||
|
|
| **VIEWER** | observe:read | View-only: dashboard, app status, logs, deployment history |
|
||
|
|
|
||
|
|
Frontend RBAC implementation:
|
||
|
|
- `usePermissions()` hook reads roles from auth store, returns permission checks
|
||
|
|
- `<RequirePermission permission="apps:deploy">` wrapper component hides children if unauthorized
|
||
|
|
- Buttons/actions disabled with tooltip "Insufficient permissions" for unauthorized roles
|
||
|
|
- Navigation items hidden entirely if user has no access to any action on that page
|
||
|
|
|
||
|
|
## Pages
|
||
|
|
|
||
|
|
### Login (`/login`)
|
||
|
|
- Logto OIDC redirect button
|
||
|
|
- Handles callback at `/callback`
|
||
|
|
- Stores tokens, redirects to `/`
|
||
|
|
|
||
|
|
### Dashboard (`/`)
|
||
|
|
- Tenant overview: name, tier badge, license expiry
|
||
|
|
- Environment count, total app count
|
||
|
|
- Running/failed/stopped app summary (KPI strip)
|
||
|
|
- Recent deployments table (last 10)
|
||
|
|
- Quick actions: "New Environment", "View Observability Dashboard"
|
||
|
|
- **All roles** can view
|
||
|
|
|
||
|
|
### Environments (`/environments`)
|
||
|
|
- Table: name (display_name), slug, app count, status badge
|
||
|
|
- "Create Environment" button (ADMIN+ only, enforces tier limit)
|
||
|
|
- Click row → navigate to environment detail
|
||
|
|
- **All roles** can view list
|
||
|
|
|
||
|
|
### Environment Detail (`/environments/:id`)
|
||
|
|
- Environment name (editable inline for ADMIN+), slug, status
|
||
|
|
- App list table: name, slug, deployment status, agent status, last deployed
|
||
|
|
- "New App" button (DEVELOPER+ only) — opens JAR upload dialog
|
||
|
|
- "Delete Environment" button (ADMIN+ only, disabled if apps exist)
|
||
|
|
- **All roles** can view
|
||
|
|
|
||
|
|
### App Detail (`/environments/:eid/apps/:aid`)
|
||
|
|
- Header: app name, slug, environment breadcrumb
|
||
|
|
- **Status card**: current deployment status (BUILDING/STARTING/RUNNING/FAILED/STOPPED) with auto-refresh polling (3s)
|
||
|
|
- **Agent status card**: registered/not, state, route IDs, link to observability dashboard
|
||
|
|
- **JAR info**: filename, size, checksum, upload date
|
||
|
|
- **Routing card**: exposed port, route URL (clickable), edit button (ADMIN+)
|
||
|
|
- **Actions bar**:
|
||
|
|
- Deploy (DEVELOPER+) — triggers new deployment
|
||
|
|
- Stop (DEVELOPER+)
|
||
|
|
- Restart (DEVELOPER+)
|
||
|
|
- Re-upload JAR (DEVELOPER+) — file picker dialog
|
||
|
|
- Delete app (ADMIN+) — confirmation dialog
|
||
|
|
- **Deployment history**: table with version, status, timestamps, error messages
|
||
|
|
- **Container logs**: LogViewer component from design system, auto-refresh, stream filter (stdout/stderr)
|
||
|
|
- **All roles** can view status/logs/history
|
||
|
|
|
||
|
|
### License (`/license`)
|
||
|
|
- Current tier badge, features enabled/disabled, limits
|
||
|
|
- Expiry date, days remaining
|
||
|
|
- **All roles** can view
|
||
|
|
|
||
|
|
## Sidebar Navigation
|
||
|
|
|
||
|
|
```
|
||
|
|
🐪 Cameleer SaaS
|
||
|
|
─────────────────
|
||
|
|
📊 Dashboard
|
||
|
|
🌍 Environments
|
||
|
|
└ {env-name} (expandable, shows apps)
|
||
|
|
└ {app-name}
|
||
|
|
📄 License
|
||
|
|
─────────────────
|
||
|
|
👁 View Dashboard → (links to /dashboard)
|
||
|
|
─────────────────
|
||
|
|
🔒 Logged in as {name}
|
||
|
|
Logout
|
||
|
|
```
|
||
|
|
|
||
|
|
- Sidebar uses `Sidebar` + `TreeView` components from design system
|
||
|
|
- Environment → App hierarchy is collapsible
|
||
|
|
- "View Dashboard" is an external link to `/dashboard` (cameleer3-server SPA)
|
||
|
|
- Sidebar collapses on small screens (responsive)
|
||
|
|
|
||
|
|
## API Integration
|
||
|
|
|
||
|
|
The SaaS shell talks to cameleer-saas REST API. All endpoints already exist from Phases 1-4.
|
||
|
|
|
||
|
|
### API Client Setup
|
||
|
|
|
||
|
|
- Vite proxy: `/api` → `http://localhost:8080` (dev mode)
|
||
|
|
- Production: Traefik routes `/api` to cameleer-saas
|
||
|
|
- Auth middleware injects Bearer token
|
||
|
|
- Handles 401/403 with refresh + redirect
|
||
|
|
|
||
|
|
### React Query Hooks
|
||
|
|
|
||
|
|
```
|
||
|
|
useTenant() → GET /api/tenants/{id}
|
||
|
|
useLicense(tenantId) → GET /api/tenants/{tid}/license
|
||
|
|
|
||
|
|
useEnvironments(tenantId) → GET /api/tenants/{tid}/environments
|
||
|
|
useCreateEnvironment(tenantId) → POST /api/tenants/{tid}/environments
|
||
|
|
useUpdateEnvironment(tenantId, eid) → PATCH /api/tenants/{tid}/environments/{eid}
|
||
|
|
useDeleteEnvironment(tenantId, eid) → DELETE /api/tenants/{tid}/environments/{eid}
|
||
|
|
|
||
|
|
useApps(environmentId) → GET /api/environments/{eid}/apps
|
||
|
|
useCreateApp(environmentId) → POST /api/environments/{eid}/apps (multipart)
|
||
|
|
useDeleteApp(environmentId, appId) → DELETE /api/environments/{eid}/apps/{aid}
|
||
|
|
useUpdateRouting(environmentId, aid) → PATCH /api/environments/{eid}/apps/{aid}/routing
|
||
|
|
|
||
|
|
useDeploy(appId) → POST /api/apps/{aid}/deploy
|
||
|
|
useDeployments(appId) → GET /api/apps/{aid}/deployments
|
||
|
|
useDeployment(appId, did) → GET /api/apps/{aid}/deployments/{did} (poll 3s)
|
||
|
|
useStop(appId) → POST /api/apps/{aid}/stop
|
||
|
|
useRestart(appId) → POST /api/apps/{aid}/restart
|
||
|
|
|
||
|
|
useAgentStatus(appId) → GET /api/apps/{aid}/agent-status
|
||
|
|
useObservabilityStatus(appId) → GET /api/apps/{aid}/observability-status
|
||
|
|
useLogs(appId) → GET /api/apps/{aid}/logs
|
||
|
|
```
|
||
|
|
|
||
|
|
## File Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
ui/
|
||
|
|
├── index.html
|
||
|
|
├── package.json
|
||
|
|
├── vite.config.ts
|
||
|
|
├── tsconfig.json
|
||
|
|
├── src/
|
||
|
|
│ ├── main.tsx — React root + providers
|
||
|
|
│ ├── router.tsx — React Router config
|
||
|
|
│ ├── auth/
|
||
|
|
│ │ ├── auth-store.ts — Zustand store (same keys as cameleer3-server)
|
||
|
|
│ │ ├── LoginPage.tsx
|
||
|
|
│ │ ├── CallbackPage.tsx
|
||
|
|
│ │ └── ProtectedRoute.tsx
|
||
|
|
│ ├── api/
|
||
|
|
│ │ ├── client.ts — fetch wrapper with auth middleware
|
||
|
|
│ │ └── hooks.ts — React Query hooks for all endpoints
|
||
|
|
│ ├── hooks/
|
||
|
|
│ │ └── usePermissions.ts — RBAC permission checks
|
||
|
|
│ ├── components/
|
||
|
|
│ │ ├── RequirePermission.tsx — RBAC wrapper
|
||
|
|
│ │ ├── Layout.tsx — AppShell + Sidebar + Breadcrumbs
|
||
|
|
│ │ ├── EnvironmentTree.tsx — Sidebar tree (envs → apps)
|
||
|
|
│ │ └── DeploymentStatusBadge.tsx
|
||
|
|
│ ├── pages/
|
||
|
|
│ │ ├── DashboardPage.tsx
|
||
|
|
│ │ ├── EnvironmentsPage.tsx
|
||
|
|
│ │ ├── EnvironmentDetailPage.tsx
|
||
|
|
│ │ ├── AppDetailPage.tsx
|
||
|
|
│ │ └── LicensePage.tsx
|
||
|
|
│ └── types/
|
||
|
|
│ └── api.ts — TypeScript types matching backend DTOs
|
||
|
|
```
|
||
|
|
|
||
|
|
## Traefik Routing
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
cameleer-saas:
|
||
|
|
labels:
|
||
|
|
# Existing API routes:
|
||
|
|
- traefik.http.routers.api.rule=PathPrefix(`/api`)
|
||
|
|
- traefik.http.routers.forwardauth.rule=Path(`/auth/verify`)
|
||
|
|
# New SPA route:
|
||
|
|
- traefik.http.routers.spa.rule=PathPrefix(`/`)
|
||
|
|
- traefik.http.routers.spa.priority=1
|
||
|
|
- traefik.http.services.spa.loadbalancer.server.port=8080
|
||
|
|
```
|
||
|
|
|
||
|
|
Spring Boot serves the SPA from `src/main/resources/static/` (built by Vite into this directory). A catch-all controller returns `index.html` for all non-API routes (SPA client-side routing).
|
||
|
|
|
||
|
|
## Build Integration
|
||
|
|
|
||
|
|
### Vite Build → Spring Boot Static Resources
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# In ui/
|
||
|
|
npm run build
|
||
|
|
# Output: ui/dist/
|
||
|
|
|
||
|
|
# Copy to Spring Boot static resources
|
||
|
|
cp -r ui/dist/* src/main/resources/static/
|
||
|
|
```
|
||
|
|
|
||
|
|
This can be automated in the Maven build via `frontend-maven-plugin` or a simple shell script in CI.
|
||
|
|
|
||
|
|
### CI Pipeline
|
||
|
|
|
||
|
|
Add a `ui-build` step before `mvn verify`:
|
||
|
|
1. `cd ui && npm ci && npm run build`
|
||
|
|
2. Copy `ui/dist/` to `src/main/resources/static/`
|
||
|
|
3. `mvn clean verify` packages the SPA into the JAR
|
||
|
|
|
||
|
|
### Development
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Terminal 1: backend
|
||
|
|
mvn spring-boot:run
|
||
|
|
|
||
|
|
# Terminal 2: frontend (Vite dev server with API proxy)
|
||
|
|
cd ui && npm run dev
|
||
|
|
```
|
||
|
|
|
||
|
|
Vite dev server proxies `/api` to `localhost:8080`.
|
||
|
|
|
||
|
|
## SPA Catch-All Controller
|
||
|
|
|
||
|
|
Spring Boot needs a catch-all to serve `index.html` for SPA routes:
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Controller
|
||
|
|
public class SpaController {
|
||
|
|
@GetMapping(value = {"/", "/login", "/callback", "/environments/**", "/license"})
|
||
|
|
public String spa() {
|
||
|
|
return "forward:/index.html";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
This ensures React Router handles client-side routing. API routes (`/api/**`) are not caught — they go to the existing REST controllers.
|
||
|
|
|
||
|
|
## Design System Integration
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"dependencies": {
|
||
|
|
"@cameleer/design-system": "0.1.31"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Registry configuration in `.npmrc`:
|
||
|
|
```
|
||
|
|
@cameleer:registry=https://gitea.siegeln.net/api/packages/cameleer/npm/
|
||
|
|
```
|
||
|
|
|
||
|
|
Import in `main.tsx`:
|
||
|
|
```tsx
|
||
|
|
import '@cameleer/design-system/style.css';
|
||
|
|
import { ThemeProvider, ToastProvider, BreadcrumbProvider } from '@cameleer/design-system';
|
||
|
|
```
|
||
|
|
|
||
|
|
## Verification Plan
|
||
|
|
|
||
|
|
1. `npm run dev` starts Vite dev server, SPA loads at localhost:5173
|
||
|
|
2. Login redirects to Logto, callback stores tokens
|
||
|
|
3. Dashboard shows tenant overview with correct data from API
|
||
|
|
4. Environment list loads, create/rename/delete works (ADMIN+)
|
||
|
|
5. App upload (JAR + metadata) works, app appears in list
|
||
|
|
6. Deploy triggers async deployment, status polls and updates live
|
||
|
|
7. Agent status shows registered/connected
|
||
|
|
8. Container logs stream in LogViewer
|
||
|
|
9. "View Dashboard" link navigates to `/dashboard` (cameleer3-server SPA)
|
||
|
|
10. Shared auth: no re-login when switching between SPAs
|
||
|
|
11. RBAC: VIEWER cannot see deploy button, DEVELOPER cannot delete environments
|
||
|
|
12. Production build: `npm run build` + `mvn package` produces JAR with embedded SPA
|
||
|
|
|
||
|
|
## What Phase 9 Does NOT Touch
|
||
|
|
|
||
|
|
- No changes to cameleer3-server or its SPA
|
||
|
|
- No billing UI (Phase 6)
|
||
|
|
- No team management (Logto org admin — deferred)
|
||
|
|
- No tenant settings/profile page
|
||
|
|
- No super-admin multi-tenant view
|