Skip to content

Commit c80ce48

Browse files
authored
[server] allow fetching task specific prebuild-logs (#19812)
* [server] allow fetching task specific prebuild-logs * [dashboard] prebuild details shows tabs per task
1 parent 6f5d953 commit c80ce48

File tree

17 files changed

+432
-143
lines changed

17 files changed

+432
-143
lines changed

components/dashboard/src/components/WorkspaceLogs.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ import { cn } from "@podkit/lib/cn";
1515

1616
const darkTheme: ITheme = {
1717
// What written on DevTool dark:bg-gray-800 is
18-
background: "rgb(35,33,30)", // Tailwind's warmGray 800 https://tailwindcss.com/docs/customizing-colors
18+
background: "#23211E", // Tailwind's warmGray 50 https://tailwindcss.com/docs/customizing-colors
1919
};
2020
const lightTheme: ITheme = {
21-
background: "#F5F5F4", // Tailwind's warmGray 100 https://tailwindcss.com/docs/customizing-colors
21+
background: "#F9F9F9", // Tailwind's warmGray 800 https://tailwindcss.com/docs/customizing-colors
2222
foreground: "#78716C", // Tailwind's warmGray 500 https://tailwindcss.com/docs/customizing-colors
2323
cursor: "#78716C", // Tailwind's warmGray 500 https://tailwindcss.com/docs/customizing-colors
2424
};
@@ -62,7 +62,7 @@ export default function WorkspaceLogs(props: WorkspaceLogsProps) {
6262
terminal.dispose();
6363
};
6464
// eslint-disable-next-line react-hooks/exhaustive-deps
65-
}, []);
65+
}, [props.logsEmitter]);
6666

6767
const resizeDebounced = debounce(
6868
() => {
@@ -101,7 +101,7 @@ export default function WorkspaceLogs(props: WorkspaceLogsProps) {
101101
<div
102102
className={cn(
103103
props.classes || "mt-6 h-72 w-11/12 lg:w-3/5 rounded-xl overflow-hidden",
104-
"bg-gray-100 dark:bg-gray-800 relative text-left",
104+
"bg-pk-surface-secondary relative text-left",
105105
)}
106106
>
107107
<div

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

+9-5
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,32 @@ import { useEffect, useState } from "react";
1010
import { matchPrebuildError, onDownloadPrebuildLogsUrl } from "@gitpod/public-api-common/lib/prebuild-utils";
1111
import { Disposable } from "@gitpod/gitpod-protocol";
1212

13-
export function usePrebuildLogsEmitter(prebuildId: string) {
13+
export function usePrebuildLogsEmitter(prebuildId: string, taskId: string) {
1414
const [emitter] = useState(new EventEmitter());
1515
const [isLoading, setIsLoading] = useState(true);
1616
const [disposable, setDisposable] = useState<Disposable | undefined>();
1717

1818
useEffect(() => {
1919
setIsLoading(true);
20-
}, [prebuildId]);
20+
}, [prebuildId, taskId]);
2121

2222
useEffect(() => {
2323
const controller = new AbortController();
2424
const watch = async () => {
25+
if (!prebuildId || !taskId) {
26+
return;
27+
}
2528
let dispose: () => void | undefined;
2629
controller.signal.addEventListener("abort", () => {
2730
dispose?.();
2831
});
2932
const prebuild = await prebuildClient.getPrebuild({ prebuildId });
30-
if (!prebuild.prebuild?.status?.logUrl) {
33+
const task = prebuild.prebuild?.status?.taskLogs?.find((log) => log.taskId === taskId);
34+
if (!task) {
3135
throw new Error("no prebuild logs url found");
3236
}
3337
dispose = onDownloadPrebuildLogsUrl(
34-
prebuild.prebuild.status.logUrl,
38+
task.logUrl,
3539
(msg) => {
3640
const error = matchPrebuildError(msg);
3741
if (!error) {
@@ -62,6 +66,6 @@ export function usePrebuildLogsEmitter(prebuildId: string) {
6266
return () => {
6367
controller.abort();
6468
};
65-
}, [emitter, prebuildId]);
69+
}, [emitter, prebuildId, taskId]);
6670
return { emitter, isLoading, disposable };
6771
}

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

+44-5
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,15 @@ export const PrebuildDetailPage: FC = () => {
6262
const { toast, dismissToast } = useToast();
6363
const [currentPrebuild, setCurrentPrebuild] = useState<Prebuild | undefined>();
6464
const [logNotFound, setLogNotFound] = useState(false);
65+
const [selectedTaskId, setSelectedTaskId] = useState<string | undefined>(undefined);
66+
67+
const taskId = selectedTaskId ?? prebuild?.status?.taskLogs.filter((f) => f.logUrl)[0]?.taskId ?? "0";
6568

6669
const {
6770
emitter: logEmitter,
6871
isLoading: isStreamingLogs,
6972
disposable: disposeStreamingLogs,
70-
} = usePrebuildLogsEmitter(prebuildId);
73+
} = usePrebuildLogsEmitter(prebuildId, taskId);
7174
const {
7275
isFetching: isTriggeringPrebuild,
7376
refetch: triggerPrebuild,
@@ -245,8 +248,8 @@ export const PrebuildDetailPage: FC = () => {
245248
</div>
246249
</div>
247250
</div>
248-
<div className="px-6 py-4 flex flex-col gap-1 border-pk-border-base">
249-
<div className="flex gap-1 items-center">
251+
<div className="pt-4 flex flex-col gap-1 border-pk-border-base">
252+
<div className="px-6 flex gap-1 items-center">
250253
{prebuildPhase.icon}
251254
<span className="capitalize">{prebuildPhase.description}</span>{" "}
252255
{isStreamingLogs && (
@@ -256,8 +259,43 @@ export const PrebuildDetailPage: FC = () => {
256259
)}
257260
</div>
258261
{prebuild.status?.message && (
259-
<div className="text-pk-content-secondary truncate">{prebuild.status.message}</div>
262+
<div className="px-6 text-pk-content-secondary truncate">
263+
{prebuild.status.message}
264+
</div>
260265
)}
266+
<div className="flex mt-3 h-10">
267+
{prebuild.status?.taskLogs
268+
.filter((t) => t.logUrl)
269+
.map((task) => {
270+
const commonClasses =
271+
"pt-2 px-4 rounded-t-lg border border-pk-border-base border-b-0 border-l-0";
272+
if (task.taskId === taskId) {
273+
return (
274+
<div
275+
className={
276+
commonClasses +
277+
" bg-pk-surface-secondary z-10 relative -mb-px"
278+
}
279+
>
280+
{task.taskLabel}
281+
</div>
282+
);
283+
}
284+
return (
285+
<div
286+
onClick={() => {
287+
setSelectedTaskId(task.taskId);
288+
}}
289+
className={
290+
commonClasses +
291+
" text-pk-content-secondary hover:text-pk-content-primary hover:bg-pk-surface-tertiary cursor-pointer"
292+
}
293+
>
294+
{task.taskLabel}
295+
</div>
296+
);
297+
})}
298+
</div>
261299
</div>
262300
<div className="h-112 border-pk-border-base">
263301
<Suspense fallback={<div />}>
@@ -281,8 +319,9 @@ export const PrebuildDetailPage: FC = () => {
281319
) : (
282320
<WorkspaceLogs
283321
classes="h-full w-full"
284-
xtermClasses="absolute top-0 left-0 bottom-0 right-0 ml-6 my-0"
322+
xtermClasses="absolute top-0 left-0 bottom-0 right-0 ml-6 my-0 mt-4"
285323
logsEmitter={logEmitter}
324+
key={taskId}
286325
/>
287326
)}
288327
</Suspense>

components/gitpod-db/src/typeorm/workspace-db-impl.spec.db.ts

+16
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,20 @@ export class WorkspaceSpec {
246246
expect(secondPage).length(3);
247247
expect(secondPage[0].id).not.eq(firstPage[0].id);
248248
}
249+
250+
@test()
251+
async testPrebuildLookupContainsWorkspace(): Promise<void> {
252+
await this.setupPrebuilds();
253+
254+
const firstPage = await this.wsDB.findPrebuiltWorkspacesByOrganization(
255+
this.org,
256+
{ limit: 3, offset: 0 },
257+
{},
258+
this.defaultSorting,
259+
);
260+
expect(firstPage).length(3);
261+
for (const prebuild of firstPage) {
262+
expect(prebuild.workspace).to.not.be.undefined;
263+
}
264+
}
249265
}

components/gitpod-db/src/typeorm/workspace-db-impl.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import { TransactionalDBImpl } from "./transactional-db-impl";
5858
import { TypeORM } from "./typeorm";
5959
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
6060
import { DBProject } from "./entity/db-project";
61+
import { PrebuiltWorkspaceWithWorkspace } from "@gitpod/gitpod-protocol/src/protocol";
6162

6263
type RawTo<T> = (instance: WorkspaceInstance, ws: Workspace) => T;
6364
interface OrderBy {
@@ -997,7 +998,7 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
997998
projectId: string,
998999
branch?: string,
9991000
limit?: number,
1000-
): Promise<PrebuiltWorkspace[]> {
1001+
): Promise<PrebuiltWorkspaceWithWorkspace[]> {
10011002
const repo = await this.getPrebuiltWorkspaceRepo();
10021003

10031004
const query = repo
@@ -1014,7 +1015,7 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
10141015
}
10151016

10161017
const res = await query.getMany();
1017-
return res;
1018+
return res as PrebuiltWorkspaceWithWorkspace[];
10181019
}
10191020

10201021
/**
@@ -1043,7 +1044,7 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
10431044
field: string;
10441045
order: "ASC" | "DESC";
10451046
},
1046-
): Promise<PrebuiltWorkspace[]> {
1047+
): Promise<PrebuiltWorkspaceWithWorkspace[]> {
10471048
const repo = await this.getPrebuiltWorkspaceRepo();
10481049
const query = repo
10491050
.createQueryBuilder("pws")
@@ -1114,7 +1115,7 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
11141115
);
11151116
}
11161117

1117-
return query.getMany();
1118+
return (await query.getMany()) as PrebuiltWorkspaceWithWorkspace[];
11181119
}
11191120

11201121
async findPrebuiltWorkspaceById(id: string): Promise<PrebuiltWorkspace | undefined> {

components/gitpod-db/src/workspace-db.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
AdminGetWorkspacesQuery,
2222
SnapshotState,
2323
WorkspaceSession,
24+
PrebuiltWorkspaceWithWorkspace,
2425
} from "@gitpod/gitpod-protocol";
2526

2627
export type MaybeWorkspace = Workspace | undefined;
@@ -165,7 +166,11 @@ export interface WorkspaceDB {
165166

166167
hardDeleteWorkspace(workspaceID: string): Promise<void>;
167168

168-
findPrebuiltWorkspacesByProject(projectId: string, branch?: string, limit?: number): Promise<PrebuiltWorkspace[]>;
169+
findPrebuiltWorkspacesByProject(
170+
projectId: string,
171+
branch?: string,
172+
limit?: number,
173+
): Promise<PrebuiltWorkspaceWithWorkspace[]>;
169174
findPrebuiltWorkspacesByOrganization(
170175
organizationId: string,
171176
pagination: {
@@ -184,7 +189,7 @@ export interface WorkspaceDB {
184189
field: string;
185190
order: "ASC" | "DESC";
186191
},
187-
): Promise<PrebuiltWorkspace[]>;
192+
): Promise<PrebuiltWorkspaceWithWorkspace[]>;
188193
findPrebuiltWorkspaceById(prebuildId: string): Promise<PrebuiltWorkspace | undefined>;
189194

190195
storePrebuildInfo(prebuildInfo: PrebuildInfo): Promise<void>;

components/gitpod-protocol/src/protocol.ts

+2
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,8 @@ export interface PrebuiltWorkspace {
912912
snapshot?: string;
913913
}
914914

915+
export type PrebuiltWorkspaceWithWorkspace = PrebuiltWorkspace & { workspace: Workspace };
916+
915917
export namespace PrebuiltWorkspace {
916918
export function isDone(pws: PrebuiltWorkspace) {
917919
return (

components/gitpod-protocol/src/teams-projects-protocol.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { PrebuiltWorkspaceState, WorkspaceClasses } from "./protocol";
7+
import { PrebuiltWorkspaceState, Workspace, WorkspaceClasses } from "./protocol";
88
import { v4 as uuidv4 } from "uuid";
99
import { DeepPartial } from "./util/deep-partial";
1010

@@ -148,6 +148,7 @@ export interface ProjectUsage {
148148
export interface PrebuildWithStatus {
149149
info: PrebuildInfo;
150150
status: PrebuiltWorkspaceState;
151+
workspace: Workspace;
151152
error?: string;
152153
}
153154

components/public-api/gitpod/v1/prebuild.proto

+8
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ message PrebuildStatus {
118118
// message is an optional human-readable message detailing the current phase
119119
string message = 3;
120120
string log_url = 4;
121+
repeated TaskLog task_logs = 5;
122+
}
123+
124+
message TaskLog {
125+
string task_id = 1;
126+
string task_label = 2;
127+
string task_json = 3;
128+
string log_url = 4;
121129
}
122130

123131
message PrebuildPhase {

0 commit comments

Comments
 (0)