diff --git a/dashboard/public/exospheresmall-dark.png b/dashboard/public/exospheresmall-dark.png new file mode 100644 index 00000000..c7cb28ab Binary files /dev/null and b/dashboard/public/exospheresmall-dark.png differ diff --git a/dashboard/src/app/api/manual-retry-state/route.ts b/dashboard/src/app/api/manual-retry-state/route.ts new file mode 100644 index 00000000..7ab60d67 --- /dev/null +++ b/dashboard/src/app/api/manual-retry-state/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const API_BASE_URL = process.env.EXOSPHERE_STATE_MANAGER_URI || 'http://localhost:8000'; +const API_KEY = process.env.EXOSPHERE_API_KEY; + +export async function POST(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const namespace = searchParams.get('namespace'); + const stateId = searchParams.get('stateId'); + + if (!namespace || !stateId) { + return NextResponse.json({ error: 'Namespace and stateId are required' }, { status: 400 }); + } + + if (!API_KEY) { + return NextResponse.json({ error: 'API key not configured' }, { status: 500 }); + } + + const body = await request.json(); + + if (!body.fanout_id) { + return NextResponse.json({ error: 'fanout_id is required in request body' }, { status: 400 }); + } + + const response = await fetch(`${API_BASE_URL}/v0/namespace/${namespace}/state/${stateId}/manual-retry`, { + method: 'POST', + headers: { + 'X-API-Key': API_KEY, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`State manager API error: ${response.status} ${response.statusText} - ${errorText}`); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error retrying state:', error); + return NextResponse.json( + { error: 'Failed to retry state' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/dashboard/src/app/globals.css b/dashboard/src/app/globals.css index 353c9d91..5ab90515 100644 --- a/dashboard/src/app/globals.css +++ b/dashboard/src/app/globals.css @@ -38,8 +38,8 @@ --input: #1a2a5a; /* Navy input background */ --ring: #87ceeb; /* Sky blue focus ring */ --chart-1: #87ceeb; /* Sky blue */ - --chart-2: #4ade80; /* Green accent */ - --chart-3: #fbbf24; /* Yellow accent */ + --chart-2: #66d1b5; /* Green accent */ + --chart-3: #ffed9e; /* Yellow accent */ --chart-4: #ff6b8a; /* Pink accent */ --chart-5: #a78bfa; /* Purple accent */ --sidebar: #0a1a4a; @@ -52,6 +52,44 @@ --sidebar-ring: #87ceeb; } +/* Light Mode Variables */ +.light { + --background: #ffffff; /* White background */ + --foreground: #031035; /* Dark navy text */ + --card: #f2f7fb; /* Light card background */ + --card-foreground: #031035; + --popover: #ffffff; + --popover-foreground: #031035; + --primary: #031035; /* Dark navy primary (keeping dark blue accent) */ + --primary-foreground: #ffffff; + --secondary: #f1f5f9; /* Light gray secondary */ + --secondary-foreground: #031035; + --muted: #f1f5f9; /* Light muted background */ + --muted-foreground: #64748b; /* Medium gray text */ + --accent: #031035; /* Keep dark blue accent */ + --accent-light: #0a1a4a; /* Keep dark blue accent */ + --accent-lighter: #1a2a5a; /* Keep dark blue accent */ + --accent-lightest: #2a3a6a; /* Keep dark blue accent */ + --accent-foreground: #ffffff; + --destructive: #dc2626; /* Red for errors in light mode */ + --border: #e2e8f0; /* Light border */ + --input: #ffffff; /* White input background */ + --ring: #87ceeb; /* Keep sky blue focus ring */ + --chart-1: #87ceeb; /* Sky blue */ + --chart-2: #4ade80; /* Green accent */ + --chart-3: #cca301; /* Yellow accent */ + --chart-4: #ff6b8a; /* Pink accent */ + --chart-5: #a78bfa; /* Purple accent */ + --sidebar: #f8fafc; + --sidebar-foreground: #031035; + --sidebar-primary: #031035; /* Dark navy for sidebar primary in light mode */ + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #f1f5f9; + --sidebar-accent-foreground: #031035; + --sidebar-border: #e2e8f0; + --sidebar-ring: #87ceeb; +} + /* Custom Scrollbar Styling */ ::-webkit-scrollbar { width: 8px; @@ -113,7 +151,18 @@ } } - +/* Light mode react-flow nodes */ +.light .react-flow__node{ + background-color: var(--card); + color: var(--card-foreground); + border: 1px solid var(--border); + &:hover{ + background-color: var(--muted); + } + &:active{ + background-color: var(--muted); + } +} @theme inline { --color-background: var(--background); @@ -169,9 +218,13 @@ @apply bg-background text-foreground font-sans; } - /* Custom select dropdown styling for better dark theme support */ + /* Custom select dropdown styling for better theme support */ select { - color-scheme: dark; + color-scheme: light dark; + } + + .light select { + color-scheme: light; } select option { diff --git a/dashboard/src/app/graph/[namespace]/[runId]/page.tsx b/dashboard/src/app/graph/[namespace]/[runId]/page.tsx new file mode 100644 index 00000000..200869f4 --- /dev/null +++ b/dashboard/src/app/graph/[namespace]/[runId]/page.tsx @@ -0,0 +1,138 @@ +'use client'; + +import React, { useState, useCallback } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { GraphVisualization } from '@/components/GraphVisualization'; +import { GraphTemplateDetail } from '@/components/GraphTemplateDetail'; +import { ThemeToggle } from '@/components/ThemeToggle'; +import { Button } from '@/components/ui/button'; +import { ArrowLeft } from 'lucide-react'; +import { clientApiService } from '@/services/clientApi'; +import { UpsertGraphTemplateResponse } from '@/types/state-manager'; + +export default function GraphPage() { + const router = useRouter(); + const params = useParams(); + + const namespace = params?.namespace as string; + const runId = params?.runId as string; + + // Graph template state + const [graphTemplate, setGraphTemplate] = useState(null); + const [isLoadingTemplate, setIsLoadingTemplate] = useState(false); + const [templateError, setTemplateError] = useState(null); + + const handleBack = () => { + // Go back to the previous page or close the tab if opened from external link + if (typeof window !== 'undefined') { + if (window.history.length > 1) { + router.back(); + } else { + window.close(); + } + } else { + // Fallback for SSR + router.back(); + } + }; + + const handleOpenGraphTemplate = useCallback(async (graphName: string) => { + if (!graphName || !namespace) return; + + try { + setIsLoadingTemplate(true); + setTemplateError(null); + const template = await clientApiService.getGraphTemplate(namespace, graphName); + // Add name and namespace to the template + template.name = graphName; + template.namespace = namespace; + setGraphTemplate(template); + } catch (err) { + setTemplateError(err instanceof Error ? err.message : 'Failed to load graph template'); + } finally { + setIsLoadingTemplate(false); + } + }, [namespace]); + + const handleCloseGraphTemplate = () => { + setGraphTemplate(null); + setTemplateError(null); + }; + + if (!namespace || !runId) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+
+ +
+
+

+ Graph Visualization +

+

+ Namespace: {namespace} | Run: {runId} +

+
+
+ +
+
+
+ + {/* Main Content */} +
+ +
+ + {/* Graph Template Detail Modal - Inline at bottom */} +
+ {templateError && ( +
+

{templateError}

+
+ )} + + {isLoadingTemplate && ( +
+
+
+

Loading graph template...

+
+
+ )} + + +
+
+ ); +} \ No newline at end of file diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx index 76934dfe..9d9c20e8 100644 --- a/dashboard/src/app/layout.tsx +++ b/dashboard/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { ThemeProvider } from "@/contexts/ThemeContext"; import "./globals.css"; const geistSans = Geist({ @@ -24,10 +25,38 @@ export default function RootLayout({ }>) { return ( + +