Skip to content

Commit

Permalink
[dashboard, server] Add global custom timeout preference (#16503)
Browse files Browse the repository at this point in the history
* [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]>
  • Loading branch information
iQQBot and gtsiolis authored Mar 3, 2023
1 parent 568a574 commit 5ee2a66
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 13 deletions.
62 changes: 61 additions & 1 deletion components/dashboard/src/user-settings/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +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 Alert from "../components/Alert";
import { Link } from "react-router-dom";

export default function Preferences() {
const { user } = useContext(UserContext);
Expand All @@ -29,6 +31,22 @@ export default function Preferences() {
}
};

const [workspaceTimeout, setWorkspaceTimeout] = useState<string>(user?.additionalData?.workspaceTimeout ?? "");
const actuallySetWorkspaceTimeout = useCallback(async (value: string) => {
try {
await getGitpodService().server.updateWorkspaceTimeoutSetting({ workspaceTimeout: value });
} catch (e) {
alert("Cannot set custom workspace timeout: " + e.message);
}
}, []);

const [allowConfigureWorkspaceTimeout, setAllowConfigureWorkspaceTimeout] = useState<boolean>(false);
useEffect(() => {
getGitpodService()
.server.maySetTimeout()
.then((r) => setAllowConfigureWorkspaceTimeout(r));
}, []);

return (
<div>
<PageWithSettingsSubMenu>
Expand Down Expand Up @@ -72,6 +90,48 @@ export default function Preferences() {
</p>
</div>
</div>

<h3 className="mt-12">Inactivity Timeout </h3>
<p className="text-base text-gray-500 dark:text-gray-400">
Workspaces will stop after a period of inactivity without any user input.
</p>
<div className="mt-4 max-w-xl">
<h4>Default Workspace Timeout</h4>

{!allowConfigureWorkspaceTimeout && (
<Alert type="message">
Upgrade organization <Link to="/billing">billing</Link> plan to use a custom inactivity
timeout.
</Alert>
)}

{allowConfigureWorkspaceTimeout && (
<>
<span className="flex mt-2">
<input
type="text"
className="w-96 h-9"
value={workspaceTimeout}
disabled={!allowConfigureWorkspaceTimeout}
placeholder="e.g. 30m"
onChange={(e) => setWorkspaceTimeout(e.target.value)}
/>
<button
className="secondary ml-2"
disabled={!allowConfigureWorkspaceTimeout}
onClick={() => actuallySetWorkspaceTimeout(workspaceTimeout)}
>
Save Changes
</button>
</span>
<div className="mt-1">
<p className="text-gray-500 dark:text-gray-400">
Use minutes or hours, like <strong>30m</strong> or <strong>2h</strong>.
</p>
</div>
</>
)}
</div>
</PageWithSettingsSubMenu>
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
SSHPublicKeyValue,
IDESettings,
EnvVarWithValue,
WorkspaceTimeoutSetting,
} from "./protocol";
import {
Team,
Expand Down Expand Up @@ -314,6 +315,8 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
getNotifications(): Promise<AppNotification[]>;

getSupportedWorkspaceClasses(): Promise<SupportedWorkspaceClass[]>;
maySetTimeout(): Promise<boolean>;
updateWorkspaceTimeoutSetting(setting: Partial<WorkspaceTimeoutSetting>): Promise<void>;
}

export interface AppNotification {
Expand Down
9 changes: 8 additions & 1 deletion components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,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<WorkspaceTimeoutSetting> {
platforms?: UserPlatform[];
emailNotificationSettings?: EmailNotificationSettings;
featurePreview?: boolean;
Expand Down
11 changes: 2 additions & 9 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<boolean> {
return this.entitlementService.maySetTimeout(user, new Date());
}

public async controlAdmission(ctx: TraceContext, workspaceId: string, level: "owner" | "everyone"): Promise<void> {
traceAPIParams(ctx, { workspaceId, level });
traceWI(ctx, { workspaceId });
Expand Down
2 changes: 2 additions & 0 deletions components/server/src/auth/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ const defaultFunctions: FunctionsConfig = {
setUsageLimit: { group: "default", points: 1 },
getNotifications: { group: "default", points: 1 },
getSupportedWorkspaceClasses: { group: "default", points: 1 },
maySetTimeout: { group: "default", points: 1 },
updateWorkspaceTimeoutSetting: { group: "default", points: 1 },
};

function getConfig(config: RateLimiterConfig): RateLimiterConfig {
Expand Down
34 changes: 33 additions & 1 deletion components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -494,6 +499,33 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
return user;
}

public async maySetTimeout(ctx: TraceContext): Promise<boolean> {
const user = this.checkUser("maySetTimeout");
await this.guardAccess({ kind: "user", subject: user }, "get");

return await this.entitlementService.maySetTimeout(user, new Date());
}

public async updateWorkspaceTimeoutSetting(
ctx: TraceContext,
setting: Partial<WorkspaceTimeoutSetting>,
): Promise<void> {
traceAPIParams(ctx, { setting });
if (setting.workspaceTimeout) {
WorkspaceTimeoutDuration.validate(setting.workspaceTimeout);
}

const user = this.checkAndBlockUser("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<void> {
this.checkUser("sendPhoneNumberVerificationToken");
return this.verificationService.sendVerificationToken(formatPhoneNumber(rawPhoneNumber));
Expand Down
16 changes: 15 additions & 1 deletion components/server/src/workspace/workspace-starter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 || [];

Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 5ee2a66

Please sign in to comment.