Skip to content

Commit

Permalink
Migrate WorkspaceService.CreateAndStartWorkspace
Browse files Browse the repository at this point in the history
  • Loading branch information
mustard-mh committed Nov 21, 2023
1 parent b9189bf commit bbe8ef2
Show file tree
Hide file tree
Showing 34 changed files with 2,607 additions and 509 deletions.
2 changes: 1 addition & 1 deletion components/dashboard/src/data/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import * as EnvVarClasses from "@gitpod/public-api/lib/gitpod/v1/envvar_pb";
// This is used to version the cache
// If data we cache changes in a non-backwards compatible way, increment this version
// That will bust any previous cache versions a client may have stored
const CACHE_VERSION = "6";
const CACHE_VERSION = "7";

export function noPersistence(queryKey: QueryKey): QueryKey {
return [...queryKey, "no-persistence"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,34 @@
* See License.AGPL.txt in the project root for license information.
*/

import { GitpodServer, WorkspaceCreationResult } from "@gitpod/gitpod-protocol";
import { useMutation } from "@tanstack/react-query";
import { getGitpodService } from "../../service/service";
import { useState } from "react";
import { StartWorkspaceError } from "../../start/StartPage";
import { workspaceClient } from "../../service/public-api";
import {
CreateAndStartWorkspaceRequest,
CreateAndStartWorkspaceResponse,
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { PartialMessage } from "@bufbuild/protobuf";
import { ConnectError } from "@connectrpc/connect";

export const useCreateWorkspaceMutation = () => {
const [isStarting, setIsStarting] = useState(false);
const mutation = useMutation<WorkspaceCreationResult, StartWorkspaceError, GitpodServer.CreateWorkspaceOptions>({
const mutation = useMutation<
CreateAndStartWorkspaceResponse,
ConnectError,
PartialMessage<CreateAndStartWorkspaceRequest>
>({
mutationFn: async (options) => {
return await getGitpodService().server.createWorkspace(options);
return await workspaceClient.createAndStartWorkspace(options);
},
onMutate: async (options: GitpodServer.CreateWorkspaceOptions) => {
onMutate: async (options: PartialMessage<CreateAndStartWorkspaceRequest>) => {
setIsStarting(true);
},
onError: (error) => {
setIsStarting(false);
},
onSuccess: (result) => {
if (result && result.createdWorkspaceId) {
if (result.workspace?.id) {
// successfully started a workspace, wait a bit before we allow to start another one
setTimeout(() => {
setIsStarting(false);
Expand All @@ -34,7 +42,7 @@ export const useCreateWorkspaceMutation = () => {
},
});
return {
createWorkspace: (options: GitpodServer.CreateWorkspaceOptions) => {
createWorkspace: (options: PartialMessage<CreateAndStartWorkspaceRequest>) => {
return mutation.mutateAsync(options);
},
// Can we use mutation.isLoading here instead?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* See License.AGPL.txt in the project root for license information.
*/

import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCurrentOrg } from "../organizations/orgs-query";
import { workspaceClient } from "../../service/public-api";
import { Workspace } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
Expand Down Expand Up @@ -57,3 +57,29 @@ export function getListWorkspacesQueryKey(orgId?: string) {
}
return ["workspaces", "list", orgId];
}

export function useUpdateWorkspaceInCache() {
const queryClient = useQueryClient();

return (workspace: Workspace, invalidateQuery: boolean) => {
if (!workspace.organizationId || !workspace.id) {
return;
}
let foundWorkspaces = false;
const queryKey = getListWorkspacesQueryKey(workspace.organizationId);
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspaceData) => {
return oldWorkspaceData?.map((info) => {
if (info.id !== workspace.id) {
return info;
}
foundWorkspaces = true;
return workspace;
});
});

// If the instance was for a workspace we don't have, it should get returned w/ an updated query
if (invalidateQuery || !foundWorkspaces) {
queryClient.invalidateQueries({ queryKey });
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* 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 { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { workspaceClient } from "../../service/public-api";
import { StartWorkspaceRequest, StartWorkspaceResponse } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { PartialMessage } from "@bufbuild/protobuf";
import { ConnectError } from "@connectrpc/connect";
import { useUpdateWorkspaceInCache } from "./list-workspaces-query";

export const useStartWorkspaceMutation = () => {
const updateWorkspace = useUpdateWorkspaceInCache();
const [isStarting, setIsStarting] = useState(false);
const mutation = useMutation<StartWorkspaceResponse, ConnectError, PartialMessage<StartWorkspaceRequest>>({
mutationFn: async (options) => {
return await workspaceClient.createAndStartWorkspace(options);
},
onMutate: async (options: PartialMessage<StartWorkspaceRequest>) => {
setIsStarting(true);
},
onError: (error) => {
setIsStarting(false);
},
onSuccess: (result) => {
if (result.workspace?.id) {
// successfully started a workspace, wait a bit before we allow to start another one
setTimeout(() => {
setIsStarting(false);
}, 4000);
} else {
setIsStarting(false);
}

if (!result.workspace) {
return;
}
updateWorkspace(result.workspace, false);
},
});
return {
startWorkspace: (options: PartialMessage<StartWorkspaceRequest>) => {
return mutation.mutateAsync(options);
},
// Can we use mutation.isLoading here instead?
isStarting,
error: mutation.error,
reset: mutation.reset,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const useToggleWorkspacedPinnedMutation = () => {
return await getGitpodService().server.updateWorkspaceUserPin(workspaceId, "toggle");
},
onSuccess: (_, { workspaceId }) => {
// TODO: use `useUpdateWorkspaceInCache` after respond Workspace object
const queryKey = getListWorkspacesQueryKey(org.data?.id);

// Update workspace.pinned to account for the toggle so it's reflected immediately
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const useToggleWorkspaceSharedMutation = () => {
if (level === AdmissionLevel.UNSPECIFIED) {
return;
}
// TODO: use `useUpdateWorkspaceInCache` after respond Workspace object
const queryKey = getListWorkspacesQueryKey(org.data?.id);

// Update workspace.shareable to the level we set so it's reflected immediately
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const useUpdateWorkspaceDescriptionMutation = () => {
return await getGitpodService().server.setWorkspaceDescription(workspaceId, newDescription);
},
onSuccess: (_, { workspaceId, newDescription }) => {
// TODO: use `useUpdateWorkspaceInCache` after respond Workspace object
const queryKey = getListWorkspacesQueryKey(org.data?.id);

// pro-actively update workspace description rather than reload all workspaces
Expand Down
47 changes: 47 additions & 0 deletions components/dashboard/src/service/json-rpc-workspace-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import { CallOptions, PromiseClient } from "@connectrpc/connect";
import { PartialMessage } from "@bufbuild/protobuf";
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/v1/workspace_connect";
import {
CreateAndStartWorkspaceRequest,
CreateAndStartWorkspaceResponse,
GetWorkspaceRequest,
GetWorkspaceResponse,
StartWorkspaceRequest,
StartWorkspaceResponse,
WatchWorkspaceStatusRequest,
WatchWorkspaceStatusResponse,
ListWorkspacesRequest,
Expand Down Expand Up @@ -109,4 +113,47 @@ export class JsonRpcWorkspaceClient implements PromiseClient<typeof WorkspaceSer
response.pagination.total = resultTotal;
return response;
}

async createAndStartWorkspace(
request: PartialMessage<CreateAndStartWorkspaceRequest>,
_options?: CallOptions | undefined,
) {
if (request.source?.case !== "contextUrl") {
throw new ConnectError("not implemented", Code.Unimplemented);
}
if (!request.organizationId || !uuidValidate(request.organizationId)) {
throw new ConnectError("organizationId is required", Code.InvalidArgument);
}
if (!request.editor) {
throw new ConnectError("editor is required", Code.InvalidArgument);
}
const response = await getGitpodService().server.createWorkspace({
organizationId: request.organizationId,
ignoreRunningWorkspaceOnSameCommit: true,
contextUrl: request.source.value,
forceDefaultConfig: request.forceDefaultConfig,
workspaceClass: request.workspaceClass,
ideSettings: {
defaultIde: request.editor.name,
useLatestVersion: request.editor.version === "latest",
},
});
const workspace = await this.getWorkspace({ workspaceId: response.createdWorkspaceId });
const result = new CreateAndStartWorkspaceResponse();
result.workspace = workspace.workspace;
return result;
}

async startWorkspace(request: PartialMessage<StartWorkspaceRequest>, _options?: CallOptions | undefined) {
if (!request.workspaceId) {
throw new ConnectError("workspaceId is required", Code.InvalidArgument);
}
await getGitpodService().server.startWorkspace(request.workspaceId, {
forceDefaultImage: request.forceDefaultConfig,
});
const workspace = await this.getWorkspace({ workspaceId: request.workspaceId });
const result = new StartWorkspaceResponse();
result.workspace = workspace.workspace;
return result;
}
}
41 changes: 24 additions & 17 deletions components/dashboard/src/start/StartWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@
* See License.AGPL.txt in the project root for license information.
*/

import {
DisposableCollection,
GitpodServer,
RateLimiterError,
StartWorkspaceResult,
WorkspaceImageBuild,
} from "@gitpod/gitpod-protocol";
import { DisposableCollection, RateLimiterError, WorkspaceImageBuild } from "@gitpod/gitpod-protocol";
import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import EventEmitter from "events";
Expand All @@ -26,8 +20,15 @@ import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage";
import ConnectToSSHModal from "../workspaces/ConnectToSSHModal";
import Alert from "../components/Alert";
import { workspaceClient, workspacesService } from "../service/public-api";
import { GetWorkspaceRequest, Workspace, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import {
GetWorkspaceRequest,
StartWorkspaceRequest,
StartWorkspaceResponse,
Workspace,
WorkspacePhase_Phase,
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { disposableWatchWorkspaceStatus } from "../data/workspaces/listen-to-workspace-ws-messages";
import { PartialMessage } from "@bufbuild/protobuf";

const sessionId = v4();

Expand Down Expand Up @@ -96,6 +97,7 @@ export interface StartWorkspaceState {
ownerToken?: string;
}

// TODO: use Function Components
export default class StartWorkspace extends React.Component<StartWorkspaceProps, StartWorkspaceState> {
private ideFrontendService: IDEFrontendService | undefined;

Expand Down Expand Up @@ -205,22 +207,23 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,

const { workspaceId } = this.props;
try {
const result = await this.startWorkspaceRateLimited(workspaceId, { forceDefaultImage });
const result = await this.startWorkspaceRateLimited(workspaceId, { forceDefaultConfig: forceDefaultImage });
if (!result) {
throw new Error("No result!");
}
console.log("/start: started workspace instance: " + result.instanceID);
console.log("/start: started workspace instance: " + result.workspace?.status?.instanceId);

// redirect to workspaceURL if we are not yet running in an iframe
if (!this.props.runsInIFrame && result.workspaceURL) {
if (!this.props.runsInIFrame && result.workspace?.status?.workspaceUrl) {
// before redirect, make sure we actually have the auth cookie set!
await this.ensureWorkspaceAuth(result.instanceID, true);
this.redirectTo(result.workspaceURL);
await this.ensureWorkspaceAuth(result.workspace.status.instanceId, true);
this.redirectTo(result.workspace.status.workspaceUrl);
return;
}
// TODO: Remove this once we use `useStartWorkspaceMutation`
// Start listening too instance updates - and explicitly query state once to guarantee we get at least one update
// (needed for already started workspaces, and not hanging in 'Starting ...' for too long)
this.fetchWorkspaceInfo(result.instanceID);
this.fetchWorkspaceInfo(result.workspace?.status?.instanceId);
} catch (error) {
const normalizedError = typeof error === "string" ? { message: error } : error;
console.error(normalizedError);
Expand All @@ -241,12 +244,16 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
*/
protected async startWorkspaceRateLimited(
workspaceId: string,
options: GitpodServer.StartWorkspaceOptions,
): Promise<StartWorkspaceResult> {
options: PartialMessage<StartWorkspaceRequest>,
): Promise<StartWorkspaceResponse> {
let retries = 0;
while (true) {
try {
return await getGitpodService().server.startWorkspace(workspaceId, options);
// TODO: use `useStartWorkspaceMutation`
return await workspaceClient.startWorkspace({
...options,
workspaceId,
});
} catch (err) {
if (err?.code !== ErrorCodes.TOO_MANY_REQUESTS) {
throw err;
Expand Down
Loading

0 comments on commit bbe8ef2

Please sign in to comment.