Skip to content

Commit 274b9e6

Browse files
committed
[server] incremental workspaces
1 parent 449bcc2 commit 274b9e6

File tree

7 files changed

+127
-149
lines changed

7 files changed

+127
-149
lines changed

components/dashboard/src/settings/Preferences.tsx

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,32 @@ import SelectableCardSolid from "../components/SelectableCardSolid";
99
import { getGitpodService } from "../service/service";
1010
import { ThemeContext } from "../theme-context";
1111
import { UserContext } from "../user-context";
12-
import { trackEvent } from "../Analytics";
1312
import SelectIDE from "./SelectIDE";
1413
import SelectWorkspaceClass from "./selectClass";
1514
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
1615
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
16+
import CheckBox from "../components/CheckBox";
17+
import { AdditionalUserData } from "@gitpod/gitpod-protocol";
1718

1819
type Theme = "light" | "dark" | "system";
1920

2021
export default function Preferences() {
21-
const { user, userBillingMode } = useContext(UserContext);
22+
const { user, userBillingMode, setUser } = useContext(UserContext);
2223
const { setIsDark } = useContext(ThemeContext);
2324

25+
const updateAdditionalData = async (value: Partial<AdditionalUserData>) => {
26+
if (!user) {
27+
return;
28+
}
29+
const newAdditionalData = {
30+
...user?.additionalData,
31+
...value,
32+
};
33+
user.additionalData = newAdditionalData;
34+
setUser({ ...user });
35+
await getGitpodService().server.updateLoggedInUser(user);
36+
};
37+
const [dotfileRepo, setDotfileRepo] = useState<string>(user?.additionalData?.dotfileRepo || "");
2438
const [theme, setTheme] = useState<Theme>(localStorage.theme || "system");
2539
const actuallySetTheme = (theme: Theme) => {
2640
if (theme === "dark" || theme === "light") {
@@ -35,27 +49,49 @@ export default function Preferences() {
3549
setTheme(theme);
3650
};
3751

38-
const [dotfileRepo, setDotfileRepo] = useState<string>(user?.additionalData?.dotfileRepo || "");
39-
const actuallySetDotfileRepo = async (value: string) => {
40-
const additionalData = user?.additionalData || {};
41-
const prevDotfileRepo = additionalData.dotfileRepo || "";
42-
additionalData.dotfileRepo = value;
43-
await getGitpodService().server.updateLoggedInUser({ additionalData });
44-
if (value !== prevDotfileRepo) {
45-
trackEvent("dotfile_repo_changed", {
46-
previous: prevDotfileRepo,
47-
current: value,
48-
});
49-
}
50-
};
51-
5252
return (
5353
<div>
5454
<PageWithSettingsSubMenu title="Preferences" subtitle="Configure user preferences.">
5555
<h3>Editor</h3>
5656
<p className="text-base text-gray-500 dark:text-gray-400">Choose the editor for opening workspaces.</p>
5757
<SelectIDE location="preferences" />
58+
<h3 className="mt-12">Workspaces</h3>
5859
<SelectWorkspaceClass enabled={BillingMode.canSetWorkspaceClass(userBillingMode)} />
60+
<CheckBox
61+
title="Never wait for running prebuilds"
62+
desc={
63+
<span>
64+
Whether to ignore any still running prebuilds when starting a fresh workspace. Enabling this
65+
will skip the prebuild is running dialog on workspace starts and start a workspace without a
66+
prebuild or based on a previous prebuild if enabled (see below).
67+
</span>
68+
}
69+
checked={!!user?.additionalData?.ignoreRunnningPrebuilds}
70+
onChange={(e) => updateAdditionalData({ ignoreRunnningPrebuilds: e.target.checked })}
71+
/>
72+
<CheckBox
73+
title="Allow incremental workspaces"
74+
desc={
75+
<span>
76+
Whether new workspaces can be started based on prebuilds that ran on older Git commits and
77+
get incrementally updated.
78+
</span>
79+
}
80+
checked={!!user?.additionalData?.allowUsingPreviousPrebuilds}
81+
onChange={(e) => updateAdditionalData({ allowUsingPreviousPrebuilds: e.target.checked })}
82+
/>
83+
<CheckBox
84+
title="Never ask when there are running workspaces on the same commit"
85+
desc={
86+
<span>
87+
Whether to ignore any running workspaces on the same commit, when starting a fresh
88+
workspace. Enabling this will skip the dialog about already running workspaces when starting
89+
a fresh workspace and always default to creating a fresh one.
90+
</span>
91+
}
92+
checked={!!user?.additionalData?.ignoreRunningWorkspaceOnSameCommit}
93+
onChange={(e) => updateAdditionalData({ ignoreRunningWorkspaceOnSameCommit: e.target.checked })}
94+
/>
5995
<h3 className="mt-12">Theme</h3>
6096
<p className="text-base text-gray-500 dark:text-gray-400">Early bird or night owl? Choose your side.</p>
6197
<div className="mt-4 space-x-3 flex">
@@ -123,7 +159,7 @@ export default function Preferences() {
123159
placeholder="e.g. https://github.com/username/dotfiles"
124160
onChange={(e) => setDotfileRepo(e.target.value)}
125161
/>
126-
<button className="secondary ml-2" onClick={() => actuallySetDotfileRepo(dotfileRepo)}>
162+
<button className="secondary ml-2" onClick={() => updateAdditionalData({ dotfileRepo })}>
127163
Save Changes
128164
</button>
129165
</span>

components/dashboard/src/settings/selectClass.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ export default function SelectWorkspaceClass(props: SelectWorkspaceClassProps) {
5757
} else {
5858
return (
5959
<div>
60-
<h3 className="mt-12">Workspaces</h3>
6160
<p className="text-base text-gray-500 dark:text-gray-400">
6261
Choose the workspace machine type for your workspaces.
6362
</p>

components/dashboard/src/start/CreateWorkspace.tsx

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66

77
import React, { useEffect, useContext, useState } from "react";
88
import {
9-
CreateWorkspaceMode,
109
WorkspaceCreationResult,
1110
RunningWorkspacePrebuildStarting,
1211
ContextURL,
13-
DisposableCollection,
1412
Team,
13+
GitpodServer,
14+
User,
1515
} from "@gitpod/gitpod-protocol";
1616
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
1717
import Modal from "../components/Modal";
@@ -48,22 +48,25 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
4848
this.state = { stillParsing: true };
4949
}
5050

51+
static contextType = UserContext;
52+
5153
componentDidMount() {
5254
this.createWorkspace();
5355
}
5456

55-
async createWorkspace(mode = CreateWorkspaceMode.SelectIfRunning, forceDefaultConfig = false) {
57+
async createWorkspace(opts?: Omit<GitpodServer.CreateWorkspaceOptions, "contextUrl">) {
5658
// Invalidate any previous result.
5759
this.setState({ result: undefined, stillParsing: true });
58-
5960
// We assume anything longer than 3 seconds is no longer just parsing the context URL (i.e. it's now creating a workspace).
6061
let timeout = setTimeout(() => this.setState({ stillParsing: false }), 3000);
61-
62+
const user: User = this.context.user;
6263
try {
6364
const result = await getGitpodService().server.createWorkspace({
6465
contextUrl: this.props.contextUrl,
65-
mode,
66-
forceDefaultConfig,
66+
ignoreRunningPrebuild: !!user.additionalData?.ignoreRunnningPrebuilds,
67+
allowUsingPreviousPrebuilds: !!user.additionalData?.allowUsingPreviousPrebuilds,
68+
ignoreRunningWorkspaceOnSameCommit: !!user.additionalData?.ignoreRunningWorkspaceOnSameCommit,
69+
...opts,
6770
});
6871
if (result.workspaceURL) {
6972
window.location.href = result.workspaceURL;
@@ -148,7 +151,7 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
148151
<button
149152
className=""
150153
onClick={() => {
151-
this.createWorkspace(CreateWorkspaceMode.Default, true);
154+
this.createWorkspace({ forceDefaultConfig: true });
152155
}}
153156
>
154157
Continue with default configuration
@@ -262,7 +265,9 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
262265
</>
263266
</div>
264267
<div className="flex justify-end mt-6">
265-
<button onClick={() => this.createWorkspace(CreateWorkspaceMode.Default)}>New Workspace</button>
268+
<button onClick={() => this.createWorkspace({ ignoreRunningWorkspaceOnSameCommit: true })}>
269+
New Workspace
270+
</button>
266271
</div>
267272
</Modal>
268273
);
@@ -271,10 +276,12 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
271276
<RunningPrebuildView
272277
runningPrebuild={result.runningWorkspacePrebuild}
273278
onUseLastSuccessfulPrebuild={() =>
274-
this.createWorkspace(CreateWorkspaceMode.UseLastSuccessfulPrebuild)
279+
this.createWorkspace({ allowUsingPreviousPrebuilds: true, ignoreRunningPrebuild: true })
280+
}
281+
onIgnorePrebuild={() =>
282+
this.createWorkspace({ allowUsingPreviousPrebuilds: false, ignoreRunningPrebuild: true })
275283
}
276-
onIgnorePrebuild={() => this.createWorkspace(CreateWorkspaceMode.ForceNew)}
277-
onPrebuildSucceeded={() => this.createWorkspace(CreateWorkspaceMode.UsePrebuild)}
284+
onPrebuildSucceeded={() => this.createWorkspace()}
278285
/>
279286
);
280287
}
@@ -545,23 +552,18 @@ function RunningPrebuildView(props: RunningPrebuildViewProps) {
545552
const { showUseLastSuccessfulPrebuild } = useContext(FeatureFlagContext);
546553

547554
useEffect(() => {
548-
const disposables = new DisposableCollection();
549-
550-
disposables.push(
551-
getGitpodService().registerClient({
552-
onInstanceUpdate: (update) => {
553-
if (update.workspaceId !== workspaceId) {
554-
return;
555-
}
556-
if (update.status.phase === "stopped") {
555+
const timeout = setTimeout(() => {
556+
getGitpodService()
557+
.server.getWorkspace(workspaceId)
558+
.then((ws) => {
559+
if (ws.latestInstance?.status?.phase === "stopped") {
557560
props.onPrebuildSucceeded();
558561
}
559-
},
560-
}),
561-
);
562+
});
563+
}, 2000);
562564

563565
return function cleanup() {
564-
disposables.dispose();
566+
clearTimeout(timeout);
565567
};
566568
// eslint-disable-next-line
567569
}, [workspaceId]);

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
WhitelistedRepository,
1313
WorkspaceImageBuild,
1414
AuthProviderInfo,
15-
CreateWorkspaceMode,
1615
Token,
1716
UserEnvVarValue,
1817
Terms,
@@ -421,7 +420,12 @@ export namespace GitpodServer {
421420
}
422421
export interface CreateWorkspaceOptions {
423422
contextUrl: string;
424-
mode?: CreateWorkspaceMode;
423+
// whether running prebuilds should be ignored.
424+
ignoreRunningPrebuild?: boolean;
425+
// whether running workspaces on the same context should be ignored. If false (default) users will be asked.
426+
ignoreRunningWorkspaceOnSameCommit?: boolean;
427+
// whether older prebuilds can be used and incrementally updated.
428+
allowUsingPreviousPrebuilds?: boolean;
425429
forceDefaultConfig?: boolean;
426430
}
427431
export interface StartWorkspaceOptions {

components/gitpod-protocol/src/protocol.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ export interface AdditionalUserData {
211211
dotfileRepo?: string;
212212
// preferred workspace classes
213213
workspaceClasses?: WorkspaceClasses;
214+
// whether running prebuilds should be ignored on start
215+
ignoreRunnningPrebuilds?: boolean;
216+
// whether new workspaces can start on older prebuilds and incrementally update
217+
allowUsingPreviousPrebuilds?: boolean;
218+
// whether running workspaces for the same git context should be ignored on start so that users are not prompted
219+
ignoreRunningWorkspaceOnSameCommit?: boolean;
214220
// additional user profile data
215221
profile?: ProfileDetails;
216222
}
@@ -1377,19 +1383,6 @@ export interface WorkspaceCreationResult {
13771383
}
13781384
export type RunningWorkspacePrebuildStarting = "queued" | "starting" | "running";
13791385

1380-
export enum CreateWorkspaceMode {
1381-
// Default returns a running prebuild if there is any, otherwise creates a new workspace (using a prebuild if one is available)
1382-
Default = "default",
1383-
// ForceNew creates a new workspace irrespective of any running prebuilds. This mode is guaranteed to actually create a workspace - but may degrade user experience as currently runnig prebuilds are ignored.
1384-
ForceNew = "force-new",
1385-
// UsePrebuild polls the database waiting for a currently running prebuild to become available. This mode exists to handle the db-sync delay.
1386-
UsePrebuild = "use-prebuild",
1387-
// SelectIfRunning returns a list of currently running workspaces for the context URL if there are any, otherwise falls back to Default mode
1388-
SelectIfRunning = "select-if-running",
1389-
// UseLastSuccessfulPrebuild returns ...
1390-
UseLastSuccessfulPrebuild = "use-last-successful-prebuild",
1391-
}
1392-
13931386
export namespace WorkspaceCreationResult {
13941387
export function is(data: any): data is WorkspaceCreationResult {
13951388
return (

0 commit comments

Comments
 (0)