Skip to content

Commit e6f07ee

Browse files
Cleanup watching prebuilds (#19969)
* Fix dropped errors on available prebuilds * Watch prebuild cleanup * Remove extra file change from merge resolve * Make not found errors task-specific * Remove suspense where it's not needed * Fix doubled logs
1 parent f3ceb6b commit e6f07ee

File tree

4 files changed

+80
-74
lines changed

4 files changed

+80
-74
lines changed

components/dashboard/src/data/prebuilds/prebuild-logs-emitter.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { prebuildClient } from "../../service/public-api";
8-
import { useEffect, useState } from "react";
8+
import { useEffect, useMemo, useState } from "react";
99
import { matchPrebuildError, onDownloadPrebuildLogsUrl } from "@gitpod/public-api-common/lib/prebuild-utils";
1010
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
1111
import { ReplayableEventEmitter } from "../../utils";
@@ -23,7 +23,12 @@ type LogEventTypes = {
2323
* @param taskId ID of the task to watch.
2424
*/
2525
export function usePrebuildLogsEmitter(prebuildId: string, taskId: string) {
26-
const [emitter] = useState(new ReplayableEventEmitter<LogEventTypes>());
26+
const emitter = useMemo(
27+
() => new ReplayableEventEmitter<LogEventTypes>(),
28+
// We would like to re-create the emitter when the prebuildId or taskId changes, so that logs of old tasks / prebuilds are not mixed with the new ones.
29+
// eslint-disable-next-line react-hooks/exhaustive-deps
30+
[prebuildId, taskId],
31+
);
2732
const [disposable, setDisposable] = useState<Disposable | undefined>();
2833

2934
useEffect(() => {

components/dashboard/src/prebuilds/detail/PrebuildDetailPage.tsx

Lines changed: 26 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66

77
import { Prebuild, PrebuildPhase_Phase, TaskLog } from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";
88
import { BreadcrumbNav } from "@podkit/breadcrumbs/BreadcrumbNav";
9-
import { Text } from "@podkit/typography/Text";
109
import { Button } from "@podkit/buttons/Button";
11-
import { FC, Suspense, useCallback, useEffect, useMemo, useState } from "react";
10+
import { FC, useCallback, useEffect, useMemo, useState } from "react";
1211
import { Redirect, useHistory, useParams } from "react-router";
1312
import dayjs from "dayjs";
1413
import { useToast } from "../../components/toasts/Toasts";
@@ -26,9 +25,10 @@ import Alert from "../../components/Alert";
2625
import { PrebuildStatus } from "../../projects/prebuild-utils";
2726
import { LoadingButton } from "@podkit/buttons/LoadingButton";
2827
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
29-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@podkit/tabs/Tabs";
28+
import { Tabs, TabsList, TabsTrigger } from "@podkit/tabs/Tabs";
3029
import { PrebuildTaskTab } from "./PrebuildTaskTab";
3130
import type { PlainMessage } from "@bufbuild/protobuf";
31+
import { PrebuildTaskErrorTab } from "./PrebuildTaskErrorTab";
3232

3333
/**
3434
* Formats a date. For today, it returns the time. For this year, it returns the month and day and time. Otherwise, it returns the full date and time.
@@ -52,29 +52,28 @@ interface Props {
5252
export const PrebuildDetailPage: FC = () => {
5353
const { prebuildId } = useParams<Props>();
5454

55-
const { data: prebuild, isLoading: isInfoLoading, error, refetch } = usePrebuildQuery(prebuildId);
55+
const { data: initialPrebuild, isLoading: isInfoLoading, error, refetch } = usePrebuildQuery(prebuildId);
56+
const [currentPrebuild, setCurrentPrebuild] = useState<Prebuild | undefined>();
57+
const prebuild = currentPrebuild ?? initialPrebuild;
5658

5759
const history = useHistory();
5860
const { toast, dismissToast } = useToast();
59-
const [currentPrebuild, setCurrentPrebuild] = useState<Prebuild | undefined>();
60-
const [logNotFound, setLogNotFound] = useState(false);
6161
const [selectedTaskId, actuallySetSelectedTaskId] = useState<string | undefined>(
6262
window.location.hash.slice(1) || undefined,
6363
);
6464

6565
const isImageBuild =
66-
currentPrebuild?.status?.phase?.name === PrebuildPhase_Phase.QUEUED &&
67-
!!currentPrebuild.status.imageBuildLogUrl;
66+
prebuild?.status?.phase?.name === PrebuildPhase_Phase.QUEUED && !!prebuild.status.imageBuildLogUrl;
6867
const taskId = useMemo(() => {
69-
if (!currentPrebuild) {
68+
if (!prebuild) {
7069
return undefined;
7170
}
7271
if (isImageBuild) {
7372
return "image-build";
7473
}
7574

76-
return selectedTaskId ?? currentPrebuild?.status?.taskLogs.filter((f) => f.logUrl)[0]?.taskId ?? undefined;
77-
}, [currentPrebuild, isImageBuild, selectedTaskId]);
75+
return selectedTaskId ?? prebuild?.status?.taskLogs.filter((f) => f.logUrl)[0]?.taskId ?? undefined;
76+
}, [isImageBuild, prebuild, selectedTaskId]);
7877

7978
const {
8079
isFetching: isTriggeringPrebuild,
@@ -101,11 +100,7 @@ export const PrebuildDetailPage: FC = () => {
101100
);
102101

103102
useEffect(() => {
104-
setLogNotFound(false);
105103
const disposable = watchPrebuild(prebuildId, (prebuild) => {
106-
if (currentPrebuild?.status?.phase?.name === PrebuildPhase_Phase.ABORTED) {
107-
return true;
108-
}
109104
setCurrentPrebuild(prebuild);
110105

111106
return isPrebuildDone(prebuild);
@@ -114,21 +109,21 @@ export const PrebuildDetailPage: FC = () => {
114109
return () => {
115110
disposable.dispose();
116111
};
117-
}, [prebuildId, currentPrebuild?.status?.phase?.name]);
112+
}, [prebuildId]);
118113

119114
const prebuildTasks = useMemo(() => {
120115
const validTasks: Omit<PlainMessage<TaskLog>, "taskJson">[] =
121-
currentPrebuild?.status?.taskLogs.filter((t) => t.logUrl) ?? [];
116+
prebuild?.status?.taskLogs.filter((t) => t.logUrl) ?? [];
122117
if (isImageBuild) {
123118
validTasks.unshift({
124119
taskId: "image-build",
125120
taskLabel: "Image Build",
126-
logUrl: currentPrebuild?.status?.imageBuildLogUrl!, // we know this is defined because we're in the isImageBuild branch
121+
logUrl: prebuild?.status?.imageBuildLogUrl!, // we know this is defined because we're in the isImageBuild branch
127122
});
128123
}
129124

130125
return validTasks;
131-
}, [currentPrebuild?.status, isImageBuild]);
126+
}, [isImageBuild, prebuild?.status?.imageBuildLogUrl, prebuild?.status?.taskLogs]);
132127

133128
useEffect(() => {
134129
history.listen(() => {
@@ -157,9 +152,8 @@ export const PrebuildDetailPage: FC = () => {
157152
}
158153
}, [prebuild, cancelPrebuildMutation]);
159154

160-
const isError = logNotFound || prebuildTasks.length === 0;
161-
162-
if (newPrebuildID) {
155+
// For some reason, we sometimes hit a case where the newPrebuildID is actually set without us triggering the query.
156+
if (newPrebuildID && prebuild?.id !== newPrebuildID) {
163157
return <Redirect to={repositoriesRoutes.PrebuildDetail(newPrebuildID)} />;
164158
}
165159

@@ -201,8 +195,7 @@ export const PrebuildDetailPage: FC = () => {
201195
)}
202196
</div>
203197
) : (
204-
prebuild &&
205-
currentPrebuild && (
198+
prebuild && (
206199
<div className={"border border-pk-border-base rounded-xl py-6 divide-y"}>
207200
<div className="px-6 pb-4">
208201
<div className="flex flex-col gap-2">
@@ -236,10 +229,10 @@ export const PrebuildDetailPage: FC = () => {
236229
</div>
237230
<div className="flex flex-col gap-1 border-pk-border-base">
238231
<div className="py-4 px-6 flex flex-col gap-1">
239-
<PrebuildStatus prebuild={currentPrebuild} />
240-
{currentPrebuild?.status?.message && (
232+
<PrebuildStatus prebuild={prebuild} />
233+
{prebuild?.status?.message && (
241234
<div className="text-pk-content-secondary truncate">
242-
{currentPrebuild?.status.message}
235+
{prebuild?.status.message}
243236
</div>
244237
)}
245238
</div>
@@ -257,54 +250,20 @@ export const PrebuildDetailPage: FC = () => {
257250
</TabsTrigger>
258251
))}
259252
</TabsList>
260-
{!isError ? (
253+
{prebuildTasks.length !== 0 ? (
261254
prebuildTasks.map(({ taskId }) => (
262-
<PrebuildTaskTab
263-
key={taskId}
264-
taskId={taskId}
265-
prebuild={currentPrebuild}
266-
onLogNotFound={() => setLogNotFound(true)}
267-
/>
255+
<PrebuildTaskTab key={taskId} taskId={taskId} prebuild={prebuild} />
268256
))
269257
) : (
270-
<TabsContent
271-
value={taskId ?? "empty-tab"}
272-
className="h-112 mt-0 border-pk-border-base"
273-
>
274-
<Suspense fallback={<div />}>
275-
<div className="px-6 py-4 h-full w-full bg-pk-surface-primary text-base flex items-center justify-center">
276-
<Text className="w-80 text-center">
277-
{logNotFound ? (
278-
<>
279-
Logs of this prebuild are inaccessible. Use{" "}
280-
<code>gp validate --prebuild --headless</code> in a
281-
workspace to see logs and debug prebuild issues.{" "}
282-
<a
283-
href="https://www.gitpod.io/docs/configure/workspaces#validate-your-gitpod-configuration"
284-
target="_blank"
285-
rel="noreferrer noopener"
286-
className="gp-link"
287-
>
288-
Learn more
289-
</a>
290-
.
291-
</>
292-
) : (
293-
<>
294-
No prebuild tasks defined in <code>.gitpod.yml</code>{" "}
295-
for this prebuild
296-
</>
297-
)}
298-
</Text>
299-
</div>
300-
</Suspense>
301-
</TabsContent>
258+
<PrebuildTaskErrorTab>
259+
No prebuild tasks defined in <code>.gitpod.yml</code> for this prebuild
260+
</PrebuildTaskErrorTab>
302261
)}
303262
</Tabs>
304263
</div>
305264
<div className="px-6 pt-6 flex justify-between border-pk-border-base">
306265
{[PrebuildPhase_Phase.BUILDING, PrebuildPhase_Phase.QUEUED].includes(
307-
currentPrebuild?.status?.phase?.name ?? PrebuildPhase_Phase.UNSPECIFIED,
266+
prebuild?.status?.phase?.name ?? PrebuildPhase_Phase.UNSPECIFIED,
308267
) ? (
309268
<LoadingButton
310269
loading={cancelPrebuildMutation.isLoading}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { TabsContent } from "@podkit/tabs/Tabs";
8+
import { PropsWithChildren } from "react";
9+
import { Text } from "@podkit/typography/Text";
10+
11+
type Props = {
12+
taskId?: string;
13+
};
14+
export const PrebuildTaskErrorTab = ({ taskId, children }: PropsWithChildren<Props>) => {
15+
return (
16+
<TabsContent value={taskId ?? "empty-tab"} className="h-112 mt-0 border-pk-border-base">
17+
<div className="px-6 py-4 h-full w-full bg-pk-surface-primary text-base flex items-center justify-center">
18+
<Text className="w-80 text-center">{children}</Text>
19+
</div>
20+
</TabsContent>
21+
);
22+
};

components/dashboard/src/prebuilds/detail/PrebuildTaskTab.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ import { useToast } from "../../components/toasts/Toasts";
1212
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
1313
import { TabsContent } from "@podkit/tabs/Tabs";
1414
import { PersistedToastID } from "./PrebuildDetailPage";
15+
import { PrebuildTaskErrorTab } from "./PrebuildTaskErrorTab";
1516

1617
const WorkspaceLogs = React.lazy(() => import("../../components/WorkspaceLogs"));
1718

1819
type Props = {
1920
taskId: string;
2021
prebuild: Prebuild;
21-
onLogNotFound: () => void;
2222
};
23-
export const PrebuildTaskTab = ({ taskId, prebuild, onLogNotFound }: Props) => {
23+
export const PrebuildTaskTab = ({ taskId, prebuild }: Props) => {
2424
const { emitter: logEmitter, disposable: disposeStreamingLogs } = usePrebuildLogsEmitter(prebuild.id, taskId);
25+
const [error, setError] = React.useState<ApplicationError | undefined>();
2526
const { toast } = useToast();
2627

2728
useEffect(() => {
@@ -36,7 +37,7 @@ export const PrebuildTaskTab = ({ taskId, prebuild, onLogNotFound }: Props) => {
3637
return;
3738
}
3839
if (err instanceof ApplicationError && err.code === ErrorCodes.NOT_FOUND) {
39-
// We don't want to show a toast for this error, because it's handled by `notFoundError` in `PrebuildDetailPage`
40+
// We don't want to show a toast for this error, we handle it in the UI
4041
return;
4142
}
4243
if (err?.message) {
@@ -46,7 +47,7 @@ export const PrebuildTaskTab = ({ taskId, prebuild, onLogNotFound }: Props) => {
4647

4748
const logErrorListener = (err: ApplicationError) => {
4849
if (err.code === ErrorCodes.NOT_FOUND) {
49-
onLogNotFound();
50+
setError(err);
5051
return;
5152
}
5253

@@ -59,8 +60,27 @@ export const PrebuildTaskTab = ({ taskId, prebuild, onLogNotFound }: Props) => {
5960
return () => {
6061
logEmitter.removeListener("error", errorListener);
6162
logEmitter.removeListener("logs-error", logErrorListener);
63+
setError(undefined);
6264
};
63-
}, [logEmitter, onLogNotFound, taskId, toast]);
65+
}, [logEmitter, taskId, toast]);
66+
67+
if (error) {
68+
return (
69+
<PrebuildTaskErrorTab taskId={taskId}>
70+
Logs of this prebuild task are inaccessible. Use <code>gp validate --prebuild --headless</code> in a
71+
workspace to see logs and debug prebuild issues.{" "}
72+
<a
73+
href="https://www.gitpod.io/docs/configure/workspaces#validate-your-gitpod-configuration"
74+
target="_blank"
75+
rel="noreferrer noopener"
76+
className="gp-link"
77+
>
78+
Learn more
79+
</a>
80+
.
81+
</PrebuildTaskErrorTab>
82+
);
83+
}
6484

6585
return (
6686
<TabsContent value={taskId} className="h-112 mt-0 border-pk-border-base">

0 commit comments

Comments
 (0)