Skip to content

Commit 205d36d

Browse files
Configuration Details: Workspace Defaults (#19072)
* Configuration Details: Workspace Defaults 🤷‍♂️ sort imports * Foof * Add descriptions * Change layout * Fix heading child * Change heading container * Fix styling * A proper config field * Delete hover states * Implement the update logic * Fix size updates * Simplify mutation * Remove cancel button and add a save one for ws options * Margins * Update title * Prettier Paddings * Remove one-childed fragment * Fix heading shenanigans * LoadingState component * Wrap with label * Spacing * 🙄
1 parent 32aa1bb commit 205d36d

File tree

16 files changed

+256
-20
lines changed

16 files changed

+256
-20
lines changed

components/dashboard/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"@gitpod/gitpod-protocol": "0.1.5",
1111
"@gitpod/public-api": "0.1.5",
1212
"@radix-ui/react-dropdown-menu": "^2.0.6",
13+
"@radix-ui/react-label": "^2.0.2",
1314
"@radix-ui/react-popover": "^1.0.7",
15+
"@radix-ui/react-radio-group": "^1.1.3",
1416
"@radix-ui/react-tooltip": "^1.0.7",
1517
"@stripe/react-stripe-js": "^1.7.2",
1618
"@stripe/stripe-js": "^1.29.0",

components/dashboard/src/app/AppLoading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { FunctionComponent, useEffect, useState } from "react";
88
import { Heading3, Subheading } from "../components/typography/headings";
99
import gitpodIcon from "../icons/gitpod.svg";
10-
import { Delayed } from "../components/Delayed";
10+
import { Delayed } from "@podkit/loading/Delayed";
1111

1212
function useDelay(wait: number) {
1313
const [done, setDone] = useState(false);

components/dashboard/src/app/AppRoutes.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ const ConfigurationGeneral = React.lazy(
8383
() => import(/* webpackPrefetch: true */ "../repositories/detail/ConfigurationDetailGeneral"),
8484
);
8585

86+
const ConfigurationWorkspaces = React.lazy(
87+
() => import(/* webpackPrefetch: true */ "../repositories/detail/ConfigurationDetailWorkspaces"),
88+
);
89+
8690
export const AppRoutes = () => {
8791
const hash = getURLHash();
8892
const user = useCurrentUser();
@@ -220,6 +224,7 @@ export const AppRoutes = () => {
220224
<>
221225
<Route exact path="/repositories" component={ConfigurationListPage} />
222226
<Route exact path="/repositories/:id" component={ConfigurationGeneral} />
227+
<Route exact path="/repositories/:id/workspaces" component={ConfigurationWorkspaces} />
223228
</>
224229
)}
225230
{/* basic redirect for old team slugs */}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) 2023 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 * as React from "react";
8+
import * as LabelPrimitive from "@radix-ui/react-label";
9+
import { cva, VariantProps } from "class-variance-authority";
10+
import { cn } from "@podkit/lib/cn";
11+
12+
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
13+
14+
export const Label = React.forwardRef<
15+
React.ElementRef<typeof LabelPrimitive.Root>,
16+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
17+
>(({ className, ...props }, ref) => (
18+
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
19+
));
20+
Label.displayName = LabelPrimitive.Root.displayName;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright (c) 2023 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 { cn } from "@podkit/lib/cn";
8+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
9+
import { Circle } from "lucide-react";
10+
import React from "react";
11+
12+
export const RadioGroupItem = React.forwardRef<
13+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
14+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
15+
>(({ className, ...props }, ref) => {
16+
return (
17+
<RadioGroupPrimitive.Item
18+
ref={ref}
19+
className={cn(
20+
"aspect-square h-4 w-4 my-0 rounded-full border-2 ring-offset-white dark:ring-offset-gray-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 p-0 text-black dark:text-gray-200 border-black dark:border-gray-200 bg-inherit",
21+
className,
22+
)}
23+
{...props}
24+
>
25+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
26+
<Circle className="h-1.5 w-1.5 fill-current text-current" />
27+
</RadioGroupPrimitive.Indicator>
28+
</RadioGroupPrimitive.Item>
29+
);
30+
});
31+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
32+
33+
export type RadioListItem = {
34+
radio: React.ReactElement;
35+
label: React.ReactNode;
36+
hint?: React.ReactNode;
37+
};
38+
39+
export const RadioGroup = React.forwardRef<
40+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
41+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
42+
>(({ className, ...props }, ref) => {
43+
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
44+
});
45+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Copyright (c) 2023 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 { Loader2 } from "lucide-react";
8+
import { Delayed } from "./Delayed";
9+
10+
export const LoadingState = () => {
11+
return (
12+
<Delayed>
13+
<Loader2 className="animate-spin" />
14+
</Delayed>
15+
);
16+
};

components/dashboard/src/data/projects/project-queries.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Project } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol";
88
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
99
import { useCurrentOrg } from "../organizations/orgs-query";
1010
import { listAllProjects, projectsService } from "../../service/public-api";
11-
import { PartialProject } from "@gitpod/gitpod-protocol";
11+
import type { PartialProject } from "@gitpod/gitpod-protocol";
1212
import { getGitpodService } from "../../service/service";
1313

1414
const BASE_KEY = "projects";
@@ -79,15 +79,15 @@ export const useUpdateProject = () => {
7979
const { data: org } = useCurrentOrg();
8080
const client = useQueryClient();
8181

82-
return useMutation<void, Error, PartialProject>(async ({ id, name }) => {
82+
return useMutation<void, Error, PartialProject>(async (settings) => {
8383
if (!org) {
8484
throw new Error("No org currently selected");
8585
}
8686

87-
await getGitpodService().server.updateProjectPartial({ id, name });
87+
await getGitpodService().server.updateProjectPartial(settings);
8888

8989
// Invalidate project
90-
client.invalidateQueries(getProjectQueryKey(org.id, id));
90+
client.invalidateQueries(getProjectQueryKey(org.id, settings.id));
9191
// Invalidate project list queries
9292
client.invalidateQueries(getListProjectsQueryKey(org.id));
9393
});

components/dashboard/src/dedicated-setup/DedicatedSetup.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { useConfetti } from "../contexts/ConfettiContext";
1212
import { SetupCompleteStep } from "./SetupCompleteStep";
1313
import { useOIDCClientsQuery } from "../data/oidc-clients/oidc-clients-query";
1414
import { useCurrentOrg } from "../data/organizations/orgs-query";
15-
import { Delayed } from "../components/Delayed";
1615
import { SpinnerLoader } from "../components/Loader";
1716
import { getGitpodService } from "../service/service";
1817
import { UserContext } from "../user-context";
@@ -21,6 +20,7 @@ import { useQueryParams } from "../hooks/use-query-params";
2120
import { useDocumentTitle } from "../hooks/use-document-title";
2221
import { forceDedicatedSetupParam } from "./use-show-dedicated-setup";
2322
import { Organization } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
23+
import { Delayed } from "@podkit/loading/Delayed";
2424

2525
type Props = {
2626
onComplete: () => void;

components/dashboard/src/repositories/detail/ConfigurationDetailPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function ConfigurationDetailPage({ children, configurationQuery, id }: Pa
3030
return (
3131
<div className="w-full">
3232
<BreadcrumbNav
33-
pageTitle="Repository Configuration"
33+
pageTitle="Imported repositories"
3434
pageDescription={data?.name ?? ""}
3535
backLink="/repositories"
3636
/>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright (c) 2023 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 { FC } from "react";
8+
import { useParams } from "react-router";
9+
import { ConfigurationDetailPage } from "./ConfigurationDetailPage";
10+
import { useConfiguration } from "../../data/configurations/configuration-queries";
11+
import { ConfigurationWorkspaceSizeOptions } from "./workspaces/WorkpaceSizeOptions";
12+
13+
type PageRouteParams = {
14+
id: string;
15+
};
16+
const ConfigurationDetailWorkspaces: FC = () => {
17+
const { id } = useParams<PageRouteParams>();
18+
const configurationQuery = useConfiguration(id);
19+
const { data } = configurationQuery;
20+
21+
return (
22+
<ConfigurationDetailPage configurationQuery={configurationQuery} id={id}>
23+
{data && <ConfigurationWorkspaceSizeOptions configuration={data} />}
24+
</ConfigurationDetailPage>
25+
);
26+
};
27+
28+
export default ConfigurationDetailWorkspaces;

components/dashboard/src/repositories/detail/general/ConfigurationName.tsx

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

77
import type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
8-
import { Button } from "@podkit/buttons/Button";
98
import { LoadingButton } from "@podkit/buttons/LoadingButton";
109
import { FC, useCallback, useState } from "react";
1110
import { TextInputField } from "../../../components/forms/TextInputField";
@@ -67,16 +66,6 @@ export const ConfigurationNameForm: FC<Props> = ({ configuration }) => {
6766
<LoadingButton type="submit" disabled={!nameChanged} loading={updateProject.isLoading}>
6867
Save
6968
</LoadingButton>
70-
<Button
71-
type="button"
72-
variant="secondary"
73-
disabled={!nameChanged}
74-
onClick={() => {
75-
setProjectName(configuration.name);
76-
}}
77-
>
78-
Cancel
79-
</Button>
8069
</div>
8170
</form>
8271
</ConfigurationSettingsField>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Copyright (c) 2023 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 type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
8+
import React, { useCallback, useState } from "react";
9+
import { useWorkspaceClasses } from "../../../data/workspaces/workspace-classes-query";
10+
import { Label } from "@podkit/forms/Label";
11+
import { RadioGroup, RadioGroupItem } from "@podkit/forms/RadioListField";
12+
import { TextMuted } from "@podkit/typography/TextMuted";
13+
import { Heading2 } from "@podkit/typography/Headings";
14+
import { ConfigurationSettingsField } from "../ConfigurationSettingsField";
15+
import { useUpdateProject } from "../../../data/projects/project-queries";
16+
import { useToast } from "../../../components/toasts/Toasts";
17+
import { LoadingButton } from "@podkit/buttons/LoadingButton";
18+
import { LoadingState } from "@podkit/loading/LoadingState";
19+
20+
interface Props {
21+
configuration: Configuration;
22+
}
23+
24+
export const ConfigurationWorkspaceSizeOptions = ({ configuration }: Props) => {
25+
const [selectedValue, setSelectedValue] = useState(
26+
configuration.workspaceSettings?.workspaceClass || "g1-standard",
27+
);
28+
const classChanged = selectedValue !== configuration.workspaceSettings?.workspaceClass;
29+
30+
const updateProject = useUpdateProject();
31+
const { data: classes, isError, isLoading } = useWorkspaceClasses();
32+
33+
const { toast } = useToast();
34+
35+
const setWorkspaceClass = useCallback(
36+
async (e: React.FormEvent) => {
37+
e.preventDefault();
38+
39+
updateProject.mutate(
40+
{
41+
id: configuration.id,
42+
settings: {
43+
workspaceClasses: {
44+
regular: selectedValue,
45+
},
46+
},
47+
},
48+
{
49+
onSuccess: () => {
50+
// todo: use optimistic updates when we introduce configuration update hooks
51+
toast({ message: "Workspace size updated" });
52+
},
53+
onError: (e) => {
54+
toast({ message: `Failed updating workspace size: ${e.message}` });
55+
},
56+
},
57+
);
58+
},
59+
[configuration.id, selectedValue, toast, updateProject],
60+
);
61+
62+
if (isError || !classes) {
63+
return <div>Something went wrong</div>;
64+
}
65+
66+
if (isLoading) {
67+
return <LoadingState />;
68+
}
69+
70+
return (
71+
<ConfigurationSettingsField className="px-8 py-6">
72+
<form onSubmit={setWorkspaceClass}>
73+
<div className="mb-4">
74+
<Heading2 className="text-base">Workspace Size Options</Heading2>
75+
<TextMuted>Choose the size of your workspace based on the resources you need.</TextMuted>
76+
</div>
77+
<RadioGroup value={selectedValue} onValueChange={setSelectedValue}>
78+
{classes.map((wsClass) => (
79+
<Label className="flex items-start space-x-2 my-2">
80+
<RadioGroupItem value={wsClass.id} />
81+
<div className="flex flex-col space-y-2">
82+
<span className="font-bold">{wsClass.displayName}</span>
83+
<span>{wsClass.description}</span>
84+
</div>
85+
</Label>
86+
))}
87+
</RadioGroup>
88+
<LoadingButton
89+
className="mt-8"
90+
type="submit"
91+
disabled={!classChanged}
92+
loading={updateProject.isLoading}
93+
>
94+
Save
95+
</LoadingButton>
96+
</form>
97+
</ConfigurationSettingsField>
98+
);
99+
};

components/dashboard/src/teams/Members.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ import searchIcon from "../icons/search.svg";
1717
import { organizationClient } from "../service/public-api";
1818
import { useCurrentUser } from "../user-context";
1919
import { SpinnerLoader } from "../components/Loader";
20-
import { Delayed } from "../components/Delayed";
2120
import { InputField } from "../components/forms/InputField";
2221
import { InputWithCopy } from "../components/InputWithCopy";
2322
import { OrganizationMember, OrganizationRole } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
2423
import { useListOrganizationMembers, useOrganizationMembersInvalidator } from "../data/organizations/members-query";
2524
import { useInvitationId, useInviteInvalidator } from "../data/organizations/invite-query";
25+
import { Delayed } from "@podkit/loading/Delayed";
2626

2727
export default function MembersPage() {
2828
const user = useCurrentUser();

components/dashboard/src/user-settings/Integrations.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import Alert from "../components/Alert";
1212
import { CheckboxInputField, CheckboxListField } from "../components/forms/CheckboxInputField";
1313
import ConfirmationModal from "../components/ConfirmationModal";
1414
import { ContextMenuEntry } from "../components/ContextMenu";
15-
import { Delayed } from "../components/Delayed";
1615
import InfoBox from "../components/InfoBox";
1716
import { ItemsList } from "../components/ItemsList";
1817
import { SpinnerLoader } from "../components/Loader";
@@ -30,6 +29,7 @@ import { SelectAccountModal } from "./SelectAccountModal";
3029
import { useAuthProviders } from "../data/auth-providers/auth-provider-query";
3130
import { useFeatureFlag } from "../data/featureflag-query";
3231
import { EmptyMessage } from "../components/EmptyMessage";
32+
import { Delayed } from "@podkit/loading/Delayed";
3333

3434
export default function Integrations() {
3535
return (

0 commit comments

Comments
 (0)