Skip to content

Add default workspace image to org setting #18723

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b7dd2e6
Add default workspace image to org setting
mustard-mh Sep 14, 2023
5cb3118
[db] add migration
mustard-mh Sep 15, 2023
90a851e
fixup
mustard-mh Sep 15, 2023
2c6c5d1
[dashboard] add org default image settings
mustard-mh Sep 15, 2023
03262da
fixup
mustard-mh Sep 15, 2023
88e8909
dashboard fixup
mustard-mh Sep 15, 2023
bc074ba
Add server image test TODO
mustard-mh Sep 15, 2023
5080271
[server] assign global workspace default image
mustard-mh Sep 15, 2023
7652208
[dashboard] allow to submit empty string (will fallback to global def…
mustard-mh Sep 15, 2023
d22e969
[gp-cli] support gp validate with default image
mustard-mh Sep 15, 2023
7e6e29c
[dashboard] save default image
mustard-mh Sep 15, 2023
f347e3b
fixup
mustard-mh Sep 15, 2023
0a7b1ff
fixup
mustard-mh Sep 15, 2023
dea9564
[gp-cli] improve output
mustard-mh Sep 15, 2023
ae32c3a
[gp-cli] improve gp init
mustard-mh Sep 15, 2023
2771ff4
[gp-cli] gp validate compatibility
mustard-mh Sep 15, 2023
e2a7849
Update components/dashboard/src/teams/TeamSettings.tsx
mustard-mh Sep 18, 2023
f875e63
Remove org id / get org settings in supervisor
mustard-mh Sep 18, 2023
2756caf
Remove `WorkspaceConfigContext`
mustard-mh Sep 18, 2023
c8ccb52
Add unit tests
mustard-mh Sep 18, 2023
8dd6695
Rename to `DefaultWorkspaceImage`
mustard-mh Sep 18, 2023
06cfd08
Update components/dashboard/src/teams/TeamSettings.tsx
mustard-mh Sep 18, 2023
1941376
Add empty image fallback to supervisor
mustard-mh Sep 18, 2023
c2df290
Fix default workspace image setup
mustard-mh Sep 18, 2023
4987594
Update org settings fields
mustard-mh Sep 18, 2023
5f0daea
fixup
mustard-mh Sep 18, 2023
97ae60f
Allow empty image to set to default one
mustard-mh Sep 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ import { getGitpodService } from "../../service/service";
import { getOrgSettingsQueryKey, OrgSettingsResult } from "./org-settings-query";
import { useCurrentOrg } from "./orgs-query";

type UpdateOrganizationSettingsArgs = Pick<OrganizationSettings, "workspaceSharingDisabled">;
type UpdateOrganizationSettingsArgs = Partial<
Pick<OrganizationSettings, "workspaceSharingDisabled" | "defaultWorkspaceImage">
>;

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

return useMutation<OrganizationSettings, Error, UpdateOrganizationSettingsArgs>({
mutationFn: async ({ workspaceSharingDisabled }) => {
return await getGitpodService().server.updateOrgSettings(teamId, { workspaceSharingDisabled });
mutationFn: async ({ workspaceSharingDisabled, defaultWorkspaceImage }) => {
return await getGitpodService().server.updateOrgSettings(teamId, {
workspaceSharingDisabled,
defaultWorkspaceImage,
});
},
onSuccess: (newData, _) => {
const queryKey = getOrgSettingsQueryKey(teamId);
Expand Down
24 changes: 12 additions & 12 deletions components/dashboard/src/menu/OrganizationSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,19 @@ export default function OrganizationSelector() {
separator: false,
link: "/usage",
});
}

// Show billing & settings if user is an owner of current org
if (currentOrg.data && currentOrg.data.isOwner) {
if (billingMode?.mode === "usage-based") {
linkEntries.push({
title: "Billing",
customContent: <LinkEntry>Billing</LinkEntry>,
active: false,
separator: false,
link: "/billing",
});
// Show billing if user is an owner of current org
if (currentOrg.data.isOwner) {
if (billingMode?.mode === "usage-based") {
linkEntries.push({
title: "Billing",
customContent: <LinkEntry>Billing</LinkEntry>,
active: false,
separator: false,
link: "/billing",
});
}
}
// Org settings is available for all members, but only owner can change them
linkEntries.push({
title: "Settings",
customContent: <LinkEntry>Settings</LinkEntry>,
Expand Down
15 changes: 10 additions & 5 deletions components/dashboard/src/teams/OrgSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function OrgSettingsPage({ children }: OrgSettingsPageProps) {
billingMode: orgBillingMode.data,
ssoEnabled: oidcServiceEnabled,
orgGitAuthProviders,
isOwner: org.data?.isOwner,
}),
[org.data, orgBillingMode.data, oidcServiceEnabled, orgGitAuthProviders],
);
Expand All @@ -51,8 +52,11 @@ export function OrgSettingsPage({ children }: OrgSettingsPageProps) {
);
}

// TODO: redirect when current page is not included in menu
const onlyForOwner = false;

// After we've loaded, ensure user is an owner, if not, redirect
if (!org.data?.isOwner) {
if (onlyForOwner && !org.data?.isOwner) {
return <Redirect to={"/"} />;
}

Expand All @@ -68,27 +72,28 @@ function getTeamSettingsMenu(params: {
billingMode?: BillingMode;
ssoEnabled?: boolean;
orgGitAuthProviders: boolean;
isOwner?: boolean;
}) {
const { billingMode, ssoEnabled, orgGitAuthProviders } = params;
const { billingMode, ssoEnabled, orgGitAuthProviders, isOwner } = params;
const result = [
{
title: "General",
link: [`/settings`],
},
];
if (ssoEnabled) {
if (isOwner && ssoEnabled) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are effectively reimplementing the permissions here in an implicit way. Instead of assuming that owners have access we should rely on FGA to figure that out. Not necessarily as part of this PR, though because it involves more discussion and work.
cc @akosyakov @geropl

result.push({
title: "SSO",
link: [`/sso`],
});
}
if (orgGitAuthProviders) {
if (isOwner && orgGitAuthProviders) {
result.push({
title: "Git Providers",
link: [`/settings/git`],
});
}
if (billingMode?.mode !== "none") {
if (isOwner && billingMode?.mode !== "none") {
// The Billing page handles billing mode itself, so: always show it!
result.push({
title: "Billing",
Expand Down
143 changes: 106 additions & 37 deletions components/dashboard/src/teams/TeamSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { OrganizationSettings } from "@gitpod/gitpod-protocol";
import React, { useCallback, useState } from "react";
import React, { useCallback, useState, useEffect } from "react";
import Alert from "../components/Alert";
import { Button } from "../components/Button";
import { CheckboxInputField } from "../components/forms/CheckboxInputField";
Expand All @@ -14,13 +14,14 @@ import { TextInputField } from "../components/forms/TextInputField";
import { Heading2, Subheading } from "../components/typography/headings";
import { useUpdateOrgSettingsMutation } from "../data/organizations/update-org-settings-mutation";
import { useOrgSettingsQuery } from "../data/organizations/org-settings-query";
import { useCurrentOrg, useOrganizationsInvalidator } from "../data/organizations/orgs-query";
import { OrganizationInfo, useCurrentOrg, useOrganizationsInvalidator } from "../data/organizations/orgs-query";
import { useUpdateOrgMutation } from "../data/organizations/update-org-mutation";
import { useOnBlurError } from "../hooks/use-onblur-error";
import { teamsService } from "../service/public-api";
import { gitpodHostUrl } from "../service/service";
import { useCurrentUser } from "../user-context";
import { OrgSettingsPage } from "./OrgSettingsPage";
import { useToast } from "../components/toasts/Toasts";

export default function TeamSettingsPage() {
const user = useCurrentUser();
Expand All @@ -31,21 +32,6 @@ export default function TeamSettingsPage() {
const [teamName, setTeamName] = useState(org?.name || "");
const [updated, setUpdated] = useState(false);
const updateOrg = useUpdateOrgMutation();
const { data: settings, isLoading } = useOrgSettingsQuery();
const updateTeamSettings = useUpdateOrgSettingsMutation();

const handleUpdateTeamSettings = useCallback(
(newSettings: Partial<OrganizationSettings>) => {
if (!org?.id) {
throw new Error("no organization selected");
}
updateTeamSettings.mutate({
...settings,
...newSettings,
});
},
[updateTeamSettings, org?.id, settings],
);

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

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

const updateTeamInformation = useCallback(
async (e: React.FormEvent) => {
if (!org?.isOwner) {
return;
}
e.preventDefault();

if (!orgFormIsValid) {
Expand All @@ -74,7 +63,7 @@ export default function TeamSettingsPage() {
console.error(error);
}
},
[orgFormIsValid, updateOrg, teamName],
[orgFormIsValid, updateOrg, teamName, org],
);

const deleteTeam = useCallback(async () => {
Expand All @@ -99,12 +88,6 @@ export default function TeamSettingsPage() {
<span>{updateOrg.error.message || "unknown error"}</span>
</Alert>
)}
{updateTeamSettings.isError && (
<Alert type="error" closable={true} className="mb-2 max-w-xl rounded-md">
<span>Failed to update organization settings: </span>
<span>{updateTeamSettings.error.message || "unknown error"}</span>
</Alert>
)}
{updated && (
<Alert type="message" closable={true} className="mb-2 max-w-xl rounded-md">
Organization name has been updated.
Expand All @@ -117,30 +100,27 @@ export default function TeamSettingsPage() {
value={teamName}
error={teamNameError.message}
onChange={setTeamName}
disabled={!org?.isOwner}
onBlur={teamNameError.onBlur}
/>

<Button className="mt-4" htmlType="submit" disabled={org?.name === teamName || !orgFormIsValid}>
Update Organization
</Button>

<Heading2 className="pt-12">Collaboration & Sharing</Heading2>
<CheckboxInputField
label="Workspace Sharing"
hint="Allow workspaces created within an Organization to share the workspace with any authenticated user."
checked={!settings?.workspaceSharingDisabled}
onChange={(checked) => handleUpdateTeamSettings({ workspaceSharingDisabled: !checked })}
disabled={isLoading}
/>
{org?.isOwner && (
<Button className="mt-4" htmlType="submit" disabled={org?.name === teamName || !orgFormIsValid}>
Update Organization
</Button>
)}
</form>

{user?.organizationId !== org?.id && (
<OrgSettingsForm org={org} />

{user?.organizationId !== org?.id && org?.isOwner && (
<>
<Heading2 className="pt-12">Delete Organization</Heading2>
<Subheading className="pb-4 max-w-2xl">
Deleting this organization will also remove all associated data, including projects and
workspaces. Deleted organizations cannot be restored!
</Subheading>

<button className="danger secondary" onClick={() => setModal(true)}>
Delete Organization
</button>
Expand Down Expand Up @@ -185,3 +165,92 @@ export default function TeamSettingsPage() {
</>
);
}

function OrgSettingsForm(props: { org?: OrganizationInfo }) {
const { org } = props;
const { data: settings, isLoading } = useOrgSettingsQuery();
const updateTeamSettings = useUpdateOrgSettingsMutation();
const [defaultWorkspaceImage, setDefaultWorkspaceImage] = useState(settings?.defaultWorkspaceImage ?? "");
const { toast } = useToast();

useEffect(() => {
if (!settings) {
return;
}
setDefaultWorkspaceImage(settings.defaultWorkspaceImage ?? "");
}, [settings]);

const handleUpdateTeamSettings = useCallback(
async (newSettings: Partial<OrganizationSettings>) => {
if (!org?.id) {
throw new Error("no organization selected");
}
if (!org.isOwner) {
throw new Error("no organization settings change permission");
}
try {
await updateTeamSettings.mutateAsync({
// We don't want to have original setting passed, since defaultWorkspaceImage could be undefined
// to bring compatibility when we're going to change Gitpod install value's defaultImage setting
...newSettings,
});
if (newSettings.defaultWorkspaceImage) {
toast("Default workspace image has been updated.");
}
} catch (error) {
console.error(error);
toast(
error.message
? "Failed to update organization settings: " + error.message
: "Oh no, there was a problem with our service.",
);
}
},
[updateTeamSettings, org?.id, org?.isOwner, toast],
);

return (
<form
onSubmit={(e) => {
e.preventDefault();
handleUpdateTeamSettings({ defaultWorkspaceImage });
}}
>
<Heading2 className="pt-12">Collaboration & Sharing</Heading2>
<Subheading className="max-w-2xl">
Choose which workspace images you want to use for your workspaces.
</Subheading>

{updateTeamSettings.isError && (
<Alert type="error" closable={true} className="mb-2 max-w-xl rounded-md">
<span>Failed to update organization settings: </span>
<span>{updateTeamSettings.error.message || "unknown error"}</span>
</Alert>
)}

<CheckboxInputField
label="Workspace Sharing"
hint="Allow workspaces created within an Organization to share the workspace with any authenticated user."
checked={!settings?.workspaceSharingDisabled}
onChange={(checked) => handleUpdateTeamSettings({ workspaceSharingDisabled: !checked })}
disabled={isLoading || !org?.isOwner}
/>

<Heading2 className="pt-12">Workspace Settings</Heading2>
<TextInputField
label="Default Image"
// TODO: Provide document links
hint="Use any official Gitpod Docker image, or Docker image reference"
value={defaultWorkspaceImage}
onChange={setDefaultWorkspaceImage}
disabled={isLoading || !org?.isOwner}
/>
Comment on lines +240 to +247
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue(non-blocking): Minor UX issue, but could we use a placeholder for now when someone is deleting the text input_ here? Feels a bit unintuitive when you delete all text and click save that there's a default value overriding the empty text input. Could be improved later.


{org?.isOwner && (
<Button htmlType="submit" className="mt-4" disabled={!org.isOwner}>
Update Default Image
</Button>
)}
</form>
);
}
Loading