Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/teams/TeamPolicies.tsx
2501 views
1
/**
2
* Copyright (c) 2021 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, useCallback, useEffect, useState } from "react";
9
import Alert from "../components/Alert";
10
import { CheckboxInputField } from "../components/forms/CheckboxInputField";
11
import { Heading2, Heading3, Subheading } from "../components/typography/headings";
12
import { useIsOwner } from "../data/organizations/members-query";
13
import { useOrgSettingsQuery } from "../data/organizations/org-settings-query";
14
import { useCurrentOrg } from "../data/organizations/orgs-query";
15
import { useUpdateOrgSettingsMutation } from "../data/organizations/update-org-settings-mutation";
16
import { OrgSettingsPage } from "./OrgSettingsPage";
17
import { ConfigurationSettingsField } from "../repositories/detail/ConfigurationSettingsField";
18
import { useDocumentTitle } from "../hooks/use-document-title";
19
import { useOrgBillingMode } from "../data/billing-mode/org-billing-mode-query";
20
import { converter } from "../service/public-api";
21
import { useToast } from "../components/toasts/Toasts";
22
import type { PlainMessage } from "@bufbuild/protobuf";
23
import { WorkspaceTimeoutDuration } from "@gitpod/gitpod-protocol";
24
import { Link } from "react-router-dom";
25
import { InputField } from "../components/forms/InputField";
26
import { TextInput } from "../components/forms/TextInputField";
27
import { LoadingButton } from "@podkit/buttons/LoadingButton";
28
import { MaxParallelWorkspaces } from "./policies/MaxParallelWorkspaces";
29
import { WorkspaceClassesEnterpriseCallout } from "./policies/WorkspaceClassesEnterpriseCallout";
30
import { EditorOptions } from "./policies/EditorOptions";
31
import { RolePermissionsRestrictions } from "./policies/RoleRestrictions";
32
import { OrgWorkspaceClassesOptions } from "./policies/OrgWorkspaceClassesOptions";
33
import { useDefaultOrgTimeoutQuery } from "../data/organizations/default-org-timeout-query";
34
import { useInstallationConfiguration } from "../data/installation/installation-config-query";
35
36
export default function TeamPoliciesPage() {
37
useDocumentTitle("Organization Settings - Policies");
38
const { toast } = useToast();
39
const org = useCurrentOrg().data;
40
const isOwner = useIsOwner();
41
42
const { data: settings, isLoading } = useOrgSettingsQuery();
43
const updateTeamSettings = useUpdateOrgSettingsMutation();
44
45
const { data: installationConfig } = useInstallationConfiguration();
46
const isDedicatedInstallation = installationConfig?.isDedicatedInstallation ?? true; // we bias towards being on dedicated so the callout doesn't show when we're not sure
47
48
const billingMode = useOrgBillingMode();
49
const [workspaceTimeout, setWorkspaceTimeout] = useState<string | undefined>(undefined);
50
const [allowTimeoutChangeByMembers, setAllowTimeoutChangeByMembers] = useState<boolean | undefined>(undefined);
51
const [workspaceTimeoutSettingError, setWorkspaceTimeoutSettingError] = useState<string | undefined>(undefined);
52
53
const defaultOrgTimeout = useDefaultOrgTimeoutQuery();
54
55
const handleUpdateTeamSettings = useCallback(
56
async (newSettings: Partial<PlainMessage<OrganizationSettings>>, options?: { throwMutateError?: boolean }) => {
57
if (!org?.id) {
58
throw new Error("no organization selected");
59
}
60
if (!isOwner) {
61
throw new Error("no organization settings change permission");
62
}
63
try {
64
await updateTeamSettings.mutateAsync(newSettings);
65
setWorkspaceTimeoutSettingError(undefined);
66
toast("Organization settings updated");
67
} catch (error) {
68
if (options?.throwMutateError) {
69
throw error;
70
}
71
toast(`Failed to update organization settings: ${error.message}`);
72
console.error(error);
73
}
74
},
75
[updateTeamSettings, org?.id, isOwner, toast],
76
);
77
78
useEffect(() => {
79
setWorkspaceTimeout(
80
settings?.timeoutSettings?.inactivity
81
? converter.toDurationString(settings.timeoutSettings.inactivity)
82
: undefined,
83
);
84
setAllowTimeoutChangeByMembers(!settings?.timeoutSettings?.denyUserTimeouts);
85
}, [settings?.timeoutSettings]);
86
87
const handleUpdateOrganizationTimeoutSettings = useCallback(
88
(e: FormEvent<HTMLFormElement>) => {
89
e.preventDefault();
90
try {
91
if (workspaceTimeout) {
92
WorkspaceTimeoutDuration.validate(workspaceTimeout);
93
}
94
} catch (error) {
95
setWorkspaceTimeoutSettingError(error.message);
96
return;
97
}
98
99
// Nothing has changed
100
if (workspaceTimeout === undefined && allowTimeoutChangeByMembers === undefined) {
101
return;
102
}
103
104
handleUpdateTeamSettings({
105
timeoutSettings: {
106
inactivity: converter.toDurationOpt(workspaceTimeout),
107
denyUserTimeouts: !allowTimeoutChangeByMembers,
108
},
109
});
110
},
111
[workspaceTimeout, allowTimeoutChangeByMembers, handleUpdateTeamSettings],
112
);
113
114
const isPaidOrDedicated =
115
billingMode.data?.mode === "none" || (billingMode.data?.mode === "usage-based" && billingMode.data?.paid);
116
117
return (
118
<>
119
<OrgSettingsPage>
120
<div className="space-y-8">
121
<div>
122
<Heading2>Policies</Heading2>
123
<Subheading>
124
Restrict workspace classes, editors and sharing across your organization.
125
</Subheading>
126
</div>
127
128
<ConfigurationSettingsField>
129
<Heading3>Collaboration and sharing</Heading3>
130
131
{updateTeamSettings.isError && (
132
<Alert type="error" closable={true} className="mb-2 max-w-xl rounded-md">
133
<span>Failed to update organization settings: </span>
134
<span>{updateTeamSettings.error.message || "unknown error"}</span>
135
</Alert>
136
)}
137
138
<CheckboxInputField
139
label="Workspace Sharing"
140
hint="Allow workspaces created within an Organization to share the workspace with any authenticated user."
141
checked={!settings?.workspaceSharingDisabled}
142
onChange={(checked) => handleUpdateTeamSettings({ workspaceSharingDisabled: !checked })}
143
disabled={isLoading || !isOwner}
144
/>
145
</ConfigurationSettingsField>
146
147
<ConfigurationSettingsField>
148
<Heading3>Workspace timeouts</Heading3>
149
{!isPaidOrDedicated && (
150
<Alert type="info" className="my-3">
151
Setting Workspace timeouts is only available for organizations on a paid plan. Visit{" "}
152
<Link to={"/billing"} className="gp-link">
153
Billing
154
</Link>{" "}
155
to upgrade your plan.
156
</Alert>
157
)}
158
<form onSubmit={handleUpdateOrganizationTimeoutSettings}>
159
<InputField
160
label="Default workspace timeout"
161
error={workspaceTimeoutSettingError}
162
hint={
163
<span>
164
Use minutes or hours, like <span className="font-semibold">30m</span> or{" "}
165
<span className="font-semibold">2h</span>. If not set, your organization's
166
default of <span className="font-semibold">{defaultOrgTimeout}</span> will be
167
used.
168
</span>
169
}
170
>
171
<TextInput
172
value={workspaceTimeout ?? ""}
173
placeholder="e.g. 30m"
174
onChange={setWorkspaceTimeout}
175
disabled={updateTeamSettings.isLoading || !isOwner || !isPaidOrDedicated}
176
/>
177
</InputField>
178
<CheckboxInputField
179
label="Allow members to change workspace timeouts"
180
hint="Allow users to change the timeout duration for their workspaces as well as setting a default one in their user settings."
181
checked={!!allowTimeoutChangeByMembers}
182
containerClassName="my-4"
183
onChange={setAllowTimeoutChangeByMembers}
184
disabled={updateTeamSettings.isLoading || !isOwner || !isPaidOrDedicated}
185
/>
186
<LoadingButton
187
type="submit"
188
loading={updateTeamSettings.isLoading}
189
disabled={
190
!isOwner ||
191
!isPaidOrDedicated ||
192
(workspaceTimeout ===
193
converter.toDurationStringOpt(settings?.timeoutSettings?.inactivity) &&
194
allowTimeoutChangeByMembers === !settings?.timeoutSettings?.denyUserTimeouts)
195
}
196
>
197
Save
198
</LoadingButton>
199
</form>
200
</ConfigurationSettingsField>
201
202
<MaxParallelWorkspaces
203
isOwner={isOwner}
204
isLoading={updateTeamSettings.isLoading}
205
settings={settings}
206
handleUpdateTeamSettings={handleUpdateTeamSettings}
207
isPaidOrDedicated={isPaidOrDedicated}
208
/>
209
210
<OrgWorkspaceClassesOptions
211
isOwner={isOwner}
212
settings={settings}
213
handleUpdateTeamSettings={handleUpdateTeamSettings}
214
/>
215
216
{!isDedicatedInstallation && <WorkspaceClassesEnterpriseCallout />}
217
218
<EditorOptions
219
isOwner={isOwner}
220
settings={settings}
221
handleUpdateTeamSettings={handleUpdateTeamSettings}
222
/>
223
224
<RolePermissionsRestrictions
225
settings={settings}
226
isOwner={isOwner}
227
handleUpdateTeamSettings={handleUpdateTeamSettings}
228
/>
229
</div>
230
</OrgSettingsPage>
231
</>
232
);
233
}
234
235