This commit is contained in:
mangomqy
2025-11-13 02:54:06 +00:00
commit c5e51ed069
254 changed files with 54901 additions and 0 deletions

38
frontend/src/app/App.tsx Normal file
View File

@ -0,0 +1,38 @@
/**
* Main Application Component
* 主应用组件
*/
import { useLocation, useNavigate } from "react-router-dom";
import { useMemo } from "react";
import { AppRoutes } from "./routes/AppRoutes";
import { useAuth } from "./providers";
import { getNavItems } from "./constants/navigation";
/**
* Application root component
* Manages routing and global state
*/
export default function App() {
const location = useLocation();
const navigate = useNavigate();
const { isAuthenticated, login, logout } = useAuth();
// Generate navigation items based on current location
const navItems = useMemo(
() => getNavItems(location.pathname, navigate),
[location.pathname, navigate]
);
return (
<AppRoutes
isAuthenticated={isAuthenticated}
userName="User"
navItems={navItems}
onLogin={login}
onLogout={logout}
/>
);
}

View File

@ -0,0 +1,130 @@
/**
* Navigation Configuration
* 导航配置 - 集中管理导航菜单项
*/
import { Home, Settings, Server, Database, Package, LineChart } from "lucide-react";
/**
* Navigation item type
*/
export interface NavItem {
key: string;
label: string;
icon: React.ReactNode;
active?: boolean;
onClick?: () => void;
children?: NavItem[];
}
/**
* Page info type for header display
*/
export interface PageInfo {
title: string;
icon: React.ReactNode;
}
/**
* Get navigation items
* @param currentPath - Current route path
* @param navigate - Navigation function
* @returns Navigation items array
*/
export const getNavItems = (
currentPath: string,
navigate: (path: string) => void
): NavItem[] => [
{
key: "home",
label: "Home",
icon: <Home className="w-4 h-4 text-secondary" />,
active: currentPath === "/home",
onClick: () => navigate("/home"),
},
// Configuration
{
key: "configuration",
label: "Configuration",
icon: <Settings className="w-4 h-4 text-brand-accent" />,
children: [
{
key: "configuration-clusters",
label: "Clusters",
icon: <Server className="w-4 h-4 text-accent-teal" />,
active: currentPath === "/configuration/clusters",
onClick: () => navigate("/configuration/clusters"),
},
{
key: "configuration-registries",
label: "Registries",
icon: <Database className="w-4 h-4 text-brand-light" />,
active: currentPath === "/configuration/registries",
onClick: () => navigate("/configuration/registries"),
},
],
},
// Monitoring - 监控资源状态
{
key: "monitoring",
label: "Monitoring",
icon: <LineChart className="w-4 h-4 text-accent-teal" />,
children: [
{
key: "monitoring-clusters",
label: "Clusters",
icon: <Server className="w-4 h-4 text-accent-teal" />,
active: currentPath === "/monitoring/clusters",
onClick: () => navigate("/monitoring/clusters"),
},
],
},
// Artifact - 浏览和部署制品
{
key: "artifact",
label: "Artifact",
icon: <Package className="w-4 h-4 text-brand-light" />,
children: [
{
key: "artifact-registries",
label: "Registries",
icon: <Database className="w-4 h-4 text-brand-light" />,
active: currentPath === "/artifact/registries",
onClick: () => navigate("/artifact/registries"),
},
{
key: "artifact-instances",
label: "Instances",
icon: <Package className="w-4 h-4 text-brand-accent" />,
active: currentPath === "/artifact/instances",
onClick: () => navigate("/artifact/instances"),
},
],
},
];
/**
* Get page header info based on current path
* @param pathname - Current route pathname
* @returns Page info object
*/
export const getPageInfo = (pathname: string): PageInfo => {
if (pathname === "/artifact/registries") {
return { title: "Artifact Browser", icon: <Package className="w-6 h-6 text-brand-light" /> };
}
if (pathname === "/artifact/instances") {
return { title: "Artifact - Instances", icon: <Package className="w-6 h-6 text-brand-accent" /> };
}
if (pathname === "/configuration/clusters") {
return { title: "Configuration - Clusters", icon: <Server className="w-6 h-6 text-accent-teal" /> };
}
if (pathname === "/configuration/registries") {
return { title: "Configuration - Registries", icon: <Database className="w-6 h-6 text-brand-light" /> };
}
if (pathname === "/monitoring/clusters") {
return { title: "Monitoring - Clusters", icon: <LineChart className="w-6 h-6 text-accent-teal" /> };
}
return { title: "OCDP Platform", icon: <Home className="w-6 h-6 text-secondary" /> };
};

11
frontend/src/app/index.ts Normal file
View File

