Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/prebuilds/detail/PrebuildDetailPage.tsx
2501 views
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 { Prebuild, PrebuildPhase_Phase, TaskLog } from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";
8
import { BreadcrumbNav } from "@podkit/breadcrumbs/BreadcrumbNav";
9
import { Button } from "@podkit/buttons/Button";
10
import { FC, useCallback, useEffect, useMemo, useState } from "react";
11
import { useHistory, useParams } from "react-router";
12
import dayjs from "dayjs";
13
import { useToast } from "../../components/toasts/Toasts";
14
import {
15
isPrebuildDone,
16
useCancelPrebuildMutation,
17
usePrebuildQuery,
18
useTriggerPrebuildMutation,
19
watchPrebuild,
20
} from "../../data/prebuilds/prebuild-queries";
21
import { LinkButton } from "@podkit/buttons/LinkButton";
22
import { repositoriesRoutes } from "../../repositories/repositories.routes";
23
import { LoadingState } from "@podkit/loading/LoadingState";
24
import Alert from "../../components/Alert";
25
import { PrebuildStatus } from "../../projects/prebuild-utils";
26
import { LoadingButton } from "@podkit/buttons/LoadingButton";
27
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
28
import { Tabs, TabsList, TabsTrigger } from "@podkit/tabs/Tabs";
29
import { PrebuildTaskTab } from "./PrebuildTaskTab";
30
import type { PlainMessage } from "@bufbuild/protobuf";
31
import { PrebuildTaskErrorTab } from "./PrebuildTaskErrorTab";
32
import Tooltip from "../../components/Tooltip";
33
34
/**
35
* 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.
36
*/
37
const formatDate = (date: dayjs.Dayjs): string => {
38
if (date.isSame(dayjs(), "day")) {
39
return date.format("[today at] h:mm A");
40
}
41
42
if (date.isSame(dayjs(), "year")) {
43
return date.format("MMM D [at] h:mm A");
44
}
45
46
return date.format("MMM D, YYYY [at] h:mm A");
47
};
48
49
interface Props {
50
prebuildId: string;
51
}
52
export const PrebuildDetailPage: FC = () => {
53
const { prebuildId } = useParams<Props>();
54
55
const { data: initialPrebuild, isLoading: isInfoLoading, error, refetch } = usePrebuildQuery(prebuildId);
56
const [currentPrebuild, setCurrentPrebuild] = useState<Prebuild | undefined>();
57
58
let prebuild = initialPrebuild;
59
if (currentPrebuild && prebuildId === currentPrebuild.id) {
60
// Make sure we update only if it's the same prebuild
61
prebuild = currentPrebuild;
62
}
63
64
const history = useHistory();
65
const { toast } = useToast();
66
const [selectedTaskId, actuallySetSelectedTaskId] = useState<string | undefined>();
67
68
const hashTaskId = window.location.hash.slice(1);
69
useEffect(() => {
70
actuallySetSelectedTaskId(hashTaskId || undefined);
71
}, [hashTaskId]);
72
73
const isImageBuild =
74
prebuild?.status?.phase?.name === PrebuildPhase_Phase.QUEUED && !!prebuild.status.imageBuildLogUrl;
75
const taskId = useMemo(() => {
76
if (!prebuild) {
77
return undefined;
78
}
79
if (isImageBuild) {
80
return "image-build";
81
}
82
83
return selectedTaskId ?? prebuild?.status?.taskLogs.filter((f) => f.logUrl)[0]?.taskId ?? undefined;
84
}, [isImageBuild, prebuild, selectedTaskId]);
85
86
const triggerPrebuildMutation = useTriggerPrebuildMutation(prebuild?.configurationId, prebuild?.ref);
87
const cancelPrebuildMutation = useCancelPrebuildMutation();
88
89
const [isTriggeringNewPrebuild, setTriggeringNewPrebuild] = useState(false);
90
const triggerPrebuild = useCallback(async () => {
91
if (!prebuild) {
92
return;
93
}
94
95
try {
96
setTriggeringNewPrebuild(true);
97
await triggerPrebuildMutation.mutateAsync(undefined, {
98
onSuccess: (newPrebuildId) => {
99
history.push(repositoriesRoutes.PrebuildDetail(newPrebuildId));
100
},
101
onError: (error) => {
102
if (error instanceof ApplicationError) {
103
toast("Failed to trigger prebuild: " + error.message);
104
}
105
},
106
onSettled: () => {
107
setTriggeringNewPrebuild(false);
108
},
109
});
110
} catch (error) {
111
console.error("Could not trigger prebuild", error);
112
}
113
}, [history, prebuild, toast, triggerPrebuildMutation]);
114
115
const triggeredDate = useMemo(() => dayjs(prebuild?.status?.startTime?.toDate()), [prebuild?.status?.startTime]);
116
const triggeredString = useMemo(() => formatDate(triggeredDate), [triggeredDate]);
117
const stopDate = useMemo(() => {
118
if (!prebuild?.status?.stopTime) {
119
return undefined;
120
}
121
return dayjs(prebuild.status.stopTime.toDate());
122
}, [prebuild?.status?.stopTime]);
123
const stopString = useMemo(() => (stopDate ? formatDate(stopDate) : undefined), [stopDate]);
124
const durationString = useMemo(() => {
125
if (!prebuild?.status?.startTime || !prebuild?.status?.stopTime) {
126
return undefined;
127
}
128
const duration = dayjs.duration(
129
prebuild.status.stopTime.toDate().getTime() - prebuild.status.startTime.toDate().getTime(),
130
"milliseconds",
131
);
132
133
const s = duration.get("s");
134
const m = duration.get("m");
135
const h = duration.get("h");
136
if (h >= 1) {
137
return `${h}h ${m}m ${s}s`;
138
}
139
if (m >= 1) {
140
return `${m}m ${s}s`;
141
}
142
return `${s}s`;
143
}, [prebuild?.status?.startTime, prebuild?.status?.stopTime]);
144
145
const setSelectedTaskId = useCallback(
146
(taskId: string) => {
147
actuallySetSelectedTaskId(taskId);
148
149
history.push({
150
hash: taskId,
151
});
152
},
153
[history],
154
);
155
156
useEffect(() => {
157
const disposable = watchPrebuild(prebuildId, (prebuild) => {
158
setCurrentPrebuild(prebuild);
159
160
return isPrebuildDone(prebuild);
161
});
162
163
return () => {
164
disposable.dispose();
165
};
166
}, [prebuildId]);
167
168
const prebuildTasks = useMemo(() => {
169
const validTasks: Omit<PlainMessage<TaskLog>, "taskJson">[] =
170
prebuild?.status?.taskLogs.filter((t) => t.logUrl) ?? [];
171
if (isImageBuild) {
172
validTasks.unshift({
173
taskId: "image-build",
174
taskLabel: "Image Build",
175
logUrl: prebuild?.status?.imageBuildLogUrl!, // we know this is defined because we're in the isImageBuild branch
176
});
177
}
178
179
return validTasks;
180
}, [isImageBuild, prebuild?.status?.imageBuildLogUrl, prebuild?.status?.taskLogs]);
181
182
const notFoundError = error instanceof ApplicationError && error.code === ErrorCodes.NOT_FOUND;
183
184
const cancelPrebuild = useCallback(async () => {
185
if (!prebuild) {
186
return;
187
}
188
189
try {
190
await cancelPrebuildMutation.mutateAsync(prebuild.id);
191
} catch (error) {
192
console.error("Could not cancel prebuild", error);
193
}
194
}, [prebuild, cancelPrebuildMutation]);
195
196
return (
197
<div className="w-full">
198
<BreadcrumbNav
199
pageTitle="Prebuild history"
200
pageDescription={
201
!isInfoLoading && (
202
<>
203
<span className="font-semibold">{prebuild?.configurationName ?? "unknown repository"}</span>{" "}
204
<span className="text-pk-content-secondary">{prebuild?.ref ?? ""}</span>
205
</>
206
)
207
}
208
backLink={repositoriesRoutes.Prebuilds()}
209
/>
210
<div className="app-container mb-8">
211
{isInfoLoading && (
212
<div className="flex justify-center">
213
<LoadingState />
214
</div>
215
)}
216
{error ? (
217
<div className="flex flex-col gap-4">
218
<Alert type="error">
219
<span>Failed to load prebuild</span>
220
<pre>{notFoundError ? "Prebuild not found" : error.message}</pre>
221
</Alert>
222
{!notFoundError && (
223
<Button
224
variant="destructive"
225
onClick={() => {
226
refetch();
227
}}
228
>
229
Retry
230
</Button>
231
)}
232
</div>
233
) : (
234
prebuild && (
235
<div className={"border border-pk-border-base rounded-xl pt-6 pb-3 divide-y"}>
236
<div className="px-6 pb-4">
237
<div className="flex flex-col gap-2">
238
<div className="flex justify-between">
239
<div className="space-y-2 font-semibold text-pk-content-primary truncate">
240
{prebuild.commit?.message}{" "}
241
{prebuild.commit?.sha && (
242
<span>
243
<Tooltip content={prebuild.commit.sha}>
244
(
245
<span className="font-mono">
246
{prebuild.commit.sha.slice(0, 7)}
247
</span>
248
)
249
</Tooltip>
250
</span>
251
)}
252
<div className="flex gap-1 items-center">
253
<img
254
className="w-5 h-5 rounded-full"
255
src={prebuild.commit?.author?.avatarUrl}
256
alt=""
257
/>
258
<span className="text-pk-content-secondary">
259
{prebuild.commit?.author?.name}
260
</span>
261
</div>
262
</div>
263
<div className="text-pk-content-secondary flex-none">
264
{triggeredString && (
265
<>
266
<div>
267
Triggered:{" "}
268
<time
269
dateTime={triggeredDate.toISOString()}
270
title={triggeredDate.toString()}
271
>
272
{triggeredString}
273
</time>
274
</div>
275
{stopDate && (
276
<>
277
<div>
278
Stopped:{" "}
279
<time
280
dateTime={stopDate.toISOString()}
281
title={stopDate.toString()}
282
>
283
{stopString}
284
</time>
285
</div>
286
<div>Duration: {durationString}</div>
287
</>
288
)}
289
</>
290
)}
291
</div>
292
</div>
293
</div>
294
</div>
295
<div className="flex flex-col gap-1 border-pk-border-base">
296
<div className="py-4 px-6 flex flex-col gap-1">
297
<PrebuildStatus prebuild={prebuild} />
298
{prebuild?.status?.message && (
299
<div className="text-pk-content-secondary line-clamp-2">
300
{prebuild?.status.message}
301
</div>
302
)}
303
</div>
304
<Tabs value={taskId ?? "empty-tab"} onValueChange={setSelectedTaskId} className="p-0">
305
<TabsList className="overflow-x-auto max-w-full p-0 h-auto items-end">
306
{prebuildTasks.map((task) => (
307
<TabsTrigger
308
value={task.taskId}
309
key={prebuildId + task.taskId}
310
data-analytics={JSON.stringify({ dnt: true })}
311
className="mt-1 font-normal text-base pt-2 px-4 rounded-t-lg border border-pk-border-base border-b-0 border-l-0 data-[state=active]:bg-pk-surface-secondary data-[state=active]:z-10 data-[state=active]:relative last:mr-1"
312
disabled={task.taskId !== "image-build" && isImageBuild}
313
>
314
{task.taskLabel}
315
</TabsTrigger>
316
))}
317
</TabsList>
318
{prebuildTasks.length !== 0 ? (
319
prebuildTasks.map(({ taskId }) => (
320
<PrebuildTaskTab
321
key={prebuildId + taskId}
322
taskId={taskId}
323
prebuild={prebuild}
324
/>
325
))
326
) : (
327
<PrebuildTaskErrorTab>
328
No prebuild tasks defined in <code>.gitpod.yml</code> for this prebuild
329
</PrebuildTaskErrorTab>
330
)}
331
</Tabs>
332
</div>
333
<div className="px-6 pt-6 pb-3 flex justify-between border-pk-border-base overflow-y-hidden gap-4">
334
{[PrebuildPhase_Phase.BUILDING, PrebuildPhase_Phase.QUEUED].includes(
335
prebuild?.status?.phase?.name ?? PrebuildPhase_Phase.UNSPECIFIED,
336
) ? (
337
<LoadingButton
338
loading={cancelPrebuildMutation.isLoading}
339
disabled={cancelPrebuildMutation.isLoading}
340
onClick={cancelPrebuild}
341
variant={"destructive"}
342
>
343
Cancel Prebuild
344
</LoadingButton>
345
) : (
346
<LoadingButton
347
loading={isTriggeringNewPrebuild}
348
disabled={
349
isTriggeringNewPrebuild ||
350
!prebuild.configurationId ||
351
!prebuild.commit?.sha
352
}
353
onClick={() => triggerPrebuild()}
354
>{`Rerun Prebuild (${prebuild.ref})`}</LoadingButton>
355
)}
356
<div className="gap-4 flex justify-right">
357
<LinkButton
358
disabled={!prebuild?.id}
359
href={repositoriesRoutes.PrebuildsSettings(prebuild.configurationId)}
360
variant="secondary"
361
>
362
View Prebuild Settings
363
</LinkButton>
364
<LinkButton
365
disabled={prebuild?.status?.phase?.name !== PrebuildPhase_Phase.AVAILABLE}
366
href={`/#open-prebuild/${prebuild?.id}/${prebuild?.contextUrl}`}
367
variant="secondary"
368
>
369
Open Debug Workspace
370
</LinkButton>
371
</div>
372
</div>
373
</div>
374
)
375
)}
376
</div>
377
</div>
378
);
379
};
380
381