Merge pull request #633 from CREDO23/feat/migrate-tojotai-tanstack-logs

[Feat] Logs | Migrate to jotai & tanstack
This commit is contained in:
Rohan Verma 2025-12-26 21:42:24 -08:00 committed by GitHub
commit 9eeecfaf4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 447 additions and 243 deletions

View file

@ -15,6 +15,7 @@ import {
useReactTable, useReactTable,
type VisibilityState, type VisibilityState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { useAtomValue } from "jotai";
import { import {
Activity, Activity,
AlertCircle, AlertCircle,
@ -44,8 +45,13 @@ import {
import { AnimatePresence, motion, type Variants } from "motion/react"; import { AnimatePresence, motion, type Variants } from "motion/react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useTranslations } from "next-intl"; 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 { toast } from "sonner";
import {
createLogMutationAtom,
deleteLogMutationAtom,
updateLogMutationAtom,
} from "@/atoms/logs/log-mutation.atoms";
import { JsonMetadataViewer } from "@/components/json-metadata-viewer"; import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
import { import {
AlertDialog, AlertDialog,
@ -89,7 +95,8 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } 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"; import { cn } from "@/lib/utils";
// Define animation variants for reuse // Define animation variants for reuse
@ -334,13 +341,50 @@ export default function LogsManagePage() {
const params = useParams(); const params = useParams();
const searchSpaceId = Number(params.search_space_id); const searchSpaceId = Number(params.search_space_id);
const { const { mutateAsync: deleteLogMutation } = useAtomValue(deleteLogMutationAtom);
logs, const { mutateAsync: updateLogMutation } = useAtomValue(updateLogMutationAtom);
loading: logsLoading, const { mutateAsync: createLogMutation } = useAtomValue(createLogMutationAtom);
error: logsError,
refreshLogs, const createLog = useCallback(
deleteLog, async (data: CreateLogRequest) => {
} = useLogs(searchSpaceId); 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 { const {
summary, summary,
loading: summaryLoading, loading: summaryLoading,
@ -408,7 +452,7 @@ export default function LogsManagePage() {
return; 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 { try {
const results = await Promise.all(deletePromises); const results = await Promise.all(deletePromises);
@ -437,7 +481,7 @@ export default function LogsManagePage() {
<LogsContext.Provider <LogsContext.Provider
value={{ value={{
deleteLog: deleteLog || (() => Promise.resolve(false)), deleteLog: deleteLog || (() => Promise.resolve(false)),
refreshLogs: refreshLogs || (() => Promise.resolve()), refreshLogs: () => refreshLogs().then(() => void 0),
}} }}
> >
<motion.div <motion.div
@ -524,7 +568,7 @@ export default function LogsManagePage() {
table={table} table={table}
logs={logs} logs={logs}
loading={logsLoading} loading={logsLoading}
error={logsError} error={logsError?.message ?? null}
onRefresh={refreshLogs} onRefresh={refreshLogs}
id={id} id={id}
t={t} t={t}

View 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) });
},
};
});

View 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>;

View file

@ -1,7 +1,8 @@
"use client"; "use client";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query";
import { toast } from "sonner"; import { useCallback, useMemo } from "react";
import { authenticatedFetch } from "@/lib/auth-utils"; 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 LogLevel = "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL";
export type LogStatus = "IN_PROGRESS" | "SUCCESS" | "FAILED"; export type LogStatus = "IN_PROGRESS" | "SUCCESS" | "FAILED";
@ -50,267 +51,89 @@ export interface LogSummary {
} }
export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { 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 // Memoize filters to prevent infinite re-renders
const memoizedFilters = useMemo(() => filters, [JSON.stringify(filters)]); const memoizedFilters = useMemo(() => filters, [JSON.stringify(filters)]);
const buildQueryParams = useCallback( const buildQueryParams = useCallback(
(customFilters: LogFilters = {}) => { (customFilters: LogFilters = {}) => {
const params = new URLSearchParams(); const params: Record<string, string> = {};
const allFilters = { ...memoizedFilters, ...customFilters }; const allFilters = { ...memoizedFilters, ...customFilters };
if (allFilters.search_space_id) { 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) { if (allFilters.level) {
params.append("level", allFilters.level); params["level"] = allFilters.level;
} }
if (allFilters.status) { if (allFilters.status) {
params.append("status", allFilters.status); params["status"] = allFilters.status;
} }
if (allFilters.source) { if (allFilters.source) {
params.append("source", allFilters.source); params["source"] = allFilters.source;
} }
if (allFilters.start_date) { if (allFilters.start_date) {
params.append("start_date", allFilters.start_date); params["start_date"] = allFilters.start_date;
} }
if (allFilters.end_date) { if (allFilters.end_date) {
params.append("end_date", allFilters.end_date); params["end_date"] = allFilters.end_date;
} }
return params.toString(); return params;
}, },
[memoizedFilters] [memoizedFilters]
); );
const fetchLogs = useCallback( const {
async (customFilters: LogFilters = {}, options: { skip?: number; limit?: number } = {}) => { data: logs,
try { isLoading: loading,
setLoading(true); error,
refetch,
const params = new URLSearchParams(buildQueryParams(customFilters)); } = useQuery({
if (options.skip !== undefined) params.append("skip", options.skip.toString()); queryKey: cacheKeys.logs.withQueryParams({
if (options.limit !== undefined) params.append("limit", options.limit.toString()); search_space_id: searchSpaceId,
skip: 0,
const response = await authenticatedFetch( limit: 5,
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs?${params}`, ...buildQueryParams(filters ?? {}),
{ method: "GET" } }),
); queryFn: () =>
logsApiService.getLogs({
if (!response.ok) { queryParams: {
const errorData = await response.json().catch(() => ({})); search_space_id: searchSpaceId,
throw new Error(errorData.detail || "Failed to fetch logs"); skip: 0,
} limit: 5,
...buildQueryParams(filters ?? {}),
const data = await response.json(); },
setLogs(data); }),
setError(null); enabled: !!searchSpaceId,
return data; staleTime: 3 * 60 * 1000,
} 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;
}
}, []);
return { return {
logs, logs: logs ?? [],
loading, loading,
error, error,
refreshLogs, refreshLogs: refetch,
createLog,
updateLog,
deleteLog,
getLog,
fetchLogs,
}; };
} }
// Separate hook for log summary // Separate hook for log summary
export function useLogsSummary(searchSpaceId: number, hours: number = 24) { export function useLogsSummary(searchSpaceId: number, hours: number = 24) {
const [summary, setSummary] = useState<LogSummary | null>(null); const {
const [loading, setLoading] = useState(true); data: summary,
const [error, setError] = useState<string | null>(null); 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 () => { return { summary, loading, error, refreshSummary: refetch };
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 };
} }

View 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();

View file

@ -1,4 +1,5 @@
import type { GetDocumentsRequest } from "@/contracts/types/document.types"; 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"; import type { GetSearchSpacesRequest } from "@/contracts/types/search-space.types";
export const cacheKeys = { export const cacheKeys = {
@ -18,6 +19,13 @@ export const cacheKeys = {
typeCounts: (searchSpaceId?: string) => ["documents", "type-counts", searchSpaceId] as const, typeCounts: (searchSpaceId?: string) => ["documents", "type-counts", searchSpaceId] as const,
byChunk: (chunkId: string) => ["documents", "by-chunk", chunkId] 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: { newLLMConfigs: {
all: (searchSpaceId: number) => ["new-llm-configs", searchSpaceId] as const, all: (searchSpaceId: number) => ["new-llm-configs", searchSpaceId] as const,
byId: (configId: number) => ["new-llm-configs", "detail", configId] as const, byId: (configId: number) => ["new-llm-configs", "detail", configId] as const,