Skip to content

Commit 3301313

Browse files
committed
feat: implement namespace management and UI enhancements
- Added a new API endpoint to fetch available namespaces, improving data retrieval for the dashboard. - Integrated a Select component for namespace selection in the Dashboard and RunsTable components, enhancing user experience. - Updated the global styles to adjust font size for better readability. - Refactored the Dashboard component to fetch both configuration and namespaces on mount, ensuring a smoother user experience. These changes collectively enhance the functionality and usability of the dashboard.
1 parent 1b13e7d commit 3301313

11 files changed

Lines changed: 191 additions & 16 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
const API_BASE_URL = process.env.EXOSPHERE_STATE_MANAGER_URI || 'http://localhost:8000';
4+
const API_KEY = process.env.EXOSPHERE_API_KEY;
5+
6+
export async function GET(request: NextRequest) {
7+
try {
8+
if (!API_KEY) {
9+
return NextResponse.json({ error: 'API key not configured' }, { status: 500 });
10+
}
11+
12+
const response = await fetch(`${API_BASE_URL}/v0/namespaces`, {
13+
headers: {
14+
'X-API-Key': API_KEY,
15+
'Content-Type': 'application/json',
16+
},
17+
});
18+
19+
if (!response.ok) {
20+
throw new Error(`State manager API error: ${response.status} ${response.statusText}`);
21+
}
22+
23+
const data = await response.json();
24+
return NextResponse.json(data);
25+
} catch (error) {
26+
console.error('Error fetching namespaces:', error);
27+
return NextResponse.json(
28+
{ error: 'Failed to fetch namespaces' },
29+
{ status: 500 }
30+
);
31+
}
32+
}

