Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 45
feat: implement light mode and theme toggle functionality#406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
11f37bfb77107174cd2d4fa425e471df4d345c8943c96e767e7ad975942960dFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 }); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+12
to
+14
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate namespace/stateId to prevent path abuse and tighten input contract Currently any string (including slashes or path traversal sequences) is accepted and interpolated into the downstream path. Constrain to a safe charset and reasonable length. Apply this diff: - if (!namespace || !stateId) {- return NextResponse.json({ error: 'Namespace and stateId are required' }, { status: 400 });- }+ if (!namespace || !stateId) {+ return NextResponse.json({ error: 'Namespace and stateId are required' }, { status: 400 });+ }+ const safeId = /^[A-Za-z0-9._-]{1,128}$/;+ if (!safeId.test(namespace) || !safeId.test(stateId)) {+ return NextResponse.json({ error: 'Invalid namespace or stateId format' }, { status: 400 });+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||||||||
| 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 }); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+22
to
+24
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The validation for the request body is not fully robust.
Suggested change
nk-ag marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page.
Comment on lines
+22
to
+24
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Validate fanout_id as UUID v4 Downstream expects a UUID; enforce format early. - if (!body.fanout_id) {+ const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;+ if (!body.fanout_id || !uuidV4.test(body.fanout_id)) {
return NextResponse.json({ error: 'fanout_id is required in request body' }, { status: 400 });
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||||||||
| 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), | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+26
to
+33
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a request timeout to the downstream fetch External call has no timeout; a hung downstream will tie up server resources. - const response = await fetch(`${API_BASE_URL}/v0/namespace/${namespace}/state/${stateId}/manual-retry`, {+ const controller = new AbortController();+ const timeoutId = setTimeout(() => controller.abort(), 10_000);+ 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),+ body: JSON.stringify(body),+ signal: controller.signal,
});
+ clearTimeout(timeoutId);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||
nk-ag marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
| console.error('Error retrying state:', error); | ||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||
| { error: 'Failed to retry state' }, | ||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } | ||
nk-ag marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| @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; | ||
| } | ||
nk-ag marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| select option { | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<UpsertGraphTemplateResponse | null>(null); | ||||||||||||||||||
| const [isLoadingTemplate, setIsLoadingTemplate] = useState(false); | ||||||||||||||||||
| const [templateError, setTemplateError] = useState<string | null>(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); | ||||||||||||||||||
Comment on lines
+47
to
+49
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mutating the
Suggested change
| ||||||||||||||||||
| } 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 ( | ||||||||||||||||||
| <div className="min-h-screen bg-background flex items-center justify-center"> | ||||||||||||||||||
| <div className="text-center"> | ||||||||||||||||||
| <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div> | ||||||||||||||||||
| <p className="text-muted-foreground">Loading...</p> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| return ( | ||||||||||||||||||
| <div className="min-h-screen bg-background"> | ||||||||||||||||||
| {/* Header */} | ||||||||||||||||||
| <header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-10"> | ||||||||||||||||||
| <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | ||||||||||||||||||
| <div className="flex items-center justify-between h-16"> | ||||||||||||||||||
| <div className="flex items-center space-x-4"> | ||||||||||||||||||
| <Button | ||||||||||||||||||
| onClick={handleBack} | ||||||||||||||||||
| variant="ghost" | ||||||||||||||||||
| size="sm" | ||||||||||||||||||
| className="flex items-center space-x-2" | ||||||||||||||||||
| > | ||||||||||||||||||
| <ArrowLeft className="w-4 h-4" /> | ||||||||||||||||||
| <span>Back</span> | ||||||||||||||||||
| </Button> | ||||||||||||||||||
| <div className="h-6 w-px bg-border" /> | ||||||||||||||||||
| <div> | ||||||||||||||||||
| <h1 className="text-xl font-semibold text-foreground"> | ||||||||||||||||||
| Graph Visualization | ||||||||||||||||||
| </h1> | ||||||||||||||||||
| <p className="text-sm text-muted-foreground"> | ||||||||||||||||||
| Namespace: {namespace} | Run: {runId} | ||||||||||||||||||
| </p> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| <ThemeToggle /> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| </header> | ||||||||||||||||||
| {/* Main Content */} | ||||||||||||||||||
| <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | ||||||||||||||||||
| <GraphVisualization | ||||||||||||||||||
| namespace={namespace} | ||||||||||||||||||
| runId={runId} | ||||||||||||||||||
| onGraphTemplateRequest={handleOpenGraphTemplate} | ||||||||||||||||||
| /> | ||||||||||||||||||
| </main> | ||||||||||||||||||
| {/* Graph Template Detail Modal - Inline at bottom */} | ||||||||||||||||||
| <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-8"> | ||||||||||||||||||
| {templateError && ( | ||||||||||||||||||
| <div className="mb-4 p-4 bg-destructive/10 border border-destructive/20 rounded-lg"> | ||||||||||||||||||
| <p className="text-sm text-destructive">{templateError}</p> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| )} | ||||||||||||||||||
| {isLoadingTemplate && ( | ||||||||||||||||||
| <div className="mb-4 p-4 bg-muted rounded-lg"> | ||||||||||||||||||
| <div className="flex items-center"> | ||||||||||||||||||
| <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary mr-2"></div> | ||||||||||||||||||
| <p className="text-sm text-muted-foreground">Loading graph template...</p> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| )} | ||||||||||||||||||
| <GraphTemplateDetail | ||||||||||||||||||
| graphTemplate={graphTemplate} | ||||||||||||||||||
| isOpen={!!graphTemplate} | ||||||||||||||||||
| onClose={handleCloseGraphTemplate} | ||||||||||||||||||
| /> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||
| import type { Metadata } from "next"; | ||||||
| import { Geist, Geist_Mono } from "next/font/google"; | ||||||
| import { ThemeProvider } from "@/contexts/ThemeContext"; | ||||||
nk-ag marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||||||
| import "./globals.css"; | ||||||
| const geistSans = Geist({ | ||||||
| @@ -24,10 +25,38 @@ export default function RootLayout({ | ||||||
| }>) { | ||||||
| return ( | ||||||
| <html lang="en"> | ||||||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Silence harmless class hydration diff on . Add - <html lang="en">+ <html lang="en" suppressHydrationWarning>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||
| <head> | ||||||
| <script | ||||||
| dangerouslySetInnerHTML={{ | ||||||
| __html: ` | ||||||
| (function() { | ||||||
| try { | ||||||
| // Ensure we don't add duplicate classes | ||||||
| document.documentElement.classList.remove('light', 'dark'); | ||||||
| var theme = localStorage.getItem('theme'); | ||||||
| if (theme && (theme === 'light' || theme === 'dark')) { | ||||||
| document.documentElement.classList.add(theme); | ||||||
| } else { | ||||||
| var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | ||||||
| document.documentElement.classList.add(prefersDark ? 'dark' : 'light'); | ||||||
| } | ||||||
| } catch (e) { | ||||||
| // Fallback to dark theme if there's any error | ||||||
| document.documentElement.classList.remove('light', 'dark'); | ||||||
| document.documentElement.classList.add('dark'); | ||||||
| } | ||||||
| })(); | ||||||
| `, | ||||||
| }} | ||||||
| /> | ||||||
| </head> | ||||||
| <body | ||||||
| className={`${geistSans.variable} ${geistMono.variable} antialiased`} | ||||||
| > | ||||||
| {children} | ||||||
| <ThemeProvider> | ||||||
| {children} | ||||||
| </ThemeProvider> | ||||||
| </body> | ||||||
| </html> | ||||||
| ); | ||||||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enforce HTTPS for API_BASE_URL in production
Reduce misconfig/SSRF risk by requiring https when not running locally.
📝 Committable suggestion
🤖 Prompt for AI Agents