Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate CreateAndStartWorkspace method in dashboard #19076

Merged
merged 3 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/dashboard/src/data/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import * as SCMClasses from "@gitpod/public-api/lib/gitpod/v1/scm_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 = "7";
const CACHE_VERSION = "8";

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 @@ -23,6 +23,7 @@ export const useToggleWorkspacedPinnedMutation = () => {
return await getGitpodService().server.updateWorkspaceUserPin(workspaceId, "toggle");
},
onSuccess: (_, { workspaceId }) => {
// TODO: use `useUpdateWorkspaceInCache` after respond Workspace object, see EXP-960
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, see EXP-960
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, see EXP-960
const queryKey = getListWorkspacesQueryKey(org.data?.id);

// pro-actively update workspace description rather than reload all workspaces
Expand Down
50 changes: 50 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,50 @@ 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 ApplicationError(ErrorCodes.UNIMPLEMENTED, "not implemented");
}
if (!request.organizationId || !uuidValidate(request.organizationId)) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId is required");
}
if (!request.editor) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "editor is required");
}
if (!request.source.value) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "source is required");
}
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 ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
}
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;
}
}
37 changes: 19 additions & 18 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,9 +20,10 @@ 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 { watchWorkspaceStatus } from "../data/workspaces/listen-to-workspace-ws-messages";
import { Button } from "@podkit/buttons/Button";
import { GetWorkspaceRequest, StartWorkspaceRequest, StartWorkspaceResponse, Workspace, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { PartialMessage } from "@bufbuild/protobuf";

const sessionId = v4();

Expand Down Expand Up @@ -97,6 +92,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 @@ -195,7 +191,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
}
}

async startWorkspace(restart = false, forceDefaultImage = false) {
async startWorkspace(restart = false, forceDefaultConfig = false) {
const state = this.state;
if (state) {
if (!restart && state.startedInstanceId /* || state.errorMessage */) {
Expand All @@ -206,22 +202,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 });
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 @@ -242,12 +239,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
60 changes: 29 additions & 31 deletions components/dashboard/src/workspaces/CreateWorkspacePage.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 {
AdditionalUserData,
CommitContext,
GitpodServer,
SuggestedRepository,
WithReferrerContext,
} from "@gitpod/gitpod-protocol";
import { AdditionalUserData, CommitContext, SuggestedRepository, WithReferrerContext } from "@gitpod/gitpod-protocol";
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
Expand Down Expand Up @@ -46,6 +40,8 @@ import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_
import { WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { Button } from "@podkit/buttons/Button";
import { LoadingButton } from "@podkit/buttons/LoadingButton";
import { CreateAndStartWorkspaceRequest } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { PartialMessage } from "@bufbuild/protobuf";

export function CreateWorkspacePage() {
const { user, setUser } = useContext(UserContext);
Expand Down Expand Up @@ -176,12 +172,20 @@ export function CreateWorkspacePage() {
const [selectAccountError, setSelectAccountError] = useState<SelectAccountPayload | undefined>(undefined);

const createWorkspace = useCallback(
async (options?: Omit<GitpodServer.CreateWorkspaceOptions, "contextUrl" | "organizationId">) => {
async (options?: Omit<PartialMessage<CreateAndStartWorkspaceRequest>, "contextUrl" | "organizationId">) => {
// add options from search params
const opts = options || {};

// we already have shown running workspaces to the user
opts.ignoreRunningWorkspaceOnSameCommit = true;
if (!contextURL) {
return;
}

const organizationId = currentOrg?.id;
if (!organizationId) {
// We need an organizationId for this group of users
console.error("Skipping createWorkspace");
return;
}

// if user received an INVALID_GITPOD_YML yml for their contextURL they can choose to proceed using default configuration
if (workspaceContext.error?.code === ErrorCodes.INVALID_GITPOD_YML) {
Expand All @@ -191,22 +195,12 @@ export function CreateWorkspacePage() {
if (!opts.workspaceClass) {
opts.workspaceClass = selectedWsClass;
}
if (!opts.ideSettings) {
opts.ideSettings = {
defaultIde: selectedIde,
useLatestVersion: useLatestIde,
if (!opts.editor) {
opts.editor = {
name: selectedIde,
version: useLatestIde ? "latest" : undefined,
};
}
if (!contextURL) {
return;
}

const organizationId = currentOrg?.id;
if (!organizationId) {
// We need an organizationId for this group of users
console.error("Skipping createWorkspace");
return;
}

try {
if (createWorkspaceMutation.isStarting) {
Expand All @@ -215,18 +209,22 @@ export function CreateWorkspacePage() {
}
// we wait at least 5 secs
const timeout = new Promise((resolve) => setTimeout(resolve, 5000));

const result = await createWorkspaceMutation.createWorkspace({
contextUrl: contextURL,
organizationId,
projectId: selectedProjectID,
source: {
case: "contextUrl",
value: contextURL,
},
...opts,
organizationId,
configurationId: selectedProjectID,
});
await storeAutoStartOptions();
await timeout;
if (result.workspaceURL) {
window.location.href = result.workspaceURL;
} else if (result.createdWorkspaceId) {
history.push(`/start/#${result.createdWorkspaceId}`);
if (result.workspace?.status?.workspaceUrl) {
window.location.href = result.workspace.status.workspaceUrl;
} else if (result.workspace!.id) {
history.push(`/start/#${result.workspace!.id}`);
}
} catch (error) {
console.log(error);
Expand Down
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/messaging/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ export const ErrorCodes = {
// 501 EE Feature
EE_FEATURE: 501 as const,

// 521 Unimplemented
UNIMPLEMENTED: 521 as const,

// 555 EE License Required
EE_LICENSE_REQUIRED: 555 as const,

Expand Down
1 change: 0 additions & 1 deletion components/public-api/gitpod/v1/configuration.proto
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ service ConfigurationService {
// Updates a configuration.
rpc UpdateConfiguration(UpdateConfigurationRequest) returns (UpdateConfigurationResponse) {}


// Deletes a configuration.
rpc DeleteConfiguration(DeleteConfigurationRequest) returns (DeleteConfigurationResponse) {}
}
Expand Down
Loading
Loading