mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-21 18:55:16 +02:00
Merge pull request #633 from CREDO23/feat/migrate-tojotai-tanstack-logs
[Feat] Logs | Migrate to jotai & tanstack
This commit is contained in:
commit
9eeecfaf4a
6 changed files with 447 additions and 243 deletions
|
|
@ -15,6 +15,7 @@ import {
|
|||
useReactTable,
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
Activity,
|
||||
AlertCircle,
|
||||
|
|
@ -44,8 +45,13 @@ import {
|
|||
import { AnimatePresence, motion, type Variants } from "motion/react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useContext, useId, useMemo, useRef, useState } from "react";
|
||||
import React, { useCallback, useContext, useId, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
createLogMutationAtom,
|
||||
deleteLogMutationAtom,
|
||||
updateLogMutationAtom,
|
||||
} from "@/atoms/logs/log-mutation.atoms";
|
||||
import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
|
||||
import {
|
||||
AlertDialog,
|
||||
|
|
@ -89,7 +95,8 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { type Log, type LogLevel, type LogStatus, useLogs, useLogsSummary } from "@/hooks/use-logs";
|
||||
import type { CreateLogRequest, Log, UpdateLogRequest } from "@/contracts/types/log.types";
|
||||
import { type LogLevel, type LogStatus, useLogs, useLogsSummary } from "@/hooks/use-logs";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Define animation variants for reuse
|
||||
|
|
@ -334,13 +341,50 @@ export default function LogsManagePage() {
|
|||
const params = useParams();
|
||||
const searchSpaceId = Number(params.search_space_id);
|
||||
|
||||
const {
|
||||
logs,
|
||||
loading: logsLoading,
|
||||
error: logsError,
|
||||
refreshLogs,
|
||||
deleteLog,
|
||||
} = useLogs(searchSpaceId);
|
||||
const { mutateAsync: deleteLogMutation } = useAtomValue(deleteLogMutationAtom);
|
||||
const { mutateAsync: updateLogMutation } = useAtomValue(updateLogMutationAtom);
|
||||
const { mutateAsync: createLogMutation } = useAtomValue(createLogMutationAtom);
|
||||
|
||||
const createLog = useCallback(
|
||||
async (data: CreateLogRequest) => {
|
||||
try {
|
||||
await createLogMutation(data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to create log:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[createLogMutation]
|
||||
);
|
||||
|
||||
const updateLog = useCallback(
|
||||
async (logId: number, data: UpdateLogRequest) => {
|
||||
try {
|
||||
await updateLogMutation({ logId, data });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to update log:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[updateLogMutation]
|
||||
);
|
||||
|
||||
const deleteLog = useCallback(
|
||||
async (id: number) => {
|
||||
try {
|
||||
await deleteLogMutation({ id });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete log:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[deleteLogMutation]
|
||||
);
|
||||
|
||||
const { logs, loading: logsLoading, error: logsError, refreshLogs } = useLogs(searchSpaceId);
|
||||
const {
|
||||
summary,
|
||||
loading: summaryLoading,
|
||||
|
|
@ -408,7 +452,7 @@ export default function LogsManagePage() {
|
|||
return;
|
||||
}
|
||||
|
||||
const deletePromises = selectedRows.map((row) => deleteLog(row.original.id));
|
||||
const deletePromises = selectedRows.map((row) => deleteLog(row.original.id)); // Already passes { id } via wrapper
|
||||
|
||||
try {
|
||||
const results = await Promise.all(deletePromises);
|
||||
|
|
@ -437,7 +481,7 @@ export default function LogsManagePage() {
|
|||
<LogsContext.Provider
|
||||
value={{
|
||||
deleteLog: deleteLog || (() => Promise.resolve(false)),
|
||||
refreshLogs: refreshLogs || (() => Promise.resolve()),
|
||||
refreshLogs: () => refreshLogs().then(() => void 0),
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
|
|
@ -524,7 +568,7 @@ export default function LogsManagePage() {
|
|||
table={table}
|
||||
logs={logs}
|
||||
loading={logsLoading}
|
||||
error={logsError}
|
||||
error={logsError?.message ?? null}
|
||||
onRefresh={refreshLogs}
|
||||
id={id}
|
||||
t={t}
|
||||
|
|
|
|||
68
surfsense_web/atoms/logs/log-mutation.atoms.ts
Normal file
68
surfsense_web/atoms/logs/log-mutation.atoms.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { atomWithMutation } from "jotai-tanstack-query";
|
||||
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import type {
|
||||
CreateLogRequest,
|
||||
DeleteLogRequest,
|
||||
UpdateLogRequest,
|
||||
} from "@/contracts/types/log.types";
|
||||
import { logsApiService } from "@/lib/apis/logs-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { queryClient } from "@/lib/query-client/client";
|
||||
|
||||
/**
|
||||
* Create Log Mutation
|
||||
*/
|
||||
export const createLogMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeSearchSpaceIdAtom);
|
||||
return {
|
||||
mutationKey: cacheKeys.logs.list(searchSpaceId ?? undefined),
|
||||
enabled: !!searchSpaceId,
|
||||
mutationFn: async (request: CreateLogRequest) => logsApiService.createLog(request),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: cacheKeys.logs.list(searchSpaceId ?? undefined) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(searchSpaceId ?? undefined),
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Update Log Mutation
|
||||
*/
|
||||
export const updateLogMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeSearchSpaceIdAtom);
|
||||
return {
|
||||
mutationKey: cacheKeys.logs.list(searchSpaceId ?? undefined),
|
||||
enabled: !!searchSpaceId,
|
||||
mutationFn: async ({ logId, data }: { logId: number; data: UpdateLogRequest }) =>
|
||||
logsApiService.updateLog(logId, data),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: cacheKeys.logs.detail(variables.logId) });
|
||||
queryClient.invalidateQueries({ queryKey: cacheKeys.logs.list(searchSpaceId ?? undefined) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(searchSpaceId ?? undefined),
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete Log Mutation
|
||||
*/
|
||||
export const deleteLogMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeSearchSpaceIdAtom);
|
||||
return {
|
||||
mutationKey: cacheKeys.logs.list(searchSpaceId ?? undefined),
|
||||
enabled: !!searchSpaceId,
|
||||
mutationFn: async (request: DeleteLogRequest) => logsApiService.deleteLog(request),
|
||||
onSuccess: (_data, request) => {
|
||||
queryClient.invalidateQueries({ queryKey: cacheKeys.logs.list(searchSpaceId ?? undefined) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(searchSpaceId ?? undefined),
|
||||
});
|
||||
if (request?.id)
|
||||
queryClient.invalidateQueries({ queryKey: cacheKeys.logs.detail(request.id) });
|
||||
},
|
||||
};
|
||||
});
|
||||
133
surfsense_web/contracts/types/log.types.ts
Normal file
133
surfsense_web/contracts/types/log.types.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { z } from "zod";
|
||||
import { paginationQueryParams } from ".";
|
||||
|
||||
/**
|
||||
* ENUMS
|
||||
*/
|
||||
export const logLevelEnum = z.enum(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]);
|
||||
|
||||
export const logStatusEnum = z.enum(["IN_PROGRESS", "SUCCESS", "FAILED"]);
|
||||
|
||||
/**
|
||||
* Base log schema
|
||||
*/
|
||||
export const log = z.object({
|
||||
id: z.number(),
|
||||
level: logLevelEnum,
|
||||
status: logStatusEnum,
|
||||
message: z.string(),
|
||||
source: z.string().nullable().optional(),
|
||||
log_metadata: z.record(z.string(), z.any()).nullable().optional(),
|
||||
created_at: z.string(),
|
||||
search_space_id: z.number(),
|
||||
});
|
||||
|
||||
export const logBase = log.omit({ id: true, created_at: true });
|
||||
|
||||
/**
|
||||
* Create log
|
||||
*/
|
||||
export const createLogRequest = logBase.extend({ search_space_id: z.number() });
|
||||
export const createLogResponse = log;
|
||||
|
||||
/**
|
||||
* Update log
|
||||
*/
|
||||
export const updateLogRequest = logBase.partial();
|
||||
export const updateLogResponse = log;
|
||||
|
||||
/**
|
||||
* Delete log
|
||||
*/
|
||||
export const deleteLogRequest = z.object({ id: z.number() });
|
||||
export const deleteLogResponse = z.object({
|
||||
message: z.string().default("Log deleted successfully"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Get logs (list)
|
||||
*/
|
||||
export const logFilters = z.object({
|
||||
search_space_id: z.number().optional(),
|
||||
level: logLevelEnum.optional(),
|
||||
status: logStatusEnum.optional(),
|
||||
source: z.string().optional(),
|
||||
start_date: z.string().optional(),
|
||||
end_date: z.string().optional(),
|
||||
});
|
||||
|
||||
export const getLogsRequest = z.object({
|
||||
queryParams: paginationQueryParams
|
||||
.extend({
|
||||
search_space_id: z.number().optional(),
|
||||
level: logLevelEnum.optional(),
|
||||
status: logStatusEnum.optional(),
|
||||
source: z.string().optional(),
|
||||
start_date: z.string().optional(),
|
||||
end_date: z.string().optional(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
export const getLogsResponse = z.array(log);
|
||||
|
||||
/**
|
||||
* Get single log
|
||||
*/
|
||||
export const getLogRequest = z.object({ id: z.number() });
|
||||
export const getLogResponse = log;
|
||||
|
||||
/**
|
||||
* Log summary (used for summary dashboard)
|
||||
*/
|
||||
export const logActiveTask = z.object({
|
||||
id: z.number(),
|
||||
task_name: z.string(),
|
||||
message: z.string(),
|
||||
started_at: z.string(),
|
||||
source: z.string().nullable().optional(),
|
||||
});
|
||||
export const logFailure = z.object({
|
||||
id: z.number(),
|
||||
task_name: z.string(),
|
||||
message: z.string(),
|
||||
failed_at: z.string(),
|
||||
source: z.string().nullable().optional(),
|
||||
error_details: z.string().nullable().optional(),
|
||||
});
|
||||
export const logSummary = z.object({
|
||||
total_logs: z.number(),
|
||||
time_window_hours: z.number(),
|
||||
by_status: z.record(z.string(), z.number()),
|
||||
by_level: z.record(z.string(), z.number()),
|
||||
by_source: z.record(z.string(), z.number()),
|
||||
active_tasks: z.array(logActiveTask),
|
||||
recent_failures: z.array(logFailure),
|
||||
});
|
||||
export const getLogSummaryRequest = z.object({
|
||||
search_space_id: z.number(),
|
||||
hours: z.number().optional(),
|
||||
});
|
||||
export const getLogSummaryResponse = logSummary;
|
||||
|
||||
/**
|
||||
* Typescript types
|
||||
*/
|
||||
export type Log = z.infer<typeof log>;
|
||||
export type LogLevelEnum = z.infer<typeof logLevelEnum>;
|
||||
export type LogStatusEnum = z.infer<typeof logStatusEnum>;
|
||||
export type LogFilters = z.infer<typeof logFilters>;
|
||||
export type CreateLogRequest = z.infer<typeof createLogRequest>;
|
||||
export type CreateLogResponse = z.infer<typeof createLogResponse>;
|
||||
export type UpdateLogRequest = z.infer<typeof updateLogRequest>;
|
||||
export type UpdateLogResponse = z.infer<typeof updateLogResponse>;
|
||||
export type DeleteLogRequest = z.infer<typeof deleteLogRequest>;
|
||||
export type DeleteLogResponse = z.infer<typeof deleteLogResponse>;
|
||||
export type GetLogsRequest = z.infer<typeof getLogsRequest>;
|
||||
export type GetLogsResponse = z.infer<typeof getLogsResponse>;
|
||||
export type GetLogRequest = z.infer<typeof getLogRequest>;
|
||||
export type GetLogResponse = z.infer<typeof getLogResponse>;
|
||||
export type LogSummary = z.infer<typeof logSummary>;
|
||||
export type LogFailure = z.infer<typeof logFailure>;
|
||||
export type LogActiveTask = z.infer<typeof logActiveTask>;
|
||||
export type GetLogSummaryRequest = z.infer<typeof getLogSummaryRequest>;
|
||||
export type GetLogSummaryResponse = z.infer<typeof getLogSummaryResponse>;
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
"use client";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { logsApiService } from "@/lib/apis/logs-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
export type LogLevel = "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL";
|
||||
export type LogStatus = "IN_PROGRESS" | "SUCCESS" | "FAILED";
|
||||
|
|
@ -50,267 +51,89 @@ export interface LogSummary {
|
|||
}
|
||||
|
||||
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 params: Record<string, string> = {};
|
||||
|
||||
const allFilters = { ...memoizedFilters, ...customFilters };
|
||||
|
||||
if (allFilters.search_space_id) {
|
||||
params.append("search_space_id", allFilters.search_space_id.toString());
|
||||
params["search_space_id"] = allFilters.search_space_id.toString();
|
||||
}
|
||||
if (allFilters.level) {
|
||||
params.append("level", allFilters.level);
|
||||
params["level"] = allFilters.level;
|
||||
}
|
||||
if (allFilters.status) {
|
||||
params.append("status", allFilters.status);
|
||||
params["status"] = allFilters.status;
|
||||
}
|
||||
if (allFilters.source) {
|
||||
params.append("source", allFilters.source);
|
||||
params["source"] = allFilters.source;
|
||||
}
|
||||
if (allFilters.start_date) {
|
||||
params.append("start_date", allFilters.start_date);
|
||||
params["start_date"] = allFilters.start_date;
|
||||
}
|
||||
if (allFilters.end_date) {
|
||||
params.append("end_date", allFilters.end_date);
|
||||
params["end_date"] = allFilters.end_date;
|
||||
}
|
||||
|
||||
return params.toString();
|
||||
return params;
|
||||
},
|
||||
[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 authenticatedFetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs?${params}`,
|
||||
{ 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
|
||||
// Use silent: true to suppress toast notifications (for internal/background operations)
|
||||
const createLog = useCallback(
|
||||
async (logData: Omit<Log, "id" | "created_at">, options?: { silent?: boolean }) => {
|
||||
const { silent = false } = options || {};
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs`,
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
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]);
|
||||
// Only show toast if not silent
|
||||
if (!silent) {
|
||||
toast.success("Log created successfully");
|
||||
}
|
||||
return newLog;
|
||||
} catch (err: any) {
|
||||
// Only show error toast if not silent
|
||||
if (!silent) {
|
||||
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 authenticatedFetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`,
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
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 authenticatedFetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`,
|
||||
{ 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 authenticatedFetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`,
|
||||
{ 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;
|
||||
}
|
||||
}, []);
|
||||
const {
|
||||
data: logs,
|
||||
isLoading: loading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: cacheKeys.logs.withQueryParams({
|
||||
search_space_id: searchSpaceId,
|
||||
skip: 0,
|
||||
limit: 5,
|
||||
...buildQueryParams(filters ?? {}),
|
||||
}),
|
||||
queryFn: () =>
|
||||
logsApiService.getLogs({
|
||||
queryParams: {
|
||||
search_space_id: searchSpaceId,
|
||||
skip: 0,
|
||||
limit: 5,
|
||||
...buildQueryParams(filters ?? {}),
|
||||
},
|
||||
}),
|
||||
enabled: !!searchSpaceId,
|
||||
staleTime: 3 * 60 * 1000,
|
||||
});
|
||||
|
||||
return {
|
||||
logs,
|
||||
logs: logs ?? [],
|
||||
loading,
|
||||
error,
|
||||
refreshLogs,
|
||||
createLog,
|
||||
updateLog,
|
||||
deleteLog,
|
||||
getLog,
|
||||
fetchLogs,
|
||||
refreshLogs: refetch,
|
||||
};
|
||||
}
|
||||
|
||||
// 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 {
|
||||
data: summary,
|
||||
isLoading: loading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: cacheKeys.logs.summary(searchSpaceId),
|
||||
queryFn: () =>
|
||||
logsApiService.getLogSummary({
|
||||
search_space_id: searchSpaceId,
|
||||
hours: hours,
|
||||
}),
|
||||
enabled: !!searchSpaceId,
|
||||
staleTime: 3 * 60 * 1000,
|
||||
});
|
||||
|
||||
const fetchSummary = useCallback(async () => {
|
||||
if (!searchSpaceId) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await authenticatedFetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/search-space/${searchSpaceId}/summary?hours=${hours}`,
|
||||
{ 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 };
|
||||
return { summary, loading, error, refreshSummary: refetch };
|
||||
}
|
||||
|
|
|
|||
128
surfsense_web/lib/apis/logs-api.service.ts
Normal file
128
surfsense_web/lib/apis/logs-api.service.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import {
|
||||
type CreateLogRequest,
|
||||
createLogRequest,
|
||||
createLogResponse,
|
||||
type DeleteLogRequest,
|
||||
deleteLogRequest,
|
||||
deleteLogResponse,
|
||||
type GetLogRequest,
|
||||
type GetLogSummaryRequest,
|
||||
type GetLogsRequest,
|
||||
getLogRequest,
|
||||
getLogResponse,
|
||||
getLogSummaryRequest,
|
||||
getLogSummaryResponse,
|
||||
getLogsRequest,
|
||||
getLogsResponse,
|
||||
type Log,
|
||||
log,
|
||||
type UpdateLogRequest,
|
||||
updateLogRequest,
|
||||
updateLogResponse,
|
||||
} from "@/contracts/types/log.types";
|
||||
import { ValidationError } from "../error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
class LogsApiService {
|
||||
/**
|
||||
* Get a list of logs with optional filtering and pagination
|
||||
*/
|
||||
getLogs = async (request: GetLogsRequest) => {
|
||||
const parsedRequest = getLogsRequest.safeParse(request);
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
// Transform query params to be string values
|
||||
const transformedQueryParams = parsedRequest.data.queryParams
|
||||
? Object.fromEntries(
|
||||
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => {
|
||||
// Handle array values (document_type)
|
||||
if (Array.isArray(v)) {
|
||||
return [k, v.join(",")];
|
||||
}
|
||||
return [k, String(v)];
|
||||
})
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const queryParams = transformedQueryParams
|
||||
? new URLSearchParams(transformedQueryParams).toString()
|
||||
: "";
|
||||
return baseApiService.get(`/api/v1/logs?${queryParams}`, getLogsResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single log by ID
|
||||
*/
|
||||
getLog = async (request: GetLogRequest) => {
|
||||
const parsedRequest = getLogRequest.safeParse(request);
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
return baseApiService.get(`/api/v1/logs/${request.id}`, getLogResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a log entry
|
||||
*/
|
||||
createLog = async (request: CreateLogRequest) => {
|
||||
const parsedRequest = createLogRequest.safeParse(request);
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
return baseApiService.post(`/api/v1/logs`, createLogResponse, {
|
||||
body: parsedRequest.data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a log entry
|
||||
*/
|
||||
updateLog = async (logId: number, request: UpdateLogRequest) => {
|
||||
const parsedRequest = updateLogRequest.safeParse(request);
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
return baseApiService.put(`/api/v1/logs/${logId}`, updateLogResponse, {
|
||||
body: parsedRequest.data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a log entry
|
||||
*/
|
||||
deleteLog = async (request: DeleteLogRequest) => {
|
||||
const parsedRequest = deleteLogRequest.safeParse(request);
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
return baseApiService.delete(`/api/v1/logs/${parsedRequest.data.id}`, deleteLogResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get summary for logs by search space
|
||||
*/
|
||||
getLogSummary = async (request: GetLogSummaryRequest) => {
|
||||
const parsedRequest = getLogSummaryRequest.safeParse(request);
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
const { search_space_id, hours } = parsedRequest.data;
|
||||
const url = `/api/v1/logs/search-space/${search_space_id}/summary${hours ? `?hours=${hours}` : ""}`;
|
||||
return baseApiService.get(url, getLogSummaryResponse);
|
||||
};
|
||||
}
|
||||
|
||||
export const logsApiService = new LogsApiService();
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { GetDocumentsRequest } from "@/contracts/types/document.types";
|
||||
import type { GetLogsRequest } from "@/contracts/types/log.types";
|
||||
import type { GetSearchSpacesRequest } from "@/contracts/types/search-space.types";
|
||||
|
||||
export const cacheKeys = {
|
||||
|
|
@ -18,6 +19,13 @@ export const cacheKeys = {
|
|||
typeCounts: (searchSpaceId?: string) => ["documents", "type-counts", searchSpaceId] as const,
|
||||
byChunk: (chunkId: string) => ["documents", "by-chunk", chunkId] as const,
|
||||
},
|
||||
logs: {
|
||||
list: (searchSpaceId?: number | string) => ["logs", "list", searchSpaceId] as const,
|
||||
detail: (logId: number | string) => ["logs", "detail", logId] as const,
|
||||
summary: (searchSpaceId?: number | string) => ["logs", "summary", searchSpaceId] as const,
|
||||
withQueryParams: (queries: GetLogsRequest["queryParams"]) =>
|
||||
["logs", "with-query-params", ...(queries ? Object.values(queries) : [])] as const,
|
||||
},
|
||||
newLLMConfigs: {
|
||||
all: (searchSpaceId: number) => ["new-llm-configs", searchSpaceId] as const,
|
||||
byId: (configId: number) => ["new-llm-configs", "detail", configId] as const,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue