From 8b5649e76d1c9cc210a2e200c8c174b7f0432ad9 Mon Sep 17 00:00:00 2001 From: Pudong Zheng Date: Mon, 20 Feb 2023 16:53:36 +0000 Subject: [PATCH 1/5] [dashboard] add custom global timeout user preference Co-authored-by: George Tsiolis --- .../src/user-settings/Preferences.tsx | 64 +++++++++++++++++++ components/gitpod-protocol/src/protocol.ts | 3 + 2 files changed, 67 insertions(+) diff --git a/components/dashboard/src/user-settings/Preferences.tsx b/components/dashboard/src/user-settings/Preferences.tsx index 3d208466d2979a..ebdb8a59db35b0 100644 --- a/components/dashboard/src/user-settings/Preferences.tsx +++ b/components/dashboard/src/user-settings/Preferences.tsx @@ -11,6 +11,8 @@ import { trackEvent } from "../Analytics"; import SelectIDE from "./SelectIDE"; import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu"; import { ThemeSelector } from "../components/ThemeSelector"; +import CheckBox from "../components/CheckBox"; +import { WorkspaceTimeoutDuration } from "@gitpod/gitpod-protocol"; export default function Preferences() { const { user } = useContext(UserContext); @@ -29,6 +31,32 @@ export default function Preferences() { } }; + const [disabledClosedTimeout, setDisabledClosedTimeout] = useState( + user?.additionalData?.disabledClosedTimeout ?? false, + ); + const actuallySetDisabledClosedTimeout = async (value: boolean) => { + try { + const additionalData = user?.additionalData || {}; + additionalData.disabledClosedTimeout = value; + await getGitpodService().server.updateLoggedInUser({ additionalData }); + setDisabledClosedTimeout(value); + } catch (e) { + alert("Cannot set custom workspace timeout: " + e.message); + } + }; + + const [workspaceTimeout, setWorkspaceTimeout] = useState(user?.additionalData?.workspaceTimeout ?? ""); + const actuallySetWorkspaceTimeout = async (value: string) => { + try { + const timeout = WorkspaceTimeoutDuration.validate(value); + const additionalData = user?.additionalData || {}; + additionalData.workspaceTimeout = timeout; + await getGitpodService().server.updateLoggedInUser({ additionalData }); + } catch (e) { + alert("Cannot set custom workspace timeout: " + e.message); + } + }; + return (
@@ -72,6 +100,42 @@ export default function Preferences() {

+ +

Inactivity Timeout

+

+ By default, workspaces stop following 30 minutes without user input (e.g. keystrokes or terminal + input commands). You can increase the workspace timeout up to a maximum of 24 hours. +

+
+

Default Inactivity Timeout

+ + setWorkspaceTimeout(e.target.value)} + /> + + +
+

+ Use minutes or hours, like 30m or 2h. +

+
+ + Don't change workspace inactivity timeout when closing the editor.} + checked={disabledClosedTimeout} + onChange={(e) => actuallySetDisabledClosedTimeout(e.target.checked)} + /> +
); diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 665b018fb9c886..417ec12adfc485 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -254,6 +254,9 @@ export interface AdditionalUserData { // whether the user has been migrated to team attribution. // a corresponding feature flag (team_only_attribution) triggers the migration. isMigratedToTeamOnlyAttribution?: boolean; + // user globol workspace timeout + workspaceTimeout?: string; + disabledClosedTimeout?: boolean; } export namespace AdditionalUserData { export function set(user: User, partialData: Partial): User { From c98eae2fae706eb6a2f3fe021e7d920eafe5b895 Mon Sep 17 00:00:00 2001 From: Pudong Zheng Date: Fri, 24 Feb 2023 16:15:25 +0000 Subject: [PATCH 2/5] [server] allow user custom global timeout --- components/gitpod-protocol/src/protocol.ts | 1 + .../server/src/workspace/workspace-starter.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 417ec12adfc485..3256a3e1d18fd0 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -256,6 +256,7 @@ export interface AdditionalUserData { isMigratedToTeamOnlyAttribution?: boolean; // user globol workspace timeout workspaceTimeout?: string; + // control whether to enable the closed timeout of a workspace, i.e. close web ide, disconnect ssh connection disabledClosedTimeout?: boolean; } export namespace AdditionalUserData { diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index e993746e78b42b..aa4d7c1ad5f5f4 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -57,6 +57,7 @@ import { Project, GitpodServer, IDESettings, + WorkspaceTimeoutDuration, } from "@gitpod/gitpod-protocol"; import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; @@ -1437,6 +1438,7 @@ export class WorkspaceStarter { lastValidWorkspaceInstanceId, ); const userTimeoutPromise = this.entitlementService.getDefaultWorkspaceTimeout(user, new Date()); + const allowSetTimeoutPromise = this.entitlementService.maySetTimeout(user, new Date()); let featureFlags = instance.configuration!.featureFlags || []; @@ -1467,7 +1469,19 @@ export class WorkspaceStarter { spec.setClass(instance.workspaceClass!); if (workspace.type === "regular") { - spec.setTimeout(await userTimeoutPromise); + const [defaultTimeout, allowSetTimeout] = await Promise.all([userTimeoutPromise, allowSetTimeoutPromise]); + spec.setTimeout(defaultTimeout); + if (allowSetTimeout) { + if (user.additionalData?.workspaceTimeout) { + try { + let timeout = WorkspaceTimeoutDuration.validate(user.additionalData?.workspaceTimeout); + spec.setTimeout(timeout); + } catch (err) {} + } + if (user.additionalData?.disabledClosedTimeout === true) { + spec.setClosedTimeout("0"); + } + } } spec.setAdmission(admissionLevel); const sshKeys = await this.userDB.trace(traceCtx).getSSHPublicKeys(user.id); From 00c798680c918a070ce84b134f9f49d524462ad0 Mon Sep 17 00:00:00 2001 From: Pudong Zheng Date: Tue, 28 Feb 2023 07:36:35 +0000 Subject: [PATCH 3/5] [server] add `supportConfigureWorkspaceTimeout` and `updateWorkspaceTimeoutSetting` api --- .../gitpod-protocol/src/gitpod-service.ts | 3 ++ components/gitpod-protocol/src/protocol.ts | 13 ++++--- components/server/src/auth/rate-limiter.ts | 2 ++ .../src/workspace/gitpod-server-impl.ts | 34 ++++++++++++++++++- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index eb0f8d5ba30acc..cdb914675b54d4 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -28,6 +28,7 @@ import { SSHPublicKeyValue, IDESettings, EnvVarWithValue, + WorkspaceTimeoutSetting, } from "./protocol"; import { Team, @@ -312,6 +313,8 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, getNotifications(): Promise; getSupportedWorkspaceClasses(): Promise; + supportConfigureWorkspaceTimeout(): Promise; + updateWorkspaceTimeoutSetting(setting: Partial): Promise; } export interface AppNotification { diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 3256a3e1d18fd0..29423283f61b70 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -233,7 +233,14 @@ export namespace User { } } -export interface AdditionalUserData { +export interface WorkspaceTimeoutSetting { + // user globol workspace timeout + workspaceTimeout: string; + // control whether to enable the closed timeout of a workspace, i.e. close web ide, disconnect ssh connection + disabledClosedTimeout: boolean; +} + +export interface AdditionalUserData extends Partial { platforms?: UserPlatform[]; emailNotificationSettings?: EmailNotificationSettings; featurePreview?: boolean; @@ -254,10 +261,6 @@ export interface AdditionalUserData { // whether the user has been migrated to team attribution. // a corresponding feature flag (team_only_attribution) triggers the migration. isMigratedToTeamOnlyAttribution?: boolean; - // user globol workspace timeout - workspaceTimeout?: string; - // control whether to enable the closed timeout of a workspace, i.e. close web ide, disconnect ssh connection - disabledClosedTimeout?: boolean; } export namespace AdditionalUserData { export function set(user: User, partialData: Partial): User { diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index 8274d473ee3db1..dc838817b929c5 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -232,6 +232,8 @@ const defaultFunctions: FunctionsConfig = { setUsageLimit: { group: "default", points: 1 }, getNotifications: { group: "default", points: 1 }, getSupportedWorkspaceClasses: { group: "default", points: 1 }, + supportConfigureWorkspaceTimeout: { group: "default", points: 1 }, + updateWorkspaceTimeoutSetting: { group: "default", points: 1 }, }; function getConfig(config: RateLimiterConfig): RateLimiterConfig { diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 30e79a008816b7..a7033d6010b690 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -163,7 +163,12 @@ import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol"; import { PartialProject } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol"; import { ClientMetadata } from "../websocket/websocket-connection-manager"; import { ConfigurationService } from "../config/configuration-service"; -import { EnvVarWithValue, ProjectEnvVar } from "@gitpod/gitpod-protocol/lib/protocol"; +import { + AdditionalUserData, + EnvVarWithValue, + ProjectEnvVar, + WorkspaceTimeoutSetting, +} from "@gitpod/gitpod-protocol/lib/protocol"; import { InstallationAdminSettings, TelemetryData } from "@gitpod/gitpod-protocol"; import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred"; import { InstallationAdminTelemetryDataProvider } from "../installation-admin/telemetry-data-provider"; @@ -494,6 +499,33 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { return user; } + public async supportConfigureWorkspaceTimeout(ctx: TraceContext): Promise { + const user = this.checkUser("supportConfigureWorkspaceTimeout"); + await this.guardAccess({ kind: "user", subject: user }, "get"); + + return await this.entitlementService.maySetTimeout(user, new Date()); + } + + public async updateWorkspaceTimeoutSetting( + ctx: TraceContext, + setting: Partial, + ): Promise { + traceAPIParams(ctx, { setting }); + if (setting.workspaceTimeout) { + WorkspaceTimeoutDuration.validate(setting.workspaceTimeout); + } + + const user = this.checkUser("updateWorkspaceTimeoutSetting"); + await this.guardAccess({ kind: "user", subject: user }, "update"); + + if (!(await this.entitlementService.maySetTimeout(user, new Date()))) { + throw new Error("configure workspace timeout only available for paid user."); + } + + AdditionalUserData.set(user, setting); + await this.userDB.updateUserPartial(user); + } + public async sendPhoneNumberVerificationToken(ctx: TraceContext, rawPhoneNumber: string): Promise { this.checkUser("sendPhoneNumberVerificationToken"); return this.verificationService.sendVerificationToken(formatPhoneNumber(rawPhoneNumber)); From 9ee6e9d305133a4185a198a89bb4c57be172e8e8 Mon Sep 17 00:00:00 2001 From: Pudong Date: Wed, 1 Mar 2023 13:54:51 +0000 Subject: [PATCH 4/5] [dashboard] use new api to configure workspace timeout setting --- .../src/user-settings/Preferences.tsx | 100 +++++++++--------- 1 file changed, 48 insertions(+), 52 deletions(-) diff --git a/components/dashboard/src/user-settings/Preferences.tsx b/components/dashboard/src/user-settings/Preferences.tsx index ebdb8a59db35b0..09baadc70c70b8 100644 --- a/components/dashboard/src/user-settings/Preferences.tsx +++ b/components/dashboard/src/user-settings/Preferences.tsx @@ -4,15 +4,15 @@ * See License.AGPL.txt in the project root for license information. */ -import { useContext, useState } from "react"; +import { useCallback, useContext, useEffect, useState } from "react"; import { getGitpodService } from "../service/service"; import { UserContext } from "../user-context"; import { trackEvent } from "../Analytics"; import SelectIDE from "./SelectIDE"; import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu"; import { ThemeSelector } from "../components/ThemeSelector"; -import CheckBox from "../components/CheckBox"; -import { WorkspaceTimeoutDuration } from "@gitpod/gitpod-protocol"; +import Alert from "../components/Alert"; +import { Link } from "react-router-dom"; export default function Preferences() { const { user } = useContext(UserContext); @@ -31,31 +31,21 @@ export default function Preferences() { } }; - const [disabledClosedTimeout, setDisabledClosedTimeout] = useState( - user?.additionalData?.disabledClosedTimeout ?? false, - ); - const actuallySetDisabledClosedTimeout = async (value: boolean) => { - try { - const additionalData = user?.additionalData || {}; - additionalData.disabledClosedTimeout = value; - await getGitpodService().server.updateLoggedInUser({ additionalData }); - setDisabledClosedTimeout(value); - } catch (e) { - alert("Cannot set custom workspace timeout: " + e.message); - } - }; - const [workspaceTimeout, setWorkspaceTimeout] = useState(user?.additionalData?.workspaceTimeout ?? ""); - const actuallySetWorkspaceTimeout = async (value: string) => { + const actuallySetWorkspaceTimeout = useCallback(async (value: string) => { try { - const timeout = WorkspaceTimeoutDuration.validate(value); - const additionalData = user?.additionalData || {}; - additionalData.workspaceTimeout = timeout; - await getGitpodService().server.updateLoggedInUser({ additionalData }); + await getGitpodService().server.updateWorkspaceTimeoutSetting({ workspaceTimeout: value }); } catch (e) { alert("Cannot set custom workspace timeout: " + e.message); } - }; + }, []); + + const [allowConfigureWorkspaceTimeout, setAllowConfigureWorkspaceTimeout] = useState(false); + useEffect(() => { + getGitpodService() + .server.supportConfigureWorkspaceTimeout() + .then((r) => setAllowConfigureWorkspaceTimeout(r)); + }, []); return (
@@ -103,38 +93,44 @@ export default function Preferences() {

Inactivity Timeout

- By default, workspaces stop following 30 minutes without user input (e.g. keystrokes or terminal - input commands). You can increase the workspace timeout up to a maximum of 24 hours. + Workspaces will stop after a period of inactivity without any user input.

-

Default Inactivity Timeout

- - setWorkspaceTimeout(e.target.value)} - /> - - -
-

- Use minutes or hours, like 30m or 2h. -

-
+

Default Workspace Timeout

+ + {!allowConfigureWorkspaceTimeout && ( + + Upgrade organization billing plan to use a custom inactivity + timeout. + + )} - Don't change workspace inactivity timeout when closing the editor.} - checked={disabledClosedTimeout} - onChange={(e) => actuallySetDisabledClosedTimeout(e.target.checked)} - /> + {allowConfigureWorkspaceTimeout && ( + <> + + setWorkspaceTimeout(e.target.value)} + /> + + +
+

+ Use minutes or hours, like 30m or 2h. +

+
+ + )}
From 93c093de88d4f07d002ece462d49b6a943ec352d Mon Sep 17 00:00:00 2001 From: Pudong Date: Thu, 2 Mar 2023 16:51:49 +0000 Subject: [PATCH 5/5] [dashboard, server] use maySetTimeout --- .../dashboard/src/user-settings/Preferences.tsx | 2 +- components/gitpod-protocol/src/gitpod-service.ts | 2 +- .../server/ee/src/workspace/gitpod-server-impl.ts | 11 ++--------- components/server/src/auth/rate-limiter.ts | 2 +- components/server/src/workspace/gitpod-server-impl.ts | 6 +++--- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/components/dashboard/src/user-settings/Preferences.tsx b/components/dashboard/src/user-settings/Preferences.tsx index 09baadc70c70b8..081a49b4072141 100644 --- a/components/dashboard/src/user-settings/Preferences.tsx +++ b/components/dashboard/src/user-settings/Preferences.tsx @@ -43,7 +43,7 @@ export default function Preferences() { const [allowConfigureWorkspaceTimeout, setAllowConfigureWorkspaceTimeout] = useState(false); useEffect(() => { getGitpodService() - .server.supportConfigureWorkspaceTimeout() + .server.maySetTimeout() .then((r) => setAllowConfigureWorkspaceTimeout(r)); }, []); diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index cdb914675b54d4..f7446cddc973de 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -313,7 +313,7 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, getNotifications(): Promise; getSupportedWorkspaceClasses(): Promise; - supportConfigureWorkspaceTimeout(): Promise; + maySetTimeout(): Promise; updateWorkspaceTimeoutSetting(setting: Partial): Promise; } diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 949e07bebfde14..bb527b35ffb66c 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -411,7 +411,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { await this.requireEELicense(Feature.FeatureSetTimeout); const user = this.checkUser("setWorkspaceTimeout"); - if (!(await this.maySetTimeout(user))) { + if (!(await this.entitlementService.maySetTimeout(user, new Date()))) { throw new ResponseError(ErrorCodes.PLAN_PROFESSIONAL_REQUIRED, "Plan upgrade is required"); } @@ -455,7 +455,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const user = this.checkUser("getWorkspaceTimeout"); - const canChange = await this.maySetTimeout(user); + const canChange = await this.entitlementService.maySetTimeout(user, new Date()); const workspace = await this.internalGetWorkspace(workspaceId, this.workspaceDb.trace(ctx)); const runningInstance = await this.workspaceDb.trace(ctx).findRunningInstance(workspaceId); @@ -494,13 +494,6 @@ export class GitpodServerEEImpl extends GitpodServerImpl { return PrebuiltWorkspace.isDone(pws); } - /** - * gitpod.io Extension point for implementing eligibility checks. Throws a ResponseError if not eligible. - */ - protected async maySetTimeout(user: User): Promise { - return this.entitlementService.maySetTimeout(user, new Date()); - } - public async controlAdmission(ctx: TraceContext, workspaceId: string, level: "owner" | "everyone"): Promise { traceAPIParams(ctx, { workspaceId, level }); traceWI(ctx, { workspaceId }); diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index dc838817b929c5..f86f11dfc56f6d 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -232,7 +232,7 @@ const defaultFunctions: FunctionsConfig = { setUsageLimit: { group: "default", points: 1 }, getNotifications: { group: "default", points: 1 }, getSupportedWorkspaceClasses: { group: "default", points: 1 }, - supportConfigureWorkspaceTimeout: { group: "default", points: 1 }, + maySetTimeout: { group: "default", points: 1 }, updateWorkspaceTimeoutSetting: { group: "default", points: 1 }, }; diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index a7033d6010b690..07bc1b47bbfe71 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -499,8 +499,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { return user; } - public async supportConfigureWorkspaceTimeout(ctx: TraceContext): Promise { - const user = this.checkUser("supportConfigureWorkspaceTimeout"); + public async maySetTimeout(ctx: TraceContext): Promise { + const user = this.checkUser("maySetTimeout"); await this.guardAccess({ kind: "user", subject: user }, "get"); return await this.entitlementService.maySetTimeout(user, new Date()); @@ -515,7 +515,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { WorkspaceTimeoutDuration.validate(setting.workspaceTimeout); } - const user = this.checkUser("updateWorkspaceTimeoutSetting"); + const user = this.checkAndBlockUser("updateWorkspaceTimeoutSetting"); await this.guardAccess({ kind: "user", subject: user }, "update"); if (!(await this.entitlementService.maySetTimeout(user, new Date()))) {