diff --git a/components/dashboard/src/projects/ProjectSettings.tsx b/components/dashboard/src/projects/ProjectSettings.tsx index 4b54eab470fdf7..1b4ee466496842 100644 --- a/components/dashboard/src/projects/ProjectSettings.tsx +++ b/components/dashboard/src/projects/ProjectSettings.tsx @@ -4,7 +4,7 @@ * See License-AGPL.txt in the project root for license information. */ -import { useContext } from "react"; +import { useContext, useEffect, useState } from "react"; import { useLocation } from "react-router"; import { Project, ProjectSettings, Team } from "@gitpod/gitpod-protocol"; import CheckBox from "../components/CheckBox"; @@ -14,6 +14,8 @@ import { PageWithSubMenu } from "../components/PageWithSubMenu"; import PillLabel from "../components/PillLabel"; import { ProjectContext } from "./project-context"; import { FeatureFlagContext } from "../contexts/FeatureFlagContext"; +import SelectWorkspaceClass from "../settings/selectClass"; +import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; export function getProjectSettingsMenu(project?: Project, team?: Team) { const teamOrUserSlug = !!team ? "t/" + team.slug : "projects"; @@ -48,6 +50,14 @@ export function ProjectSettingsPage(props: { project?: Project; children?: React export default function () { const { showPersistentVolumeClaimUI } = useContext(FeatureFlagContext); const { project, setProject } = useContext(ProjectContext); + const [teamBillingMode, setTeamBillingMode] = useState(undefined); + const { teams } = useContext(TeamsContext); + const team = getCurrentTeam(useLocation(), teams); + useEffect(() => { + if (team) { + getGitpodService().server.getBillingModeForTeam(team.id).then(setTeamBillingMode); + } + }, [team]); if (!project) return null; @@ -59,6 +69,15 @@ export default function () { setProject({ ...project, settings: newSettings }); }; + const setWorkspaceClass = async (value: string) => { + if (!project) { + return value; + } + const before = project.settings?.workspaceClasses?.regular; + updateProjectSettings({ workspaceClasses: { prebuild: value, regular: value } }); + return before; + }; + return (

Prebuilds

@@ -142,8 +161,12 @@ export default function () { {showPersistentVolumeClaimUI && ( <> -

-

Workspaces

+ + {!BillingMode.canSetWorkspaceClass(teamBillingMode) &&

Workspaces

} diff --git a/components/dashboard/src/settings/Preferences.tsx b/components/dashboard/src/settings/Preferences.tsx index 3a79d7e6c21010..99763cd42d7c69 100644 --- a/components/dashboard/src/settings/Preferences.tsx +++ b/components/dashboard/src/settings/Preferences.tsx @@ -14,6 +14,7 @@ import SelectIDE from "./SelectIDE"; import SelectWorkspaceClass from "./selectClass"; import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu"; import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; +import { WorkspaceClasses } from "@gitpod/gitpod-protocol"; type Theme = "light" | "dark" | "system"; @@ -49,13 +50,30 @@ export default function Preferences() { } }; + const setWorkspaceClass = async (value: string) => { + const additionalData = user?.additionalData || {}; + const prevWorkspaceClass = additionalData?.workspaceClasses?.regular; + const workspaceClasses = (additionalData?.workspaceClasses || {}) as WorkspaceClasses; + workspaceClasses.regular = value; + workspaceClasses.prebuild = value; + additionalData.workspaceClasses = workspaceClasses; + if (value !== prevWorkspaceClass) { + await getGitpodService().server.updateLoggedInUser({ additionalData }); + } + return prevWorkspaceClass; + }; + return (

Editor

Choose the editor for opening workspaces.

- +

Theme

Early bird or night owl? Choose your side.

diff --git a/components/dashboard/src/settings/selectClass.tsx b/components/dashboard/src/settings/selectClass.tsx index 7f7b14373ec366..2d3b397626c5cc 100644 --- a/components/dashboard/src/settings/selectClass.tsx +++ b/components/dashboard/src/settings/selectClass.tsx @@ -4,33 +4,25 @@ * See License-AGPL.txt in the project root for license information. */ -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { getGitpodService } from "../service/service"; -import { UserContext } from "../user-context"; import { trackEvent } from "../Analytics"; -import { WorkspaceClasses } from "@gitpod/gitpod-protocol"; import WorkspaceClass from "../components/WorkspaceClass"; import { SupportedWorkspaceClass } from "@gitpod/gitpod-protocol/lib/workspace-class"; interface SelectWorkspaceClassProps { enabled: boolean; + workspaceClass?: string; + setWorkspaceClass: (value: string) => Promise; } export default function SelectWorkspaceClass(props: SelectWorkspaceClassProps) { - const { user } = useContext(UserContext); - - const [workspaceClass, setWorkspaceClass] = useState(user?.additionalData?.workspaceClasses?.regular || ""); + const [workspaceClass, setWorkspaceClass] = useState(props.workspaceClass); const actuallySetWorkspaceClass = async (value: string) => { - const additionalData = user?.additionalData || {}; - const prevWorkspaceClass = additionalData?.workspaceClasses?.regular || ""; - const workspaceClasses = (additionalData?.workspaceClasses || {}) as WorkspaceClasses; - workspaceClasses.regular = value; - workspaceClasses.prebuild = value; - additionalData.workspaceClasses = workspaceClasses; - if (value !== prevWorkspaceClass) { - await getGitpodService().server.updateLoggedInUser({ additionalData }); + const previousValue = await props.setWorkspaceClass(value); + if (previousValue !== value) { trackEvent("workspace_class_changed", { - previous: prevWorkspaceClass, + previous: previousValue, current: value, }); setWorkspaceClass(value); diff --git a/components/gitpod-protocol/src/teams-projects-protocol.ts b/components/gitpod-protocol/src/teams-projects-protocol.ts index 0f839812d0e4a6..7de2881894d47c 100644 --- a/components/gitpod-protocol/src/teams-projects-protocol.ts +++ b/components/gitpod-protocol/src/teams-projects-protocol.ts @@ -4,7 +4,7 @@ * See License-AGPL.txt in the project root for license information. */ -import { PrebuiltWorkspaceState } from "./protocol"; +import { PrebuiltWorkspaceState, WorkspaceClasses } from "./protocol"; import { v4 as uuidv4 } from "uuid"; import { DeepPartial } from "./util/deep-partial"; import { WebhookEvent } from "./webhook-event"; @@ -21,6 +21,8 @@ export interface ProjectSettings { allowUsingPreviousPrebuilds?: boolean; // how many commits in the commit history a prebuild is good (undefined and 0 means every commit is prebuilt) prebuildEveryNthCommit?: number; + // preferred workspace classes + workspaceClasses?: WorkspaceClasses; } export interface Project { diff --git a/components/server/ee/src/container-module.ts b/components/server/ee/src/container-module.ts index 8eb6049c1a79bf..de87e2370401e5 100644 --- a/components/server/ee/src/container-module.ts +++ b/components/server/ee/src/container-module.ts @@ -46,8 +46,6 @@ import { ChargebeeService } from "./user/chargebee-service"; import { StripeService } from "./user/stripe-service"; import { EligibilityService } from "./user/eligibility-service"; import { AccountStatementProvider } from "./user/account-statement-provider"; -import { WorkspaceStarterEE } from "./workspace/workspace-starter"; -import { WorkspaceStarter } from "../../src/workspace/workspace-starter"; import { UserDeletionService } from "../../src/user/user-deletion-service"; import { BlockedUserFilter } from "../../src/auth/blocked-user-filter"; import { EMailDomainService, EMailDomainServiceImpl } from "./auth/email-domain-service"; @@ -105,9 +103,6 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is bind(UserDeletionServiceEE).toSelf().inSingletonScope(); rebind(UserDeletionService).to(UserDeletionServiceEE).inSingletonScope(); - // workspace management - rebind(WorkspaceStarter).to(WorkspaceStarterEE).inSingletonScope(); - // acounting bind(AccountService).to(AccountServiceImpl).inSingletonScope(); bind(SubscriptionService).toSelf().inSingletonScope(); diff --git a/components/server/ee/src/prebuilds/prebuild-manager.ts b/components/server/ee/src/prebuilds/prebuild-manager.ts index 1d7523f4ba7fb7..fe6c8e0b96c6f3 100644 --- a/components/server/ee/src/prebuilds/prebuild-manager.ts +++ b/components/server/ee/src/prebuilds/prebuild-manager.ts @@ -259,7 +259,7 @@ export class PrebuildManager { } else { span.setTag("starting", true); const projectEnvVars = await projectEnvVarsPromise; - await this.workspaceStarter.startWorkspace({ span }, workspace, user, [], projectEnvVars, { + await this.workspaceStarter.startWorkspace({ span }, workspace, user, project, [], projectEnvVars, { excludeFeatureFlags: ["full_workspace_backup"], }); } @@ -273,7 +273,12 @@ export class PrebuildManager { } } - async retriggerPrebuild(ctx: TraceContext, user: User, workspaceId: string): Promise { + async retriggerPrebuild( + ctx: TraceContext, + user: User, + project: Project | undefined, + workspaceId: string, + ): Promise { const span = TraceContext.startSpan("retriggerPrebuild", ctx); span.setTag("workspaceId", workspaceId); try { @@ -297,7 +302,7 @@ export class PrebuildManager { if (workspace.projectId) { projectEnvVars = await this.projectService.getProjectEnvironmentVariables(workspace.projectId); } - await this.workspaceStarter.startWorkspace({ span }, workspace, user, [], projectEnvVars); + await this.workspaceStarter.startWorkspace({ span }, workspace, user, project, [], projectEnvVars); return { prebuildId: prebuild.id, wsid: workspace.id, done: false }; } catch (err) { TraceContext.setError({ span }, err); diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 69dbd79716e2bb..960f6ec56518bc 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -1060,7 +1060,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl { // queued for long than a minute? Let's retrigger console.warn("Retriggering queued prebuild.", prebuiltWorkspace); try { - await this.prebuildManager.retriggerPrebuild(ctx, user, workspaceID); + const project = prebuiltWorkspace.projectId + ? await this.projectDB.findProjectById(prebuiltWorkspace.projectId) + : undefined; + await this.prebuildManager.retriggerPrebuild(ctx, user, project, workspaceID); } catch (err) { console.error(err); } diff --git a/components/server/ee/src/workspace/workspace-starter.ts b/components/server/ee/src/workspace/workspace-starter.ts deleted file mode 100644 index 340273e55e288b..00000000000000 --- a/components/server/ee/src/workspace/workspace-starter.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2021 Gitpod GmbH. All rights reserved. - * Licensed under the Gitpod Enterprise Source Code License, - * See License.enterprise.txt in the project root folder. - */ - -import { Workspace, User, WorkspaceInstance, NamedWorkspaceFeatureFlag } from "@gitpod/gitpod-protocol"; -import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; -import { injectable } from "inversify"; -import { IDEConfig } from "../../../src/ide-service"; -import { WorkspaceStarter } from "../../../src/workspace/workspace-starter"; - -@injectable() -export class WorkspaceStarterEE extends WorkspaceStarter { - /** - * Creates a new instance for a given workspace and its owner - * - * @param workspace the workspace to create an instance for - */ - protected async newInstance( - ctx: TraceContext, - workspace: Workspace, - previousInstance: WorkspaceInstance | undefined, - user: User, - excludeFeatureFlags: NamedWorkspaceFeatureFlag[], - ideConfig: IDEConfig, - ): Promise { - const instance = await super.newInstance( - ctx, - workspace, - previousInstance, - user, - excludeFeatureFlags, - ideConfig, - ); - - return instance; - } -} diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index dc16808cb6c0ba..c1a278234ddadd 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -718,7 +718,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "Cannot (re-)start a deleted workspace."); } const userEnvVars = this.userDB.getEnvVars(user.id); - let projectEnvVarsPromise = this.internalGetProjectEnvVars(workspace.projectId); + const projectEnvVarsPromise = this.internalGetProjectEnvVars(workspace.projectId); + const projectPromise = workspace.projectId + ? this.projectDB.findProjectById(workspace.projectId) + : Promise.resolve(undefined); await mayStartPromise; @@ -727,6 +730,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { ctx, workspace, user, + await projectPromise, await userEnvVars, await projectEnvVarsPromise, { @@ -1199,6 +1203,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { ctx, workspace, user, + project, await envVars, await projectEnvVarsPromise, ); @@ -2980,6 +2985,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { let user = this.checkAndBlockUser("getSupportedWorkspaceClasses"); let selectedClass = await WorkspaceClasses.getConfiguredOrUpgradeFromLegacy( user, + undefined, this.config.workspaceClasses, this.entitlementService, ); diff --git a/components/server/src/workspace/workspace-classes.ts b/components/server/src/workspace/workspace-classes.ts index fe64dc8c159b95..685f94123dcdd9 100644 --- a/components/server/src/workspace/workspace-classes.ts +++ b/components/server/src/workspace/workspace-classes.ts @@ -5,7 +5,7 @@ */ import { WorkspaceDB } from "@gitpod/gitpod-db/lib"; -import { User, Workspace } from "@gitpod/gitpod-protocol"; +import { Project, User, Workspace } from "@gitpod/gitpod-protocol"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { EntitlementService } from "../billing/entitlement-service"; @@ -159,9 +159,13 @@ export namespace WorkspaceClasses { */ export async function getConfiguredOrUpgradeFromLegacy( user: User, + project: Project | undefined, classes: WorkspaceClassesConfig, entitlementService: EntitlementService, ): Promise { + if (project?.settings?.workspaceClasses?.regular) { + return project?.settings?.workspaceClasses?.regular; + } if (user.additionalData?.workspaceClasses?.regular) { return user.additionalData?.workspaceClasses?.regular; } diff --git a/components/server/src/workspace/workspace-starter.spec.ts b/components/server/src/workspace/workspace-starter.spec.ts index 1978645cc56032..5226cc48600d6c 100644 --- a/components/server/src/workspace/workspace-starter.spec.ts +++ b/components/server/src/workspace/workspace-starter.spec.ts @@ -326,6 +326,7 @@ async function execute(builder: WorkspaceClassTestBuilder, expectedClass: string workspace, previousInstance, user, + undefined, entitlementService, config, workspaceDb, diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index f37793f0c0ff59..96dfd1ffaaf5c4 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -60,6 +60,7 @@ import { WithReferrerContext, EnvVarWithValue, BillingTier, + Project, } from "@gitpod/gitpod-protocol"; import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; @@ -204,6 +205,7 @@ export async function getWorkspaceClassForInstance( workspace: Workspace, previousInstance: WorkspaceInstance | undefined, user: User, + project: Project | undefined, entitlementService: EntitlementService, config: WorkspaceClassesConfig, workspaceDb: DBWithTracing, @@ -217,17 +219,22 @@ export async function getWorkspaceClassForInstance( if (prebuildClass) { const userClass = await WorkspaceClasses.getConfiguredOrUpgradeFromLegacy( user, + project, config, entitlementService, ); workspaceClass = WorkspaceClasses.selectClassForRegular(prebuildClass, userClass, config); + } else if (project?.settings?.workspaceClasses?.regular) { + workspaceClass = project?.settings?.workspaceClasses?.regular; } else if (user.additionalData?.workspaceClasses?.regular) { workspaceClass = user.additionalData?.workspaceClasses?.regular; } } if (workspace.type == "prebuild") { - if (user.additionalData?.workspaceClasses?.prebuild) { + if (project?.settings?.workspaceClasses?.prebuild) { + workspaceClass = project?.settings?.workspaceClasses?.prebuild; + } else if (user.additionalData?.workspaceClasses?.prebuild) { workspaceClass = user.additionalData?.workspaceClasses?.prebuild; } } @@ -283,6 +290,7 @@ export class WorkspaceStarter { ctx: TraceContext, workspace: Workspace, user: User, + project: Project | undefined, userEnvVars: UserEnvVar[], projectEnvVars: ProjectEnvVar[], options?: StartWorkspaceOptions, @@ -360,6 +368,7 @@ export class WorkspaceStarter { workspace, lastValidWorkspaceInstance, user, + project, options.excludeFeatureFlags || [], ideConfig, ), @@ -768,6 +777,7 @@ export class WorkspaceStarter { workspace: Workspace, previousInstance: WorkspaceInstance | undefined, user: User, + project: Project | undefined, excludeFeatureFlags: NamedWorkspaceFeatureFlag[], ideConfig: IDEConfig, ): Promise { @@ -874,6 +884,7 @@ export class WorkspaceStarter { workspace, previousInstance, user, + project, this.entitlementService, this.config.workspaceClasses, this.workspaceDb,