@ -0,0 +1,11 @@
/**
* App Module - Unified Export
* 应用模块统一导出
*/
export { default as App } from "./App";
export * from "./providers";
export * from "./routes/RouteGuard";
export * from "./constants/navigation";

View File

@ -0,0 +1,23 @@
/**
* Auth Context
* Authentication context - separated for Fast Refresh compatibility
*/
import { createContext } from "react";
import type { AuthResponse } from "@/api";
export interface User {
username: string;
role?: string;
}
export interface AuthContextType {
token: string | null;
user: User | null;
isAuthenticated: boolean;
login: (response: AuthResponse) => void;
logout: () => void;
}
export const AuthContext = createContext<AuthContextType | undefined>(undefined);

View File

@ -0,0 +1,103 @@
/**
* Authentication Provider
* 认证提供者 - 管理用户认证状态和 token
*/
import { useState, useEffect } from "react";
import type { ReactNode } from "react";
import type { AuthResponse } from "@/api";
import { setAuthToken } from "@/api";
import { AuthContext, type User } from "./AuthContext";
interface AuthProviderProps {
children: ReactNode;
devMode?: boolean;
}
/**
* Auth Provider Component
* Manages authentication state and provides auth context
*/
export const AuthProvider = ({ children, devMode = false }: AuthProviderProps) => {
const [token, setToken] = useState<string | null>(devMode ? "dev-token" : null);
const [user, setUser] = useState<User | null>(null);
// Initialize: read token and user from localStorage
useEffect(() => {
if (devMode) {
const devUser: User = {
username: "dev-user",
role: "admin",
};
localStorage.setItem("access_token", "dev-token");
localStorage.setItem("user", JSON.stringify(devUser));
setToken("dev-token");
setUser(devUser);
return;
}
const storedToken = localStorage.getItem("access_token");
const storedUser = localStorage.getItem("user");
if (storedToken) {
setToken(storedToken);
}
if (storedUser) {
try {
setUser(JSON.parse(storedUser));
} catch (e) {
console.error("Failed to parse stored user:", e);
}
}
}, [devMode]);
// Sync token changes to axios headers
useEffect(() => {
setAuthToken(token);
}, [token]);
// Handle login (JWT format)
const login = (response: AuthResponse) => {
// JWT 格式: { accessToken, refreshToken, username, ... }
const accessToken = response.accessToken || "";
const refreshToken = response.refreshToken || "";
localStorage.setItem("access_token", accessToken);
localStorage.setItem("refresh_token", refreshToken);
const user: User = {
username: response.username || "",
role: "user", // 后端暂未返回 role默认为 user
};
localStorage.setItem("user", JSON.stringify(user));
setToken(accessToken);
setUser(user);
};
// Handle logout
const logout = () => {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
setToken(null);
setUser(null);
setAuthToken(null);
};
return (
<AuthContext.Provider
value={{
token,
user,
isAuthenticated: !!token,
login,
logout,
}}
>
{children}
</AuthContext.Provider>
);
};
// useAuth hook is now exported from ./useAuth.ts for Fast Refresh compatibility

View File

@ -0,0 +1,9 @@
/**
* Providers - Unified Export
* 提供者统一导出
*/
export { AuthProvider } from "./AuthProvider";
export { useAuth } from "./useAuth";

View File

@ -0,0 +1,19 @@
/**
* useAuth Hook
* Auth context hook - separated for Fast Refresh compatibility
*/
import { useContext } from "react";
import { AuthContext } from "./AuthContext";
/**
* Hook to use auth context
*/
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

View File

