diff --git a/frontend/apps/hub/src/domains/project/components/actors/actors-provider.tsx b/frontend/apps/hub/src/domains/project/components/actors/actors-provider.tsx index f052169835..8562e3f85a 100644 --- a/frontend/apps/hub/src/domains/project/components/actors/actors-provider.tsx +++ b/frontend/apps/hub/src/domains/project/components/actors/actors-provider.tsx @@ -1,5 +1,5 @@ import { router } from "@/app"; -import { queryClient } from "@/queries/global"; +import { queryClient, rivetClient } from "@/queries/global"; import { type FilterValue, toRecord } from "@rivet-gg/components"; import { currentActorIdAtom, @@ -16,6 +16,8 @@ import { actorsQueryAtom, actorsInternalFilterAtom, type Actor, + actorEnvironmentAtom, + exportLogsHandlerAtom, } from "@rivet-gg/components/actors"; import { InfiniteQueryObserver, @@ -78,6 +80,22 @@ export function ActorsProvider({ store.set(currentActorIdAtom, actorId); }, [actorId]); + // biome-ignore lint/correctness/useExhaustiveDependencies: store is not a dependency + useEffect(() => { + store.set(actorEnvironmentAtom, { projectNameId, environmentNameId }); + }, [projectNameId, environmentNameId]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: store is not a dependency + useEffect(() => { + store.set(exportLogsHandlerAtom, async ({ projectNameId, environmentNameId, queryJson }) => { + return rivetClient.actors.logs.export({ + project: projectNameId, + environment: environmentNameId, + queryJson, + }); + }); + }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: store is not a dependency useEffect(() => { store.set(actorFiltersAtom, { diff --git a/frontend/apps/hub/src/domains/project/queries/actors/mutations.ts b/frontend/apps/hub/src/domains/project/queries/actors/mutations.ts index 78387e5ae5..51b60836c6 100644 --- a/frontend/apps/hub/src/domains/project/queries/actors/mutations.ts +++ b/frontend/apps/hub/src/domains/project/queries/actors/mutations.ts @@ -314,3 +314,22 @@ export const useDeleteRouteMutation = ({ }, }); }; + +export const useExportActorLogsMutation = () => { + return useMutation({ + mutationFn: async ({ + projectNameId, + environmentNameId, + queryJson, + }: { + projectNameId: string; + environmentNameId: string; + queryJson: string; + }) => + rivetClient.actors.logs.export({ + project: projectNameId, + environment: environmentNameId, + queryJson, + }), + }); +}; diff --git a/frontend/packages/components/src/actors/actor-context.tsx b/frontend/packages/components/src/actors/actor-context.tsx index 9d3b76983d..59e5c4ce99 100644 --- a/frontend/packages/components/src/actors/actor-context.tsx +++ b/frontend/packages/components/src/actors/actor-context.tsx @@ -122,6 +122,16 @@ export const actorRegionsAtom = atom([ export const actorBuildsAtom = atom([]); +export const actorEnvironmentAtom = atom<{ projectNameId: string; environmentNameId: string } | null>(null); + +export type ExportLogsHandler = (params: { + projectNameId: string; + environmentNameId: string; + queryJson: string; +}) => Promise<{ url: string }>; + +export const exportLogsHandlerAtom = atom(null); + export const actorsInternalFilterAtom = atom<{ fn: (actor: Actor) => boolean; }>(); diff --git a/frontend/packages/components/src/actors/actor-download-logs-button.tsx b/frontend/packages/components/src/actors/actor-download-logs-button.tsx index b8dcff1d1c..4cd5878474 100644 --- a/frontend/packages/components/src/actors/actor-download-logs-button.tsx +++ b/frontend/packages/components/src/actors/actor-download-logs-button.tsx @@ -1,54 +1,59 @@ import { Button, WithTooltip } from "@rivet-gg/components"; import { Icon, faSave } from "@rivet-gg/icons"; -import saveAs from "file-saver"; -import { - type Settings, - useActorDetailsSettings, -} from "./actor-details-settings"; -import { type LogsTypeFilter, filterLogs } from "./actor-logs"; -import type { ActorAtom, LogsAtom } from "./actor-context"; -import { selectAtom } from "jotai/utils"; -import { type Atom, atom, useAtom } from "jotai"; +import { type LogsTypeFilter } from "./actor-logs"; +import type { ActorAtom } from "./actor-context"; +import { actorEnvironmentAtom, exportLogsHandlerAtom } from "./actor-context"; +import { atom, useAtom, useAtomValue } from "jotai"; +import { useState } from "react"; const downloadLogsAtom = atom( null, - ( + async ( get, _set, { + actorId, typeFilter, filter, - settings, - logs: logsAtom, }: { + actorId: string; typeFilter?: LogsTypeFilter; filter?: string; - settings: Settings; - logs: Atom; }, ) => { - const { logs } = get(get(logsAtom)); + const environment = get(actorEnvironmentAtom); + const exportHandler = get(exportLogsHandlerAtom); - const combined = filterLogs({ - typeFilter: typeFilter ?? "all", - filter: filter ?? "", - logs, - }); + if (!environment || !exportHandler) { + throw new Error("Environment or export handler not available"); + } - const lines = combined.map((log) => { - const timestamp = new Date(log.timestamp).toISOString(); - if (settings.showTimestamps) { - return `[${timestamp}] ${log.message || log.line}`; - } - return log.message || log.line; + // Build query JSON for the API + // Based on the GET logs endpoint usage, we need to build a query + const query: any = { + actorIds: [actorId], + }; + + // Add stream filter based on typeFilter + if (typeFilter === "output") { + query.stream = 0; // stdout + } else if (typeFilter === "errors") { + query.stream = 1; // stderr + } + + // Add text search if filter is provided + if (filter) { + query.searchText = filter; + } + + const result = await exportHandler({ + projectNameId: environment.projectNameId, + environmentNameId: environment.environmentNameId, + queryJson: JSON.stringify(query), }); - saveAs( - new Blob([lines.join("\n")], { - type: "text/plain;charset=utf-8", - }), - "logs.txt", - ); + // Open the presigned URL in a new tab to download + window.open(result.url, "_blank"); }, ); @@ -63,29 +68,41 @@ export function ActorDownloadLogsButton({ typeFilter, filter, }: ActorDownloadLogsButtonProps) { - const [settings] = useActorDetailsSettings(); - + const [isDownloading, setIsDownloading] = useState(false); const [, downloadLogs] = useAtom(downloadLogsAtom); + const actorData = useAtomValue(actor); + + const handleDownload = async () => { + try { + setIsDownloading(true); + await downloadLogs({ + actorId: actorData.id, + typeFilter, + filter, + }); + } catch (error) { + console.error("Failed to download logs:", error); + } finally { + setIsDownloading(false); + } + }; return ( - downloadLogs({ - typeFilter, - filter, - settings, - logs: selectAtom(actor, (a) => a.logs), - }) - } + onClick={handleDownload} + disabled={isDownloading} > - + } />