diff --git a/surfsense_web/app/dashboard/[search_space_id]/api-key/api-key-client.tsx b/surfsense_web/app/dashboard/api-key/api-key-client.tsx
similarity index 100%
rename from surfsense_web/app/dashboard/[search_space_id]/api-key/api-key-client.tsx
rename to surfsense_web/app/dashboard/api-key/api-key-client.tsx
diff --git a/surfsense_web/app/dashboard/[search_space_id]/api-key/client-wrapper.tsx b/surfsense_web/app/dashboard/api-key/client-wrapper.tsx
similarity index 100%
rename from surfsense_web/app/dashboard/[search_space_id]/api-key/client-wrapper.tsx
rename to surfsense_web/app/dashboard/api-key/client-wrapper.tsx
diff --git a/surfsense_web/app/dashboard/[search_space_id]/api-key/page.tsx b/surfsense_web/app/dashboard/api-key/page.tsx
similarity index 100%
rename from surfsense_web/app/dashboard/[search_space_id]/api-key/page.tsx
rename to surfsense_web/app/dashboard/api-key/page.tsx
diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx
index 176c9bf..c0ca562 100644
--- a/surfsense_web/app/dashboard/page.tsx
+++ b/surfsense_web/app/dashboard/page.tsx
@@ -1,14 +1,15 @@
"use client";
-import React from 'react'
+import React, { useEffect, useState } from 'react'
import Link from 'next/link'
import { motion } from 'framer-motion'
import { Button } from '@/components/ui/button'
-import { Plus, Search, Trash2, AlertCircle, Loader2, LogOut } from 'lucide-react'
+import { Plus, Search, Trash2, AlertCircle, Loader2 } from 'lucide-react'
import { Tilt } from '@/components/ui/tilt'
import { Spotlight } from '@/components/ui/spotlight'
import { Logo } from '@/components/Logo';
import { ThemeTogglerComponent } from '@/components/theme/theme-toggle';
+import { UserDropdown } from '@/components/UserDropdown';
import { toast } from 'sonner';
import {
AlertDialog,
@@ -28,8 +29,17 @@ import {
} from "@/components/ui/alert";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { useSearchSpaces } from '@/hooks/use-search-spaces';
+import { apiClient } from '@/lib/api';
import { useRouter } from 'next/navigation';
+interface User {
+ id: string;
+ email: string;
+ is_active: boolean;
+ is_superuser: boolean;
+ is_verified: boolean;
+}
+
/**
* Formats a date string into a readable format
* @param dateString - The date string to format
@@ -147,17 +157,47 @@ const DashboardPage = () => {
const router = useRouter();
const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces();
+
+ // User state management
+ const [user, setUser] = useState(null);
+ const [isLoadingUser, setIsLoadingUser] = useState(true);
+ const [userError, setUserError] = useState(null);
+
+ // Fetch user details
+ useEffect(() => {
+ const fetchUser = async () => {
+ try {
+ if (typeof window === 'undefined') return;
+
+ try {
+ const userData = await apiClient.get('users/me');
+ setUser(userData);
+ setUserError(null);
+ } catch (error) {
+ console.error('Error fetching user:', error);
+ setUserError(error instanceof Error ? error.message : 'Unknown error occurred');
+ } finally {
+ setIsLoadingUser(false);
+ }
+ } catch (error) {
+ console.error('Error in fetchUser:', error);
+ setIsLoadingUser(false);
+ }
+ };
+
+ fetchUser();
+ }, []);
+
+ // Create user object for UserDropdown
+ const customUser = {
+ name: user?.email ? user.email.split('@')[0] : 'User',
+ email: user?.email || (isLoadingUser ? 'Loading...' : userError ? 'Error loading user' : 'Unknown User'),
+ avatar: '/icon-128.png', // Default avatar
+ };
if (loading) return ;
if (error) return ;
- const handleLogout = () => {
- if (typeof window !== 'undefined') {
- localStorage.removeItem('surfsense_bearer_token');
- router.push('/');
- }
- };
-
const handleDeleteSearchSpace = async (id: number) => {
// Send DELETE request to the API
try {
@@ -201,18 +241,10 @@ const DashboardPage = () => {
-
-
-
-
+
+
+
+
diff --git a/surfsense_web/components/UserDropdown.tsx b/surfsense_web/components/UserDropdown.tsx
new file mode 100644
index 0000000..30ac879
--- /dev/null
+++ b/surfsense_web/components/UserDropdown.tsx
@@ -0,0 +1,92 @@
+"use client"
+
+import {
+ BadgeCheck,
+ ChevronsUpDown,
+ LogOut,
+ Settings,
+} from "lucide-react"
+
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@/components/ui/avatar"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { Button } from "@/components/ui/button"
+import { useRouter, useParams } from "next/navigation"
+
+export function UserDropdown({
+ user,
+}: {
+ user: {
+ name: string
+ email: string
+ avatar: string
+ }
+}) {
+ const router = useRouter()
+
+ const handleLogout = () => {
+ if (typeof window !== 'undefined') {
+ localStorage.removeItem('surfsense_bearer_token');
+ router.push('/');
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
{user.name}
+
+ {user.email}
+
+
+
+
+
+
+ router.push(`/dashboard/api-key`)}>
+
+ API Key
+
+
+
+
+ router.push(`/settings`)}>
+
+ Settings
+
+
+
+ Log out
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx
index 9859328..0039d3b 100644
--- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx
+++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx
@@ -31,14 +31,6 @@ interface SearchSpace {
user_id: string;
}
-interface User {
- id: string;
- email: string;
- is_active: boolean;
- is_superuser: boolean;
- is_verified: boolean;
-}
-
interface AppSidebarProviderProps {
searchSpaceId: string;
navSecondary: {
@@ -58,20 +50,17 @@ interface AppSidebarProviderProps {
}[];
}
-export function AppSidebarProvider({
- searchSpaceId,
- navSecondary,
- navMain
+export function AppSidebarProvider({
+ searchSpaceId,
+ navSecondary,
+ navMain
}: AppSidebarProviderProps) {
const [recentChats, setRecentChats] = useState<{ name: string; url: string; icon: string; id: number; search_space_id: number; actions: { name: string; icon: string; onClick: () => void }[] }[]>([]);
const [searchSpace, setSearchSpace] = useState
(null);
- const [user, setUser] = useState(null);
const [isLoadingChats, setIsLoadingChats] = useState(true);
const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true);
- const [isLoadingUser, setIsLoadingUser] = useState(true);
const [chatError, setChatError] = useState(null);
const [searchSpaceError, setSearchSpaceError] = useState(null);
- const [userError, setUserError] = useState(null);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number, name: string } | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
@@ -82,33 +71,6 @@ export function AppSidebarProvider({
setIsClient(true);
}, []);
- // Fetch user details
- useEffect(() => {
- const fetchUser = async () => {
- try {
- // Only run on client-side
- if (typeof window === 'undefined') return;
-
- try {
- // Use the API client instead of direct fetch
- const userData = await apiClient.get('users/me');
- setUser(userData);
- setUserError(null);
- } catch (error) {
- console.error('Error fetching user:', error);
- setUserError(error instanceof Error ? error.message : 'Unknown error occurred');
- } finally {
- setIsLoadingUser(false);
- }
- } catch (error) {
- console.error('Error in fetchUser:', error);
- setIsLoadingUser(false);
- }
- };
-
- fetchUser();
- }, []);
-
// Fetch recent chats
useEffect(() => {
const fetchRecentChats = async () => {
@@ -119,9 +81,9 @@ export function AppSidebarProvider({
try {
// Use the API client instead of direct fetch - filter by current search space ID
const chats: Chat[] = await apiClient.get(`api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}`);
-
+
// Sort chats by created_at in descending order (newest first)
- const sortedChats = chats.sort((a, b) =>
+ const sortedChats = chats.sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
// console.log("sortedChats", sortedChats);
@@ -171,7 +133,7 @@ export function AppSidebarProvider({
// Set up a refresh interval (every 5 minutes)
const intervalId = setInterval(fetchRecentChats, 5 * 60 * 1000);
-
+
// Clean up interval on component unmount
return () => clearInterval(intervalId);
}, [searchSpaceId]);
@@ -179,16 +141,16 @@ export function AppSidebarProvider({
// Handle delete chat
const handleDeleteChat = async () => {
if (!chatToDelete) return;
-
+
try {
setIsDeleting(true);
-
+
// Use the API client instead of direct fetch
await apiClient.delete(`api/v1/chats/${chatToDelete.id}`);
-
+
// Close dialog and refresh chats
setRecentChats(recentChats.filter(chat => chat.id !== chatToDelete.id));
-
+
} catch (error) {
console.error('Error deleting chat:', error);
} finally {
@@ -226,15 +188,15 @@ export function AppSidebarProvider({
}, [searchSpaceId]);
// Create a fallback chat if there's an error or no chats
- const fallbackChats = chatError || (!isLoadingChats && recentChats.length === 0)
- ? [{
- name: chatError ? "Error loading chats" : "No recent chats",
- url: "#",
- icon: chatError ? "AlertCircle" : "MessageCircleMore",
- id: 0,
- search_space_id: Number(searchSpaceId),
- actions: []
- }]
+ const fallbackChats = chatError || (!isLoadingChats && recentChats.length === 0)
+ ? [{
+ name: chatError ? "Error loading chats" : "No recent chats",
+ url: "#",
+ icon: chatError ? "AlertCircle" : "MessageCircleMore",
+ id: 0,
+ search_space_id: Number(searchSpaceId),
+ actions: []
+ }]
: [];
// Use fallback chats if there's an error or no chats
@@ -249,22 +211,14 @@ export function AppSidebarProvider({
};
}
- // Create user object for AppSidebar
- const customUser = {
- name: isClient && user?.email ? user.email.split('@')[0] : 'User',
- email: isClient ? (user?.email || (isLoadingUser ? 'Loading...' : userError ? 'Error loading user' : 'Unknown User')) : 'Loading...',
- avatar: '/icon-128.png', // Default avatar
- };
-
return (
<>
-
+
{/* Delete Confirmation Dialog - Only render on client */}
{isClient && (