Skip to content

Commit fa3cca4

Browse files
authored
Migrate CreateAndStartWorkspace method in dashboard (#19076)
* Migrate WorkspaceService.CreateAndStartWorkspace * Add unit tests * Fix rebase build error
1 parent 70517fb commit fa3cca4

37 files changed

+2820
-569
lines changed

components/dashboard/src/data/setup.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import * as SCMClasses from "@gitpod/public-api/lib/gitpod/v1/scm_pb";
2929
// This is used to version the cache
3030
// If data we cache changes in a non-backwards compatible way, increment this version
3131
// That will bust any previous cache versions a client may have stored
32-
const CACHE_VERSION = "7";
32+
const CACHE_VERSION = "8";
3333

3434
export function noPersistence(queryKey: QueryKey): QueryKey {
3535
return [...queryKey, "no-persistence"];

components/dashboard/src/data/workspaces/create-workspace-mutation.ts

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

7-
import { GitpodServer, WorkspaceCreationResult } from "@gitpod/gitpod-protocol";
87
import { useMutation } from "@tanstack/react-query";
9-
import { getGitpodService } from "../../service/service";
108
import { useState } from "react";
11-
import { StartWorkspaceError } from "../../start/StartPage";
9+
import { workspaceClient } from "../../service/public-api";
10+
import {
11+
CreateAndStartWorkspaceRequest,
12+
CreateAndStartWorkspaceResponse,
13+
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
14+
import { PartialMessage } from "@bufbuild/protobuf";
15+
import { ConnectError } from "@connectrpc/connect";
1216

1317
export const useCreateWorkspaceMutation = () => {
1418
const [isStarting, setIsStarting] = useState(false);
15-
const mutation = useMutation<WorkspaceCreationResult, StartWorkspaceError, GitpodServer.CreateWorkspaceOptions>({
19+
const mutation = useMutation<
20+
CreateAndStartWorkspaceResponse,
21+
ConnectError,
22+
PartialMessage<CreateAndStartWorkspaceRequest>
23+
>({
1624
mutationFn: async (options) => {
17-
return await getGitpodService().server.createWorkspace(options);
25+
return await workspaceClient.createAndStartWorkspace(options);
1826
},
19-
onMutate: async (options: GitpodServer.CreateWorkspaceOptions) => {
27+
onMutate: async (options: PartialMessage<CreateAndStartWorkspaceRequest>) => {
2028
setIsStarting(true);
2129
},
2230
onError: (error) => {
2331
setIsStarting(false);
2432
},
2533
onSuccess: (result) => {
26-
if (result && result.createdWorkspaceId) {
34+
if (result.workspace?.id) {
2735
// successfully started a workspace, wait a bit before we allow to start another one
2836
setTimeout(() => {
2937
setIsStarting(false);
@@ -34,7 +42,7 @@ export const useCreateWorkspaceMutation = () => {
3442
},
3543
});
3644
return {
37-
createWorkspace: (options: GitpodServer.CreateWorkspaceOptions) => {
45+
createWorkspace: (options: PartialMessage<CreateAndStartWorkspaceRequest>) => {
3846
return mutation.mutateAsync(options);
3947
},
4048
// Can we use mutation.isLoading here instead?

components/dashboard/src/data/workspaces/toggle-workspace-pinned-mutation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const useToggleWorkspacedPinnedMutation = () => {
2323
return await getGitpodService().server.updateWorkspaceUserPin(workspaceId, "toggle");
2424
},
2525
onSuccess: (_, { workspaceId }) => {
26+
// TODO: use `useUpdateWorkspaceInCache` after respond Workspace object, see EXP-960
2627
const queryKey = getListWorkspacesQueryKey(org.data?.id);
2728

2829
// Update workspace.pinned to account for the toggle so it's reflected immediately

components/dashboard/src/data/workspaces/toggle-workspace-shared-mutation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const useToggleWorkspaceSharedMutation = () => {
3333
if (level === AdmissionLevel.UNSPECIFIED) {
3434
return;
3535
}
36+
// TODO: use `useUpdateWorkspaceInCache` after respond Workspace object, see EXP-960
3637
const queryKey = getListWorkspacesQueryKey(org.data?.id);
3738

3839
// Update workspace.shareable to the level we set so it's reflected immediately

components/dashboard/src/data/workspaces/update-workspace-description-mutation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const useUpdateWorkspaceDescriptionMutation = () => {
2323
return await getGitpodService().server.setWorkspaceDescription(workspaceId, newDescription);
2424
},
2525
onSuccess: (_, { workspaceId, newDescription }) => {
26+
// TODO: use `useUpdateWorkspaceInCache` after respond Workspace object, see EXP-960
2627
const queryKey = getListWorkspacesQueryKey(org.data?.id);
2728

2829
// pro-actively update workspace description rather than reload all workspaces

components/dashboard/src/service/json-rpc-workspace-client.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import { CallOptions, PromiseClient } from "@connectrpc/connect";
88
import { PartialMessage } from "@bufbuild/protobuf";
99
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/v1/workspace_connect";
1010
import {
11+
CreateAndStartWorkspaceRequest,
12+
CreateAndStartWorkspaceResponse,
1113
GetWorkspaceRequest,
1214
GetWorkspaceResponse,
15+
StartWorkspaceRequest,
16+
StartWorkspaceResponse,
1317
WatchWorkspaceStatusRequest,
1418
WatchWorkspaceStatusResponse,
1519
ListWorkspacesRequest,
@@ -109,4 +113,50 @@ export class JsonRpcWorkspaceClient implements PromiseClient<typeof WorkspaceSer
109113
response.pagination.total = resultTotal;
110114
return response;
111115
}
116+
117+
async createAndStartWorkspace(
118+
request: PartialMessage<CreateAndStartWorkspaceRequest>,
119+
_options?: CallOptions | undefined,
120+
) {
121+
if (request.source?.case !== "contextUrl") {
122+
throw new ApplicationError(ErrorCodes.UNIMPLEMENTED, "not implemented");
123+
}
124+
if (!request.organizationId || !uuidValidate(request.organizationId)) {
125+
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId is required");
126+
}
127+
if (!request.editor) {
128+
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "editor is required");
129+
}
130+
if (!request.source.value) {
131+
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "source is required");
132+
}
133+
const response = await getGitpodService().server.createWorkspace({
134+
organizationId: request.organizationId,
135+
ignoreRunningWorkspaceOnSameCommit: true,
136+
contextUrl: request.source.value,
137+
forceDefaultConfig: request.forceDefaultConfig,
138+
workspaceClass: request.workspaceClass,
139+
ideSettings: {
140+
defaultIde: request.editor.name,
141+
useLatestVersion: request.editor.version === "latest",
142+
},
143+
});
144+
const workspace = await this.getWorkspace({ workspaceId: response.createdWorkspaceId });
145+
const result = new CreateAndStartWorkspaceResponse();
146+
result.workspace = workspace.workspace;
147+
return result;
148+
}
149+
150+
async startWorkspace(request: PartialMessage<StartWorkspaceRequest>, _options?: CallOptions | undefined) {
151+
if (!request.workspaceId) {
152+
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
153+
}
154+
await getGitpodService().server.startWorkspace(request.workspaceId, {
155+
forceDefaultImage: request.forceDefaultConfig,
156+
});
157+
const workspace = await this.getWorkspace({ workspaceId: request.workspaceId });
158+
const result = new StartWorkspaceResponse();
159+
result.workspace = workspace.workspace;
160+
return result;
161+
}
112162
}

components/dashboard/src/start/StartWorkspace.tsx

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

7-
import {
8-
DisposableCollection,
9-
GitpodServer,
10-
RateLimiterError,
11-
StartWorkspaceResult,
12-
WorkspaceImageBuild,
13-
} from "@gitpod/gitpod-protocol";
7+
import { DisposableCollection, RateLimiterError, WorkspaceImageBuild } from "@gitpod/gitpod-protocol";
148
import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
159
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
1610
import EventEmitter from "events";
@@ -26,9 +20,10 @@ import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage";
2620
import ConnectToSSHModal from "../workspaces/ConnectToSSHModal";
2721
import Alert from "../components/Alert";
2822
import { workspaceClient, workspacesService } from "../service/public-api";
29-
import { GetWorkspaceRequest, Workspace, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
3023
import { watchWorkspaceStatus } from "../data/workspaces/listen-to-workspace-ws-messages";
3124
import { Button } from "@podkit/buttons/Button";
25+
import { GetWorkspaceRequest, StartWorkspaceRequest, StartWorkspaceResponse, Workspace, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
26+
import { PartialMessage } from "@bufbuild/protobuf";
3227

3328
const sessionId = v4();
3429

@@ -97,6 +92,7 @@ export interface StartWorkspaceState {
9792
ownerToken?: string;
9893
}
9994

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

@@ -195,7 +191,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
195191
}
196192
}
197193

198-
async startWorkspace(restart = false, forceDefaultImage = false) {
194+
async startWorkspace(restart = false, forceDefaultConfig = false) {
199195
const state = this.state;
200196
if (state) {
201197
if (!restart && state.startedInstanceId /* || state.errorMessage */) {
@@ -206,22 +202,23 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
206202

207203
const { workspaceId } = this.props;
208204
try {
209-
const result = await this.startWorkspaceRateLimited(workspaceId, { forceDefaultImage });
205+
const result = await this.startWorkspaceRateLimited(workspaceId, { forceDefaultConfig });
210206
if (!result) {
211207
throw new Error("No result!");
212208
}
213-
console.log("/start: started workspace instance: " + result.instanceID);
209+
console.log("/start: started workspace instance: " + result.workspace?.status?.instanceId);
214210

215211
// redirect to workspaceURL if we are not yet running in an iframe
216-
if (!this.props.runsInIFrame && result.workspaceURL) {
212+
if (!this.props.runsInIFrame && result.workspace?.status?.workspaceUrl) {
217213
// before redirect, make sure we actually have the auth cookie set!
218-
await this.ensureWorkspaceAuth(result.instanceID, true);
219-
this.redirectTo(result.workspaceURL);
214+
await this.ensureWorkspaceAuth(result.workspace.status.instanceId, true);
215+
this.redirectTo(result.workspace.status.workspaceUrl);
220216
return;
221217
}
218+
// TODO: Remove this once we use `useStartWorkspaceMutation`
222219
// Start listening too instance updates - and explicitly query state once to guarantee we get at least one update
223220
// (needed for already started workspaces, and not hanging in 'Starting ...' for too long)
224-
this.fetchWorkspaceInfo(result.instanceID);
221+
this.fetchWorkspaceInfo(result.workspace?.status?.instanceId);
225222
} catch (error) {
226223
const normalizedError = typeof error === "string" ? { message: error } : error;
227224
console.error(normalizedError);
@@ -242,12 +239,16 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
242239
*/
243240
protected async startWorkspaceRateLimited(
244241
workspaceId: string,
245-
options: GitpodServer.StartWorkspaceOptions,
246-
): Promise<StartWorkspaceResult> {
242+
options: PartialMessage<StartWorkspaceRequest>,
243+
): Promise<StartWorkspaceResponse> {
247244
let retries = 0;
248245
while (true) {
249246
try {
250-
return await getGitpodService().server.startWorkspace(workspaceId, options);
247+
// TODO: use `useStartWorkspaceMutation`
248+
return await workspaceClient.startWorkspace({
249+
...options,
250+
workspaceId,
251+
});
251252
} catch (err) {
252253
if (err?.code !== ErrorCodes.TOO_MANY_REQUESTS) {
253254
throw err;

components/dashboard/src/workspaces/CreateWorkspacePage.tsx

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

7-
import {
8-
AdditionalUserData,
9-
CommitContext,
10-
GitpodServer,
11-
SuggestedRepository,
12-
WithReferrerContext,
13-
} from "@gitpod/gitpod-protocol";
7+
import { AdditionalUserData, CommitContext, SuggestedRepository, WithReferrerContext } from "@gitpod/gitpod-protocol";
148
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
159
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
1610
import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
@@ -46,6 +40,8 @@ import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_
4640
import { WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
4741
import { Button } from "@podkit/buttons/Button";
4842
import { LoadingButton } from "@podkit/buttons/LoadingButton";
43+
import { CreateAndStartWorkspaceRequest } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
44+
import { PartialMessage } from "@bufbuild/protobuf";
4945

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

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

183-
// we already have shown running workspaces to the user
184-
opts.ignoreRunningWorkspaceOnSameCommit = true;
179+
if (!contextURL) {
180+
return;
181+
}
182+
183+
const organizationId = currentOrg?.id;
184+
if (!organizationId) {
185+
// We need an organizationId for this group of users
186+
console.error("Skipping createWorkspace");
187+
return;
188+
}
185189

186190
// if user received an INVALID_GITPOD_YML yml for their contextURL they can choose to proceed using default configuration
187191
if (workspaceContext.error?.code === ErrorCodes.INVALID_GITPOD_YML) {
@@ -191,22 +195,12 @@ export function CreateWorkspacePage() {
191195
if (!opts.workspaceClass) {
192196
opts.workspaceClass = selectedWsClass;
193197
}
194-
if (!opts.ideSettings) {
195-
opts.ideSettings = {
196-
defaultIde: selectedIde,
197-
useLatestVersion: useLatestIde,
198+
if (!opts.editor) {
199+
opts.editor = {
200+
name: selectedIde,
201+
version: useLatestIde ? "latest" : undefined,
198202
};
199203
}
200-
if (!contextURL) {
201-
return;
202-
}
203-
204-
const organizationId = currentOrg?.id;
205-
if (!organizationId) {
206-
// We need an organizationId for this group of users
207-
console.error("Skipping createWorkspace");
208-
return;
209-
}
210204

211205
try {
212206
if (createWorkspaceMutation.isStarting) {
@@ -215,18 +209,22 @@ export function CreateWorkspacePage() {
215209
}
216210
// we wait at least 5 secs
217211
const timeout = new Promise((resolve) => setTimeout(resolve, 5000));
212+
218213
const result = await createWorkspaceMutation.createWorkspace({
219-
contextUrl: contextURL,
220-
organizationId,
221-
projectId: selectedProjectID,
214+
source: {
215+
case: "contextUrl",
216+
value: contextURL,
217+
},
222218
...opts,
219+
organizationId,
220+
configurationId: selectedProjectID,
223221
});
224222
await storeAutoStartOptions();
225223
await timeout;
226-
if (result.workspaceURL) {
227-
window.location.href = result.workspaceURL;
228-
} else if (result.createdWorkspaceId) {
229-
history.push(`/start/#${result.createdWorkspaceId}`);
224+
if (result.workspace?.status?.workspaceUrl) {
225+
window.location.href = result.workspace.status.workspaceUrl;
226+
} else if (result.workspace!.id) {
227+
history.push(`/start/#${result.workspace!.id}`);
230228
}
231229
} catch (error) {
232230
console.log(error);

components/gitpod-protocol/src/messaging/error.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ export const ErrorCodes = {
144144
// 501 EE Feature
145145
EE_FEATURE: 501 as const,
146146

147+
// 521 Unimplemented
148+
UNIMPLEMENTED: 521 as const,
149+
147150
// 555 EE License Required
148151
EE_LICENSE_REQUIRED: 555 as const,
149152

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ service ConfigurationService {
5050
// Updates a configuration.
5151
rpc UpdateConfiguration(UpdateConfigurationRequest) returns (UpdateConfigurationResponse) {}
5252

53-
5453
// Deletes a configuration.
5554
rpc DeleteConfiguration(DeleteConfigurationRequest) returns (DeleteConfigurationResponse) {}
5655
}

0 commit comments

Comments
 (0)