mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-02 02:29:08 +00:00
Merge pull request #195 from MODSetter/dev
feat(FRONTEND): Moved User Dropdown to Search Space Dashboard
This commit is contained in:
commit
5cc8373205
7 changed files with 169 additions and 98 deletions
|
@ -1,14 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { Button } from '@/components/ui/button'
|
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 { Tilt } from '@/components/ui/tilt'
|
||||||
import { Spotlight } from '@/components/ui/spotlight'
|
import { Spotlight } from '@/components/ui/spotlight'
|
||||||
import { Logo } from '@/components/Logo';
|
import { Logo } from '@/components/Logo';
|
||||||
import { ThemeTogglerComponent } from '@/components/theme/theme-toggle';
|
import { ThemeTogglerComponent } from '@/components/theme/theme-toggle';
|
||||||
|
import { UserDropdown } from '@/components/UserDropdown';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
|
@ -28,8 +29,17 @@ import {
|
||||||
} from "@/components/ui/alert";
|
} from "@/components/ui/alert";
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { useSearchSpaces } from '@/hooks/use-search-spaces';
|
import { useSearchSpaces } from '@/hooks/use-search-spaces';
|
||||||
|
import { apiClient } from '@/lib/api';
|
||||||
import { useRouter } from 'next/navigation';
|
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
|
* Formats a date string into a readable format
|
||||||
* @param dateString - The date string to format
|
* @param dateString - The date string to format
|
||||||
|
@ -147,17 +157,47 @@ const DashboardPage = () => {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces();
|
const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces();
|
||||||
|
|
||||||
|
// User state management
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [isLoadingUser, setIsLoadingUser] = useState(true);
|
||||||
|
const [userError, setUserError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Fetch user details
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userData = await apiClient.get<User>('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 <LoadingScreen />;
|
if (loading) return <LoadingScreen />;
|
||||||
if (error) return <ErrorScreen message={error} />;
|
if (error) return <ErrorScreen message={error} />;
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.removeItem('surfsense_bearer_token');
|
|
||||||
router.push('/');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteSearchSpace = async (id: number) => {
|
const handleDeleteSearchSpace = async (id: number) => {
|
||||||
// Send DELETE request to the API
|
// Send DELETE request to the API
|
||||||
try {
|
try {
|
||||||
|
@ -201,18 +241,10 @@ const DashboardPage = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Button
|
<UserDropdown user={customUser} />
|
||||||
variant="ghost"
|
<ThemeTogglerComponent />
|
||||||
size="icon"
|
</div>
|
||||||
onClick={handleLogout}
|
|
||||||
className="h-9 w-9 rounded-full"
|
|
||||||
aria-label="Logout"
|
|
||||||
>
|
|
||||||
<LogOut className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<ThemeTogglerComponent />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-6 mt-6">
|
<div className="flex flex-col space-y-6 mt-6">
|
||||||
|
|
92
surfsense_web/components/UserDropdown.tsx
Normal file
92
surfsense_web/components/UserDropdown.tsx
Normal file
|
@ -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 (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="relative h-10 w-10 rounded-full"
|
||||||
|
>
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<AvatarImage src={user.avatar} alt={user.name} />
|
||||||
|
<AvatarFallback>{user.name.charAt(0).toUpperCase()}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-56"
|
||||||
|
align="end"
|
||||||
|
forceMount
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="font-normal">
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-sm font-medium leading-none">{user.name}</p>
|
||||||
|
<p className="text-xs leading-none text-muted-foreground">
|
||||||
|
{user.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
|
||||||
|
<DropdownMenuItem onClick={() => router.push(`/dashboard/api-key`)}>
|
||||||
|
<BadgeCheck className="mr-2 h-4 w-4" />
|
||||||
|
API Key
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => router.push(`/settings`)}>
|
||||||
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
|
Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleLogout}>
|
||||||
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
|
Log out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
|
@ -31,14 +31,6 @@ interface SearchSpace {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
is_active: boolean;
|
|
||||||
is_superuser: boolean;
|
|
||||||
is_verified: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AppSidebarProviderProps {
|
interface AppSidebarProviderProps {
|
||||||
searchSpaceId: string;
|
searchSpaceId: string;
|
||||||
navSecondary: {
|
navSecondary: {
|
||||||
|
@ -58,20 +50,17 @@ interface AppSidebarProviderProps {
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppSidebarProvider({
|
export function AppSidebarProvider({
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
navSecondary,
|
navSecondary,
|
||||||
navMain
|
navMain
|
||||||
}: AppSidebarProviderProps) {
|
}: 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 [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<SearchSpace | null>(null);
|
const [searchSpace, setSearchSpace] = useState<SearchSpace | null>(null);
|
||||||
const [user, setUser] = useState<User | null>(null);
|
|
||||||
const [isLoadingChats, setIsLoadingChats] = useState(true);
|
const [isLoadingChats, setIsLoadingChats] = useState(true);
|
||||||
const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true);
|
const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true);
|
||||||
const [isLoadingUser, setIsLoadingUser] = useState(true);
|
|
||||||
const [chatError, setChatError] = useState<string | null>(null);
|
const [chatError, setChatError] = useState<string | null>(null);
|
||||||
const [searchSpaceError, setSearchSpaceError] = useState<string | null>(null);
|
const [searchSpaceError, setSearchSpaceError] = useState<string | null>(null);
|
||||||
const [userError, setUserError] = useState<string | null>(null);
|
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const [chatToDelete, setChatToDelete] = useState<{ id: number, name: string } | null>(null);
|
const [chatToDelete, setChatToDelete] = useState<{ id: number, name: string } | null>(null);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
@ -82,33 +71,6 @@ export function AppSidebarProvider({
|
||||||
setIsClient(true);
|
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<User>('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
|
// Fetch recent chats
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRecentChats = async () => {
|
const fetchRecentChats = async () => {
|
||||||
|
@ -119,9 +81,9 @@ export function AppSidebarProvider({
|
||||||
try {
|
try {
|
||||||
// Use the API client instead of direct fetch - filter by current search space ID
|
// Use the API client instead of direct fetch - filter by current search space ID
|
||||||
const chats: Chat[] = await apiClient.get<Chat[]>(`api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}`);
|
const chats: Chat[] = await apiClient.get<Chat[]>(`api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}`);
|
||||||
|
|
||||||
// Sort chats by created_at in descending order (newest first)
|
// 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()
|
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||||
);
|
);
|
||||||
// console.log("sortedChats", sortedChats);
|
// console.log("sortedChats", sortedChats);
|
||||||
|
@ -171,7 +133,7 @@ export function AppSidebarProvider({
|
||||||
|
|
||||||
// Set up a refresh interval (every 5 minutes)
|
// Set up a refresh interval (every 5 minutes)
|
||||||
const intervalId = setInterval(fetchRecentChats, 5 * 60 * 1000);
|
const intervalId = setInterval(fetchRecentChats, 5 * 60 * 1000);
|
||||||
|
|
||||||
// Clean up interval on component unmount
|
// Clean up interval on component unmount
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [searchSpaceId]);
|
}, [searchSpaceId]);
|
||||||
|
@ -179,16 +141,16 @@ export function AppSidebarProvider({
|
||||||
// Handle delete chat
|
// Handle delete chat
|
||||||
const handleDeleteChat = async () => {
|
const handleDeleteChat = async () => {
|
||||||
if (!chatToDelete) return;
|
if (!chatToDelete) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
|
|
||||||
// Use the API client instead of direct fetch
|
// Use the API client instead of direct fetch
|
||||||
await apiClient.delete(`api/v1/chats/${chatToDelete.id}`);
|
await apiClient.delete(`api/v1/chats/${chatToDelete.id}`);
|
||||||
|
|
||||||
// Close dialog and refresh chats
|
// Close dialog and refresh chats
|
||||||
setRecentChats(recentChats.filter(chat => chat.id !== chatToDelete.id));
|
setRecentChats(recentChats.filter(chat => chat.id !== chatToDelete.id));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting chat:', error);
|
console.error('Error deleting chat:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -226,15 +188,15 @@ export function AppSidebarProvider({
|
||||||
}, [searchSpaceId]);
|
}, [searchSpaceId]);
|
||||||
|
|
||||||
// Create a fallback chat if there's an error or no chats
|
// Create a fallback chat if there's an error or no chats
|
||||||
const fallbackChats = chatError || (!isLoadingChats && recentChats.length === 0)
|
const fallbackChats = chatError || (!isLoadingChats && recentChats.length === 0)
|
||||||
? [{
|
? [{
|
||||||
name: chatError ? "Error loading chats" : "No recent chats",
|
name: chatError ? "Error loading chats" : "No recent chats",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: chatError ? "AlertCircle" : "MessageCircleMore",
|
icon: chatError ? "AlertCircle" : "MessageCircleMore",
|
||||||
id: 0,
|
id: 0,
|
||||||
search_space_id: Number(searchSpaceId),
|
search_space_id: Number(searchSpaceId),
|
||||||
actions: []
|
actions: []
|
||||||
}]
|
}]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Use fallback chats if there's an error or no chats
|
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppSidebar
|
<AppSidebar
|
||||||
user={customUser}
|
|
||||||
navSecondary={updatedNavSecondary}
|
navSecondary={updatedNavSecondary}
|
||||||
navMain={navMain}
|
navMain={navMain}
|
||||||
RecentChats={isClient ? displayChats : []}
|
RecentChats={isClient ? displayChats : []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog - Only render on client */}
|
{/* Delete Confirmation Dialog - Only render on client */}
|
||||||
{isClient && (
|
{isClient && (
|
||||||
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
|
|
@ -23,7 +23,6 @@ import { Logo } from "@/components/Logo";
|
||||||
import { NavMain } from "@/components/sidebar/nav-main"
|
import { NavMain } from "@/components/sidebar/nav-main"
|
||||||
import { NavProjects } from "@/components/sidebar/nav-projects"
|
import { NavProjects } from "@/components/sidebar/nav-projects"
|
||||||
import { NavSecondary } from "@/components/sidebar/nav-secondary"
|
import { NavSecondary } from "@/components/sidebar/nav-secondary"
|
||||||
import { NavUser } from "@/components/sidebar/nav-user"
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
|
@ -143,11 +142,6 @@ const defaultData = {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
||||||
user?: {
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
avatar: string
|
|
||||||
}
|
|
||||||
navMain?: {
|
navMain?: {
|
||||||
title: string
|
title: string
|
||||||
url: string
|
url: string
|
||||||
|
@ -178,7 +172,6 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppSidebar({
|
export function AppSidebar({
|
||||||
user = defaultData.user,
|
|
||||||
navMain = defaultData.navMain,
|
navMain = defaultData.navMain,
|
||||||
navSecondary = defaultData.navSecondary,
|
navSecondary = defaultData.navSecondary,
|
||||||
RecentChats = defaultData.RecentChats,
|
RecentChats = defaultData.RecentChats,
|
||||||
|
@ -232,9 +225,9 @@ export function AppSidebar({
|
||||||
{processedRecentChats.length > 0 && <NavProjects chats={processedRecentChats} />}
|
{processedRecentChats.length > 0 && <NavProjects chats={processedRecentChats} />}
|
||||||
<NavSecondary items={processedNavSecondary} className="mt-auto" />
|
<NavSecondary items={processedNavSecondary} className="mt-auto" />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
{/* <SidebarFooter>
|
||||||
<NavUser user={user} />
|
footer
|
||||||
</SidebarFooter>
|
</SidebarFooter> */}
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue