Skip to content

Commit c7f8c35

Browse files
authored
[dashboard] integrate v2 WorkspaceService.getWorkspace (#18884)
1 parent da92328 commit c7f8c35

17 files changed

+1224
-240
lines changed

components/dashboard/src/components/PendingChangesDropdown.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,34 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { WorkspaceInstance } from "@gitpod/gitpod-protocol";
87
import ContextMenu, { ContextMenuEntry } from "./ContextMenu";
98
import CaretDown from "../icons/CaretDown.svg";
9+
import { WorkspaceGitStatus } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_pb";
1010

11-
export default function PendingChangesDropdown(props: { workspaceInstance?: WorkspaceInstance }) {
12-
const repo = props.workspaceInstance?.gitStatus;
11+
export default function PendingChangesDropdown({ gitStatus }: { gitStatus?: WorkspaceGitStatus }) {
1312
const headingStyle = "text-gray-500 dark:text-gray-400 text-left";
1413
const itemStyle = "text-gray-400 dark:text-gray-500 text-left -mt-5";
1514
const menuEntries: ContextMenuEntry[] = [];
1615
let totalChanges = 0;
17-
if (repo) {
18-
if ((repo.totalUntrackedFiles || 0) > 0) {
19-
totalChanges += repo.totalUntrackedFiles || 0;
16+
if (gitStatus) {
17+
if ((gitStatus.totalUntrackedFiles || 0) > 0) {
18+
totalChanges += gitStatus.totalUntrackedFiles || 0;
2019
menuEntries.push({ title: "Untracked Files", customFontStyle: headingStyle });
21-
(repo.untrackedFiles || []).forEach((item) =>
20+
(gitStatus.untrackedFiles || []).forEach((item) =>
2221
menuEntries.push({ title: item, customFontStyle: itemStyle }),
2322
);
2423
}
25-
if ((repo.totalUncommitedFiles || 0) > 0) {
26-
totalChanges += repo.totalUncommitedFiles || 0;
24+
if ((gitStatus.totalUncommitedFiles || 0) > 0) {
25+
totalChanges += gitStatus.totalUncommitedFiles || 0;
2726
menuEntries.push({ title: "Uncommitted Files", customFontStyle: headingStyle });
28-
(repo.uncommitedFiles || []).forEach((item) =>
27+
(gitStatus.uncommitedFiles || []).forEach((item) =>
2928
menuEntries.push({ title: item, customFontStyle: itemStyle }),
3029
);
3130
}
32-
if ((repo.totalUnpushedCommits || 0) > 0) {
33-
totalChanges += repo.totalUnpushedCommits || 0;
31+
if ((gitStatus.totalUnpushedCommits || 0) > 0) {
32+
totalChanges += gitStatus.totalUnpushedCommits || 0;
3433
menuEntries.push({ title: "Unpushed Commits", customFontStyle: headingStyle });
35-
(repo.unpushedCommits || []).forEach((item) =>
34+
(gitStatus.unpushedCommits || []).forEach((item) =>
3635
menuEntries.push({ title: item, customFontStyle: itemStyle }),
3736
);
3837
}

components/dashboard/src/components/PrebuildLogs.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import EventEmitter from "events";
88
import React, { Suspense, useCallback, useEffect, useState } from "react";
99
import {
10-
WorkspaceInstance,
1110
DisposableCollection,
1211
WorkspaceImageBuild,
1312
HEADLESS_LOG_STREAM_STATUS_CODE_REGEX,
@@ -16,6 +15,8 @@ import {
1615
} from "@gitpod/gitpod-protocol";
1716
import { getGitpodService } from "../service/service";
1817
import { PrebuildStatus } from "../projects/Prebuilds";
18+
import { converter, workspaceClient } from "../service/public-api";
19+
import { GetWorkspaceRequest, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_pb";
1920

2021
const WorkspaceLogs = React.lazy(() => import("./WorkspaceLogs"));
2122

@@ -27,7 +28,13 @@ export interface PrebuildLogsProps {
2728
}
2829

2930
export default function PrebuildLogs(props: PrebuildLogsProps) {
30-
const [workspaceInstance, setWorkspaceInstance] = useState<WorkspaceInstance | undefined>();
31+
const [workspace, setWorkspace] = useState<
32+
| {
33+
phase?: WorkspacePhase_Phase;
34+
instanceId?: string;
35+
}
36+
| undefined
37+
>();
3138
const [error, setError] = useState<Error | undefined>();
3239
const [logsEmitter] = useState(new EventEmitter());
3340
const [prebuild, setPrebuild] = useState<PrebuildWithStatus | undefined>();
@@ -54,13 +61,18 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
5461
if (!props.workspaceId) {
5562
return;
5663
}
57-
setWorkspaceInstance(undefined);
64+
setWorkspace(undefined);
5865
setPrebuild(undefined);
5966

6067
// Try get hold of a recent WorkspaceInfo
6168
try {
62-
const info = await getGitpodService().server.getWorkspace(props.workspaceId);
63-
setWorkspaceInstance(info?.latestInstance);
69+
const request = new GetWorkspaceRequest();
70+
request.id = props.workspaceId;
71+
const response = await workspaceClient.getWorkspace(request);
72+
setWorkspace({
73+
instanceId: response.item?.status?.instanceId,
74+
phase: response.item?.status?.phase?.name,
75+
});
6476
} catch (err) {
6577
console.error(err);
6678
setError(err);
@@ -85,7 +97,10 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
8597
getGitpodService().registerClient({
8698
onInstanceUpdate: (instance) => {
8799
if (props.workspaceId === instance.workspaceId) {
88-
setWorkspaceInstance(instance);
100+
setWorkspace({
101+
instanceId: instance.id,
102+
phase: converter.toPhase(instance),
103+
});
89104
}
90105
},
91106
onWorkspaceImageBuildLogs: (
@@ -112,24 +127,24 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
112127

113128
useEffect(() => {
114129
const workspaceId = props.workspaceId;
115-
if (!workspaceId || !workspaceInstance?.status.phase) {
130+
if (!workspaceId || !workspace?.phase) {
116131
return;
117132
}
118133

119134
const disposables = new DisposableCollection();
120-
switch (workspaceInstance.status.phase) {
135+
switch (workspace.phase) {
121136
// "building" means we're building the Docker image for the prebuild's workspace so the workspace hasn't started yet.
122-
case "building":
137+
case WorkspacePhase_Phase.IMAGEBUILD:
123138
// Try to grab image build logs
124139
disposables.push(retryWatchWorkspaceImageBuildLogs(workspaceId));
125140
break;
126141
// When we're "running" we want to switch to the logs from the actual prebuild workspace, instead
127142
// When the prebuild has "stopped", we still want to go for the logs
128-
case "running":
129-
case "stopped":
143+
case WorkspacePhase_Phase.RUNNING:
144+
case WorkspacePhase_Phase.STOPPED:
130145
disposables.push(
131146
watchHeadlessLogs(
132-
workspaceInstance.id,
147+
workspace.instanceId!,
133148
(chunk) => {
134149
logsEmitter.emit("logs", chunk);
135150
},
@@ -140,7 +155,7 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
140155
return function cleanup() {
141156
disposables.dispose();
142157
};
143-
}, [logsEmitter, props.workspaceId, workspaceInstance?.id, workspaceInstance?.status.phase]);
158+
}, [logsEmitter, props.workspaceId, workspace?.instanceId, workspace?.phase]);
144159

145160
return (
146161
<div className="rounded-xl overflow-hidden bg-gray-100 dark:bg-gray-800 flex flex-col mb-8">
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright (c) 2023 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 { Code, ConnectError, PromiseClient } from "@connectrpc/connect";
8+
import { PartialMessage } from "@bufbuild/protobuf";
9+
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_connect";
10+
import { GetWorkspaceRequest, GetWorkspaceResponse } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_pb";
11+
import { converter } from "./public-api";
12+
import { getGitpodService } from "./service";
13+
14+
export class JsonRpcWorkspaceClient implements PromiseClient<typeof WorkspaceService> {
15+
async getWorkspace(request: PartialMessage<GetWorkspaceRequest>): Promise<GetWorkspaceResponse> {
16+
if (!request.id) {
17+
throw new ConnectError("id is required", Code.InvalidArgument);
18+
}
19+
const info = await getGitpodService().server.getWorkspace(request.id);
20+
const workspace = converter.toWorkspace(info);
21+
const result = new GetWorkspaceResponse();
22+
result.item = workspace;
23+
return result;
24+
}
25+
}

components/dashboard/src/service/public-api.ts

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,44 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { createPromiseClient } from "@connectrpc/connect";
7+
import { Code, ConnectError, PromiseClient, createPromiseClient } from "@connectrpc/connect";
88
import { createConnectTransport } from "@connectrpc/connect-web";
9+
import { MethodKind, ServiceType } from "@bufbuild/protobuf";
10+
import { TeamMemberInfo, TeamMemberRole, User } from "@gitpod/gitpod-protocol";
11+
import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter";
912
import { Project as ProtocolProject, Team as ProtocolTeam } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol";
1013
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connect";
14+
import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connect";
15+
import { ProjectsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_connect";
16+
import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb";
1117
import { TeamsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_connect";
18+
import { Team, TeamMember, TeamRole } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb";
1219
import { TokensService } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_connect";
13-
import { ProjectsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_connect";
14-
import { WorkspacesService } from "@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connect";
15-
import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connect";
20+
import { WorkspacesService as WorkspaceV1Service } from "@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connect";
21+
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_connect";
1622
import { getMetricsInterceptor } from "@gitpod/public-api/lib/metrics";
17-
import { Team } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb";
18-
import { TeamMemberInfo, TeamMemberRole } from "@gitpod/gitpod-protocol";
19-
import { TeamMember, TeamRole } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb";
20-
import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb";
23+
import { getExperimentsClient } from "../experiments/client";
24+
import { JsonRpcWorkspaceClient } from "./json-rpc-workspace-client";
2125

2226
const transport = createConnectTransport({
2327
baseUrl: `${window.location.protocol}//${window.location.host}/public-api`,
2428
interceptors: [getMetricsInterceptor()],
2529
});
2630

31+
export const converter = new PublicAPIConverter();
32+
2733
export const helloService = createPromiseClient(HelloService, transport);
2834
export const teamsService = createPromiseClient(TeamsService, transport);
2935
export const personalAccessTokensService = createPromiseClient(TokensService, transport);
3036
export const projectsService = createPromiseClient(ProjectsService, transport);
31-
export const workspacesService = createPromiseClient(WorkspacesService, transport);
37+
/**
38+
* @deprecated use workspaceClient instead
39+
*/
40+
export const workspacesService = createPromiseClient(WorkspaceV1Service, transport);
3241
export const oidcService = createPromiseClient(OIDCService, transport);
3342

43+
export const workspaceClient = createServiceClient(WorkspaceService, new JsonRpcWorkspaceClient());
44+
3445
export function publicApiTeamToProtocol(team: Team): ProtocolTeam {
3546
return {
3647
id: team.id,
@@ -120,3 +131,78 @@ export function projectToProtocol(project: Project): ProtocolProject {
120131
},
121132
};
122133
}
134+
135+
let user: User | undefined;
136+
export function updateUser(newUser: User | undefined) {
137+
user = newUser;
138+
}
139+
140+
function createServiceClient<T extends ServiceType>(type: T, jsonRpcClient?: PromiseClient<T>): PromiseClient<T> {
141+
return new Proxy(createPromiseClient(type, transport), {
142+
get(grpcClient, prop) {
143+
const experimentsClient = getExperimentsClient();
144+
// TODO(ak) remove after migration
145+
async function resolveClient(): Promise<PromiseClient<T>> {
146+
if (!jsonRpcClient) {
147+
return grpcClient;
148+
}
149+
// TODO(ak): is not going to work for getLoggedInUser itself
150+
const [isPublicAPIEnabled, isFgaChecksEnabled] = await Promise.all([
151+
experimentsClient.getValueAsync("dashboard_public_api_enabled", false, {
152+
user,
153+
gitpodHost: window.location.host,
154+
}),
155+
experimentsClient.getValueAsync("centralizedPermissions", false, {
156+
user,
157+
gitpodHost: window.location.host,
158+
}),
159+
]);
160+
if (isPublicAPIEnabled && isFgaChecksEnabled) {
161+
return grpcClient;
162+
}
163+
return jsonRpcClient;
164+
}
165+
/**
166+
* The original application error is retained using gRPC metadata to ensure that existing error handling remains intact.
167+
*/
168+
function handleError(e: any): unknown {
169+
if (e instanceof ConnectError) {
170+
throw converter.fromError(e);
171+
}
172+
throw e;
173+
}
174+
return (...args: any[]) => {
175+
const method = type.methods[prop as string];
176+
if (!method) {
177+
throw new ConnectError("unimplemented", Code.Unimplemented);
178+
}
179+
180+
// TODO(ak) default timeouts
181+
// TODO(ak) retry on unavailable?
182+
183+
if (method.kind === MethodKind.Unary || method.kind === MethodKind.ClientStreaming) {
184+
return (async () => {
185+
try {
186+
const client = await resolveClient();
187+
const result = await Reflect.apply(client[prop as any], client, args);
188+
return result;
189+
} catch (e) {
190+
handleError(e);
191+
}
192+
})();
193+
}
194+
return (async function* () {
195+
try {
196+
const client = await resolveClient();
197+
const generator = Reflect.apply(client[prop as any], client, args) as AsyncGenerator<any>;
198+
for await (const item of generator) {
199+
yield item;
200+
}
201+
} catch (e) {
202+
handleError(e);
203+
}
204+
})();
205+
};
206+
},
207+
});
208+
}

0 commit comments

Comments
 (0)