Skip to content

Commit 6463197

Browse files
committed
Add UI and org settings server changes
1 parent 9ddf926 commit 6463197

File tree

16 files changed

+1203
-613
lines changed

16 files changed

+1203
-613
lines changed

components/dashboard/src/components/OrgMemberPermissionsOptions.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const OrgMemberPermissionRestrictionsOptions = ({
4747
return (
4848
<div className={cn("space-y-2", className)}>
4949
{rolesAllowedToOpenArbitraryRepositories.map((entry) => (
50-
<div className="flex gap-2 items-center">
50+
<div className="flex gap-2 items-center" key={entry}>
5151
<UserIcon size={20} />
5252
<div>
5353
<span className="font-medium text-pk-content-primary capitalize">{entry}</span>
@@ -116,6 +116,7 @@ export const OrganizationRoleRestrictionModal = ({
116116
<OrganizationRoleRestrictionSwitch
117117
role={role}
118118
checked={!restrictedRoles.includes(role)}
119+
key={role}
119120
onCheckedChange={(checked) => {
120121
console.log(role, { checked });
121122
if (!checked) {

components/dashboard/src/components/WorkspaceClassesOptions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const WorkspaceClassesOptions = (props: WorkspaceClassesOptionsProps) =>
3535
return (
3636
<div className={cn("space-y-2", props.className)}>
3737
{props.classes.map((cls) => (
38-
<div className="flex gap-2 items-center">
38+
<div className="flex gap-2 items-center" key={cls.id}>
3939
<CpuIcon size={20} />
4040
<div>
4141
<span className="font-medium text-pk-content-primary">{cls.displayName}</span>

components/dashboard/src/components/forms/TextInputField.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ interface TextInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement
4444
onChange?: (newValue: string) => void;
4545
onBlur?: () => void;
4646
}
47-
4847
export const TextInput: FunctionComponent<TextInputProps> = memo(({ className, onChange, onBlur, ...props }) => {
4948
const handleChange = useCallback(
5049
(e) => {

components/dashboard/src/data/organizations/update-org-settings-mutation.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type UpdateOrganizationSettingsArgs = Partial<
2424
| "defaultRole"
2525
| "timeoutSettings"
2626
| "roleRestrictions"
27+
| "maxParallelRunningWorkspaces"
2728
>
2829
>;
2930

@@ -43,10 +44,11 @@ export const useUpdateOrgSettingsMutation = () => {
4344
defaultRole,
4445
timeoutSettings,
4546
roleRestrictions,
47+
maxParallelRunningWorkspaces,
4648
}) => {
4749
const settings = await organizationClient.updateOrganizationSettings({
4850
organizationId: teamId,
49-
workspaceSharingDisabled: workspaceSharingDisabled || false,
51+
workspaceSharingDisabled: workspaceSharingDisabled ?? false,
5052
defaultWorkspaceImage,
5153
allowedWorkspaceClasses,
5254
updatePinnedEditorVersions: !!pinnedEditorVersions,
@@ -57,6 +59,8 @@ export const useUpdateOrgSettingsMutation = () => {
5759
timeoutSettings,
5860
roleRestrictions,
5961
updateRoleRestrictions: !!roleRestrictions,
62+
maxParallelRunningWorkspaces,
63+
updateMaxParallelRunningWorkspaces: maxParallelRunningWorkspaces !== undefined,
6064
});
6165
return settings.settings!;
6266
},

components/dashboard/src/teams/TeamPolicies.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
OrgMemberPermissionRestrictionsOptions,
4545
} from "../components/OrgMemberPermissionsOptions";
4646
import { LightbulbIcon } from "lucide-react";
47+
import { MaxParallelWorkspaces } from "./policies/MaxParallelWorkspaces";
4748

4849
export default function TeamPoliciesPage() {
4950
useDocumentTitle("Organization Settings - Policies");
@@ -121,7 +122,7 @@ export default function TeamPoliciesPage() {
121122
[workspaceTimeout, allowTimeoutChangeByMembers, handleUpdateTeamSettings],
122123
);
123124

124-
const billingModeAllowsWorkspaceTimeouts =
125+
const isPaidPlan =
125126
billingMode.data?.mode === "none" || (billingMode.data?.mode === "usage-based" && billingMode.data?.paid);
126127

127128
return (
@@ -156,7 +157,7 @@ export default function TeamPoliciesPage() {
156157

157158
<ConfigurationSettingsField>
158159
<Heading3>Workspace timeouts</Heading3>
159-
{!billingModeAllowsWorkspaceTimeouts && (
160+
{!isPaidPlan && (
160161
<Alert type="info" className="my-3">
161162
Setting Workspace timeouts is only available for organizations on a paid plan. Visit{" "}
162163
<Link to={"/billing"} className="gp-link">
@@ -180,9 +181,7 @@ export default function TeamPoliciesPage() {
180181
value={workspaceTimeout ?? ""}
181182
placeholder="e.g. 30m"
182183
onChange={setWorkspaceTimeout}
183-
disabled={
184-
updateTeamSettings.isLoading || !isOwner || !billingModeAllowsWorkspaceTimeouts
185-
}
184+
disabled={updateTeamSettings.isLoading || !isOwner || !isPaidPlan}
186185
/>
187186
</InputField>
188187
<CheckboxInputField
@@ -191,19 +190,16 @@ export default function TeamPoliciesPage() {
191190
checked={!!allowTimeoutChangeByMembers}
192191
containerClassName="my-4"
193192
onChange={setAllowTimeoutChangeByMembers}
194-
disabled={
195-
updateTeamSettings.isLoading || !isOwner || !billingModeAllowsWorkspaceTimeouts
196-
}
193+
disabled={updateTeamSettings.isLoading || !isOwner || !isPaidPlan}
197194
/>
198195
<LoadingButton
199196
type="submit"
200197
loading={updateTeamSettings.isLoading}
201198
disabled={
202199
!isOwner ||
203-
!billingModeAllowsWorkspaceTimeouts ||
204-
((workspaceTimeout ===
205-
converter.toDurationString(settings?.timeoutSettings?.inactivity) ??
206-
"") &&
200+
!isPaidPlan ||
201+
(workspaceTimeout ===
202+
converter.toDurationString(settings?.timeoutSettings?.inactivity) &&
207203
allowTimeoutChangeByMembers === !settings?.timeoutSettings?.denyUserTimeouts)
208204
}
209205
>
@@ -212,6 +208,14 @@ export default function TeamPoliciesPage() {
212208
</form>
213209
</ConfigurationSettingsField>
214210

211+
<MaxParallelWorkspaces
212+
isOwner={isOwner}
213+
isLoading={updateTeamSettings.isLoading}
214+
settings={settings}
215+
handleUpdateTeamSettings={handleUpdateTeamSettings}
216+
isPaidPlan={isPaidPlan}
217+
/>
218+
215219
<OrgWorkspaceClassesOptions
216220
isOwner={isOwner}
217221
settings={settings}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
8+
import { FormEvent, useEffect, useState } from "react";
9+
import { ConfigurationSettingsField } from "../../repositories/detail/ConfigurationSettingsField";
10+
import { Heading3, Subheading } from "@podkit/typography/Headings";
11+
import { InputField } from "../../components/forms/InputField";
12+
import { TextInput } from "../../components/forms/TextInputField";
13+
import { LoadingButton } from "@podkit/buttons/LoadingButton";
14+
import { MAX_PARALLEL_WORKSPACES_FREE, MAX_PARALLEL_WORKSPACES_PAID } from "@gitpod/gitpod-protocol";
15+
import { PlainMessage } from "@bufbuild/protobuf";
16+
17+
type Props = {
18+
isOwner: boolean;
19+
isLoading: boolean;
20+
isPaidPlan: boolean;
21+
settings?: OrganizationSettings;
22+
handleUpdateTeamSettings: (
23+
newSettings: Partial<PlainMessage<OrganizationSettings>>,
24+
options?: {
25+
throwMutateError?: boolean;
26+
},
27+
) => Promise<void>;
28+
};
29+
30+
export const MaxParallelWorkspaces = ({
31+
isOwner,
32+
isLoading,
33+
settings,
34+
isPaidPlan,
35+
handleUpdateTeamSettings,
36+
}: Props) => {
37+
const [error, setError] = useState<string | undefined>(undefined);
38+
const [maxParallelWorkspaces, setMaxParallelWorkspaces] = useState<number>(
39+
settings?.maxParallelRunningWorkspaces ?? 0,
40+
);
41+
42+
const organizationDefault = isPaidPlan ? MAX_PARALLEL_WORKSPACES_PAID : MAX_PARALLEL_WORKSPACES_FREE;
43+
44+
const handleSubmit = async (e: FormEvent) => {
45+
e.preventDefault();
46+
if (maxParallelWorkspaces < 0) {
47+
setError("The maximum parallel running workspaces must be a positive number.");
48+
return;
49+
}
50+
await handleUpdateTeamSettings({
51+
maxParallelRunningWorkspaces: maxParallelWorkspaces,
52+
});
53+
};
54+
55+
useEffect(() => {
56+
setMaxParallelWorkspaces(settings?.maxParallelRunningWorkspaces ?? 0);
57+
}, [settings?.maxParallelRunningWorkspaces]);
58+
59+
return (
60+
<ConfigurationSettingsField>
61+
<Heading3>Maximum parallel running workspaces</Heading3>
62+
<Subheading>
63+
By default, every user in your organization can have <strong>{organizationDefault}</strong> workspaces
64+
running at the same time. You can change this limit below or revert to this default by specifying{" "}
65+
<strong>0</strong> as the limit.
66+
</Subheading>
67+
<form onSubmit={handleSubmit}>
68+
<InputField label="Maximum parallel running workspaces" error={error} className="mb-4">
69+
<TextInput
70+
value={maxParallelWorkspaces ?? ""}
71+
onChange={(newValue) => {
72+
setMaxParallelWorkspaces(parseInt(newValue));
73+
setError(undefined);
74+
}}
75+
disabled={isLoading || !isOwner}
76+
/>
77+
</InputField>
78+
<LoadingButton type="submit" loading={isLoading} disabled={!isOwner}>
79+
Save
80+
</LoadingButton>
81+
</form>
82+
</ConfigurationSettingsField>
83+
);
84+
};

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@ export const WORKSPACE_TIMEOUT_EXTENDED: WorkspaceTimeoutDuration = "180m";
377377
export const WORKSPACE_LIFETIME_SHORT: WorkspaceTimeoutDuration = "8h";
378378
export const WORKSPACE_LIFETIME_LONG: WorkspaceTimeoutDuration = "36h";
379379

380+
export const MAX_PARALLEL_WORKSPACES_FREE = 4;
381+
export const MAX_PARALLEL_WORKSPACES_PAID = 16;
382+
380383
export const createServiceMock = function <C extends GitpodClient, S extends GitpodServer>(
381384
methods: Partial<JsonRpcProxy<S>>,
382385
): GitpodServiceImpl<C, S> {

components/gitpod-protocol/src/teams-projects-protocol.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ export interface OrganizationSettings {
233233
timeoutSettings?: TimeoutSettings;
234234

235235
roleRestrictions?: RoleRestrictions;
236+
237+
// max number of parallel running workspaces per user
238+
maxParallelRunningWorkspaces?: number;
236239
}
237240

238241
export type TimeoutSettings = {

components/public-api/gitpod/v1/organization.proto

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,11 @@ message UpdateOrganizationSettingsRequest {
182182
// update_role_restrictions specifies whether role_restrictions should be updated
183183
optional bool update_role_restrictions = 13;
184184

185+
// update_max_parallel_running_workspaces specifies whether max_parallel_running_workspaces should be updated
186+
optional bool update_max_parallel_running_workspaces = 14;
187+
185188
// max_parallel_running_workspaces is the maximum number of workspaces that a single user can run in parallel. 0 resets to the default, which depends on the org plan
186-
optional int32 max_parallel_running_workspaces = 14;
189+
optional int32 max_parallel_running_workspaces = 15;
187190
}
188191

189192
message UpdateOrganizationSettingsResponse {

0 commit comments

Comments
 (0)