diff --git a/components/dashboard/src/settings/Preferences.tsx b/components/dashboard/src/settings/Preferences.tsx index 3a79d7e6c21010..2e322c4a8bc644 100644 --- a/components/dashboard/src/settings/Preferences.tsx +++ b/components/dashboard/src/settings/Preferences.tsx @@ -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) => { + if (!user) { + return; + } + const newAdditionalData = { + ...user?.additionalData, + ...value, + }; + user.additionalData = newAdditionalData; + setUser({ ...user }); + await getGitpodService().server.updateLoggedInUser(user); + }; + const [dotfileRepo, setDotfileRepo] = useState(user?.additionalData?.dotfileRepo || ""); const [theme, setTheme] = useState(localStorage.theme || "system"); const actuallySetTheme = (theme: Theme) => { if (theme === "dark" || theme === "light") { @@ -35,27 +49,49 @@ export default function Preferences() { setTheme(theme); }; - const [dotfileRepo, setDotfileRepo] = useState(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 (

Editor

Choose the editor for opening workspaces.

+

Workspaces

+ + 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). + + } + checked={!!user?.additionalData?.ignoreRunnningPrebuilds} + onChange={(e) => updateAdditionalData({ ignoreRunnningPrebuilds: e.target.checked })} + /> + + Whether new workspaces can be started based on prebuilds that ran on older Git commits and + get incrementally updated. + + } + checked={!!user?.additionalData?.allowUsingPreviousPrebuilds} + onChange={(e) => updateAdditionalData({ allowUsingPreviousPrebuilds: e.target.checked })} + /> + + 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. + + } + checked={!!user?.additionalData?.ignoreRunningWorkspaceOnSameCommit} + onChange={(e) => updateAdditionalData({ ignoreRunningWorkspaceOnSameCommit: e.target.checked })} + />

Theme

Early bird or night owl? Choose your side.

