Add React UI with Execution Explorer, auth, and standalone deployment
Some checks failed
CI / build (push) Failing after 1m53s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped

- Scaffold Vite + React + TypeScript frontend in ui/ with full design
  system (dark/light themes) matching the HTML mockups
- Implement Execution Explorer page: search filters, results table with
  expandable processor tree and exchange detail sidebar, pagination
- Add UI authentication: UiAuthController (login/refresh endpoints),
  JWT filter handles ui: subject prefix, CORS configuration
- Shared components: StatusPill, DurationBar, StatCard, AppBadge,
  FilterChip, Pagination — all using CSS Modules with design tokens
- API client layer: openapi-fetch with auth middleware, TanStack Query
  hooks for search/detail/snapshot queries, Zustand for state
- Standalone deployment: Nginx Dockerfile, K8s Deployment + ConfigMap +
  NodePort (30080), runtime config.js for API base URL
- Embedded mode: maven-resources-plugin copies ui/dist into JAR static
  resources, SPA forward controller for client-side routing
- CI/CD: UI build step, Docker build/push for server-ui image, K8s
  deploy step for UI, UI credential secrets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-13 13:59:22 +01:00
parent 9c2391e5d4
commit 3eb83f97d3
65 changed files with 6449 additions and 22 deletions

109
ui/src/auth/auth-store.ts Normal file
View File

@@ -0,0 +1,109 @@
import { create } from 'zustand';
import { config } from '../config';
interface AuthState {
accessToken: string | null;
refreshToken: string | null;
username: string | null;
isAuthenticated: boolean;
error: string | null;
loading: boolean;
login: (username: string, password: string) => Promise<void>;
refresh: () => Promise<boolean>;
logout: () => void;
}
function loadTokens() {
return {
accessToken: localStorage.getItem('cameleer-access-token'),
refreshToken: localStorage.getItem('cameleer-refresh-token'),
username: localStorage.getItem('cameleer-username'),
};
}
function persistTokens(access: string, refresh: string, username: string) {
localStorage.setItem('cameleer-access-token', access);
localStorage.setItem('cameleer-refresh-token', refresh);
localStorage.setItem('cameleer-username', username);
}
function clearTokens() {
localStorage.removeItem('cameleer-access-token');
localStorage.removeItem('cameleer-refresh-token');
localStorage.removeItem('cameleer-username');
}
const initial = loadTokens();
export const useAuthStore = create<AuthState>((set, get) => ({
accessToken: initial.accessToken,
refreshToken: initial.refreshToken,
username: initial.username,
isAuthenticated: !!initial.accessToken,
error: null,
loading: false,
login: async (username, password) => {
set({ loading: true, error: null });
try {
const res = await fetch(`${config.apiBaseUrl}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.message || 'Invalid credentials');
}
const { accessToken, refreshToken } = await res.json();
persistTokens(accessToken, refreshToken, username);
set({
accessToken,
refreshToken,
username,
isAuthenticated: true,
loading: false,
});
} catch (e: unknown) {
set({
error: e instanceof Error ? e.message : 'Login failed',
loading: false,
});
}
},
refresh: async () => {
const { refreshToken } = get();
if (!refreshToken) return false;
try {
const res = await fetch(`${config.apiBaseUrl}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!res.ok) return false;
const data = await res.json();
const username = get().username ?? '';
persistTokens(data.accessToken, data.refreshToken, username);
set({
accessToken: data.accessToken,
refreshToken: data.refreshToken,
isAuthenticated: true,
});
return true;
} catch {
return false;
}
},
logout: () => {
clearTokens();
set({
accessToken: null,
refreshToken: null,
username: null,
isAuthenticated: false,
error: null,
});
},
}));