@ -0,0 +1,182 @@
/**
* Application Routes Configuration
* 应用路由配置
*/
import { Routes, Route, Navigate } from "react-router-dom";
import { ProtectedRoute } from "./RouteGuard";
import AppShell from "@/shared/components/layout/AppShell";
import { getPageInfo, type NavItem } from "../constants/navigation";
import { useLocation } from "react-router-dom";
import type { AuthResponse } from "@/api";
// Feature pages
import AuthPage from "@/features/auth/pages/AuthPage";
import HomePage from "@/features/home/pages/HomePage";
import ClusterConfigPage from "@/features/configuration/clusters/pages/ClusterConfigPage";
import RegistryConfigPage from "@/features/configuration/registries/pages/RegistryConfigPage";
import ArtifactBrowserPage from "@/features/artifact/registries/pages/ArtifactBrowserPage";
import InstancesManagementPage from "@/features/artifact/instances/pages/InstancesManagementPage";
import MonitoringClustersPage from "@/features/monitoring/clusters/pages/MonitoringClustersPage";
import { ApiTest } from "@/components/ApiTest";
interface AppRoutesProps {
isAuthenticated: boolean;
userName?: string;
navItems: NavItem[];
onLogin: (tokens: AuthResponse) => void;
onLogout: () => void;
}
/**
* Main application routes
*/
export const AppRoutes = ({
isAuthenticated,
userName = "User",
navItems,
onLogin,
onLogout,
}: AppRoutesProps) => {
const location = useLocation();
const pageInfo = getPageInfo(location.pathname);
return (
<Routes>
{/* Public route - Authentication page */}
<Route
path="/"
element={
isAuthenticated ? (
<Navigate to="/home" replace />
) : (
<AuthPage onLogin={onLogin} />
)
}
/>
{/* Protected routes - wrapped in AppShell */}
<Route
path="/home"
element={
<ProtectedRoute isAuthenticated={isAuthenticated}>
<AppShell
title={pageInfo.title}
icon={pageInfo.icon}
userName={userName}
navItems={navItems}
onSignOut={onLogout}
>
<HomePage />
</AppShell>
</ProtectedRoute>
}
/>
<Route
path="/configuration/clusters"
element={
<ProtectedRoute isAuthenticated={isAuthenticated}>
<AppShell
title={pageInfo.title}
icon={pageInfo.icon}
userName={userName}
navItems={navItems}
onSignOut={onLogout}
>
<ClusterConfigPage />
</AppShell>
</ProtectedRoute>
}
/>
<Route
path="/configuration/registries"
element={
<ProtectedRoute isAuthenticated={isAuthenticated}>
<AppShell
title={pageInfo.title}
icon={pageInfo.icon}
userName={userName}
navItems={navItems}
onSignOut={onLogout}
>
<RegistryConfigPage />
</AppShell>
</ProtectedRoute>
}
/>
<Route
path="/artifact/registries"
element={
<ProtectedRoute isAuthenticated={isAuthenticated}>
<AppShell
title={pageInfo.title}
icon={pageInfo.icon}
userName={userName}
navItems={navItems}
onSignOut={onLogout}
>
<ArtifactBrowserPage />
</AppShell>
</ProtectedRoute>
}
/>
<Route
path="/artifact/instances"
element={
<ProtectedRoute isAuthenticated={isAuthenticated}>
<AppShell
title={pageInfo.title}
icon={pageInfo.icon}
userName={userName}
navItems={navItems}
onSignOut={onLogout}
>
<InstancesManagementPage />
</AppShell>
</ProtectedRoute>
}
/>
<Route
path="/monitoring/clusters"
element={
<ProtectedRoute isAuthenticated={isAuthenticated}>
<AppShell
title={pageInfo.title}
icon={pageInfo.icon}
userName={userName}
navItems={navItems}
onSignOut={onLogout}
>
<MonitoringClustersPage />
</AppShell>
</ProtectedRoute>
}
/>
{/* API Test page - Public for testing */}
<Route path="/api-test" element={<ApiTest />} />
{/* Legacy path compatibility - redirects */}
<Route path="/config" element={<Navigate to="/configuration/clusters" replace />} />
<Route path="/config/cluster" element={<Navigate to="/configuration/clusters" replace />} />
<Route path="/config/clusters" element={<Navigate to="/configuration/clusters" replace />} />
<Route path="/config/app" element={<Navigate to="/configuration/registries" replace />} />
<Route path="/config/registry" element={<Navigate to="/configuration/registries" replace />} />
<Route path="/config/registries" element={<Navigate to="/configuration/registries" replace />} />
<Route path="/artifact/registry" element={<Navigate to="/artifact/registries" replace />} />
<Route path="/artifact/instance" element={<Navigate to="/artifact/instances" replace />} />
<Route path="/monitor" element={<Navigate to="/monitoring/clusters" replace />} />
<Route path="/cluster" element={<Navigate to="/monitoring/clusters" replace />} />
<Route path="/cluster/monitor" element={<Navigate to="/monitoring/clusters" replace />} />
<Route path="/registry" element={<Navigate to="/artifact/registries" replace />} />
<Route path="/register" element={<Navigate to="/" replace />} />
</Routes>
);
};

View File

@ -0,0 +1,39 @@
/**
* Route Guard Component
* 路由守卫组件 - 处理认证和授权
*/
import { Navigate } from "react-router-dom";
import type { ReactNode } from "react";
interface RouteGuardProps {
isAuthenticated: boolean;
redirectTo?: string;
children: ReactNode;
}
/**
* Protected route wrapper
* Redirects to auth page if not authenticated
*/
export const ProtectedRoute = ({
isAuthenticated,
redirectTo = "/",
children
}: RouteGuardProps) => {
return isAuthenticated ? <>{children}</> : <Navigate to={redirectTo} replace />;
};
/**
* Public route wrapper
* Redirects to home if already authenticated
*/
export const PublicRoute = ({
isAuthenticated,
redirectTo = "/home",
children
}: RouteGuardProps) => {
return !isAuthenticated ? <>{children}</> : <Navigate to={redirectTo} replace />;
};