@@ -123,7 +159,7 @@ export default function Preferences() { placeholder="e.g. https://github.com/username/dotfiles" onChange={(e) => setDotfileRepo(e.target.value)} /> - diff --git a/components/dashboard/src/settings/selectClass.tsx b/components/dashboard/src/settings/selectClass.tsx index 7f7b14373ec366..2b15e8c64b00c2 100644 --- a/components/dashboard/src/settings/selectClass.tsx +++ b/components/dashboard/src/settings/selectClass.tsx @@ -57,7 +57,6 @@ export default function SelectWorkspaceClass(props: SelectWorkspaceClassProps) { } else { return (
-

Workspaces

Choose the workspace machine type for your workspaces.

diff --git a/components/dashboard/src/start/CreateWorkspace.tsx b/components/dashboard/src/start/CreateWorkspace.tsx index 53286c474094cd..155246449a6877 100644 --- a/components/dashboard/src/start/CreateWorkspace.tsx +++ b/components/dashboard/src/start/CreateWorkspace.tsx @@ -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"; @@ -48,22 +48,25 @@ export default class CreateWorkspace extends React.Component) { // 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; @@ -148,7 +151,7 @@ export default class CreateWorkspace extends React.Component { - this.createWorkspace(CreateWorkspaceMode.Default, true); + this.createWorkspace({ forceDefaultConfig: true }); }} > Continue with default configuration @@ -262,7 +265,9 @@ export default class CreateWorkspace extends React.Component
- +
); @@ -271,10 +276,12 @@ export default class CreateWorkspace extends React.Component - 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()} /> ); } @@ -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]); diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 563882dbb2f87e..356de08ce34f39 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -12,7 +12,6 @@ import { WhitelistedRepository, WorkspaceImageBuild, AuthProviderInfo, - CreateWorkspaceMode, Token, UserEnvVarValue, Terms, @@ -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 { diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index af8d49d34f5bd4..319a333fce67b6 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -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; } @@ -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 ( diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index e269b79e9a4c9e..dd37be59f85b3a 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -28,7 +28,6 @@ import { WorkspaceTimeoutValues, SetWorkspaceTimeoutResult, WorkspaceContext, - CreateWorkspaceMode, WorkspaceCreationResult, PrebuiltWorkspaceContext, CommitContext, @@ -975,7 +974,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl { parentCtx: TraceContext, user: User, context: WorkspaceContext, - mode: CreateWorkspaceMode, + allowUsingPreviousPrebuilds?: boolean, + forceNew?: boolean, ): Promise { const ctx = TraceContext.childContext("findPrebuiltWorkspace", parentCtx); try { @@ -998,23 +998,29 @@ export class GitpodServerEEImpl extends GitpodServerImpl { prebuiltWorkspace = await this.workspaceDb .trace(ctx) .findPrebuiltWorkspaceByCommit(cloneUrl, commitSHAs); + if (prebuiltWorkspace?.state !== "available" && allowUsingPreviousPrebuilds) { + const { config } = await this.configProvider.fetchConfig({}, user, context); + const history = await this.incrementalPrebuildsService.getCommitHistoryForContext(context, user); + prebuiltWorkspace = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild( + context, + config, + history, + user, + ); + } + if (!prebuiltWorkspace) { + return; + } } - const logPayload = { mode, cloneUrl, commit: commitSHAs, prebuiltWorkspace }; + const logPayload = { + allowUsingPreviousPrebuilds, + forceNew, + cloneUrl, + commit: commitSHAs, + prebuiltWorkspace, + }; log.debug(logCtx, "Looking for prebuilt workspace: ", logPayload); - if (prebuiltWorkspace?.state !== "available" && mode === CreateWorkspaceMode.UseLastSuccessfulPrebuild) { - const { config } = await this.configProvider.fetchConfig({}, user, context); - const history = await this.incrementalPrebuildsService.getCommitHistoryForContext(context, user); - prebuiltWorkspace = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild( - context, - config, - history, - user, - ); - } - if (!prebuiltWorkspace) { - return; - } if (prebuiltWorkspace.state === "available") { log.info(logCtx, `Found prebuilt workspace for ${cloneUrl}:${commitSHAs}`, logPayload); @@ -1025,13 +1031,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl { }; return result; } else if (prebuiltWorkspace.state === "queued" || prebuiltWorkspace.state === "building") { - if (mode === CreateWorkspaceMode.ForceNew) { + if (forceNew) { // in force mode we ignore running prebuilds as we want to start a workspace as quickly as we can. return; - // TODO(janx): Fall back to parent prebuild instead, if it's available: - // const buildWorkspace = await this.workspaceDb.trace({span}).findById(prebuiltWorkspace.buildWorkspaceId); - // const parentPrebuild = await this.workspaceDb.trace({span}).findPrebuildByID(buildWorkspace.basedOnPrebuildId); - // Also, make sure to initialize it by both printing the parent prebuild logs AND re-runnnig the before/init/prebuild tasks. } const workspaceID = prebuiltWorkspace.buildWorkspaceId; @@ -1065,70 +1067,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl { return; } - // (AT) At this point we found a running/building prebuild, which might also include - // image build in current state. - // - // The owner's client connection is automatically registered to listen on instance updates. - // For the remaining client connections which would handle `createWorkspace` and end up here, it - // also would be reasonable to listen on the instance updates of a running prebuild, or image build. - // - // We need to be forwarded the WorkspaceInstanceUpdates in the frontend, because we do not have - // any other means to reliably learn about the status about image builds, yet. - // Once we have those, we should remove this. - // - const ws = await this.workspaceDb.trace(ctx).findById(workspaceID); - if (!!ws && !!wsi && ws.ownerId !== this.user?.id) { - const resetListener = this.localMessageBroker.listenForWorkspaceInstanceUpdates( - ws.ownerId, - (ctx, instance) => { - if (instance.id === wsi.id) { - this.forwardInstanceUpdateToClient(ctx, instance); - if (instance.status.phase === "stopped") { - resetListener.dispose(); - } - } - }, - ); - this.disposables.push(resetListener); - } - const result = makeResult(wsi.id); - const inSameCluster = wsi.region === this.config.installationShortname; - if (!inSameCluster) { - if (mode === CreateWorkspaceMode.UsePrebuild) { - /* We need to wait for this prebuild to finish before we return from here. - * This creation mode is meant to be used once we have gone through default mode, have confirmation from the - * message bus that the prebuild is done, and now only have to wait for dbsync to come through. Thus, - * in this mode we'll poll the database until the prebuild is ready (or we time out). - * - * Note: This polling mechanism only makes sense if the prebuild runs in cluster different from ours. - * Otherwise there's no dbsync inbetween that we might have to wait for. - * - * DB sync interval is 2 seconds at the moment, we wait ten "ticks" for the data to be synchronized. - */ - const finishedPrebuiltWorkspace = await this.pollDatabaseUntilPrebuildIsAvailable( - ctx, - prebuiltWorkspace.id, - 20000, - ); - if (!finishedPrebuiltWorkspace) { - log.warn( - logCtx, - "did not find a finished prebuild in the database despite waiting long enough after msgbus confirmed that the prebuild had finished", - logPayload, - ); - return; - } else { - return { - title: context.title, - originalContext: context, - prebuiltWorkspace: finishedPrebuiltWorkspace, - } as PrebuiltWorkspaceContext; - } - } - } - /* This is the default mode behaviour: we present the running prebuild to the user so that they can see the logs * or choose to force the creation of a workspace. */ diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index e954b1dc8bbaf2..2a926f1a1e0853 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -30,7 +30,6 @@ import { AuthProviderInfo, CommitContext, Configuration, - CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient as GitpodApiClient, @@ -1053,12 +1052,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { traceAPIParams(ctx, { options }); const contextUrl = options.contextUrl; - const mode = options.mode || CreateWorkspaceMode.Default; let normalizedContextUrl: string = ""; let logContext: LogContext = {}; try { - const user = this.checkAndBlockUser("createWorkspace", { mode }); + const user = this.checkAndBlockUser("createWorkspace", { options }); await this.checkTermsAcceptance(); const envVars = this.userDB.getEnvVars(user.id); @@ -1070,7 +1068,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { normalizedContextUrl = this.contextParser.normalizeContextURL(contextUrl); let runningForContextPromise: Promise = Promise.resolve([]); const contextPromise = this.contextParser.handle(ctx, user, normalizedContextUrl); - if (mode === CreateWorkspaceMode.SelectIfRunning) { + if (!options.ignoreRunningWorkspaceOnSameCommit) { runningForContextPromise = this.findRunningInstancesForContext( ctx, contextPromise, @@ -1153,14 +1151,19 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } } - if (mode === CreateWorkspaceMode.SelectIfRunning && context.forceCreateNewWorkspace !== true) { + if (!options.ignoreRunningWorkspaceOnSameCommit && !context.forceCreateNewWorkspace) { const runningForContext = await runningForContextPromise; if (runningForContext.length > 0) { return { existingWorkspaces: runningForContext }; } } - const prebuiltWorkspace = await this.findPrebuiltWorkspace(ctx, user, context, mode); + const prebuiltWorkspace = await this.findPrebuiltWorkspace( + ctx, + user, + context, + options.allowUsingPreviousPrebuilds, + ); if (WorkspaceCreationResult.is(prebuiltWorkspace)) { ctx.span?.log({ prebuild: "running" }); return prebuiltWorkspace as WorkspaceCreationResult; @@ -1242,10 +1245,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } protected async findPrebuiltWorkspace( - ctx: TraceContext, + parentCtx: TraceContext, user: User, context: WorkspaceContext, - mode: CreateWorkspaceMode, + allowUsingPreviousPrebuilds?: boolean, + forceNew?: boolean, ): Promise { // prebuilds are an EE feature return undefined;