Skip to content

Commit 0ea6a76

Browse files
Fix bugs with log viewer
- Deployment status takes too long to update after it changes - Build logs never updated in realtime because the active deployment != the latest deployment
1 parent d869984 commit 0ea6a76

File tree

3 files changed

+61
-32
lines changed

3 files changed

+61
-32
lines changed

frontend/src/components/Logs.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,29 @@ type LogsProps = {
1818
type: LogType;
1919
appId?: number;
2020
follow: boolean;
21+
onLogReceived?: (
22+
line: components["schemas"]["LogLine"],
23+
pastLogsSent: boolean,
24+
) => void;
2125
};
2226

23-
export const Logs = ({ deployment, type, appId, follow }: LogsProps) => {
27+
export const Logs = ({
28+
deployment,
29+
type,
30+
appId,
31+
follow,
32+
onLogReceived,
33+
}: LogsProps) => {
2434
const [logs, setLogs] = useState<components["schemas"]["LogLine"][]>([]);
2535
const [noLogs, setNoLogs] = useState(false); // Set to true when we know there are no logs for this deployment
26-
const [done, setDone] = useState(false); // Set to true when all past logs have been sent and the viewer is up-to-date
36+
const [done, _setDone] = useState(false); // Set to true when all past logs have been sent and the viewer is up-to-date
37+
38+
const doneRef = useRef(false); // Updated at the same time as `setDone` is called so that the closure in useEventSource can access the most up-to-date value of this variable
39+
40+
const setDone = (newValue: boolean) => {
41+
doneRef.current = newValue;
42+
_setDone(newValue);
43+
};
2744

2845
const logsBody = useRef<HTMLDivElement | null>(null);
2946
const lastScroll = useRef({ scrollTop: 0, hasScrolledUp: false });
@@ -46,7 +63,7 @@ export const Logs = ({ deployment, type, appId, follow }: LogsProps) => {
4663
? `${window.location.protocol}//${window.location.host}/api/app/${appId}/logs?type=${type}`
4764
: `${window.location.protocol}//${window.location.host}/api/app/${appId}/logs?type=${type}&deploymentId=${deployment.id}`;
4865

49-
const { connecting, connected, close, reconnect } = useEventSource(
66+
const { connecting, connected, close } = useEventSource(
5067
new URL(url),
5168
["log", "pastLogsSent"],
5269
(eventName, event) => {
@@ -60,6 +77,7 @@ export const Logs = ({ deployment, type, appId, follow }: LogsProps) => {
6077
const newLine = event.data as string;
6178
setLogs((lines) => {
6279
const parsed = JSON.parse(newLine) as components["schemas"]["LogLine"];
80+
onLogReceived?.(parsed, doneRef.current);
6381
for (const existingLine of lines) {
6482
if (parsed.id && existingLine.id === parsed.id) return lines;
6583
}
@@ -79,11 +97,7 @@ export const Logs = ({ deployment, type, appId, follow }: LogsProps) => {
7997
if (!follow && done && connected) {
8098
close();
8199
}
82-
83-
if (follow && !connected && !connecting) {
84-
reconnect();
85-
}
86-
}, [done, follow, connected, connecting, close, reconnect]);
100+
}, [done, follow, connected, close]);
87101

88102
return (
89103
<>

frontend/src/hooks/useEventSource.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,51 +9,51 @@ export const useEventSource = <T extends string>(
99
const [connected, setConnected] = useState(false);
1010
const [reconnectCounter, setReconnectCounter] = useState(0);
1111
const source = useRef<EventSource | null>(null);
12+
const reconnectTimeout = useRef(500);
1213
const shouldOpen = useRef(true);
1314

14-
const setupEventSource = (
15-
reconnectCallback: (newEventSource: EventSource) => void,
16-
) => {
15+
const reconnect = () => {
16+
setReconnectCounter((current) => current + 1);
17+
};
18+
19+
const setupEventSource = () => {
1720
console.log("Creating EventSource for", url.toString());
18-
const eventSource = new EventSource(url);
1921

20-
eventSource.onopen = () => {
22+
source.current = new EventSource(url);
23+
24+
source.current.onopen = () => {
2125
setConnected(true);
2226
setHasConnected(true);
27+
reconnectTimeout.current = 500;
2328
};
24-
eventSource.onerror = () => {
29+
source.current.onerror = () => {
2530
setConnected(false);
26-
eventSource.close();
31+
source.current?.close();
2732
setTimeout(() => {
2833
if (!shouldOpen.current) return; // The component has unmounted; we shouldn't try to reconnect anymore
29-
reconnectCallback(setupEventSource(reconnectCallback));
30-
}, 500);
34+
console.log("Reconnecting");
35+
reconnect();
36+
}, reconnectTimeout.current);
37+
reconnectTimeout.current = Math.min(reconnectTimeout.current * 2, 10000);
3138
};
3239

33-
source.current = eventSource;
34-
3540
for (const eventName of eventNames) {
36-
eventSource.addEventListener(eventName, (event: MessageEvent) =>
41+
source.current.addEventListener(eventName, (event: MessageEvent) =>
3742
onMessage(eventName, event),
3843
);
3944
}
40-
41-
return eventSource;
4245
};
4346

4447
const setup = useEffectEvent(setupEventSource);
4548
const urlString = url.toString(); // Equal URLs don't have Object.is() equality, so the useEffect would be triggered on every render if we didn't convert this into a string first.
4649

4750
useEffect(() => {
4851
shouldOpen.current = true;
49-
let eventSource: EventSource;
50-
eventSource = setup((newEventSource) => {
51-
eventSource = newEventSource;
52-
});
52+
setup();
5353

5454
return () => {
5555
shouldOpen.current = false;
56-
eventSource.close();
56+
source.current?.close();
5757
};
5858
}, [urlString, reconnectCounter]);
5959

@@ -64,8 +64,5 @@ export const useEventSource = <T extends string>(
6464
source.current?.close();
6565
setConnected(false);
6666
},
67-
reconnect: () => {
68-
setReconnectCounter((current) => current + 1);
69-
},
7067
};
7168
};

frontend/src/pages/DeploymentView.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ export const DeploymentView = () => {
1515
params: { path: { appId } },
1616
});
1717

18-
const { data: deployment } = api.useSuspenseQuery(
18+
const { data: latestDeployments } = api.useSuspenseQuery(
19+
"get",
20+
"/app/{appId}/deployments",
21+
{ params: { path: { appId }, query: { page: 0, length: 1 } } },
22+
);
23+
24+
const latestDeploymentId = latestDeployments?.[0]?.id;
25+
26+
const { data: deployment, refetch: refetchDeployment } = api.useSuspenseQuery(
1927
"get",
2028
"/app/{appId}/deployments/{deploymentId}",
2129
{ params: { path: { appId, deploymentId } } },
@@ -51,7 +59,7 @@ export const DeploymentView = () => {
5159
"ERROR",
5260
].includes(deployment.status);
5361

54-
const isCurrentDeployment = app.activeDeployment == deploymentId;
62+
const isCurrentDeployment = latestDeploymentId == deploymentId;
5563

5664
return (
5765
<main className="px-8 py-10">
@@ -102,6 +110,16 @@ export const DeploymentView = () => {
102110
deployment={deployment}
103111
type="BUILD"
104112
follow={defaultLogView === "BUILD" && isCurrentDeployment}
113+
onLogReceived={(line, pastLogsSent) => {
114+
if (
115+
pastLogsSent &&
116+
(line.log === "Deployment succeeded" ||
117+
line.log.startsWith("Deployment status has been updated to "))
118+
) {
119+
// Refetch the deployment to update its status in the UI
120+
void refetchDeployment();
121+
}
122+
}}
105123
/>
106124
</TabsContent>
107125
<TabsContent value="RUNTIME">

0 commit comments

Comments
 (0)