dashboard/src/app/globals.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@
163163
@apply border-border outline-ring/50;
164164
}
165165
html {
166-
font-size: 25px; /* Increased from default 16px */
166+
font-size: 120%; /* Increased from default 16px */
167167
}
168168
body {
169169
@apply bg-background text-foreground font-sans;

dashboard/src/app/page.tsx

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
NodeRegistration,
1212
UpsertGraphTemplateRequest,
1313
UpsertGraphTemplateResponse,
14+
ListNamespacesResponse,
1415
} from '@/types/state-manager';
1516
import {
1617
GitBranch,
@@ -23,12 +24,14 @@ import {
2324
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
2425
import { Button } from '@/components/ui/button';
2526
import { Input } from '@/components/ui/input';
27+
import { Select } from '@/components/ui/select';
2628
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
2729
import { Alert, AlertDescription } from '@/components/ui/alert';
2830

2931
export default function Dashboard() {
3032
const [activeTab, setActiveTab] = useState< 'overview' | 'graph' |'runs'>('overview');
3133
const [namespace, setNamespace] = useState('default');
34+
const [availableNamespaces, setAvailableNamespaces] = useState<string[]>([]);
3235
const [graphName, setGraphName] = useState('test-graph');
3336
const [graphTemplate, setGraphTemplate] = useState<UpsertGraphTemplateRequest | null>(null);
3437

@@ -41,21 +44,32 @@ export default function Dashboard() {
4144
const [selectedGraphTemplate, setSelectedGraphTemplate] = useState<UpsertGraphTemplateResponse | null>(null);
4245
const [isGraphModalOpen, setIsGraphModalOpen] = useState(false);
4346

44-
// Fetch configuration on component mount
47+
// Fetch configuration and namespaces on component mount
4548
useEffect(() => {
46-
const fetchConfig = async () => {
49+
const fetchConfigAndNamespaces = async () => {
4750
try {
48-
const response = await fetch('/api/config');
49-
if (response.ok) {
50-
const config = await response.json();
51+
// Fetch configuration
52+
const configResponse = await fetch('/api/config');
53+
if (configResponse.ok) {
54+
const config = await configResponse.json();
5155
setNamespace(config.defaultNamespace);
5256
}
57+
58+
// Fetch available namespaces
59+
const namespacesData = await clientApiService.getNamespaces();
60+
setAvailableNamespaces(namespacesData.namespaces || []);
61+
62+
// If no namespaces available and we have a default, add it
63+
if (namespacesData.namespaces?.length === 0) {
64+
setAvailableNamespaces(['default']);
65+
}
5366
} catch (error) {
54-
console.warn('Failed to fetch config, using default namespace');
67+
console.warn('Failed to fetch config or namespaces, using defaults');
68+
setAvailableNamespaces(['default']);
5569
}
5670
};
5771

58-
fetchConfig();
72+
fetchConfigAndNamespaces();
5973
}, []);
6074

6175
const handleSaveGraphTemplate = async (template: UpsertGraphTemplateRequest) => {
@@ -117,12 +131,23 @@ export default function Dashboard() {
117131
<div className="flex items-center space-x-4">
118132
<div className="flex items-center space-x-2">
119133
<span className="text-sm text-muted-foreground">Namespace:</span>
120-
<Input
121-
type="text"
134+
<Select
122135
value={namespace}
123136
onChange={(e) => setNamespace(e.target.value)}
124137
className="w-32 h-8"
125-
/>
138+
>
139+
{availableNamespaces.map((ns) => (
140+
<option key={ns} value={ns}>
141+
{ns}
142+
</option>
143+
))}
144+
{/* Show current namespace even if not in the list */}
145+
{!availableNamespaces.includes(namespace) && (
146+
<option key={namespace} value={namespace}>
147+
{namespace}
148+
</option>
149+
)}
150+
</Select>
126151
</div>
127152
</div>
128153
</div>

dashboard/src/components/RunsTable.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
2424
import { Button } from '@/components/ui/button';
2525
import { Badge } from '@/components/ui/badge';
2626
import { Alert, AlertDescription } from '@/components/ui/alert';
27+
import { Select } from './ui/select';
2728

2829
interface RunsTableProps {
2930
namespace: string;
@@ -193,7 +194,7 @@ export const RunsTable: React.FC<RunsTableProps> = ({
193194
>
194195
Auto-refresh:
195196
</label>
196-
<select
197+
<Select
197198
id="auto-refresh-select"
198199
value={refreshInterval}
199200
onChange={(e) => setRefreshInterval(Number(e.target.value) as RefreshMs)}
@@ -206,7 +207,7 @@ export const RunsTable: React.FC<RunsTableProps> = ({
206207
{option.label}
207208
</option>
208209
))}
209-
</select>
210+
</Select>
210211
</div>
211212

212213
<Button
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as React from "react"
2+
import { ChevronDown } from "lucide-react"
3+
import { cn } from "@/lib/utils"
4+
5+
const Select = React.forwardRef<
6+
HTMLSelectElement,
7+
React.SelectHTMLAttributes<HTMLSelectElement>
8+
>(({ className, children, ...props }, ref) => {
9+
return (
10+
<div className="relative">
11+
<select
12+
className={cn(
13+
"flex h-12 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 appearance-none",
14+
className
15+
)}
16+
ref={ref}
17+
{...props}
18+
>
19+
{children}
20+
</select>
21+
<ChevronDown className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 opacity-50 pointer-events-none" />
22+
</div>
23+
)
24+
})
25+
Select.displayName = "Select"
26+
27+
export { Select }

dashboard/src/services/clientApi.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ export class ClientApiService {
6161
}
6262
return response.json();
6363
}
64+
65+
// Namespaces
66+
async getNamespaces() {
67+
const response = await fetch('/api/namespaces');
68+
if (!response.ok) {
69+
throw new Error(`Failed to fetch namespaces: ${response.statusText}`);
70+
}
71+
return response.json();
72+
}
6473
}
6574

6675
export const clientApiService = new ClientApiService();

dashboard/src/types/state-manager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ export interface ListGraphTemplatesResponse {
138138
templates: UpsertGraphTemplateResponse[];
139139
}
140140

141+
export interface ListNamespacesResponse {
142+
namespaces: string[];
143+
count: number;
144+
}
145+
141146
export interface StateListItem {
142147
id: string;
143148
node_name: string;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
Controller for listing distinct namespaces from registered nodes
3+
"""
4+
from typing import List
5+
6+
from ..models.db.registered_node import RegisteredNode
7+
from ..singletons.logs_manager import LogsManager
8+
9+
10+
async def list_namespaces(request_id: str) -> List[str]:
11+
"""
12+
List all distinct namespaces from registered nodes
13+
14+
Args:
15+
request_id: Request ID for logging
16+
17+
Returns:
18+
List of distinct namespace strings
19+
"""
20+
logger = LogsManager().get_logger()
21+
22+
try:
23+
logger.info("Listing distinct namespaces from registered nodes", x_exosphere_request_id=request_id)
24+
25+
# Use MongoDB aggregation to get distinct namespaces
26+
pipeline = [
27+
{"$group": {"_id": "$namespace"}},
28+
{"$sort": {"_id": 1}}
29+
]
30+
31+
result = await RegisteredNode.aggregate(pipeline).to_list()
32+
namespaces = [doc["_id"] for doc in result if doc["_id"]]
33+
34+
logger.info(f"Found {len(namespaces)} distinct namespaces", x_exosphere_request_id=request_id)
35+
36+
return namespaces
37+
38+
except Exception as e:
39+
logger.error(f"Error listing namespaces: {str(e)}", x_exosphere_request_id=request_id)
40+
raise

state-manager/app/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from .models.db.run import Run
2525

2626
# injecting routes
27-
from .routes import router
27+
from .routes import router, global_router
2828

2929
# importing CORS config
3030
from .config.cors import get_cors_config
@@ -87,4 +87,5 @@ async def lifespan(app: FastAPI):
8787
def health() -> dict:
8888
return {"message": "OK"}
8989

90+
app.include_router(global_router)
9091
app.include_router(router)

state-manager/app/models/list_models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ class ListGraphTemplatesResponse(BaseModel):
2323
templates: List[GraphTemplate] = Field(..., description="List of graph templates")
2424

2525

26+
class ListNamespacesResponse(BaseModel):
27+
"""Response model for listing namespaces"""
28+
namespaces: List[str] = Field(..., description="List of namespaces")
29+
count: int = Field(..., description="Number of namespaces")
30+
31+
2632
class NamespaceSummaryResponse(BaseModel):
2733
"""Response model for namespace summary"""
2834
namespace: str = Field(..., description="The namespace")

0 commit comments

Comments
 (0)