Skip to content

[server] introduce incremental workspaces #14245

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

Closed
wants to merge 1 commit into from
Closed
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
70 changes: 53 additions & 17 deletions components/dashboard/src/settings/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,32 @@ import SelectableCardSolid from "../components/SelectableCardSolid";
import { getGitpodService } from "../service/service";
import { ThemeContext } from "../theme-context";
import { UserContext } from "../user-context";
import { trackEvent } from "../Analytics";
import SelectIDE from "./SelectIDE";
import SelectWorkspaceClass from "./selectClass";
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
import CheckBox from "../components/CheckBox";
import { AdditionalUserData } from "@gitpod/gitpod-protocol";

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

export default function Preferences() {
const { user, userBillingMode } = useContext(UserContext);
const { user, userBillingMode, setUser } = useContext(UserContext);
const { setIsDark } = useContext(ThemeContext);

const updateAdditionalData = async (value: Partial<AdditionalUserData>) => {
if (!user) {
return;
}
const newAdditionalData = {
...user?.additionalData,
...value,
};
user.additionalData = newAdditionalData;
setUser({ ...user });
await getGitpodService().server.updateLoggedInUser(user);
};
const [dotfileRepo, setDotfileRepo] = useState<string>(user?.additionalData?.dotfileRepo || "");
const [theme, setTheme] = useState<Theme>(localStorage.theme || "system");
const actuallySetTheme = (theme: Theme) => {
if (theme === "dark" || theme === "light") {
Expand All @@ -35,27 +49,49 @@ export default function Preferences() {
setTheme(theme);
};

const [dotfileRepo, setDotfileRepo] = useState<string>(user?.additionalData?.dotfileRepo || "");
const actuallySetDotfileRepo = async (value: string) => {
const additionalData = user?.additionalData || {};
const prevDotfileRepo = additionalData.dotfileRepo || "";
additionalData.dotfileRepo = value;
await getGitpodService().server.updateLoggedInUser({ additionalData });
if (value !== prevDotfileRepo) {
trackEvent("dotfile_repo_changed", {
previous: prevDotfileRepo,
current: value,
});
}
};

return (
<div>
<PageWithSettingsSubMenu title="Preferences" subtitle="Configure user preferences.">
<h3>Editor</h3>
<p className="text-base text-gray-500 dark:text-gray-400">Choose the editor for opening workspaces.</p>
<SelectIDE location="preferences" />
<h3 className="mt-12">Workspaces</h3>
<SelectWorkspaceClass enabled={BillingMode.canSetWorkspaceClass(userBillingMode)} />
<CheckBox
title="Never wait for running prebuilds"
desc={
<span>
Whether to ignore any still running prebuilds when starting a fresh workspace. Enabling this
will skip the prebuild is running dialog on workspace starts and start a workspace without a
prebuild or based on a previous prebuild if enabled (see below).
</span>
}
checked={!!user?.additionalData?.ignoreRunnningPrebuilds}
onChange={(e) => updateAdditionalData({ ignoreRunnningPrebuilds: e.target.checked })}
/>
<CheckBox
title="Allow incremental workspaces"
desc={
<span>
Whether new workspaces can be started based on prebuilds that ran on older Git commits and
get incrementally updated.
</span>
}
checked={!!user?.additionalData?.allowUsingPreviousPrebuilds}
onChange={(e) => updateAdditionalData({ allowUsingPreviousPrebuilds: e.target.checked })}
/>
<CheckBox
title="Never ask when there are running workspaces on the same commit"
desc={
<span>
Whether to ignore any running workspaces on the same commit, when starting a fresh
workspace. Enabling this will skip the dialog about already running workspaces when starting
a fresh workspace and always default to creating a fresh one.
</span>
}
checked={!!user?.additionalData?.ignoreRunningWorkspaceOnSameCommit}
onChange={(e) => updateAdditionalData({ ignoreRunningWorkspaceOnSameCommit: e.target.checked })}
/>
<h3 className="mt-12">Theme</h3>
<p className="text-base text-gray-500 dark:text-gray-400">Early bird or night owl? Choose your side.</p>
<div className="mt-4 space-x-3 flex">
Expand Down Expand Up @@ -123,7 +159,7 @@ export default function Preferences() {
placeholder="e.g. https://github.com/username/dotfiles"
onChange={(e) => setDotfileRepo(e.target.value)}
/>
<button className="secondary ml-2" onClick={() => actuallySetDotfileRepo(dotfileRepo)}>
<button className="secondary ml-2" onClick={() => updateAdditionalData({ dotfileRepo })}>
Save Changes
</button>
</span>
Expand Down
1 change: 0 additions & 1 deletion components/dashboard/src/settings/selectClass.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export default function SelectWorkspaceClass(props: SelectWorkspaceClassProps) {
} else {
return (
<div>
<h3 className="mt-12">Workspaces</h3>
<p className="text-base text-gray-500 dark:text-gray-400">
Choose the workspace machine type for your workspaces.
</p>
Expand Down
52 changes: 27 additions & 25 deletions components/dashboard/src/start/CreateWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

import React, { useEffect, useContext, useState } from "react";
import {
CreateWorkspaceMode,
WorkspaceCreationResult,
RunningWorkspacePrebuildStarting,
ContextURL,
DisposableCollection,
Team,
GitpodServer,
User,
} from "@gitpod/gitpod-protocol";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import Modal from "../components/Modal";
Expand Down Expand Up @@ -48,22 +48,25 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
this.state = { stillParsing: true };
}

static contextType = UserContext;

componentDidMount() {
this.createWorkspace();
}

async createWorkspace(mode = CreateWorkspaceMode.SelectIfRunning, forceDefaultConfig = false) {
async createWorkspace(opts?: Omit<GitpodServer.CreateWorkspaceOptions, "contextUrl">) {
// Invalidate any previous result.
this.setState({ result: undefined, stillParsing: true });

// We assume anything longer than 3 seconds is no longer just parsing the context URL (i.e. it's now creating a workspace).
let timeout = setTimeout(() => this.setState({ stillParsing: false }), 3000);

const user: User = this.context.user;
try {
const result = await getGitpodService().server.createWorkspace({
contextUrl: this.props.contextUrl,
mode,
forceDefaultConfig,
ignoreRunningPrebuild: !!user.additionalData?.ignoreRunnningPrebuilds,
allowUsingPreviousPrebuilds: !!user.additionalData?.allowUsingPreviousPrebuilds,
ignoreRunningWorkspaceOnSameCommit: !!user.additionalData?.ignoreRunningWorkspaceOnSameCommit,
...opts,
});
if (result.workspaceURL) {
window.location.href = result.workspaceURL;
Expand Down Expand Up @@ -148,7 +151,7 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
<button
className=""
onClick={() => {
this.createWorkspace(CreateWorkspaceMode.Default, true);
this.createWorkspace({ forceDefaultConfig: true });
}}
>
Continue with default configuration
Expand Down Expand Up @@ -262,7 +265,9 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
</>
</div>
<div className="flex justify-end mt-6">
<button onClick={() => this.createWorkspace(CreateWorkspaceMode.Default)}>New Workspace</button>
<button onClick={() => this.createWorkspace({ ignoreRunningWorkspaceOnSameCommit: true })}>
New Workspace
</button>
</div>
</Modal>
);
Expand All @@ -271,10 +276,12 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
<RunningPrebuildView
runningPrebuild={result.runningWorkspacePrebuild}
onUseLastSuccessfulPrebuild={() =>
this.createWorkspace(CreateWorkspaceMode.UseLastSuccessfulPrebuild)
this.createWorkspace({ allowUsingPreviousPrebuilds: true, ignoreRunningPrebuild: true })
}
onIgnorePrebuild={() =>
this.createWorkspace({ allowUsingPreviousPrebuilds: false, ignoreRunningPrebuild: true })
}
onIgnorePrebuild={() => this.createWorkspace(CreateWorkspaceMode.ForceNew)}
onPrebuildSucceeded={() => this.createWorkspace(CreateWorkspaceMode.UsePrebuild)}
onPrebuildSucceeded={() => this.createWorkspace()}
/>
);
}
Expand Down Expand Up @@ -545,23 +552,18 @@ function RunningPrebuildView(props: RunningPrebuildViewProps) {
const { showUseLastSuccessfulPrebuild } = useContext(FeatureFlagContext);

useEffect(() => {
const disposables = new DisposableCollection();

disposables.push(
getGitpodService().registerClient({
onInstanceUpdate: (update) => {
if (update.workspaceId !== workspaceId) {
return;
}
if (update.status.phase === "stopped") {
const timeout = setTimeout(() => {
getGitpodService()
.server.getWorkspace(workspaceId)
.then((ws) => {
if (ws.latestInstance?.status?.phase === "stopped") {
props.onPrebuildSucceeded();
}
},
}),
);
});
}, 2000);

return function cleanup() {
disposables.dispose();
clearTimeout(timeout);
};
// eslint-disable-next-line
}, [workspaceId]);
Expand Down
8 changes: 6 additions & 2 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
WhitelistedRepository,
WorkspaceImageBuild,
AuthProviderInfo,
CreateWorkspaceMode,
Token,
UserEnvVarValue,
Terms,
Expand Down Expand Up @@ -421,7 +420,12 @@ export namespace GitpodServer {
}
export interface CreateWorkspaceOptions {
contextUrl: string;
mode?: CreateWorkspaceMode;
// whether running prebuilds should be ignored.
ignoreRunningPrebuild?: boolean;
// whether running workspaces on the same context should be ignored. If false (default) users will be asked.
ignoreRunningWorkspaceOnSameCommit?: boolean;
// whether older prebuilds can be used and incrementally updated.
allowUsingPreviousPrebuilds?: boolean;
forceDefaultConfig?: boolean;
}
export interface StartWorkspaceOptions {
Expand Down
19 changes: 6 additions & 13 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ export interface AdditionalUserData {
dotfileRepo?: string;
// preferred workspace classes
workspaceClasses?: WorkspaceClasses;
// whether running prebuilds should be ignored on start
ignoreRunnningPrebuilds?: boolean;
// whether new workspaces can start on older prebuilds and incrementally update
allowUsingPreviousPrebuilds?: boolean;
// whether running workspaces for the same git context should be ignored on start so that users are not prompted
ignoreRunningWorkspaceOnSameCommit?: boolean;
// additional user profile data
profile?: ProfileDetails;
}
Expand Down Expand Up @@ -1377,19 +1383,6 @@ export interface WorkspaceCreationResult {
}
export type RunningWorkspacePrebuildStarting = "queued" | "starting" | "running";

export enum CreateWorkspaceMode {
// Default returns a running prebuild if there is any, otherwise creates a new workspace (using a prebuild if one is available)
Default = "default",
// 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.
ForceNew = "force-new",
// UsePrebuild polls the database waiting for a currently running prebuild to become available. This mode exists to handle the db-sync delay.
UsePrebuild = "use-prebuild",
// SelectIfRunning returns a list of currently running workspaces for the context URL if there are any, otherwise falls back to Default mode
SelectIfRunning = "select-if-running",
// UseLastSuccessfulPrebuild returns ...
UseLastSuccessfulPrebuild = "use-last-successful-prebuild",
}

export namespace WorkspaceCreationResult {
export function is(data: any): data is WorkspaceCreationResult {
return (
Expand Down
Loading