Path: blob/main/components/dashboard/src/prebuilds/detail/PrebuildDetailPage.tsx
2501 views
/**1* Copyright (c) 2024 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { Prebuild, PrebuildPhase_Phase, TaskLog } from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";7import { BreadcrumbNav } from "@podkit/breadcrumbs/BreadcrumbNav";8import { Button } from "@podkit/buttons/Button";9import { FC, useCallback, useEffect, useMemo, useState } from "react";10import { useHistory, useParams } from "react-router";11import dayjs from "dayjs";12import { useToast } from "../../components/toasts/Toasts";13import {14isPrebuildDone,15useCancelPrebuildMutation,16usePrebuildQuery,17useTriggerPrebuildMutation,18watchPrebuild,19} from "../../data/prebuilds/prebuild-queries";20import { LinkButton } from "@podkit/buttons/LinkButton";21import { repositoriesRoutes } from "../../repositories/repositories.routes";22import { LoadingState } from "@podkit/loading/LoadingState";23import Alert from "../../components/Alert";24import { PrebuildStatus } from "../../projects/prebuild-utils";25import { LoadingButton } from "@podkit/buttons/LoadingButton";26import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";27import { Tabs, TabsList, TabsTrigger } from "@podkit/tabs/Tabs";28import { PrebuildTaskTab } from "./PrebuildTaskTab";29import type { PlainMessage } from "@bufbuild/protobuf";30import { PrebuildTaskErrorTab } from "./PrebuildTaskErrorTab";31import Tooltip from "../../components/Tooltip";3233/**34* 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.35*/36const formatDate = (date: dayjs.Dayjs): string => {37if (date.isSame(dayjs(), "day")) {38return date.format("[today at] h:mm A");39}4041if (date.isSame(dayjs(), "year")) {42return date.format("MMM D [at] h:mm A");43}4445return date.format("MMM D, YYYY [at] h:mm A");46};4748interface Props {49prebuildId: string;50}51export const PrebuildDetailPage: FC = () => {52const { prebuildId } = useParams<Props>();5354const { data: initialPrebuild, isLoading: isInfoLoading, error, refetch } = usePrebuildQuery(prebuildId);55const [currentPrebuild, setCurrentPrebuild] = useState<Prebuild | undefined>();5657let prebuild = initialPrebuild;58if (currentPrebuild && prebuildId === currentPrebuild.id) {59// Make sure we update only if it's the same prebuild60prebuild = currentPrebuild;61}6263const history = useHistory();64const { toast } = useToast();65const [selectedTaskId, actuallySetSelectedTaskId] = useState<string | undefined>();6667const hashTaskId = window.location.hash.slice(1);68useEffect(() => {69actuallySetSelectedTaskId(hashTaskId || undefined);70}, [hashTaskId]);7172const isImageBuild =73prebuild?.status?.phase?.name === PrebuildPhase_Phase.QUEUED && !!prebuild.status.imageBuildLogUrl;74const taskId = useMemo(() => {75if (!prebuild) {76return undefined;77}78if (isImageBuild) {79return "image-build";80}8182return selectedTaskId ?? prebuild?.status?.taskLogs.filter((f) => f.logUrl)[0]?.taskId ?? undefined;83}, [isImageBuild, prebuild, selectedTaskId]);8485const triggerPrebuildMutation = useTriggerPrebuildMutation(prebuild?.configurationId, prebuild?.ref);86const cancelPrebuildMutation = useCancelPrebuildMutation();8788const [isTriggeringNewPrebuild, setTriggeringNewPrebuild] = useState(false);89const triggerPrebuild = useCallback(async () => {90if (!prebuild) {91return;92}9394try {95setTriggeringNewPrebuild(true);96await triggerPrebuildMutation.mutateAsync(undefined, {97onSuccess: (newPrebuildId) => {98history.push(repositoriesRoutes.PrebuildDetail(newPrebuildId));99},100onError: (error) => {101if (error instanceof ApplicationError) {102toast("Failed to trigger prebuild: " + error.message);103}104},105onSettled: () => {106setTriggeringNewPrebuild(false);107},108});109} catch (error) {110console.error("Could not trigger prebuild", error);111}112}, [history, prebuild, toast, triggerPrebuildMutation]);113114const triggeredDate = useMemo(() => dayjs(prebuild?.status?.startTime?.toDate()), [prebuild?.status?.startTime]);115const triggeredString = useMemo(() => formatDate(triggeredDate), [triggeredDate]);116const stopDate = useMemo(() => {117if (!prebuild?.status?.stopTime) {118return undefined;119}120return dayjs(prebuild.status.stopTime.toDate());121}, [prebuild?.status?.stopTime]);122const stopString = useMemo(() => (stopDate ? formatDate(stopDate) : undefined), [stopDate]);123const durationString = useMemo(() => {124if (!prebuild?.status?.startTime || !prebuild?.status?.stopTime) {125return undefined;126}127const duration = dayjs.duration(128prebuild.status.stopTime.toDate().getTime() - prebuild.status.startTime.toDate().getTime(),129"milliseconds",130);131132const s = duration.get("s");133const m = duration.get("m");134const h = duration.get("h");135if (h >= 1) {136return `${h}h ${m}m ${s}s`;137}138if (m >= 1) {139return `${m}m ${s}s`;140}141return `${s}s`;142}, [prebuild?.status?.startTime, prebuild?.status?.stopTime]);143144const setSelectedTaskId = useCallback(145(taskId: string) => {146actuallySetSelectedTaskId(taskId);147148history.push({149hash: taskId,150});151},152[history],153);154155useEffect(() => {156const disposable = watchPrebuild(prebuildId, (prebuild) => {157setCurrentPrebuild(prebuild);158159return isPrebuildDone(prebuild);160});161162return () => {163disposable.dispose();164};165}, [prebuildId]);166167const prebuildTasks = useMemo(() => {168const validTasks: Omit<PlainMessage<TaskLog>, "taskJson">[] =169prebuild?.status?.taskLogs.filter((t) => t.logUrl) ?? [];170if (isImageBuild) {171validTasks.unshift({172taskId: "image-build",173taskLabel: "Image Build",174logUrl: prebuild?.status?.imageBuildLogUrl!, // we know this is defined because we're in the isImageBuild branch175});176}177178return validTasks;179}, [isImageBuild, prebuild?.status?.imageBuildLogUrl, prebuild?.status?.taskLogs]);180181const notFoundError = error instanceof ApplicationError && error.code === ErrorCodes.NOT_FOUND;182183const cancelPrebuild = useCallback(async () => {184if (!prebuild) {185return;186}187188try {189await cancelPrebuildMutation.mutateAsync(prebuild.id);190} catch (error) {191console.error("Could not cancel prebuild", error);192}193}, [prebuild, cancelPrebuildMutation]);194195return (196<div className="w-full">197<BreadcrumbNav198pageTitle="Prebuild history"199pageDescription={200!isInfoLoading && (201<>202<span className="font-semibold">{prebuild?.configurationName ?? "unknown repository"}</span>{" "}203<span className="text-pk-content-secondary">{prebuild?.ref ?? ""}</span>204</>205)206}207backLink={repositoriesRoutes.Prebuilds()}208/>209<div className="app-container mb-8">210{isInfoLoading && (211<div className="flex justify-center">212<LoadingState />213</div>214)}215{error ? (216<div className="flex flex-col gap-4">217<Alert type="error">218<span>Failed to load prebuild</span>219<pre>{notFoundError ? "Prebuild not found" : error.message}</pre>220</Alert>221{!notFoundError && (222<Button223variant="destructive"224onClick={() => {225refetch();226}}227>228Retry229</Button>230)}231</div>232) : (233prebuild && (234<div className={"border border-pk-border-base rounded-xl pt-6 pb-3 divide-y"}>235<div className="px-6 pb-4">236<div className="flex flex-col gap-2">237<div className="flex justify-between">238<div className="space-y-2 font-semibold text-pk-content-primary truncate">239{prebuild.commit?.message}{" "}240{prebuild.commit?.sha && (241<span>242<Tooltip content={prebuild.commit.sha}>243(244<span className="font-mono">245{prebuild.commit.sha.slice(0, 7)}246</span>247)248</Tooltip>249</span>250)}251<div className="flex gap-1 items-center">252<img253className="w-5 h-5 rounded-full"254src={prebuild.commit?.author?.avatarUrl}255alt=""256/>257<span className="text-pk-content-secondary">258{prebuild.commit?.author?.name}259</span>260</div>261</div>262<div className="text-pk-content-secondary flex-none">263{triggeredString && (264<>265<div>266Triggered:{" "}267<time268dateTime={triggeredDate.toISOString()}269title={triggeredDate.toString()}270>271{triggeredString}272</time>273</div>274{stopDate && (275<>276<div>277Stopped:{" "}278<time279dateTime={stopDate.toISOString()}280title={stopDate.toString()}281>282{stopString}283</time>284</div>285<div>Duration: {durationString}</div>286</>287)}288</>289)}290</div>291</div>292</div>293</div>294<div className="flex flex-col gap-1 border-pk-border-base">295<div className="py-4 px-6 flex flex-col gap-1">296<PrebuildStatus prebuild={prebuild} />297{prebuild?.status?.message && (298<div className="text-pk-content-secondary line-clamp-2">299{prebuild?.status.message}300</div>301)}302</div>303<Tabs value={taskId ?? "empty-tab"} onValueChange={setSelectedTaskId} className="p-0">304<TabsList className="overflow-x-auto max-w-full p-0 h-auto items-end">305{prebuildTasks.map((task) => (306<TabsTrigger307value={task.taskId}308key={prebuildId + task.taskId}309data-analytics={JSON.stringify({ dnt: true })}310className="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"311disabled={task.taskId !== "image-build" && isImageBuild}312>313{task.taskLabel}314</TabsTrigger>315))}316</TabsList>317{prebuildTasks.length !== 0 ? (318prebuildTasks.map(({ taskId }) => (319<PrebuildTaskTab320key={prebuildId + taskId}321taskId={taskId}322prebuild={prebuild}323/>324))325) : (326<PrebuildTaskErrorTab>327No prebuild tasks defined in <code>.gitpod.yml</code> for this prebuild328</PrebuildTaskErrorTab>329)}330</Tabs>331</div>332<div className="px-6 pt-6 pb-3 flex justify-between border-pk-border-base overflow-y-hidden gap-4">333{[PrebuildPhase_Phase.BUILDING, PrebuildPhase_Phase.QUEUED].includes(334prebuild?.status?.phase?.name ?? PrebuildPhase_Phase.UNSPECIFIED,335) ? (336<LoadingButton337loading={cancelPrebuildMutation.isLoading}338disabled={cancelPrebuildMutation.isLoading}339onClick={cancelPrebuild}340variant={"destructive"}341>342Cancel Prebuild343</LoadingButton>344) : (345<LoadingButton346loading={isTriggeringNewPrebuild}347disabled={348isTriggeringNewPrebuild ||349!prebuild.configurationId ||350!prebuild.commit?.sha351}352onClick={() => triggerPrebuild()}353>{`Rerun Prebuild (${prebuild.ref})`}</LoadingButton>354)}355<div className="gap-4 flex justify-right">356<LinkButton357disabled={!prebuild?.id}358href={repositoriesRoutes.PrebuildsSettings(prebuild.configurationId)}359variant="secondary"360>361View Prebuild Settings362</LinkButton>363<LinkButton364disabled={prebuild?.status?.phase?.name !== PrebuildPhase_Phase.AVAILABLE}365href={`/#open-prebuild/${prebuild?.id}/${prebuild?.contextUrl}`}366variant="secondary"367>368Open Debug Workspace369</LinkButton>370</div>371</div>372</div>373)374)}375</div>376</div>377);378};379380381