Skip to content

Commit 5ee2a66

Browse files
iQQBotgtsiolis
andauthored
[dashboard, server] Add global custom timeout preference (#16503)
* [dashboard] add custom global timeout user preference Co-authored-by: George Tsiolis <[email protected]> * [server] allow user custom global timeout * [server] add `supportConfigureWorkspaceTimeout` and `updateWorkspaceTimeoutSetting` api * [dashboard] use new api to configure workspace timeout setting * [dashboard, server] use maySetTimeout --------- Co-authored-by: George Tsiolis <[email protected]>
1 parent 568a574 commit 5ee2a66

File tree

7 files changed

+124
-13
lines changed

7 files changed

+124
-13
lines changed

components/dashboard/src/user-settings/Preferences.tsx

+61-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { useContext, useState } from "react";
7+
import { useCallback, useContext, useEffect, useState } from "react";
88
import { getGitpodService } from "../service/service";
99
import { UserContext } from "../user-context";
1010
import { trackEvent } from "../Analytics";
1111
import SelectIDE from "./SelectIDE";
1212
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
1313
import { ThemeSelector } from "../components/ThemeSelector";
14+
import Alert from "../components/Alert";
15+
import { Link } from "react-router-dom";
1416

1517
export default function Preferences() {
1618
const { user } = useContext(UserContext);
@@ -29,6 +31,22 @@ export default function Preferences() {
2931
}
3032
};
3133

34+
const [workspaceTimeout, setWorkspaceTimeout] = useState<string>(user?.additionalData?.workspaceTimeout ?? "");
35+
const actuallySetWorkspaceTimeout = useCallback(async (value: string) => {
36+
try {
37+
await getGitpodService().server.updateWorkspaceTimeoutSetting({ workspaceTimeout: value });
38+
} catch (e) {
39+
alert("Cannot set custom workspace timeout: " + e.message);
40+
}
41+
}, []);
42+
43+
const [allowConfigureWorkspaceTimeout, setAllowConfigureWorkspaceTimeout] = useState<boolean>(false);
44+
useEffect(() => {
45+
getGitpodService()
46+
.server.maySetTimeout()
47+
.then((r) => setAllowConfigureWorkspaceTimeout(r));
48+
}, []);
49+
3250
return (
3351
<div>
3452
<PageWithSettingsSubMenu>
@@ -72,6 +90,48 @@ export default function Preferences() {
7290
</p>
7391
</div>
7492
</div>
93+
94+
<h3 className="mt-12">Inactivity Timeout </h3>
95+
<p className="text-base text-gray-500 dark:text-gray-400">
96+
Workspaces will stop after a period of inactivity without any user input.
97+
</p>
98+
<div className="mt-4 max-w-xl">
99+
<h4>Default Workspace Timeout</h4>
100+
101+
{!allowConfigureWorkspaceTimeout && (
102+
<Alert type="message">
103+
Upgrade organization <Link to="/billing">billing</Link> plan to use a custom inactivity
104+
timeout.
105+
</Alert>
106+
)}
107+
108+
{allowConfigureWorkspaceTimeout && (
109+
<>
110+
<span className="flex mt-2">
111+
<input
112+
type="text"
113+
className="w-96 h-9"
114+
value={workspaceTimeout}
115+
disabled={!allowConfigureWorkspaceTimeout}
116+
placeholder="e.g. 30m"
117+
onChange={(e) => setWorkspaceTimeout(e.target.value)}
118+
/>
119+
<button
120+
className="secondary ml-2"
121+
disabled={!allowConfigureWorkspaceTimeout}
122+
onClick={() => actuallySetWorkspaceTimeout(workspaceTimeout)}
123+
>
124+
Save Changes
125+
</button>
126+
</span>
127+
<div className="mt-1">
128+
<p className="text-gray-500 dark:text-gray-400">
129+
Use minutes or hours, like <strong>30m</strong> or <strong>2h</strong>.
130+
</p>
131+
</div>
132+
</>
133+
)}
134+
</div>
75135
</PageWithSettingsSubMenu>
76136
</div>
77137
);

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

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
SSHPublicKeyValue,
2929
IDESettings,
3030
EnvVarWithValue,
31+
WorkspaceTimeoutSetting,
3132
} from "./protocol";
3233
import {
3334
Team,
@@ -314,6 +315,8 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
314315
getNotifications(): Promise<AppNotification[]>;
315316

316317
getSupportedWorkspaceClasses(): Promise<SupportedWorkspaceClass[]>;
318+
maySetTimeout(): Promise<boolean>;
319+
updateWorkspaceTimeoutSetting(setting: Partial<WorkspaceTimeoutSetting>): Promise<void>;
317320
}
318321

319322
export interface AppNotification {

components/gitpod-protocol/src/protocol.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,14 @@ export namespace User {
236236
}
237237
}
238238

239-
export interface AdditionalUserData {
239+
export interface WorkspaceTimeoutSetting {
240+
// user globol workspace timeout
241+
workspaceTimeout: string;
242+
// control whether to enable the closed timeout of a workspace, i.e. close web ide, disconnect ssh connection
243+
disabledClosedTimeout: boolean;
244+
}
245+
246+
export interface AdditionalUserData extends Partial<WorkspaceTimeoutSetting> {
240247
platforms?: UserPlatform[];
241248
emailNotificationSettings?: EmailNotificationSettings;
242249
featurePreview?: boolean;

components/server/ee/src/workspace/gitpod-server-impl.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
411411
await this.requireEELicense(Feature.FeatureSetTimeout);
412412
const user = this.checkUser("setWorkspaceTimeout");
413413

414-
if (!(await this.maySetTimeout(user))) {
414+
if (!(await this.entitlementService.maySetTimeout(user, new Date()))) {
415415
throw new ResponseError(ErrorCodes.PLAN_PROFESSIONAL_REQUIRED, "Plan upgrade is required");
416416
}
417417

@@ -455,7 +455,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
455455

456456
const user = this.checkUser("getWorkspaceTimeout");
457457

458-
const canChange = await this.maySetTimeout(user);
458+
const canChange = await this.entitlementService.maySetTimeout(user, new Date());
459459

460460
const workspace = await this.internalGetWorkspace(workspaceId, this.workspaceDb.trace(ctx));
461461
const runningInstance = await this.workspaceDb.trace(ctx).findRunningInstance(workspaceId);
@@ -494,13 +494,6 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
494494
return PrebuiltWorkspace.isDone(pws);
495495
}
496496

497-
/**
498-
* gitpod.io Extension point for implementing eligibility checks. Throws a ResponseError if not eligible.
499-
*/
500-
protected async maySetTimeout(user: User): Promise<boolean> {
501-
return this.entitlementService.maySetTimeout(user, new Date());
502-
}
503-
504497
public async controlAdmission(ctx: TraceContext, workspaceId: string, level: "owner" | "everyone"): Promise<void> {
505498
traceAPIParams(ctx, { workspaceId, level });
506499
traceWI(ctx, { workspaceId });

components/server/src/auth/rate-limiter.ts

+2
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ const defaultFunctions: FunctionsConfig = {
234234
setUsageLimit: { group: "default", points: 1 },
235235
getNotifications: { group: "default", points: 1 },
236236
getSupportedWorkspaceClasses: { group: "default", points: 1 },
237+
maySetTimeout: { group: "default", points: 1 },
238+
updateWorkspaceTimeoutSetting: { group: "default", points: 1 },
237239
};
238240

239241
function getConfig(config: RateLimiterConfig): RateLimiterConfig {

components/server/src/workspace/gitpod-server-impl.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,12 @@ import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
163163
import { PartialProject } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol";
164164
import { ClientMetadata } from "../websocket/websocket-connection-manager";
165165
import { ConfigurationService } from "../config/configuration-service";
166-
import { EnvVarWithValue, ProjectEnvVar } from "@gitpod/gitpod-protocol/lib/protocol";
166+
import {
167+
AdditionalUserData,
168+
EnvVarWithValue,
169+
ProjectEnvVar,
170+
WorkspaceTimeoutSetting,
171+
} from "@gitpod/gitpod-protocol/lib/protocol";
167172
import { InstallationAdminSettings, TelemetryData } from "@gitpod/gitpod-protocol";
168173
import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
169174
import { InstallationAdminTelemetryDataProvider } from "../installation-admin/telemetry-data-provider";
@@ -494,6 +499,33 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
494499
return user;
495500
}
496501

502+
public async maySetTimeout(ctx: TraceContext): Promise<boolean> {
503+
const user = this.checkUser("maySetTimeout");
504+
await this.guardAccess({ kind: "user", subject: user }, "get");
505+
506+
return await this.entitlementService.maySetTimeout(user, new Date());
507+
}
508+
509+
public async updateWorkspaceTimeoutSetting(
510+
ctx: TraceContext,
511+
setting: Partial<WorkspaceTimeoutSetting>,
512+
): Promise<void> {
513+
traceAPIParams(ctx, { setting });
514+
if (setting.workspaceTimeout) {
515+
WorkspaceTimeoutDuration.validate(setting.workspaceTimeout);
516+
}
517+
518+
const user = this.checkAndBlockUser("updateWorkspaceTimeoutSetting");
519+
await this.guardAccess({ kind: "user", subject: user }, "update");
520+
521+
if (!(await this.entitlementService.maySetTimeout(user, new Date()))) {
522+
throw new Error("configure workspace timeout only available for paid user.");
523+
}
524+
525+
AdditionalUserData.set(user, setting);
526+
await this.userDB.updateUserPartial(user);
527+
}
528+
497529
public async sendPhoneNumberVerificationToken(ctx: TraceContext, rawPhoneNumber: string): Promise<void> {
498530
this.checkUser("sendPhoneNumberVerificationToken");
499531
return this.verificationService.sendVerificationToken(formatPhoneNumber(rawPhoneNumber));

components/server/src/workspace/workspace-starter.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
Project,
5858
GitpodServer,
5959
IDESettings,
60+
WorkspaceTimeoutDuration,
6061
} from "@gitpod/gitpod-protocol";
6162
import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
6263
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
@@ -1437,6 +1438,7 @@ export class WorkspaceStarter {
14371438
lastValidWorkspaceInstanceId,
14381439
);
14391440
const userTimeoutPromise = this.entitlementService.getDefaultWorkspaceTimeout(user, new Date());
1441+
const allowSetTimeoutPromise = this.entitlementService.maySetTimeout(user, new Date());
14401442

14411443
let featureFlags = instance.configuration!.featureFlags || [];
14421444

@@ -1467,7 +1469,19 @@ export class WorkspaceStarter {
14671469
spec.setClass(instance.workspaceClass!);
14681470

14691471
if (workspace.type === "regular") {
1470-
spec.setTimeout(await userTimeoutPromise);
1472+
const [defaultTimeout, allowSetTimeout] = await Promise.all([userTimeoutPromise, allowSetTimeoutPromise]);
1473+
spec.setTimeout(defaultTimeout);
1474+
if (allowSetTimeout) {
1475+
if (user.additionalData?.workspaceTimeout) {
1476+
try {
1477+
let timeout = WorkspaceTimeoutDuration.validate(user.additionalData?.workspaceTimeout);
1478+
spec.setTimeout(timeout);
1479+
} catch (err) {}
1480+
}
1481+
if (user.additionalData?.disabledClosedTimeout === true) {
1482+
spec.setClosedTimeout("0");
1483+
}
1484+
}
14711485
}
14721486
spec.setAdmission(admissionLevel);
14731487
const sshKeys = await this.userDB.trace(traceCtx).getSSHPublicKeys(user.id);

0 commit comments

Comments
 (0)