Skip to content

Commit 74528b9

Browse files
New Prebuild Settings UI for repo configs (#19184)
* setting up settings * preload ws classes * adding heading/description to prebuild ws class * add some constants --------- Co-authored-by: Filip Troníček <[email protected]>
1 parent 5a55a7a commit 74528b9

File tree

7 files changed

+239
-32
lines changed

7 files changed

+239
-32
lines changed

components/dashboard/src/components/forms/SelectInputField.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { InputField } from "./InputField";
1111

1212
type Props = {
1313
label?: ReactNode;
14-
value: string;
14+
value: React.SelectHTMLAttributes<HTMLSelectElement>["value"];
1515
id?: string;
1616
hint?: ReactNode;
1717
error?: ReactNode;
@@ -67,7 +67,7 @@ export const SelectInputField: FunctionComponent<Props> = memo(
6767
);
6868

6969
type SelectInputProps = {
70-
value: string;
70+
value: React.SelectHTMLAttributes<HTMLSelectElement>["value"];
7171
className?: string;
7272
id?: string;
7373
disabled?: boolean;

components/dashboard/src/data/workspaces/workspace-classes-query.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { SupportedWorkspaceClass } from "@gitpod/gitpod-protocol/lib/workspace-c
88
import { useQuery } from "@tanstack/react-query";
99
import { getGitpodService } from "../../service/service";
1010

11+
export const DEFAULT_WS_CLASS = "g1-standard";
12+
1113
export const useWorkspaceClasses = () => {
1214
return useQuery<SupportedWorkspaceClass[]>({
1315
queryKey: ["workspace-classes"],

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@ import { ConfigurationDetailGeneral } from "./ConfigurationDetailGeneral";
1717
import { ConfigurationDetailWorkspaces } from "./ConfigurationDetailWorkspaces";
1818
import { ConfigurationDetailPrebuilds } from "./ConfigurationDetailPrebuilds";
1919
import { ConfigurationVariableList } from "./variables/ConfigurationVariableList";
20+
import { useWorkspaceClasses } from "../../data/workspaces/workspace-classes-query";
2021

2122
type PageRouteParams = {
2223
id: string;
2324
};
2425

2526
const ConfigurationDetailPage: FC = () => {
27+
// preload some data we may show
28+
useWorkspaceClasses();
29+
2630
const { id } = useParams<PageRouteParams>();
2731
let { path, url } = useRouteMatch();
2832

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

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,56 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { FC, useState } from "react";
7+
import { FC, useCallback, useState } from "react";
88
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
99
import { ConfigurationSettingsField } from "./ConfigurationSettingsField";
1010
import { Heading3, Subheading } from "@podkit/typography/Headings";
1111
import { Switch } from "@podkit/switch/Switch";
1212
import { TextMuted } from "@podkit/typography/TextMuted";
13+
import { PrebuildSettingsForm } from "./prebuilds/PrebuildSettingsForm";
14+
import { useConfigurationMutation } from "../../data/configurations/configuration-queries";
15+
import { useToast } from "../../components/toasts/Toasts";
1316

1417
type Props = {
1518
configuration: Configuration;
1619
};
1720
export const ConfigurationDetailPrebuilds: FC<Props> = ({ configuration }) => {
18-
// TODO: hook this up to just use configuration as state and wire up optimistic update for mutation
21+
const { toast } = useToast();
22+
const updateConfiguration = useConfigurationMutation();
23+
1924
const [enabled, setEnabled] = useState(!!configuration.prebuildSettings?.enabled);
2025

26+
const updateEnabled = useCallback(
27+
(newEnabled: boolean) => {
28+
setEnabled(newEnabled);
29+
updateConfiguration.mutate(
30+
{
31+
configurationId: configuration.id,
32+
prebuildSettings: {
33+
...configuration.prebuildSettings,
34+
enabled: newEnabled,
35+
},
36+
},
37+
{
38+
onError: (err) => {
39+
toast(
40+
<>
41+
<span>
42+
{newEnabled
43+
? "There was a problem enabling prebuilds"
44+
: "There was a problem disabling prebuilds"}
45+
</span>
46+
{err?.message && <p>{err.message}</p>}
47+
</>,
48+
);
49+
setEnabled(!newEnabled);
50+
},
51+
},
52+
);
53+
},
54+
[configuration.id, configuration.prebuildSettings, toast, updateConfiguration],
55+
);
56+
2157
return (
2258
<>
2359
<ConfigurationSettingsField>
@@ -26,10 +62,10 @@ export const ConfigurationDetailPrebuilds: FC<Props> = ({ configuration }) => {
2662

2763
<div className="flex gap-4 mt-6">
2864
{/* TODO: wrap this in a SwitchInputField that handles the switch, label and description and htmlFor/id automatically */}
29-
<Switch checked={enabled} onCheckedChange={setEnabled} id="prebuilds-enabled" />
65+
<Switch checked={enabled} onCheckedChange={updateEnabled} id="prebuilds-enabled" />
3066
<div className="flex flex-col">
3167
<label className="font-semibold" htmlFor="prebuilds-enabled">
32-
Prebuilds are enabled
68+
{enabled ? "Prebuilds are enabled" : "Prebuilds are disabled"}
3369
</label>
3470
<TextMuted>
3571
Enabling requires permissions to configure repository webhooks.{" "}
@@ -46,6 +82,8 @@ export const ConfigurationDetailPrebuilds: FC<Props> = ({ configuration }) => {
4682
</div>
4783
</div>
4884
</ConfigurationSettingsField>
85+
86+
{enabled && <PrebuildSettingsForm configuration={configuration} />}
4987
</>
5088
);
5189
};
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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 { BranchMatchingStrategy, Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
8+
import { FC, FormEvent, useCallback, useState } from "react";
9+
import { ConfigurationSettingsField } from "../ConfigurationSettingsField";
10+
import { Heading3, Subheading } from "@podkit/typography/Headings";
11+
import { InputField } from "../../../components/forms/InputField";
12+
import { PartialConfiguration, useConfigurationMutation } from "../../../data/configurations/configuration-queries";
13+
import { useToast } from "../../../components/toasts/Toasts";
14+
import { SelectInputField } from "../../../components/forms/SelectInputField";
15+
import { TextInputField } from "../../../components/forms/TextInputField";
16+
import { WorkspaceClassOptions } from "../shared/WorkspaceClassOptions";
17+
import { LoadingButton } from "@podkit/buttons/LoadingButton";
18+
import { InputFieldHint } from "../../../components/forms/InputFieldHint";
19+
import { DEFAULT_WS_CLASS } from "../../../data/workspaces/workspace-classes-query";
20+
21+
const DEFAULT_PREBUILD_COMMIT_INTERVAL = 20;
22+
23+
type Props = {
24+
configuration: Configuration;
25+
};
26+
27+
export const PrebuildSettingsForm: FC<Props> = ({ configuration }) => {
28+
const { toast } = useToast();
29+
const updateConfiguration = useConfigurationMutation();
30+
31+
const [interval, setInterval] = useState<string>(
32+
`${configuration.prebuildSettings?.prebuildInterval ?? DEFAULT_PREBUILD_COMMIT_INTERVAL}`,
33+
);
34+
const [branchStrategy, setBranchStrategy] = useState<BranchMatchingStrategy>(
35+
configuration.prebuildSettings?.branchStrategy ?? BranchMatchingStrategy.DEFAULT_BRANCH,
36+
);
37+
const [branchMatchingPattern, setBranchMatchingPattern] = useState<string>(
38+
configuration.prebuildSettings?.branchMatchingPattern || "**",
39+
);
40+
const [workspaceClass, setWorkspaceClass] = useState<string>(
41+
configuration.prebuildSettings?.workspaceClass || DEFAULT_WS_CLASS,
42+
);
43+
44+
const handleSubmit = useCallback(
45+
(e: FormEvent) => {
46+
e.preventDefault();
47+
48+
const newInterval = Math.abs(Math.min(Number.parseInt(interval), 100)) || 0;
49+
50+
const updatedConfig: PartialConfiguration = {
51+
configurationId: configuration.id,
52+
prebuildSettings: {
53+
...configuration.prebuildSettings,
54+
prebuildInterval: newInterval,
55+
branchStrategy,
56+
branchMatchingPattern,
57+
workspaceClass,
58+
},
59+
};
60+
61+
updateConfiguration.mutate(updatedConfig, {
62+
onSuccess: () => {
63+
toast("Your prebuild settings were updated.");
64+
},
65+
});
66+
},
67+
[
68+
branchMatchingPattern,
69+
branchStrategy,
70+
configuration.id,
71+
configuration.prebuildSettings,
72+
interval,
73+
toast,
74+
updateConfiguration,
75+
workspaceClass,
76+
],
77+
);
78+
79+
// TODO: Figure out if there's a better way to deal with grpc enums in the UI
80+
const handleBranchStrategyChange = useCallback((val) => {
81+
// This is pretty hacky, trying to coerce value into a number and then cast it to the enum type
82+
// Would be better if we treated these as strings instead of special enums
83+
setBranchStrategy(parseInt(val, 10) as BranchMatchingStrategy);
84+
}, []);
85+
86+
return (
87+
<ConfigurationSettingsField>
88+
<form onSubmit={handleSubmit}>
89+
<Heading3>Prebuild Settings</Heading3>
90+
<Subheading className="max-w-lg">These settings will be applied on every Prebuild.</Subheading>
91+
92+
<InputField
93+
label="Commit interval"
94+
hint="The number of commits to be skipped between prebuild runs."
95+
id="prebuild-interval"
96+
>
97+
<input
98+
type="number"
99+
id="prebuild-interval"
100+
min="0"
101+
max="100"
102+
step="5"
103+
value={interval}
104+
onChange={({ target }) => setInterval(target.value)}
105+
/>
106+
</InputField>
107+
108+
<SelectInputField
109+
label="Branch Filter"
110+
hint="Run prebuilds on the selected branches only."
111+
value={branchStrategy}
112+
onChange={handleBranchStrategyChange}
113+
>
114+
<option value={BranchMatchingStrategy.ALL_BRANCHES}>All branches</option>
115+
<option value={BranchMatchingStrategy.DEFAULT_BRANCH}>Default branch</option>
116+
<option value={BranchMatchingStrategy.MATCHED_BRANCHES}>Match branches by pattern</option>
117+
</SelectInputField>
118+
119+
{branchStrategy === BranchMatchingStrategy.MATCHED_BRANCHES && (
120+
<TextInputField
121+
label="Branch name pattern"
122+
hint="Glob patterns separated by commas are supported."
123+
value={branchMatchingPattern}
124+
onChange={setBranchMatchingPattern}
125+
/>
126+
)}
127+
128+
<Heading3 className="mt-8">Machine type</Heading3>
129+
<Subheading>Choose the workspace machine type for your prebuilds.</Subheading>
130+
131+
<WorkspaceClassOptions value={workspaceClass} onChange={setWorkspaceClass} />
132+
<InputFieldHint>Use a smaller machine type for cost optimization.</InputFieldHint>
133+
134+
<LoadingButton className="mt-8" type="submit" loading={updateConfiguration.isLoading}>
135+
Save
136+
</LoadingButton>
137+
</form>
138+
</ConfigurationSettingsField>
139+
);
140+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 { useWorkspaceClasses } from "../../../data/workspaces/workspace-classes-query";
9+
import { LoadingState } from "@podkit/loading/LoadingState";
10+
import { cn } from "@podkit/lib/cn";
11+
import Alert from "../../../components/Alert";
12+
import { Label } from "@podkit/forms/Label";
13+
import { RadioGroup, RadioGroupItem } from "@podkit/forms/RadioListField";
14+
15+
type Props = {
16+
value: string;
17+
className?: string;
18+
onChange: (newValue: string) => void;
19+
};
20+
export const WorkspaceClassOptions: FC<Props> = ({ value, className, onChange }) => {
21+
const { data: classes, isLoading } = useWorkspaceClasses();
22+
23+
if (isLoading) {
24+
return <LoadingState />;
25+
}
26+
27+
if (!classes) {
28+
return <Alert type="error">There was a problem loading workspace classes.</Alert>;
29+
}
30+
31+
return (
32+
<RadioGroup value={value} onValueChange={onChange} className={cn("my-4 gap-4", className)}>
33+
{classes.map((wsClass) => (
34+
<Label className="flex items-start space-x-2" key={wsClass.id}>
35+
<RadioGroupItem value={wsClass.id} />
36+
<div className="flex flex-col space-y-2">
37+
<span className="font-semibold">{wsClass.displayName}</span>
38+
<span>{wsClass.description}</span>
39+
</div>
40+
</Label>
41+
))}
42+
</RadioGroup>
43+
);
44+
};

components/dashboard/src/repositories/detail/workspaces/WorkpaceSizeOptions.tsx

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,25 @@
66

77
import type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
88
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";
129
import { Heading3, Subheading } from "@podkit/typography/Headings";
1310
import { ConfigurationSettingsField } from "../ConfigurationSettingsField";
1411
import { useToast } from "../../../components/toasts/Toasts";
1512
import { LoadingButton } from "@podkit/buttons/LoadingButton";
16-
import { LoadingState } from "@podkit/loading/LoadingState";
1713
import { useConfigurationMutation } from "../../../data/configurations/configuration-queries";
14+
import { WorkspaceClassOptions } from "../shared/WorkspaceClassOptions";
15+
import { DEFAULT_WS_CLASS } from "../../../data/workspaces/workspace-classes-query";
1816

1917
interface Props {
2018
configuration: Configuration;
2119
}
2220

2321
export const ConfigurationWorkspaceSizeOptions = ({ configuration }: Props) => {
2422
const [selectedValue, setSelectedValue] = useState(
25-
configuration.workspaceSettings?.workspaceClass || "g1-standard",
23+
configuration.workspaceSettings?.workspaceClass || DEFAULT_WS_CLASS,
2624
);
2725
const classChanged = selectedValue !== configuration.workspaceSettings?.workspaceClass;
2826

2927
const updateConfiguration = useConfigurationMutation();
30-
const { data: classes, isError, isLoading } = useWorkspaceClasses();
31-
3228
const { toast } = useToast();
3329

3430
const setWorkspaceClass = useCallback(
@@ -56,31 +52,14 @@ export const ConfigurationWorkspaceSizeOptions = ({ configuration }: Props) => {
5652
[configuration.id, selectedValue, toast, updateConfiguration],
5753
);
5854

59-
if (isError || !classes) {
60-
return <div>Something went wrong</div>;
61-
}
62-
63-
if (isLoading) {
64-
return <LoadingState />;
65-
}
66-
6755
return (
6856
<ConfigurationSettingsField>
6957
<form onSubmit={setWorkspaceClass}>
7058
<Heading3>Workspace Size Options</Heading3>
7159
<Subheading>Choose the size of your workspace based on the resources you need.</Subheading>
7260

73-
<RadioGroup value={selectedValue} onValueChange={setSelectedValue} className="mt-4">
74-
{classes.map((wsClass) => (
75-
<Label className="flex items-start space-x-2 my-2">
76-
<RadioGroupItem value={wsClass.id} />
77-
<div className="flex flex-col space-y-2">
78-
<span className="font-bold">{wsClass.displayName}</span>
79-
<span>{wsClass.description}</span>
80-
</div>
81-
</Label>
82-
))}
83-
</RadioGroup>
61+
<WorkspaceClassOptions value={selectedValue} onChange={setSelectedValue} />
62+
8463
<LoadingButton
8564
className="mt-8"
8665
type="submit"

0 commit comments

Comments
 (0)