feat: implement Phase 2 (Go Agent) and Phase 3 (React Frontend MVP)
Phase 2 - Go Agent Core: - gRPC client with exponential backoff reconnect logic - Command executor (PowerShell/sh cross-platform) - Proto stubs regenerated with module= option (correct output path) - gRPC upgraded to v1.79.3 (BidiStreamingClient support) Phase 3 - React Frontend MVP: - Vite + React 18 + TypeScript setup with Tailwind CSS v4 - TanStack Query for data fetching, API client + TypeScript types - Dashboard page: stats cards (agents/status/tickets) + sortable agents table - Agent detail page: CPU/RAM charts (Recharts), disk usage, shell command executor - Tickets page: CRUD with modals, filters, sortable table - Dark mode with CSS custom properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
123
Frontend/src/App.tsx
Normal file
123
Frontend/src/App.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useState } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { LayoutDashboard, Ticket, Menu, X } from 'lucide-react'
|
||||
import { DashboardPage } from './pages/DashboardPage'
|
||||
import { AgentDetailPage } from './pages/AgentDetailPage'
|
||||
import TicketsPage from './pages/TicketsPage'
|
||||
import { cn } from './lib/utils'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000,
|
||||
retry: 2,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
type Page = 'dashboard' | 'agent-detail' | 'tickets'
|
||||
|
||||
interface NavItem {
|
||||
id: Page
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: <LayoutDashboard size={18} /> },
|
||||
{ id: 'tickets', label: 'Tickets', icon: <Ticket size={18} /> },
|
||||
]
|
||||
|
||||
function AppContent() {
|
||||
const [page, setPage] = useState<Page>('dashboard')
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null)
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||
|
||||
function handleSelectAgent(agentId: string) {
|
||||
setSelectedAgentId(agentId)
|
||||
setPage('agent-detail')
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
setPage('dashboard')
|
||||
setSelectedAgentId(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground flex">
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={cn(
|
||||
'flex flex-col border-r border-border bg-card transition-all duration-200',
|
||||
sidebarOpen ? 'w-56' : 'w-14',
|
||||
)}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 px-4 py-4 border-b border-border">
|
||||
<div className="w-7 h-7 rounded-md bg-primary flex items-center justify-center text-primary-foreground font-bold text-sm flex-shrink-0">
|
||||
N
|
||||
</div>
|
||||
{sidebarOpen && (
|
||||
<span className="font-semibold text-foreground truncate">NexusRMM</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="ml-auto text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{sidebarOpen ? <X size={16} /> : <Menu size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex-1 py-3 px-2 flex flex-col gap-1">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
setPage(item.id)
|
||||
setSelectedAgentId(null)
|
||||
}}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-2 py-2 rounded-md text-sm transition-colors w-full text-left',
|
||||
page === item.id || (page === 'agent-detail' && item.id === 'dashboard')
|
||||
? 'bg-primary/15 text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent',
|
||||
)}
|
||||
>
|
||||
<span className="flex-shrink-0">{item.icon}</span>
|
||||
{sidebarOpen && <span>{item.label}</span>}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Version */}
|
||||
{sidebarOpen && (
|
||||
<div className="px-4 py-3 text-xs text-muted-foreground border-t border-border">
|
||||
NexusRMM v0.1.0
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
{page === 'dashboard' && (
|
||||
<DashboardPage onSelectAgent={handleSelectAgent} />
|
||||
)}
|
||||
{page === 'agent-detail' && selectedAgentId && (
|
||||
<AgentDetailPage agentId={selectedAgentId} onBack={handleBack} />
|
||||
)}
|
||||
{page === 'tickets' && <TicketsPage />}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContent />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
Reference in New Issue
Block a user