Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Sidebar from "@/components/sidebar";
import Navbar from "@/components/navbar";
import { ProjectProvider } from "@/lib/project-context";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand Down Expand Up @@ -33,11 +34,13 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-black text-stone-300`}
suppressHydrationWarning
>
<Sidebar />
<Navbar />
<main className="ml-20 mt-20 p-4 min-h-screen transition-all duration-300">
{children}
</main>
<ProjectProvider>
<Sidebar />
<Navbar />
<main className="ml-20 mt-20 p-4 min-h-screen transition-all duration-300">
{children}
</main>
</ProjectProvider>
</body>
</html>
);
Expand Down
367 changes: 228 additions & 139 deletions dashboard/app/memories/page.tsx

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions dashboard/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import { API_BASE_URL, getHeaders } from "@/lib/api"
import { StatCard } from "@/components/dashboard/StatCard"
import { HealthMetric } from "@/components/dashboard/HealthMetric"
import { getStatusColor, sectorColors } from "@/lib/colors"
import { useProject } from "@/lib/project-context"

// Register Chart.js components
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)

export default function Dashboard() {
const { currentProject } = useProject()
const [loading, setLoading] = useState(true)
const [qpsData, setQpsData] = useState<any[]>([])
const [healthMetrics, setHealthMetrics] = useState<any>({})
Expand All @@ -42,12 +44,16 @@ export default function Dashboard() {
clearInterval(dataInterval)
clearInterval(healthInterval)
}
}, [queryLoadPeriod])
}, [queryLoadPeriod, currentProject])

const fetchDashboardData = async () => {
try {
// Apply project isolation to all dashboard requests
const projId = currentProject
const projParam = projId ? `&project_id=${projId}` : ""

// Fetch dashboard stats
const statsRes = await fetch(`${API_BASE_URL}/dashboard/stats`, {
const statsRes = await fetch(`${API_BASE_URL}/dashboard/stats?${projParam.slice(1)}`, {
headers: getHeaders()
})
if (statsRes.ok) {
Expand Down Expand Up @@ -81,7 +87,7 @@ export default function Dashboard() {
}

// Fetch activity logs
const activityRes = await fetch(`${API_BASE_URL}/dashboard/activity?limit=20`, {
const activityRes = await fetch(`${API_BASE_URL}/dashboard/activity?limit=20${projParam}`, {
headers: getHeaders()
})
if (activityRes.ok) {
Expand All @@ -101,7 +107,7 @@ export default function Dashboard() {
}

// Fetch sector timeline
const timelineRes = await fetch(`${API_BASE_URL}/dashboard/sectors/timeline?hours=${queryLoadPeriod}`, {
const timelineRes = await fetch(`${API_BASE_URL}/dashboard/sectors/timeline?hours=${queryLoadPeriod}${projParam}`, {
headers: getHeaders()
})
if (timelineRes.ok) {
Expand Down
48 changes: 35 additions & 13 deletions dashboard/components/navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client"

import { useState, useEffect } from "react"
import { useProject } from "@/lib/project-context"

const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"

export default function Navbar() {
const [backendStatus, setBackendStatus] = useState<'online' | 'offline' | 'checking'>('checking')
const { currentProject, setCurrentProject, projects } = useProject()

useEffect(() => {
checkBackendStatus()
Expand Down Expand Up @@ -36,23 +38,42 @@ export default function Navbar() {

return (
<nav className="fixed top-0 w-full p-2 pl-20 z-40">
<div className="bg-stone-950 rounded-xl p-2 flex items-center justify-between">
<div className="bg-stone-950 rounded-xl p-2 flex items-center justify-between border border-stone-900 shadow-xl">
<div className="flex items-center">
<button className="rounded-full size-9 my-1 mr-4 flex items-center justify-between hover:bg-stone-900 hover:text-stone-200 duration-300 transition-all">
<button className="rounded-full size-9 my-1 mr-4 flex items-center justify-between hover:bg-stone-900 hover:text-stone-200 duration-300 transition-all text-stone-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-6 mx-auto">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12H12m-8.25 5.25h16.5" />
</svg>
</button>
<div className="relative space-x-2 flex items-center hover:bg-stone-900 rounded-lg w-fit p-1">
<h1 className="text-lg text-stone-200 pl-1">OpenMemory</h1>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
</svg>

<div className="flex items-center gap-4">
<div className="relative space-x-2 flex items-center hover:bg-stone-900 rounded-lg w-fit p-1 px-2 cursor-pointer transition-colors group">
<h1 className="text-lg text-stone-200 font-medium">OpenMemory</h1>
<span className="text-[10px] bg-sky-500/10 text-sky-500 border border-sky-500/20 px-1.5 rounded uppercase tracking-wider font-bold">OSS</span>
</div>

<div className="h-6 w-[1px] bg-stone-800"></div>

{/* Project Selector for scoping memories and analytics */}
<div className="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-4 text-stone-500">
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" />
</svg>
<select
value={currentProject || ""}
onChange={(e) => setCurrentProject(e.target.value || null)}
className="bg-transparent text-sm text-stone-300 outline-none cursor-pointer hover:text-stone-100 transition-colors appearance-none pr-4"
>
<option value="" className="bg-stone-950">All Projects</option>
{projects.map(p => (
<option key={p} value={p} className="bg-stone-950">{p}</option>
))}
</select>
</div>
</div>
</div>

<div className="flex items-center gap-2 mr-3">
{}
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-stone-900/50 border border-stone-800">
<div className="relative flex items-center">
<div className={`w-2 h-2 rounded-full ${backendStatus === 'online' ? 'bg-green-500 animate-pulse' :
Expand All @@ -61,13 +82,14 @@ export default function Navbar() {
}`}>
</div>
</div>
<span className="text-xs text-stone-400">
{backendStatus === 'online' ? 'Backend Online' :
backendStatus === 'offline' ? 'Backend Offline' :
<span className="text-xs text-stone-400 font-medium">
{backendStatus === 'online' ? 'System Active' :
backendStatus === 'offline' ? 'Connection Lost' :
'Checking...'}
</span>
</div> <button className="rounded-xl p-2 flex justify-center hover:bg-stone-900/50 hover:text-stone-300 border border-stone-800">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-5">
</div>
<button className="rounded-xl p-2 flex justify-center hover:bg-stone-900/50 hover:text-stone-300 border border-stone-800 transition-all group">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-5 group-hover:rotate-90 transition-transform duration-300">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</button>
Expand Down
68 changes: 68 additions & 0 deletions dashboard/lib/project-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client"

import React, { createContext, useContext, useState, useEffect } from "react"

interface ProjectContextType {
currentProject: string | null
setCurrentProject: (id: string | null) => void
projects: string[]
isLoading: boolean
}

const ProjectContext = createContext<ProjectContextType | undefined>(undefined)

export function ProjectProvider({ children }: { children: React.ReactNode }) {
const [currentProject, setCurrentProjectState] = useState<string | null>(null)
const [projects, setProjects] = useState<string[]>([])
const [isLoading, setIsLoading] = useState(true)

// Load the last selected project from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem("openmemory_current_project")
if (saved && saved !== "null") {
setCurrentProjectState(saved)
}
// Fetch the list of available projects from the backend
fetchProjects()
}, [])

const setCurrentProject = (id: string | null) => {
setCurrentProjectState(id)
// Persist selection to localStorage
if (id) {
localStorage.setItem("openmemory_current_project", id)
} else {
localStorage.removeItem("openmemory_current_project")
}
}

const fetchProjects = async () => {
setIsLoading(true)
try {
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080'
const res = await fetch(`${API_BASE_URL}/dashboard/projects`)
if (res.ok) {
const data = await res.json()
setProjects(data.projects || [])
}
} catch (e) {
console.error("Failed to fetch projects:", e)
} finally {
setIsLoading(false)
}
}

return (
<ProjectContext.Provider value={{ currentProject, setCurrentProject, projects, isLoading }}>
{children}
</ProjectContext.Provider>
)
}

export function useProject() {
const context = useContext(ProjectContext)
if (context === undefined) {
throw new Error("useProject must be used within a ProjectProvider")
}
return context
}
Loading