mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-21 18:55:16 +02:00
fix: Resolve merge conflict in documents_routes.py
- Integrated Docling ETL service with new task logging system - Maintained consistent logging pattern across all ETL services - Added progress and success/failure logging for Docling processing
This commit is contained in:
commit
f117d94ef7
34 changed files with 4160 additions and 520 deletions
|
|
@ -329,7 +329,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
|
|||
|
||||
// Helper to finish the podcast generation process
|
||||
const finishPodcastGeneration = () => {
|
||||
toast.success("All podcasts are being generated! Check the podcasts tab to see them when ready.");
|
||||
toast.success("All podcasts are being generated! Check the logs tab to see their status.");
|
||||
setPodcastDialogOpen(false);
|
||||
setSelectedChats([]);
|
||||
setSelectionMode(false);
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export default function ConnectorsPage() {
|
|||
const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined;
|
||||
|
||||
await indexConnector(selectedConnectorForIndexing, searchSpaceId, startDateStr, endDateStr);
|
||||
toast.success("Connector content indexed successfully");
|
||||
toast.success("Connector content indexing started");
|
||||
} catch (error) {
|
||||
console.error("Error indexing connector content:", error);
|
||||
toast.error(
|
||||
|
|
@ -155,7 +155,7 @@ export default function ConnectorsPage() {
|
|||
setIndexingConnectorId(connectorId);
|
||||
try {
|
||||
await indexConnector(connectorId, searchSpaceId);
|
||||
toast.success("Connector content indexed successfully");
|
||||
toast.success("Connector content indexing started");
|
||||
} catch (error) {
|
||||
console.error("Error indexing connector content:", error);
|
||||
toast.error(
|
||||
|
|
|
|||
|
|
@ -170,9 +170,9 @@ export default function FileUploader() {
|
|||
formData.append('search_space_id', search_space_id)
|
||||
|
||||
try {
|
||||
toast("File Upload", {
|
||||
description: "Files Uploading Initiated",
|
||||
})
|
||||
// toast("File Upload", {
|
||||
// description: "Files Uploading Initiated",
|
||||
// })
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL!}/api/v1/documents/fileupload`, {
|
||||
method: "POST",
|
||||
|
|
@ -188,8 +188,8 @@ export default function FileUploader() {
|
|||
|
||||
await response.json()
|
||||
|
||||
toast("Upload Successful", {
|
||||
description: "Files Uploaded Successfully",
|
||||
toast("Upload Task Initiated", {
|
||||
description: "Files Uploading Initiated",
|
||||
})
|
||||
|
||||
router.push(`/dashboard/${search_space_id}/documents`);
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ export default function DashboardLayout({
|
|||
title: "Upload Documents",
|
||||
url: `/dashboard/${search_space_id}/documents/upload`,
|
||||
},
|
||||
{
|
||||
title: "Add Webpages",
|
||||
url: `/dashboard/${search_space_id}/documents/webpage`,
|
||||
},
|
||||
// { TODO: FIX THIS AND ADD IT BACK
|
||||
// title: "Add Webpages",
|
||||
// url: `/dashboard/${search_space_id}/documents/webpage`,
|
||||
// },
|
||||
{
|
||||
title: "Add Youtube Videos",
|
||||
url: `/dashboard/${search_space_id}/documents/youtube`,
|
||||
|
|
@ -78,6 +78,13 @@ export default function DashboardLayout({
|
|||
icon: "Podcast",
|
||||
items: [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Logs",
|
||||
url: `/dashboard/${search_space_id}/logs`,
|
||||
icon: "FileText",
|
||||
items: [
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
1085
surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx
Normal file
1085
surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -981,19 +981,16 @@ const ChatPage = () => {
|
|||
const renderTerminalContent = (message: any) => {
|
||||
if (!message.annotations) return null;
|
||||
|
||||
// Get all TERMINAL_INFO annotations
|
||||
const terminalInfoAnnotations = (message.annotations as any[]).filter(
|
||||
(a) => a.type === "TERMINAL_INFO",
|
||||
);
|
||||
|
||||
// Get the latest TERMINAL_INFO annotation
|
||||
const latestTerminalInfo =
|
||||
terminalInfoAnnotations.length > 0
|
||||
? terminalInfoAnnotations[terminalInfoAnnotations.length - 1]
|
||||
: null;
|
||||
// Get all TERMINAL_INFO annotations content
|
||||
const terminalInfoAnnotations = (message.annotations as any[]).map(item => {
|
||||
if(item.type === "TERMINAL_INFO") {
|
||||
return item.content.map((a: any) => a.text)
|
||||
|
||||
}
|
||||
}).flat().filter(Boolean)
|
||||
|
||||
// Render the content of the latest TERMINAL_INFO annotation
|
||||
return latestTerminalInfo?.content.map((item: any, idx: number) => (
|
||||
return terminalInfoAnnotations.map((item: any, idx: number) => (
|
||||
<div key={idx} className="py-0.5 flex items-start text-gray-300">
|
||||
<span className="text-gray-500 text-xs mr-2 w-10 flex-shrink-0">
|
||||
[{String(idx).padStart(2, "0")}:
|
||||
|
|
@ -1008,7 +1005,7 @@ const ChatPage = () => {
|
|||
${item.type === "warning" ? "text-yellow-300" : ""}
|
||||
`}
|
||||
>
|
||||
{item.text}
|
||||
{item}
|
||||
</span>
|
||||
</div>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -1,40 +1,48 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import React from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { IconCheck, IconCopy, IconKey } from "@tabler/icons-react"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { useApiKey } from "@/hooks/use-api-key"
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { IconCheck, IconCopy, IconKey } from "@tabler/icons-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useApiKey } from "@/hooks/use-api-key";
|
||||
|
||||
const fadeIn = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1, transition: { duration: 0.4 } }
|
||||
}
|
||||
visible: { opacity: 1, transition: { duration: 0.4 } },
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ApiKeyClient = () => {
|
||||
const {
|
||||
apiKey,
|
||||
isLoading,
|
||||
copied,
|
||||
copyToClipboard
|
||||
} = useApiKey()
|
||||
|
||||
const { apiKey, isLoading, copied, copyToClipboard } = useApiKey();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="flex justify-center w-full min-h-screen py-10 px-4">
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="w-full max-w-3xl"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
|
|
@ -52,7 +60,8 @@ const ApiKeyClient = () => {
|
|||
<IconKey className="h-4 w-4" />
|
||||
<AlertTitle>Important</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your API key grants full access to your account. Never share it publicly or with unauthorized users.
|
||||
Your API key grants full access to your account. Never share it
|
||||
publicly or with unauthorized users.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</motion.div>
|
||||
|
|
@ -68,15 +77,15 @@ const ApiKeyClient = () => {
|
|||
<CardContent>
|
||||
<AnimatePresence mode="wait">
|
||||
{isLoading ? (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key="loading"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="h-10 w-full bg-muted animate-pulse rounded-md"
|
||||
className="h-10 w-full bg-muted animate-pulse rounded-md"
|
||||
/>
|
||||
) : apiKey ? (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key="api-key"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -96,9 +105,9 @@ const ApiKeyClient = () => {
|
|||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={copyToClipboard}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
|
|
@ -107,7 +116,11 @@ const ApiKeyClient = () => {
|
|||
animate={copied ? { scale: [1, 1.2, 1] } : {}}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{copied ? <IconCheck className="h-4 w-4" /> : <IconCopy className="h-4 w-4" />}
|
||||
{copied ? (
|
||||
<IconCheck className="h-4 w-4" />
|
||||
) : (
|
||||
<IconCopy className="h-4 w-4" />
|
||||
)}
|
||||
</motion.div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -118,7 +131,7 @@ const ApiKeyClient = () => {
|
|||
</TooltipProvider>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key="no-key"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
@ -133,34 +146,39 @@ const ApiKeyClient = () => {
|
|||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="mt-8"
|
||||
variants={fadeIn}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<h2 className="text-xl font-semibold mb-4 text-center">How to use your API key</h2>
|
||||
<h2 className="text-xl font-semibold mb-4 text-center">
|
||||
How to use your API key
|
||||
</h2>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="space-y-4"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={staggerContainer}
|
||||
>
|
||||
<motion.div variants={fadeIn}>
|
||||
<h3 className="font-medium mb-2 text-center">Authentication</h3>
|
||||
<h3 className="font-medium mb-2 text-center">
|
||||
Authentication
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Include your API key in the Authorization header of your requests:
|
||||
Include your API key in the Authorization header of your
|
||||
requests:
|
||||
</p>
|
||||
<motion.pre
|
||||
<motion.pre
|
||||
className="bg-muted p-3 rounded-md mt-2 overflow-x-auto"
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 10 }}
|
||||
>
|
||||
<code className="text-xs">
|
||||
Authorization: Bearer {apiKey || 'YOUR_API_KEY'}
|
||||
Authorization: Bearer {apiKey || "YOUR_API_KEY"}
|
||||
</code>
|
||||
</motion.pre>
|
||||
</motion.div>
|
||||
|
|
@ -169,8 +187,18 @@ const ApiKeyClient = () => {
|
|||
</Card>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => router.push("/dashboard")}
|
||||
className="flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 hover:bg-primary/30 transition-colors"
|
||||
aria-label="Back to Dashboard"
|
||||
type="button"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5 text-primary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyClient
|
||||
export default ApiKeyClient;
|
||||
|
|
@ -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<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 (error) return <ErrorScreen message={error} />;
|
||||
|
||||
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 = () => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleLogout}
|
||||
className="h-9 w-9 rounded-full"
|
||||
aria-label="Logout"
|
||||
>
|
||||
<LogOut className="h-5 w-5" />
|
||||
</Button>
|
||||
<ThemeTogglerComponent />
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<UserDropdown user={customUser} />
|
||||
<ThemeTogglerComponent />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-6 mt-6">
|
||||
|
|
|
|||
|
|
@ -152,3 +152,7 @@
|
|||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation'; // Add this import
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Bot, Settings, Brain } from 'lucide-react';
|
||||
import { Bot, Settings, Brain, ArrowLeft } from 'lucide-react'; // Import ArrowLeft icon
|
||||
import { ModelConfigManager } from '@/components/settings/model-config-manager';
|
||||
import { LLMRoleManager } from '@/components/settings/llm-role-manager';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter(); // Initialize router
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="container max-w-7xl mx-auto p-6 lg:p-8">
|
||||
|
|
@ -15,6 +18,15 @@ export default function SettingsPage() {
|
|||
{/* Header Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={() => router.push('/dashboard')}
|
||||
className="flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors"
|
||||
aria-label="Back to Dashboard"
|
||||
type="button"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5 text-primary" />
|
||||
</button>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
|
||||
<Settings className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
|
|
@ -57,4 +69,4 @@ export default function SettingsPage() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
101
surfsense_web/components/UserDropdown.tsx
Normal file
101
surfsense_web/components/UserDropdown.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"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 = () => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('surfsense_bearer_token');
|
||||
router.push('/');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
// Optionally, provide user feedback
|
||||
if (typeof window !== 'undefined') {
|
||||
alert('Logout failed. Please try again.');
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Plus, Search, Trash2 } from "lucide-react";
|
||||
import { MoveLeftIcon, Plus, Search, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
|
@ -33,6 +33,7 @@ import {
|
|||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
// Define the form schema with Zod
|
||||
const searchSpaceFormSchema = z.object({
|
||||
|
|
@ -59,7 +60,8 @@ export function SearchSpaceForm({
|
|||
initialData = { name: "", description: "" }
|
||||
}: SearchSpaceFormProps) {
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Initialize the form with React Hook Form and Zod validation
|
||||
const form = useForm<SearchSpaceFormValues>({
|
||||
resolver: zodResolver(searchSpaceFormSchema),
|
||||
|
|
@ -115,17 +117,32 @@ export function SearchSpaceForm({
|
|||
animate="visible"
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div className="flex flex-col space-y-2" variants={itemVariants}>
|
||||
<h2 className="text-3xl font-bold tracking-tight">
|
||||
{isEditing ? "Edit Search Space" : "Create Search Space"}
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
{isEditing
|
||||
? "Update your search space details"
|
||||
: "Create a new search space to organize your documents, chats, and podcasts."}
|
||||
</p>
|
||||
<motion.div className="flex items-center justify-between" variants={itemVariants}>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h2 className="text-3xl font-bold tracking-tight">
|
||||
{isEditing ? "Edit Search Space" : "Create Search Space"}
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
{isEditing
|
||||
? "Update your search space details"
|
||||
: "Create a new search space to organize your documents, chats, and podcasts."}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="group relative rounded-full p-3 bg-background/80 hover:bg-muted border border-border hover:border-primary/20 shadow-sm hover:shadow-md transition-all duration-200 backdrop-blur-sm"
|
||||
onClick={() => {
|
||||
router.push('/dashboard')
|
||||
}}
|
||||
>
|
||||
<MoveLeftIcon
|
||||
size={18}
|
||||
className="text-muted-foreground group-hover:text-foreground transition-colors duration-200"
|
||||
/>
|
||||
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
</button>
|
||||
|
||||
</motion.div>
|
||||
|
||||
|
||||
<motion.div
|
||||
className="w-full"
|
||||
variants={itemVariants}
|
||||
|
|
@ -190,9 +207,9 @@ export function SearchSpaceForm({
|
|||
</div>
|
||||
</Tilt>
|
||||
</motion.div>
|
||||
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleFormSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
|
|
@ -211,7 +228,7 @@ export function SearchSpaceForm({
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
|
|
@ -228,7 +245,7 @@ export function SearchSpaceForm({
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -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<SearchSpace | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoadingChats, setIsLoadingChats] = useState(true);
|
||||
const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true);
|
||||
const [isLoadingUser, setIsLoadingUser] = useState(true);
|
||||
const [chatError, setChatError] = useState<string | null>(null);
|
||||
const [searchSpaceError, setSearchSpaceError] = useState<string | null>(null);
|
||||
const [userError, setUserError] = useState<string | null>(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<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
|
||||
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<Chat[]>(`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 (
|
||||
<>
|
||||
<AppSidebar
|
||||
user={customUser}
|
||||
navSecondary={updatedNavSecondary}
|
||||
navMain={navMain}
|
||||
RecentChats={isClient ? displayChats : []}
|
||||
/>
|
||||
|
||||
|
||||
{/* Delete Confirmation Dialog - Only render on client */}
|
||||
{isClient && (
|
||||
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ import {
|
|||
Trash2,
|
||||
Podcast,
|
||||
type LucideIcon,
|
||||
FileText,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { NavMain } from "@/components/sidebar/nav-main"
|
||||
import { NavProjects } from "@/components/sidebar/nav-projects"
|
||||
import { NavSecondary } from "@/components/sidebar/nav-secondary"
|
||||
import { NavUser } from "@/components/sidebar/nav-user"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
|
|
@ -47,7 +47,8 @@ export const iconMap: Record<string, LucideIcon> = {
|
|||
Info,
|
||||
ExternalLink,
|
||||
Trash2,
|
||||
Podcast
|
||||
Podcast,
|
||||
FileText
|
||||
}
|
||||
|
||||
const defaultData = {
|
||||
|
|
@ -141,11 +142,6 @@ const defaultData = {
|
|||
}
|
||||
|
||||
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
||||
user?: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
navMain?: {
|
||||
title: string
|
||||
url: string
|
||||
|
|
@ -176,7 +172,6 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
|||
}
|
||||
|
||||
export function AppSidebar({
|
||||
user = defaultData.user,
|
||||
navMain = defaultData.navMain,
|
||||
navSecondary = defaultData.navSecondary,
|
||||
RecentChats = defaultData.RecentChats,
|
||||
|
|
@ -230,9 +225,9 @@ export function AppSidebar({
|
|||
{processedRecentChats.length > 0 && <NavProjects chats={processedRecentChats} />}
|
||||
<NavSecondary items={processedNavSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={user} />
|
||||
</SidebarFooter>
|
||||
{/* <SidebarFooter>
|
||||
footer
|
||||
</SidebarFooter> */}
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ function DropdownMenuItem({
|
|||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from './useSearchSourceConnectors';
|
||||
export * from './useSearchSourceConnectors';
|
||||
export * from './use-logs';
|
||||
313
surfsense_web/hooks/use-logs.ts
Normal file
313
surfsense_web/hooks/use-logs.ts
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
"use client"
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export type LogLevel = "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL";
|
||||
export type LogStatus = "IN_PROGRESS" | "SUCCESS" | "FAILED";
|
||||
|
||||
export interface Log {
|
||||
id: number;
|
||||
level: LogLevel;
|
||||
status: LogStatus;
|
||||
message: string;
|
||||
source?: string;
|
||||
log_metadata?: Record<string, any>;
|
||||
created_at: string;
|
||||
search_space_id: number;
|
||||
}
|
||||
|
||||
export interface LogFilters {
|
||||
search_space_id?: number;
|
||||
level?: LogLevel;
|
||||
status?: LogStatus;
|
||||
source?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
|
||||
export interface LogSummary {
|
||||
total_logs: number;
|
||||
time_window_hours: number;
|
||||
by_status: Record<string, number>;
|
||||
by_level: Record<string, number>;
|
||||
by_source: Record<string, number>;
|
||||
active_tasks: Array<{
|
||||
id: number;
|
||||
task_name: string;
|
||||
message: string;
|
||||
started_at: string;
|
||||
source?: string;
|
||||
}>;
|
||||
recent_failures: Array<{
|
||||
id: number;
|
||||
task_name: string;
|
||||
message: string;
|
||||
failed_at: string;
|
||||
source?: string;
|
||||
error_details?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) {
|
||||
const [logs, setLogs] = useState<Log[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Memoize filters to prevent infinite re-renders
|
||||
const memoizedFilters = useMemo(() => filters, [JSON.stringify(filters)]);
|
||||
|
||||
const buildQueryParams = useCallback((customFilters: LogFilters = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
const allFilters = { ...memoizedFilters, ...customFilters };
|
||||
|
||||
if (allFilters.search_space_id) {
|
||||
params.append('search_space_id', allFilters.search_space_id.toString());
|
||||
}
|
||||
if (allFilters.level) {
|
||||
params.append('level', allFilters.level);
|
||||
}
|
||||
if (allFilters.status) {
|
||||
params.append('status', allFilters.status);
|
||||
}
|
||||
if (allFilters.source) {
|
||||
params.append('source', allFilters.source);
|
||||
}
|
||||
if (allFilters.start_date) {
|
||||
params.append('start_date', allFilters.start_date);
|
||||
}
|
||||
if (allFilters.end_date) {
|
||||
params.append('end_date', allFilters.end_date);
|
||||
}
|
||||
|
||||
return params.toString();
|
||||
}, [memoizedFilters]);
|
||||
|
||||
const fetchLogs = useCallback(async (customFilters: LogFilters = {}, options: { skip?: number; limit?: number } = {}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const params = new URLSearchParams(buildQueryParams(customFilters));
|
||||
if (options.skip !== undefined) params.append('skip', options.skip.toString());
|
||||
if (options.limit !== undefined) params.append('limit', options.limit.toString());
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/?${params}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`,
|
||||
},
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || "Failed to fetch logs");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setLogs(data);
|
||||
setError(null);
|
||||
return data;
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch logs');
|
||||
console.error('Error fetching logs:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [buildQueryParams]);
|
||||
|
||||
// Initial fetch
|
||||
useEffect(() => {
|
||||
const initialFilters = searchSpaceId ? { ...memoizedFilters, search_space_id: searchSpaceId } : memoizedFilters;
|
||||
fetchLogs(initialFilters);
|
||||
}, [searchSpaceId, fetchLogs, memoizedFilters]);
|
||||
|
||||
// Function to refresh the logs list
|
||||
const refreshLogs = useCallback(async (customFilters: LogFilters = {}) => {
|
||||
const finalFilters = searchSpaceId ? { ...customFilters, search_space_id: searchSpaceId } : customFilters;
|
||||
return await fetchLogs(finalFilters);
|
||||
}, [searchSpaceId, fetchLogs]);
|
||||
|
||||
// Function to create a new log
|
||||
const createLog = useCallback(async (logData: Omit<Log, 'id' | 'created_at'>) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(logData),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || "Failed to create log");
|
||||
}
|
||||
|
||||
const newLog = await response.json();
|
||||
setLogs(prevLogs => [newLog, ...prevLogs]);
|
||||
toast.success("Log created successfully");
|
||||
return newLog;
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed to create log');
|
||||
console.error('Error creating log:', err);
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Function to update a log
|
||||
const updateLog = useCallback(async (logId: number, updateData: Partial<Omit<Log, 'id' | 'created_at' | 'search_space_id'>>) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`,
|
||||
},
|
||||
method: "PUT",
|
||||
body: JSON.stringify(updateData),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || "Failed to update log");
|
||||
}
|
||||
|
||||
const updatedLog = await response.json();
|
||||
setLogs(prevLogs =>
|
||||
prevLogs.map(log => log.id === logId ? updatedLog : log)
|
||||
);
|
||||
toast.success("Log updated successfully");
|
||||
return updatedLog;
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed to update log');
|
||||
console.error('Error updating log:', err);
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Function to delete a log
|
||||
const deleteLog = useCallback(async (logId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`,
|
||||
},
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || "Failed to delete log");
|
||||
}
|
||||
|
||||
setLogs(prevLogs => prevLogs.filter(log => log.id !== logId));
|
||||
toast.success("Log deleted successfully");
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed to delete log');
|
||||
console.error('Error deleting log:', err);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Function to get a single log
|
||||
const getLog = useCallback(async (logId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`,
|
||||
},
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || "Failed to fetch log");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed to fetch log');
|
||||
console.error('Error fetching log:', err);
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
logs,
|
||||
loading,
|
||||
error,
|
||||
refreshLogs,
|
||||
createLog,
|
||||
updateLog,
|
||||
deleteLog,
|
||||
getLog,
|
||||
fetchLogs
|
||||
};
|
||||
}
|
||||
|
||||
// Separate hook for log summary
|
||||
export function useLogsSummary(searchSpaceId: number, hours: number = 24) {
|
||||
const [summary, setSummary] = useState<LogSummary | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchSummary = useCallback(async () => {
|
||||
if (!searchSpaceId) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/search-space/${searchSpaceId}/summary?hours=${hours}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`,
|
||||
},
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || "Failed to fetch logs summary");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setSummary(data);
|
||||
setError(null);
|
||||
return data;
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch logs summary');
|
||||
console.error('Error fetching logs summary:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [searchSpaceId, hours]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSummary();
|
||||
}, [fetchSummary]);
|
||||
|
||||
const refreshSummary = useCallback(() => {
|
||||
return fetchSummary();
|
||||
}, [fetchSummary]);
|
||||
|
||||
return { summary, loading, error, refreshSummary };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue