Skip to content

Commit 8c7a725

Browse files
committed
UI reorg
1 parent 3a58ac9 commit 8c7a725

File tree

7 files changed

+454
-279
lines changed

7 files changed

+454
-279
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Copyright (c) 2025 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 { Button } from "@podkit/buttons/Button";
9+
import { useMutation } from "@tanstack/react-query";
10+
import { useState } from "react";
11+
import { IdeOptionsModifyModalProps, IdeOptions, IdeOptionsModifyModal } from "../components/IdeOptions";
12+
import { Heading3, Subheading } from "../components/typography/headings";
13+
import { useAllowedWorkspaceEditorsMemo } from "../data/ide-options/ide-options-query";
14+
import { ConfigurationSettingsField } from "../repositories/detail/ConfigurationSettingsField";
15+
16+
type Props = {
17+
settings: OrganizationSettings | undefined;
18+
isOwner: boolean;
19+
handleUpdateTeamSettings: (
20+
newSettings: Partial<OrganizationSettings>,
21+
options?: { throwMutateError?: boolean },
22+
) => Promise<void>;
23+
};
24+
export const EditorOptions = ({ isOwner, settings, handleUpdateTeamSettings }: Props) => {
25+
const [showModal, setShowModal] = useState(false);
26+
const { data: installationOptions, isLoading: installationOptionsIsLoading } = useAllowedWorkspaceEditorsMemo(
27+
undefined,
28+
{
29+
filterOutDisabled: true,
30+
ignoreScope: ["organization", "configuration"],
31+
},
32+
);
33+
const { data: orgOptions, isLoading: orgOptionsIsLoading } = useAllowedWorkspaceEditorsMemo(undefined, {
34+
filterOutDisabled: true,
35+
ignoreScope: ["configuration"],
36+
});
37+
38+
const updateMutation: IdeOptionsModifyModalProps["updateMutation"] = useMutation({
39+
mutationFn: async ({ restrictedEditors, pinnedEditorVersions }) => {
40+
const updatedRestrictedEditors = [...restrictedEditors.keys()];
41+
const updatedPinnedEditorVersions = Object.fromEntries(pinnedEditorVersions.entries());
42+
43+
await handleUpdateTeamSettings(
44+
{ restrictedEditorNames: updatedRestrictedEditors, pinnedEditorVersions: updatedPinnedEditorVersions },
45+
{ throwMutateError: true },
46+
);
47+
},
48+
});
49+
50+
const restrictedEditors = new Set<string>(settings?.restrictedEditorNames || []);
51+
const pinnedEditorVersions = new Map<string, string>(Object.entries(settings?.pinnedEditorVersions || {}));
52+
53+
return (
54+
<ConfigurationSettingsField>
55+
<Heading3>Available editors</Heading3>
56+
<Subheading>
57+
Limit the available editors in your organization. Requires <span className="font-medium">Owner</span>{" "}
58+
permissions to change.
59+
</Subheading>
60+
61+
<IdeOptions
62+
isLoading={orgOptionsIsLoading}
63+
className="mt-4"
64+
ideOptions={orgOptions}
65+
pinnedEditorVersions={pinnedEditorVersions}
66+
/>
67+
68+
{isOwner && (
69+
<Button className="mt-6" onClick={() => setShowModal(true)}>
70+
Manage Editors
71+
</Button>
72+
)}
73+
74+
{showModal && (
75+
<IdeOptionsModifyModal
76+
isLoading={installationOptionsIsLoading}
77+
ideOptions={installationOptions}
78+
restrictedEditors={restrictedEditors}
79+
pinnedEditorVersions={pinnedEditorVersions}
80+
updateMutation={updateMutation}
81+
onClose={() => setShowModal(false)}
82+
/>
83+
)}
84+
</ConfigurationSettingsField>
85+
);
86+
};

components/dashboard/src/teams/TeamNetworking.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66

77
import { isGitpodIo } from "../utils";
8-
import React from "react";
98
import { Heading2, Heading3, Subheading } from "../components/typography/headings";
109
import { OrgSettingsPage } from "./OrgSettingsPage";
1110
import { ConfigurationSettingsField } from "../repositories/detail/ConfigurationSettingsField";
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Copyright (c) 2025 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 { useCallback, useEffect, useState } from "react";
9+
import Alert from "../components/Alert";
10+
import { Heading2, Heading3, Subheading } from "../components/typography/headings";
11+
import { useIsOwner } from "../data/organizations/members-query";
12+
import { useOrgSettingsQuery } from "../data/organizations/org-settings-query";
13+
import { useCurrentOrg } from "../data/organizations/orgs-query";
14+
import { useUpdateOrgSettingsMutation } from "../data/organizations/update-org-settings-mutation";
15+
import { OrgSettingsPage } from "./OrgSettingsPage";
16+
import { ConfigurationSettingsField } from "../repositories/detail/ConfigurationSettingsField";
17+
import { useDocumentTitle } from "../hooks/use-document-title";
18+
import { useOrgBillingMode } from "../data/billing-mode/org-billing-mode-query";
19+
import { useToast } from "../components/toasts/Toasts";
20+
import type { PlainMessage } from "@bufbuild/protobuf";
21+
import { Link } from "react-router-dom";
22+
import { InputField } from "../components/forms/InputField";
23+
import { TextInput } from "../components/forms/TextInputField";
24+
import { LoadingButton } from "@podkit/buttons/LoadingButton";
25+
26+
export default function TeamOnboardingPage() {
27+
useDocumentTitle("Organization Settings - Onboarding");
28+
const { toast } = useToast();
29+
const org = useCurrentOrg().data;
30+
const isOwner = useIsOwner();
31+
32+
const { data: settings } = useOrgSettingsQuery();
33+
const updateTeamSettings = useUpdateOrgSettingsMutation();
34+
35+
const [internalLink, setInternalLink] = useState<string | undefined>(undefined);
36+
37+
const billingMode = useOrgBillingMode();
38+
39+
const handleUpdateTeamSettings = useCallback(
40+
async (newSettings: Partial<PlainMessage<OrganizationSettings>>, options?: { throwMutateError?: boolean }) => {
41+
if (!org?.id) {
42+
throw new Error("no organization selected");
43+
}
44+
if (!isOwner) {
45+
throw new Error("no organization settings change permission");
46+
}
47+
try {
48+
await updateTeamSettings.mutateAsync({
49+
...settings,
50+
...newSettings,
51+
});
52+
toast("Organization settings updated");
53+
} catch (error) {
54+
if (options?.throwMutateError) {
55+
throw error;
56+
}
57+
toast(`Failed to update organization settings: ${error.message}`);
58+
console.error(error);
59+
}
60+
},
61+
[updateTeamSettings, org?.id, isOwner, settings, toast],
62+
);
63+
64+
const handleUpdateInternalLink = useCallback(async () => {
65+
await handleUpdateTeamSettings({ onboardingSettings: { internalLink } });
66+
}, [handleUpdateTeamSettings, internalLink]);
67+
68+
useEffect(() => {
69+
// if (settings) {
70+
// setInternalLink(settings.internalLink);
71+
// }
72+
}, [settings?.timeoutSettings]);
73+
74+
const isPaidOrDedicated =
75+
billingMode.data?.mode === "none" || (billingMode.data?.mode === "usage-based" && billingMode.data?.paid);
76+
77+
return (
78+
<>
79+
<OrgSettingsPage>
80+
<div className="space-y-8">
81+
<div>
82+
<Heading2>Policies</Heading2>
83+
<Subheading>
84+
Restrict workspace classes, editors and sharing across your organization.
85+
</Subheading>
86+
</div>
87+
<ConfigurationSettingsField>
88+
<Heading3>Internal dashboard</Heading3>
89+
{!isPaidOrDedicated && (
90+
<Alert type="info" className="my-3">
91+
Setting Workspace timeouts is only available for organizations on a paid plan. Visit{" "}
92+
<Link to={"/billing"} className="gp-link">
93+
Billing
94+
</Link>{" "}
95+
to upgrade your plan.
96+
</Alert>
97+
)}
98+
<form onSubmit={handleUpdateInternalLink}>
99+
<InputField
100+
label="Default workspace timeout"
101+
error={undefined}
102+
hint={
103+
<span>
104+
Use minutes or hours, like <span className="font-semibold">30m</span> or{" "}
105+
<span className="font-semibold">2h</span>
106+
</span>
107+
}
108+
>
109+
<TextInput
110+
value={internalLink}
111+
type="url"
112+
onChange={setInternalLink}
113+
disabled={updateTeamSettings.isLoading || !isOwner || !isPaidOrDedicated}
114+
/>
115+
</InputField>
116+
<LoadingButton
117+
type="submit"
118+
loading={updateTeamSettings.isLoading}
119+
disabled={!isOwner || !isPaidOrDedicated}
120+
>
121+
Save
122+
</LoadingButton>
123+
</form>
124+
</ConfigurationSettingsField>
125+
</div>
126+
</OrgSettingsPage>
127+
</>
128+
);
129+
}

0 commit comments

Comments
 (0)