Skip to content

Commit e8c8532

Browse files
NathanFlurryMasterPtato
authored andcommitted
chore: update download logs button to use export
1 parent df80051 commit e8c8532

File tree

4 files changed

+110
-46
lines changed

4 files changed

+110
-46
lines changed

frontend/apps/hub/src/domains/project/components/actors/actors-provider.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { router } from "@/app";
2-
import { queryClient } from "@/queries/global";
2+
import { queryClient, rivetClient } from "@/queries/global";
33
import { type FilterValue, toRecord } from "@rivet-gg/components";
44
import {
55
currentActorIdAtom,
@@ -16,6 +16,8 @@ import {
1616
actorsQueryAtom,
1717
actorsInternalFilterAtom,
1818
type Actor,
19+
actorEnvironmentAtom,
20+
exportLogsHandlerAtom,
1921
} from "@rivet-gg/components/actors";
2022
import {
2123
InfiniteQueryObserver,
@@ -78,6 +80,22 @@ export function ActorsProvider({
7880
store.set(currentActorIdAtom, actorId);
7981
}, [actorId]);
8082

83+
// biome-ignore lint/correctness/useExhaustiveDependencies: store is not a dependency
84+
useEffect(() => {
85+
store.set(actorEnvironmentAtom, { projectNameId, environmentNameId });
86+
}, [projectNameId, environmentNameId]);
87+
88+
// biome-ignore lint/correctness/useExhaustiveDependencies: store is not a dependency
89+
useEffect(() => {
90+
store.set(exportLogsHandlerAtom, async ({ projectNameId, environmentNameId, queryJson }) => {
91+
return rivetClient.actors.logs.export({
92+
project: projectNameId,
93+
environment: environmentNameId,
94+
queryJson,
95+
});
96+
});
97+
}, []);
98+
8199
// biome-ignore lint/correctness/useExhaustiveDependencies: store is not a dependency
82100
useEffect(() => {
83101
store.set(actorFiltersAtom, {

frontend/apps/hub/src/domains/project/queries/actors/mutations.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,22 @@ export const useDeleteRouteMutation = ({
314314
},
315315
});
316316
};
317+
318+
export const useExportActorLogsMutation = () => {
319+
return useMutation({
320+
mutationFn: async ({
321+
projectNameId,
322+
environmentNameId,
323+
queryJson,
324+
}: {
325+
projectNameId: string;
326+
environmentNameId: string;
327+
queryJson: string;
328+
}) =>
329+
rivetClient.actors.logs.export({
330+
project: projectNameId,
331+
environment: environmentNameId,
332+
queryJson,
333+
}),
334+
});
335+
};

frontend/packages/components/src/actors/actor-context.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ export const actorRegionsAtom = atom<Region[]>([
122122

123123
export const actorBuildsAtom = atom<Build[]>([]);
124124

125+
export const actorEnvironmentAtom = atom<{ projectNameId: string; environmentNameId: string } | null>(null);
126+
127+
export type ExportLogsHandler = (params: {
128+
projectNameId: string;
129+
environmentNameId: string;
130+
queryJson: string;
131+
}) => Promise<{ url: string }>;
132+
133+
export const exportLogsHandlerAtom = atom<ExportLogsHandler | null>(null);
134+
125135
export const actorsInternalFilterAtom = atom<{
126136
fn: (actor: Actor) => boolean;
127137
}>();

frontend/packages/components/src/actors/actor-download-logs-button.tsx

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,59 @@
11
import { Button, WithTooltip } from "@rivet-gg/components";
22
import { Icon, faSave } from "@rivet-gg/icons";
3-
import saveAs from "file-saver";
4-
import {
5-
type Settings,
6-
useActorDetailsSettings,
7-
} from "./actor-details-settings";
8-
import { type LogsTypeFilter, filterLogs } from "./actor-logs";
9-
import type { ActorAtom, LogsAtom } from "./actor-context";
10-
import { selectAtom } from "jotai/utils";
11-
import { type Atom, atom, useAtom } from "jotai";
3+
import { type LogsTypeFilter } from "./actor-logs";
4+
import type { ActorAtom } from "./actor-context";
5+
import { actorEnvironmentAtom, exportLogsHandlerAtom } from "./actor-context";
6+
import { atom, useAtom, useAtomValue } from "jotai";
7+
import { useState } from "react";
128

139
const downloadLogsAtom = atom(
1410
null,
15-
(
11+
async (
1612
get,
1713
_set,
1814
{
15+
actorId,
1916
typeFilter,
2017
filter,
21-
settings,
22-
logs: logsAtom,
2318
}: {
19+
actorId: string;
2420
typeFilter?: LogsTypeFilter;
2521
filter?: string;
26-
settings: Settings;
27-
logs: Atom<LogsAtom>;
2822
},
2923
) => {
30-
const { logs } = get(get(logsAtom));
24+
const environment = get(actorEnvironmentAtom);
25+
const exportHandler = get(exportLogsHandlerAtom);
3126

32-
const combined = filterLogs({
33-
typeFilter: typeFilter ?? "all",
34-
filter: filter ?? "",
35-
logs,
36-
});
27+
if (!environment || !exportHandler) {
28+
throw new Error("Environment or export handler not available");
29+
}
3730

38-
const lines = combined.map((log) => {
39-
const timestamp = new Date(log.timestamp).toISOString();
40-
if (settings.showTimestamps) {
41-
return `[${timestamp}] ${log.message || log.line}`;
42-
}
43-
return log.message || log.line;
31+
// Build query JSON for the API
32+
// Based on the GET logs endpoint usage, we need to build a query
33+
const query: any = {
34+
actorIds: [actorId],
35+
};
36+
37+
// Add stream filter based on typeFilter
38+
if (typeFilter === "output") {
39+
query.stream = 0; // stdout
40+
} else if (typeFilter === "errors") {
41+
query.stream = 1; // stderr
42+
}
43+
44+
// Add text search if filter is provided
45+
if (filter) {
46+
query.searchText = filter;
47+
}
48+
49+
const result = await exportHandler({
50+
projectNameId: environment.projectNameId,
51+
environmentNameId: environment.environmentNameId,
52+
queryJson: JSON.stringify(query),
4453
});
4554

46-
saveAs(
47-
new Blob([lines.join("\n")], {
48-
type: "text/plain;charset=utf-8",
49-
}),
50-
"logs.txt",
51-
);
55+
// Open the presigned URL in a new tab to download
56+
window.open(result.url, "_blank");
5257
},
5358
);
5459

@@ -63,29 +68,41 @@ export function ActorDownloadLogsButton({
6368
typeFilter,
6469
filter,
6570
}: ActorDownloadLogsButtonProps) {
66-
const [settings] = useActorDetailsSettings();
67-
71+
const [isDownloading, setIsDownloading] = useState(false);
6872
const [, downloadLogs] = useAtom(downloadLogsAtom);
73+
const actorData = useAtomValue(actor);
74+
75+
const handleDownload = async () => {
76+
try {
77+
setIsDownloading(true);
78+
await downloadLogs({
79+
actorId: actorData.id,
80+
typeFilter,
81+
filter,
82+
});
83+
} catch (error) {
84+
console.error("Failed to download logs:", error);
85+
} finally {
86+
setIsDownloading(false);
87+
}
88+
};
6989

7090
return (
7191
<WithTooltip
72-
content="Download logs"
92+
content="Export logs"
7393
trigger={
7494
<Button
7595
className="ml-2 place-self-center"
7696
variant="outline"
77-
aria-label="Download logs"
97+
aria-label="Export logs"
7898
size="icon-sm"
79-
onClick={() =>
80-
downloadLogs({
81-
typeFilter,
82-
filter,
83-
settings,
84-
logs: selectAtom(actor, (a) => a.logs),
85-
})
86-
}
99+
onClick={handleDownload}
100+
disabled={isDownloading}
87101
>
88-
<Icon icon={faSave} />
102+
<Icon
103+
icon={faSave}
104+
className={isDownloading ? "animate-pulse" : ""}
105+
/>
89106
</Button>
90107
}
91108
/>

0 commit comments

Comments
 (0)