Skip to content

Commit

Permalink
[dashboard] integrate v2 WorkspaceService.getWorkspace
Browse files Browse the repository at this point in the history
  • Loading branch information
akosyakov committed Oct 16, 2023
1 parent 446315d commit 20a6d13
Show file tree
Hide file tree
Showing 15 changed files with 622 additions and 248 deletions.
32 changes: 12 additions & 20 deletions components/dashboard/src/components/PendingChangesDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,34 @@
* See License.AGPL.txt in the project root for license information.
*/

import { WorkspaceInstance, WorkspaceInstanceRepoStatus } from "@gitpod/gitpod-protocol";
import ContextMenu, { ContextMenuEntry } from "./ContextMenu";
import CaretDown from "../icons/CaretDown.svg";
import { useFeatureFlag } from "../data/featureflag-query";
import { WorkspaceGitStatus } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_pb";

export default function PendingChangesDropdown(props: { workspaceInstance?: WorkspaceInstance }) {
const liveGitStatus = useFeatureFlag("supervisor_live_git_status");
let repo: WorkspaceInstanceRepoStatus | undefined;
if (liveGitStatus) {
repo = props.workspaceInstance?.gitStatus;
} else {
repo = props.workspaceInstance?.status?.repo;
}
export default function PendingChangesDropdown({ gitStatus }: { gitStatus?: WorkspaceGitStatus }) {
const headingStyle = "text-gray-500 dark:text-gray-400 text-left";
const itemStyle = "text-gray-400 dark:text-gray-500 text-left -mt-5";
const menuEntries: ContextMenuEntry[] = [];
let totalChanges = 0;
if (repo) {
if ((repo.totalUntrackedFiles || 0) > 0) {
totalChanges += repo.totalUntrackedFiles || 0;
if (gitStatus) {
if ((gitStatus.totalUntrackedFiles || 0) > 0) {
totalChanges += gitStatus.totalUntrackedFiles || 0;
menuEntries.push({ title: "Untracked Files", customFontStyle: headingStyle });
(repo.untrackedFiles || []).forEach((item) =>
(gitStatus.untrackedFiles || []).forEach((item) =>
menuEntries.push({ title: item, customFontStyle: itemStyle }),
);
}
if ((repo.totalUncommitedFiles || 0) > 0) {
totalChanges += repo.totalUncommitedFiles || 0;
if ((gitStatus.totalUncommitedFiles || 0) > 0) {
totalChanges += gitStatus.totalUncommitedFiles || 0;
menuEntries.push({ title: "Uncommitted Files", customFontStyle: headingStyle });
(repo.uncommitedFiles || []).forEach((item) =>
(gitStatus.uncommitedFiles || []).forEach((item) =>
menuEntries.push({ title: item, customFontStyle: itemStyle }),
);
}
if ((repo.totalUnpushedCommits || 0) > 0) {
totalChanges += repo.totalUnpushedCommits || 0;
if ((gitStatus.totalUnpushedCommits || 0) > 0) {
totalChanges += gitStatus.totalUnpushedCommits || 0;
menuEntries.push({ title: "Unpushed Commits", customFontStyle: headingStyle });
(repo.unpushedCommits || []).forEach((item) =>
(gitStatus.unpushedCommits || []).forEach((item) =>
menuEntries.push({ title: item, customFontStyle: itemStyle }),
);
}
Expand Down
41 changes: 28 additions & 13 deletions components/dashboard/src/components/PrebuildLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import EventEmitter from "events";
import React, { Suspense, useCallback, useEffect, useState } from "react";
import {
WorkspaceInstance,
DisposableCollection,
WorkspaceImageBuild,
HEADLESS_LOG_STREAM_STATUS_CODE_REGEX,
Expand All @@ -16,6 +15,8 @@ import {
} from "@gitpod/gitpod-protocol";
import { getGitpodService } from "../service/service";
import { PrebuildStatus } from "../projects/Prebuilds";
import { converter, workspaceClient } from "../service/public-api";
import { GetWorkspaceRequest, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_pb";

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

Expand All @@ -27,7 +28,13 @@ export interface PrebuildLogsProps {
}

export default function PrebuildLogs(props: PrebuildLogsProps) {
const [workspaceInstance, setWorkspaceInstance] = useState<WorkspaceInstance | undefined>();
const [workspace, setWorkspace] = useState<
| {
phase?: WorkspacePhase_Phase;
instanceId?: string;
}
| undefined
>();
const [error, setError] = useState<Error | undefined>();
const [logsEmitter] = useState(new EventEmitter());
const [prebuild, setPrebuild] = useState<PrebuildWithStatus | undefined>();
Expand All @@ -54,13 +61,18 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
if (!props.workspaceId) {
return;
}
setWorkspaceInstance(undefined);
setWorkspace(undefined);
setPrebuild(undefined);

// Try get hold of a recent WorkspaceInfo
try {
const info = await getGitpodService().server.getWorkspace(props.workspaceId);
setWorkspaceInstance(info?.latestInstance);
const request = new GetWorkspaceRequest();
request.id = props.workspaceId;
const response = await workspaceClient.getWorkspace(request);
setWorkspace({
instanceId: response.item?.status?.instanceId,
phase: response.item?.status?.phase?.name,
});
} catch (err) {
console.error(err);
setError(err);
Expand All @@ -85,7 +97,10 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
getGitpodService().registerClient({
onInstanceUpdate: (instance) => {
if (props.workspaceId === instance.workspaceId) {
setWorkspaceInstance(instance);
setWorkspace({
instanceId: instance.id,
phase: converter.toPhase(instance),
});
}
},
onWorkspaceImageBuildLogs: (
Expand All @@ -112,24 +127,24 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {

useEffect(() => {
const workspaceId = props.workspaceId;
if (!workspaceId || !workspaceInstance?.status.phase) {
if (!workspaceId || !workspace?.phase) {
return;
}

const disposables = new DisposableCollection();
switch (workspaceInstance.status.phase) {
switch (workspace.phase) {
// "building" means we're building the Docker image for the prebuild's workspace so the workspace hasn't started yet.
case "building":
case WorkspacePhase_Phase.IMAGEBUILD:
// Try to grab image build logs
disposables.push(retryWatchWorkspaceImageBuildLogs(workspaceId));
break;
// When we're "running" we want to switch to the logs from the actual prebuild workspace, instead
// When the prebuild has "stopped", we still want to go for the logs
case "running":
case "stopped":
case WorkspacePhase_Phase.RUNNING:
case WorkspacePhase_Phase.STOPPED:
disposables.push(
watchHeadlessLogs(
workspaceInstance.id,
workspace.instanceId!,
(chunk) => {
logsEmitter.emit("logs", chunk);
},
Expand All @@ -140,7 +155,7 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
return function cleanup() {
disposables.dispose();
};
}, [logsEmitter, props.workspaceId, workspaceInstance?.id, workspaceInstance?.status.phase]);
}, [logsEmitter, props.workspaceId, workspace?.instanceId, workspace?.phase]);

return (
<div className="rounded-xl overflow-hidden bg-gray-100 dark:bg-gray-800 flex flex-col mb-8">
Expand Down
25 changes: 25 additions & 0 deletions components/dashboard/src/service/json-rpc-workspace-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { Code, ConnectError, PromiseClient } from "@connectrpc/connect";
import { PartialMessage } from "@bufbuild/protobuf";
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_connect";
import { GetWorkspaceRequest, GetWorkspaceResponse } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_pb";
import { converter } from "./public-api";
import { getGitpodService } from "./service";

export class JsonRpcWorkspaceClient implements PromiseClient<typeof WorkspaceService> {
async getWorkspace(request: PartialMessage<GetWorkspaceRequest>): Promise<GetWorkspaceResponse> {
if (!request.id) {
throw new ConnectError("id is required", Code.InvalidArgument);
}
const info = await getGitpodService().server.getWorkspace(request.id);
const workspace = converter.toWorkspace(info);
const result = new GetWorkspaceResponse();
result.item = workspace;
return result;
}
}
96 changes: 87 additions & 9 deletions components/dashboard/src/service/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,44 @@
* See License.AGPL.txt in the project root for license information.
*/

import { createPromiseClient } from "@connectrpc/connect";
import { Code, ConnectError, PromiseClient, createPromiseClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { MethodKind, ServiceType } from "@bufbuild/protobuf";
import { TeamMemberInfo, TeamMemberRole, User } from "@gitpod/gitpod-protocol";
import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter";
import { Project as ProtocolProject, Team as ProtocolTeam } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol";
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connect";
import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connect";
import { ProjectsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_connect";
import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb";
import { TeamsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_connect";
import { Team, TeamMember, TeamRole } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb";
import { TokensService } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_connect";
import { ProjectsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_connect";
import { WorkspacesService } from "@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connect";
import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connect";
import { WorkspacesService as WorkspaceV1Service } from "@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connect";
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/experimental/v2/workspace_connect";
import { getMetricsInterceptor } from "@gitpod/public-api/lib/metrics";
import { Team } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb";
import { TeamMemberInfo, TeamMemberRole } from "@gitpod/gitpod-protocol";
import { TeamMember, TeamRole } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb";
import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb";
import { getExperimentsClient } from "../experiments/client";
import { JsonRpcWorkspaceClient } from "./json-rpc-workspace-client";

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

export const converter = new PublicAPIConverter();

export const helloService = createPromiseClient(HelloService, transport);
export const teamsService = createPromiseClient(TeamsService, transport);
export const personalAccessTokensService = createPromiseClient(TokensService, transport);
export const projectsService = createPromiseClient(ProjectsService, transport);
export const workspacesService = createPromiseClient(WorkspacesService, transport);
/**
* @deprecated use workspaceClient instead
*/
export const workspacesService = createPromiseClient(WorkspaceV1Service, transport);
export const oidcService = createPromiseClient(OIDCService, transport);

export const workspaceClient = createServiceClient(WorkspaceService, new JsonRpcWorkspaceClient());

export function publicApiTeamToProtocol(team: Team): ProtocolTeam {
return {
id: team.id,
Expand Down Expand Up @@ -120,3 +131,70 @@ export function projectToProtocol(project: Project): ProtocolProject {
},
};
}

let user: User | undefined;
export function updateUser(newUser: User | undefined) {
user = newUser;
}

function createServiceClient<T extends ServiceType>(type: T, jsonRpcClient?: PromiseClient<T>): PromiseClient<T> {
return new Proxy(createPromiseClient(type, transport), {
get(grpcClient, prop) {
// TODO(ak) remove after migration
async function resolveClient(): Promise<PromiseClient<T>> {
if (!jsonRpcClient) {
return grpcClient;
}
const isEnabled = await getExperimentsClient().getValueAsync("dashboard_public_api_enabled", false, {
// TODO(ak): is not going to work for getLoggedInUser itself
user,
});
if (isEnabled) {
return grpcClient;
}
return jsonRpcClient;
}
/**
* The original application error is retained using gRPC metadata to ensure that existing error handling remains intact.
*/
function handleError(e: any): unknown {
if (e instanceof ConnectError) {
throw converter.fromError(e);
}
throw e;
}
return (...args: any[]) => {
const method = type.methods[prop as string];
if (!method) {
throw new ConnectError("unimplemented", Code.Unimplemented);
}

// TODO(ak) default timeouts
// TODO(ak) retry on unavailable?

if (method.kind === MethodKind.Unary || method.kind === MethodKind.ClientStreaming) {
return (async () => {
try {
const client = await resolveClient();
const result = await Reflect.apply(client[prop as any], client, args);
return result;
} catch (e) {
handleError(e);
}
})();
}
return (async function* () {
try {
const client = await resolveClient();
const generator = Reflect.apply(client[prop as any], client, args) as AsyncGenerator<any>;
for await (const item of generator) {
yield item;
}
} catch (e) {
handleError(e);
}
})();
};
},
});
}
Loading

0 comments on commit 20a6d13

Please sign in to comment.