Skip to content

Commit 04e576f

Browse files
mustard-mhfiliptronicekgtsiolis
authored
Add default workspace image to org setting (#18723)
* Add default workspace image to org setting * [db] add migration * fixup * [dashboard] add org default image settings * fixup * dashboard fixup * Add server image test TODO * [server] assign global workspace default image * [dashboard] allow to submit empty string (will fallback to global default) * [gp-cli] support gp validate with default image * [dashboard] save default image * fixup * fixup * [gp-cli] improve output * [gp-cli] improve gp init * [gp-cli] gp validate compatibility * Update components/dashboard/src/teams/TeamSettings.tsx Co-authored-by: Filip Troníček <[email protected]> * Remove org id / get org settings in supervisor * Remove `WorkspaceConfigContext` * Add unit tests * Rename to `DefaultWorkspaceImage` * Update components/dashboard/src/teams/TeamSettings.tsx Co-authored-by: George Tsiolis <[email protected]> * Add empty image fallback to supervisor * Fix default workspace image setup * Update org settings fields * fixup * Allow empty image to set to default one --------- Co-authored-by: Filip Troníček <[email protected]> Co-authored-by: George Tsiolis <[email protected]>
1 parent 1c5087b commit 04e576f

30 files changed

+729
-145
lines changed

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@ import { getGitpodService } from "../../service/service";
1010
import { getOrgSettingsQueryKey, OrgSettingsResult } from "./org-settings-query";
1111
import { useCurrentOrg } from "./orgs-query";
1212

13-
type UpdateOrganizationSettingsArgs = Pick<OrganizationSettings, "workspaceSharingDisabled">;
13+
type UpdateOrganizationSettingsArgs = Partial<
14+
Pick<OrganizationSettings, "workspaceSharingDisabled" | "defaultWorkspaceImage">
15+
>;
1416

1517
export const useUpdateOrgSettingsMutation = () => {
1618
const queryClient = useQueryClient();
1719
const team = useCurrentOrg().data;
1820
const teamId = team?.id || "";
1921

2022
return useMutation<OrganizationSettings, Error, UpdateOrganizationSettingsArgs>({
21-
mutationFn: async ({ workspaceSharingDisabled }) => {
22-
return await getGitpodService().server.updateOrgSettings(teamId, { workspaceSharingDisabled });
23+
mutationFn: async ({ workspaceSharingDisabled, defaultWorkspaceImage }) => {
24+
return await getGitpodService().server.updateOrgSettings(teamId, {
25+
workspaceSharingDisabled,
26+
defaultWorkspaceImage,
27+
});
2328
},
2429
onSuccess: (newData, _) => {
2530
const queryKey = getOrgSettingsQueryKey(teamId);

components/dashboard/src/menu/OrganizationSelector.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,19 @@ export default function OrganizationSelector() {
7070
separator: false,
7171
link: "/usage",
7272
});
73-
}
74-
75-
// Show billing & settings if user is an owner of current org
76-
if (currentOrg.data && currentOrg.data.isOwner) {
77-
if (billingMode?.mode === "usage-based") {
78-
linkEntries.push({
79-
title: "Billing",
80-
customContent: <LinkEntry>Billing</LinkEntry>,
81-
active: false,
82-
separator: false,
83-
link: "/billing",
84-
});
73+
// Show billing if user is an owner of current org
74+
if (currentOrg.data.isOwner) {
75+
if (billingMode?.mode === "usage-based") {
76+
linkEntries.push({
77+
title: "Billing",
78+
customContent: <LinkEntry>Billing</LinkEntry>,
79+
active: false,
80+
separator: false,
81+
link: "/billing",
82+
});
83+
}
8584
}
85+
// Org settings is available for all members, but only owner can change them
8686
linkEntries.push({
8787
title: "Settings",
8888
customContent: <LinkEntry>Settings</LinkEntry>,

components/dashboard/src/teams/OrgSettingsPage.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function OrgSettingsPage({ children }: OrgSettingsPageProps) {
3232
billingMode: orgBillingMode.data,
3333
ssoEnabled: oidcServiceEnabled,
3434
orgGitAuthProviders,
35+
isOwner: org.data?.isOwner,
3536
}),
3637
[org.data, orgBillingMode.data, oidcServiceEnabled, orgGitAuthProviders],
3738
);
@@ -51,8 +52,11 @@ export function OrgSettingsPage({ children }: OrgSettingsPageProps) {
5152
);
5253
}
5354

55+
// TODO: redirect when current page is not included in menu
56+
const onlyForOwner = false;
57+
5458
// After we've loaded, ensure user is an owner, if not, redirect
55-
if (!org.data?.isOwner) {
59+
if (onlyForOwner && !org.data?.isOwner) {
5660
return <Redirect to={"/"} />;
5761
}
5862

@@ -68,27 +72,28 @@ function getTeamSettingsMenu(params: {
6872
billingMode?: BillingMode;
6973
ssoEnabled?: boolean;
7074
orgGitAuthProviders: boolean;
75+
isOwner?: boolean;
7176
}) {
72-
const { billingMode, ssoEnabled, orgGitAuthProviders } = params;
77+
const { billingMode, ssoEnabled, orgGitAuthProviders, isOwner } = params;
7378
const result = [
7479
{
7580
title: "General",
7681
link: [`/settings`],
7782
},
7883
];
79-
if (ssoEnabled) {
84+
if (isOwner && ssoEnabled) {
8085
result.push({
8186
title: "SSO",
8287
link: [`/sso`],
8388
});
8489
}
85-
if (orgGitAuthProviders) {
90+
if (isOwner && orgGitAuthProviders) {
8691
result.push({
8792
title: "Git Providers",
8893
link: [`/settings/git`],
8994
});
9095
}
91-
if (billingMode?.mode !== "none") {
96+
if (isOwner && billingMode?.mode !== "none") {
9297
// The Billing page handles billing mode itself, so: always show it!
9398
result.push({
9499
title: "Billing",

components/dashboard/src/teams/TeamSettings.tsx

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { OrganizationSettings } from "@gitpod/gitpod-protocol";
8-
import React, { useCallback, useState } from "react";
8+
import React, { useCallback, useState, useEffect } from "react";
99
import Alert from "../components/Alert";
1010
import { Button } from "../components/Button";
1111
import { CheckboxInputField } from "../components/forms/CheckboxInputField";
@@ -14,13 +14,14 @@ import { TextInputField } from "../components/forms/TextInputField";
1414
import { Heading2, Subheading } from "../components/typography/headings";
1515
import { useUpdateOrgSettingsMutation } from "../data/organizations/update-org-settings-mutation";
1616
import { useOrgSettingsQuery } from "../data/organizations/org-settings-query";
17-
import { useCurrentOrg, useOrganizationsInvalidator } from "../data/organizations/orgs-query";
17+
import { OrganizationInfo, useCurrentOrg, useOrganizationsInvalidator } from "../data/organizations/orgs-query";
1818
import { useUpdateOrgMutation } from "../data/organizations/update-org-mutation";
1919
import { useOnBlurError } from "../hooks/use-onblur-error";
2020
import { teamsService } from "../service/public-api";
2121
import { gitpodHostUrl } from "../service/service";
2222
import { useCurrentUser } from "../user-context";
2323
import { OrgSettingsPage } from "./OrgSettingsPage";
24+
import { useToast } from "../components/toasts/Toasts";
2425

2526
export default function TeamSettingsPage() {
2627
const user = useCurrentUser();
@@ -31,21 +32,6 @@ export default function TeamSettingsPage() {
3132
const [teamName, setTeamName] = useState(org?.name || "");
3233
const [updated, setUpdated] = useState(false);
3334
const updateOrg = useUpdateOrgMutation();
34-
const { data: settings, isLoading } = useOrgSettingsQuery();
35-
const updateTeamSettings = useUpdateOrgSettingsMutation();
36-
37-
const handleUpdateTeamSettings = useCallback(
38-
(newSettings: Partial<OrganizationSettings>) => {
39-
if (!org?.id) {
40-
throw new Error("no organization selected");
41-
}
42-
updateTeamSettings.mutate({
43-
...settings,
44-
...newSettings,
45-
});
46-
},
47-
[updateTeamSettings, org?.id, settings],
48-
);
4935

5036
const close = () => setModal(false);
5137

@@ -60,6 +46,9 @@ export default function TeamSettingsPage() {
6046

6147
const updateTeamInformation = useCallback(
6248
async (e: React.FormEvent) => {
49+
if (!org?.isOwner) {
50+
return;
51+
}
6352
e.preventDefault();
6453

6554
if (!orgFormIsValid) {
@@ -74,7 +63,7 @@ export default function TeamSettingsPage() {
7463
console.error(error);
7564
}
7665
},
77-
[orgFormIsValid, updateOrg, teamName],
66+
[orgFormIsValid, updateOrg, teamName, org],
7867
);
7968

8069
const deleteTeam = useCallback(async () => {
@@ -99,12 +88,6 @@ export default function TeamSettingsPage() {
9988
<span>{updateOrg.error.message || "unknown error"}</span>
10089
</Alert>
10190
)}
102-
{updateTeamSettings.isError && (
103-
<Alert type="error" closable={true} className="mb-2 max-w-xl rounded-md">
104-
<span>Failed to update organization settings: </span>
105-
<span>{updateTeamSettings.error.message || "unknown error"}</span>
106-
</Alert>
107-
)}
10891
{updated && (
10992
<Alert type="message" closable={true} className="mb-2 max-w-xl rounded-md">
11093
Organization name has been updated.
@@ -117,30 +100,27 @@ export default function TeamSettingsPage() {
117100
value={teamName}
118101
error={teamNameError.message}
119102
onChange={setTeamName}
103+
disabled={!org?.isOwner}
120104
onBlur={teamNameError.onBlur}
121105
/>
122106

123-
<Button className="mt-4" htmlType="submit" disabled={org?.name === teamName || !orgFormIsValid}>
124-
Update Organization
125-
</Button>
126-
127-
<Heading2 className="pt-12">Collaboration & Sharing</Heading2>
128-
<CheckboxInputField
129-
label="Workspace Sharing"
130-
hint="Allow workspaces created within an Organization to share the workspace with any authenticated user."
131-
checked={!settings?.workspaceSharingDisabled}
132-
onChange={(checked) => handleUpdateTeamSettings({ workspaceSharingDisabled: !checked })}
133-
disabled={isLoading}
134-
/>
107+
{org?.isOwner && (
108+
<Button className="mt-4" htmlType="submit" disabled={org?.name === teamName || !orgFormIsValid}>
109+
Update Organization
110+
</Button>
111+
)}
135112
</form>
136113

137-
{user?.organizationId !== org?.id && (
114+
<OrgSettingsForm org={org} />
115+
116+
{user?.organizationId !== org?.id && org?.isOwner && (
138117
<>
139118
<Heading2 className="pt-12">Delete Organization</Heading2>
140119
<Subheading className="pb-4 max-w-2xl">
141120
Deleting this organization will also remove all associated data, including projects and
142121
workspaces. Deleted organizations cannot be restored!
143122
</Subheading>
123+
144124
<button className="danger secondary" onClick={() => setModal(true)}>
145125
Delete Organization
146126
</button>
@@ -185,3 +165,92 @@ export default function TeamSettingsPage() {
185165
</>
186166
);
187167
}
168+
169+
function OrgSettingsForm(props: { org?: OrganizationInfo }) {
170+
const { org } = props;
171+
const { data: settings, isLoading } = useOrgSettingsQuery();
172+
const updateTeamSettings = useUpdateOrgSettingsMutation();
173+
const [defaultWorkspaceImage, setDefaultWorkspaceImage] = useState(settings?.defaultWorkspaceImage ?? "");
174+
const { toast } = useToast();
175+
176+
useEffect(() => {
177+
if (!settings) {
178+
return;
179+
}
180+
setDefaultWorkspaceImage(settings.defaultWorkspaceImage ?? "");
181+
}, [settings]);
182+
183+
const handleUpdateTeamSettings = useCallback(
184+
async (newSettings: Partial<OrganizationSettings>) => {
185+
if (!org?.id) {
186+
throw new Error("no organization selected");
187+
}
188+
if (!org.isOwner) {
189+
throw new Error("no organization settings change permission");
190+
}
191+
try {
192+
await updateTeamSettings.mutateAsync({
193+
// We don't want to have original setting passed, since defaultWorkspaceImage could be undefined
194+
// to bring compatibility when we're going to change Gitpod install value's defaultImage setting
195+
...newSettings,
196+
});
197+
if (newSettings.defaultWorkspaceImage) {
198+
toast("Default workspace image has been updated.");
199+
}
200+
} catch (error) {
201+
console.error(error);
202+
toast(
203+
error.message
204+
? "Failed to update organization settings: " + error.message
205+
: "Oh no, there was a problem with our service.",
206+
);
207+
}
208+
},
209+
[updateTeamSettings, org?.id, org?.isOwner, toast],
210+
);
211+
212+
return (
213+
<form
214+
onSubmit={(e) => {
215+
e.preventDefault();
216+
handleUpdateTeamSettings({ defaultWorkspaceImage });
217+
}}
218+
>
219+
<Heading2 className="pt-12">Collaboration & Sharing</Heading2>
220+
<Subheading className="max-w-2xl">
221+
Choose which workspace images you want to use for your workspaces.
222+
</Subheading>
223+
224+
{updateTeamSettings.isError && (
225+
<Alert type="error" closable={true} className="mb-2 max-w-xl rounded-md">
226+
<span>Failed to update organization settings: </span>
227+
<span>{updateTeamSettings.error.message || "unknown error"}</span>
228+
</Alert>
229+
)}
230+
231+
<CheckboxInputField
232+
label="Workspace Sharing"
233+
hint="Allow workspaces created within an Organization to share the workspace with any authenticated user."
234+
checked={!settings?.workspaceSharingDisabled}
235+
onChange={(checked) => handleUpdateTeamSettings({ workspaceSharingDisabled: !checked })}
236+
disabled={isLoading || !org?.isOwner}
237+
/>
238+
239+
<Heading2 className="pt-12">Workspace Settings</Heading2>
240+
<TextInputField
241+
label="Default Image"
242+
// TODO: Provide document links
243+
hint="Use any official Gitpod Docker image, or Docker image reference"
244+
value={defaultWorkspaceImage}
245+
onChange={setDefaultWorkspaceImage}
246+
disabled={isLoading || !org?.isOwner}
247+
/>
248+
249+
{org?.isOwner && (
250+
<Button htmlType="submit" className="mt-4" disabled={!org.isOwner}>
251+
Update Default Image
252+
</Button>
253+
)}
254+
</form>
255+
);
256+
}

0 commit comments

Comments
 (0)