diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 63ffd85..e527104 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { useApp } from './contexts/AppContext'; -import { useToast } from './contexts/ToastContext'; import { Sidebar } from './components/Sidebar'; import { LoginPage } from './components/LoginPage'; import { createSession } from './utils/auth'; @@ -11,13 +10,16 @@ import { ServicesPage } from './pages/ServicesPage'; import { UsersPage } from './pages/UsersPage'; import { SettingsPage } from './pages/SettingsPage'; import { YamlEditorPage } from './pages/YamlEditorPage'; -import { upsertProxy, upsertService} from './services/api'; -import { Proxy, Service } from './types/proxy'; +import { Config } from './types/proxy'; +import { updateConfig, fetchConfig } from './services/api'; function App() { - const toast = useToast(); - const { config, session, setSession, fetchAndSetConfig } = useApp(); + const { config, setConfig, session, setSession } = useApp(); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); + const [lastUpdated, setLastUpdated] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [canEdit, setCanEdit] = useState(true); const handleLogin = async (username: string, password: string) => { // In a real app, validate credentials with the server @@ -25,25 +27,24 @@ function App() { setSession(newSession); }; - const handleUpdateProxy = async (proxy: Proxy)=> { - try { - await upsertProxy(proxy, session!.token); - toast.showSuccess(`Success, Add or Update Proxy ${proxy.path}`); - fetchAndSetConfig(); - } catch(e) { - toast.showError(`Error, Add or Update Proxy ${proxy.path}`); + const handleConfigUpdate = async (newConfig: Config) => { + if (!canEdit) { + setError('Cannot update configuration while data is stale. Please refresh first.'); + return; } - } - const handleUpdateService = async (serivce: Service)=> { try { - await upsertService(serivce, session!.token); - toast.showSuccess(`Success, Add or Update Service ${serivce.name}`); - fetchAndSetConfig(); - } catch(e) { - toast.showError(`Error, Add or Update Service ${serivce.name}`); + setIsLoading(true); + const updatedConfig = await updateConfig(newConfig); + setConfig(updatedConfig); + setLastUpdated(new Date()); + setError(null); + } catch (err) { + setError('Failed to update configuration'); + } finally { + setIsLoading(false); } - } + }; if (!session) { return ; @@ -59,6 +60,8 @@ function App() { ); } + console.log('session:', session); + return (
@@ -71,6 +74,9 @@ function App() { }`}>
+
+ +
} /> } /> @@ -78,8 +84,10 @@ function App() { path="/proxies" element={ + config={config} + onConfigUpdate={handleConfigUpdate} + canEdit={canEdit} + /> } /> } /> @@ -96,7 +104,7 @@ function App() { element={ {}} + onConfigUpdate={handleConfigUpdate} /> } /> @@ -106,7 +114,8 @@ function App() { element={ {}} + onConfigUpdate={handleConfigUpdate} + canEdit={canEdit} /> } /> diff --git a/frontend/src/components/ServerHealthSection.tsx b/frontend/src/components/ServerHealthSection.tsx index 7cca380..bd9b3f5 100644 --- a/frontend/src/components/ServerHealthSection.tsx +++ b/frontend/src/components/ServerHealthSection.tsx @@ -71,12 +71,12 @@ export function ServerHealthSection({ health, isLoading, error, onRefresh }: Ser

Server Health

- {/* */} +
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 41b8814..7ed1d32 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,113 +1,152 @@ +import { useState } from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { useApp } from './contexts/AppContext'; +import { Sidebar } from './components/Sidebar'; +import { LoginPage } from './components/LoginPage'; +import { createSession } from './utils/auth'; +import { DashboardPage } from './pages/DashboardPage'; +import { ProxiesPage } from './pages/ProxiesPage'; +import { ServicesPage } from './pages/ServicesPage'; +import { UsersPage } from './pages/UsersPage'; +import { SettingsPage } from './pages/SettingsPage'; +import { YamlEditorPage } from './pages/YamlEditorPage'; +import { DataFreshnessIndicator } from './components/DataFreshnessIndicator'; +import { Config } from './types/proxy'; +import { updateConfig, fetchConfig } from './services/api'; -import { NavLink } from 'react-router-dom'; -import { - LayoutDashboard, Shield, Server, Users, Settings, - FileJson, ChevronLeft, ChevronRight, LogOut -} from 'lucide-react'; -import { useApp } from '../contexts/AppContext'; -import { clearSession } from '../utils/auth'; +function App() { + const { config, setConfig, session, setSession } = useApp(); + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); + const [lastUpdated, setLastUpdated] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [canEdit, setCanEdit] = useState(true); -interface SidebarProps { - isCollapsed: boolean; - onToggle: () => void; -} - -const menuItems = [ - { id: 'dashboard', icon: LayoutDashboard, label: 'Dashboard', path: '/dashboard' }, - { id: 'proxies', icon: Shield, label: 'Proxies', path: '/proxies' }, - { id: 'services', icon: Server, label: 'Services', path: '/services' }, - { id: 'users', icon: Users, label: 'Users', path: '/users' }, - { id: 'settings', icon: Settings, label: 'Settings', path: '/settings' }, - { id: 'yaml', icon: FileJson, label: 'YAML Editor', path: '/yaml' }, -]; + const handleLogin = async (username: string, password: string) => { + // In a real app, validate credentials with the server + const newSession = createSession(username, password); + setSession(newSession); + }; -export function Sidebar({ isCollapsed, onToggle }: SidebarProps) { - const { setSession } = useApp(); + const handleConfigUpdate = async (newConfig: Config) => { + if (!canEdit) { + setError('Cannot update configuration while data is stale. Please refresh first.'); + return; + } - const handleSignOut = () => { - clearSession(); - setSession(null); - location.href = '/'; + try { + setIsLoading(true); + const updatedConfig = await updateConfig(newConfig); + setConfig(updatedConfig); + setLastUpdated(new Date()); + setError(null); + } catch (err) { + setError('Failed to update configuration'); + } finally { + setIsLoading(false); + } }; + + console.log('session::', session, config) + if (!session) { + return ; + } + + if (!config) { + return ( +
+
+
+

Loading configuration...

+
+
+ ); + } + console.log('session:', session); return ( -
-
- {import.meta.env.VITE_APP_NAME} +
+ setIsSidebarCollapsed(!isSidebarCollapsed)} /> - - {import.meta.env.VITE_APP_NAME} - -
- - - - - -
- +
+
- +
); -} \ No newline at end of file +} + +export default App; \ No newline at end of file diff --git a/frontend/src/contexts/AppContext.tsx b/frontend/src/contexts/AppContext.tsx index 8483c80..d36b29d 100644 --- a/frontend/src/contexts/AppContext.tsx +++ b/frontend/src/contexts/AppContext.tsx @@ -2,7 +2,6 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import type { Config } from '../types/proxy'; import { fetchConfig } from '../services/api'; import { getSession, Session } from '../utils/auth'; -import { clearSession } from '../utils/auth'; interface AppContextType { config: Config | null; @@ -11,7 +10,6 @@ interface AppContextType { setSession: (session: Session | null) => void; isLoading: boolean; error: string | null; - fetchAndSetConfig: () => void; } const AppContext = createContext(undefined); @@ -38,20 +36,17 @@ export function AppProvider({ children }: { children: React.ReactNode }) { }; initializeApp(); }, []); - const fetchAndSetConfig = async () => { - if (session) { - try { - let initialConfig = await fetchConfig(session.token); - setConfig(initialConfig); - } catch (err) { - setError(`${err}`); - } finally { - setIsLoading(false); - } - } - }; useEffect(() => { + const fetchAndSetConfig = async () => { + if (session) { + const initialConfig = await fetchConfig(session.token); + setTimeout(()=> { + setConfig(initialConfig); + }, 1000) + } + }; + fetchAndSetConfig(); }, [session]); @@ -61,8 +56,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) { session, setSession, isLoading, - error, - fetchAndSetConfig + error }; if (isLoading) { @@ -74,56 +68,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) { ); - } - - if (error && !config) { - return ( -
-
-
-
-
- - - -
-

Failed to Load Application

-

{error}

-
- - -
-
-
-
-
- ); } - console.log('error', error) return {children}; } diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 0a69aff..dcf26de 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -1,11 +1,12 @@ -import { useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { - Tooltip, ResponsiveContainer, - PieChart, Pie, Cell + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, + BarChart, Bar, PieChart, Pie, Cell, Legend, ComposedChart } from 'recharts'; import { - - Server, RefreshCw, + Activity, Clock, AlertTriangle, CheckCircle2, + ArrowUpRight, ArrowDownRight, Users, Shield, + Server, RefreshCw, Calendar, ChevronDown, ChevronUp } from 'lucide-react'; import type { Config } from '../types/proxy'; import type { HealthStatus } from '../services/api'; @@ -17,12 +18,81 @@ interface DashboardPageProps { config: Config; } +// Mock data generator for real-time simulation +const generatePerformanceData = () => { + const now = new Date(); + return Array.from({ length: 24 }, (_, i) => { + const time = new Date(now.getTime() - (23 - i) * 3600000); + return { + time: time.getHours().toString().padStart(2, '0') + ':00', + latency: Math.floor(Math.random() * 400) + 100, + requests: Math.floor(Math.random() * 300) + 50, + errors: Math.floor(Math.random() * 10), + successRate: Math.floor(Math.random() * 20) + 80, + }; + }); +}; + +const timeRanges = [ + { label: '24h', value: '24h' }, + { label: '7d', value: '7d' }, + { label: '30d', value: '30d' }, +]; + +const StatCard = ({ title, value, trend, icon: Icon, trendValue, loading = false }: any) => ( +
+ {loading && ( +
+ +
+ )} +
+
+

{title}

+

{value}

+
+
+ +
+
+
+ {trend === 'up' ? ( + + ) : ( + + )} + + {trendValue} + +
+
+); + +// Add new mock data generator for service performance +const generateServicePerformanceData = (services: Config['services']) => { + return services.map(service => ({ + name: service.name, + responseTime: Math.floor(Math.random() * 300) + 50, + requests: Math.floor(Math.random() * 1000) + 100, + errors: Math.floor(Math.random() * 50), + successRate: Math.floor(Math.random() * 10) + 90, + trend: Math.random() > 0.5 ? 'up' : 'down', + trendValue: Math.floor(Math.random() * 10) + 1, + })); +}; export function DashboardPage({ config }: DashboardPageProps) { - const [statusData] = useState([ + const [timeRange, setTimeRange] = useState('24h'); + const [performanceData, setPerformanceData] = useState(generatePerformanceData()); + const [loading, setLoading] = useState(false); + const [statusData, setStatusData] = useState([ { name: 'Success', value: 85, color: '#10B981' }, { name: 'Failed', value: 15, color: '#EF4444' }, ]); + const [servicePerformance, setServicePerformance] = useState(generateServicePerformanceData(config.services)); + const [sortConfig, setSortConfig] = useState({ key: 'responseTime', direction: 'asc' }); + const [expandedService, setExpandedService] = useState(null); + const [healthStatus, setHealthStatus] = useState(null); const [healthError, setHealthError] = useState(null); const [isHealthLoading, setIsHealthLoading] = useState(false); @@ -31,16 +101,28 @@ export function DashboardPage({ config }: DashboardPageProps) { // Simulate real-time updates useEffect(() => { const interval = setInterval(() => { - fetchHealth(true); + fetchHealth(); + setPerformanceData(prev => { + const newData = [...prev.slice(1), { + time: new Date().getHours().toString().padStart(2, '0') + ':00', + latency: Math.floor(Math.random() * 400) + 100, + requests: Math.floor(Math.random() * 300) + 50, + errors: Math.floor(Math.random() * 10), + successRate: Math.floor(Math.random() * 20) + 80, + }]; + return newData; + }); + fetchHealth(); }, 5000); + fetchHealth(); + - fetchHealth(); return () => clearInterval(interval); }, []); - const fetchHealth = async (silentFetching: boolean = false) => { + const fetchHealth = async () => { try { - setIsHealthLoading(silentFetching ? false: true); + setIsHealthLoading(true); setHealthError(null); const status = await fetchHealthStatus(); setHealthStatus(status); @@ -50,10 +132,54 @@ export function DashboardPage({ config }: DashboardPageProps) { setIsHealthLoading(false); } }; + + const handleTimeRangeChange = async (range: string) => { + setLoading(true); + setTimeRange(range); + // Simulate API call + setTimeout(() => { + setPerformanceData(generatePerformanceData()); + setLoading(false); + }, 1000); + }; + + const handleSort = (key: string) => { + setSortConfig(prev => ({ + key, + direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc' + })); + }; + + const sortedServicePerformance = [...servicePerformance].sort((a, b) => { + if (sortConfig.direction === 'asc') { + return a[sortConfig.key] - b[sortConfig.key]; + } + return b[sortConfig.key] - a[sortConfig.key]; + }); + return (
{/* Time Range Selector */} - +
+

Dashboard

+
+ + {timeRanges.map(range => ( + + ))} +
+
+ {/* Server Health Section */}
+
-
*/} +
+ + {/* Performance Charts */} +
+ {/* Response Time Chart */} +
+
+

Response Time

+
+ +
+ Latency + +
+
+
+ + + + + `${value}ms`} + /> + + + + +
+
+ + {/* Request Volume Chart */} +
+
+

Request Volume

+
+ +
+ Requests + + +
+ Errors + +
+
+
+ + + + + + + + + + +
+
+
+ + {/* Service Performance Table */} +
+
+

Service Performance

+
+
+ + + + + + + + + + + + {sortedServicePerformance.map((service) => ( + + setExpandedService( + expandedService === service.name ? null : service.name + )} + > + + + + + + + {expandedService === service.name && ( + + + + )} + + ))} + +
+ Service + handleSort('responseTime')} + > +
+ Response Time + {sortConfig.key === 'responseTime' && ( + sortConfig.direction === 'asc' ? + : + + )} +
+
handleSort('requests')} + > +
+ Requests + {sortConfig.key === 'requests' && ( + sortConfig.direction === 'asc' ? + : + + )} +
+
handleSort('successRate')} + > +
+ Success Rate + {sortConfig.key === 'successRate' && ( + sortConfig.direction === 'asc' ? + : + + )} +
+
+ Trend +
+
+ +
+
+ {service.name} +
+
+ {config.services.find(s => s.name === service.name)?.url} +
+
+
+
+ {service.responseTime}ms + + {service.requests} + +
+
+
+
+ {service.successRate}% +
+
+
+ {service.trend === 'up' ? ( + + ) : ( + + )} + + {service.trendValue}% + +
+
+
+ + + + + + + + + + + + +
+
+
+
+ + {/* Service Health and Status */} +
+ {/* Service Status */} +
+
+

Service Health

+ +
+
+ {config.services.map((service) => ( +
+
+ +
+

{service.name}

+

{service.url}

+
+
+
+
+

99.9%

+

Uptime

+
+ + Healthy + +
+
+ ))} +
+
+ + {/* Status Distribution */} +
+

Status Distribution

+
+ + + + {statusData.map((entry, index) => ( + + ))} + + + + +
+
+ {statusData.map((entry) => ( +
+
+
+ {entry.name} +
+ {entry.value}% +
+ ))} +
+
+
); } \ No newline at end of file diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index bd2ac72..6c7cf4c 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,4 +1,4 @@ -import type { Config, Proxy, Service } from '../types/proxy'; +import type { Config } from '../types/proxy'; const API_URL = import.meta.env.VITE_API_URL; @@ -54,40 +54,10 @@ export async function updateConfig(config: Config): Promise { return response.json(); } -export async function upsertProxy(proxy: Proxy, sessionToken: string): Promise { - const response = await fetch(`${API_URL}/admin/config/proxies`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Basic ${sessionToken}`, - }, - body: JSON.stringify(proxy), - }); - if (!response.ok) { - throw new Error('Failed to update configuration'); - } - return response.json(); -} - -export async function upsertService(service: Service, sessionToken: string): Promise { - const response = await fetch(`${API_URL}/admin/config/services`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Basic ${sessionToken}`, - }, - body: JSON.stringify(service), - }); - if (!response.ok) { - throw new Error('Failed to update configuration'); - } - return response.json(); -} - export function getApiUrl(): string { return localStorage.getItem(API_URL) || import.meta.env.VITE_API_URL; } export function setApiUrl(url: string): void { localStorage.setItem(API_URL, url); -} +} \ No newline at end of file