Skip to content

Commit fd75278

Browse files
committed
chore: extract Form components
1 parent 1924b6a commit fd75278

File tree

4 files changed

+655
-639
lines changed

4 files changed

+655
-639
lines changed
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
import TableCell from "@mui/material/TableCell";
2+
import TableRow from "@mui/material/TableRow";
3+
import type {
4+
Group,
5+
GroupSyncSettings,
6+
Organization,
7+
} from "api/typesGenerated";
8+
import { Button } from "components/Button/Button";
9+
import {
10+
HelpTooltip,
11+
HelpTooltipContent,
12+
HelpTooltipText,
13+
HelpTooltipTitle,
14+
HelpTooltipTrigger,
15+
} from "components/HelpTooltip/HelpTooltip";
16+
import { Input } from "components/Input/Input";
17+
import { Label } from "components/Label/Label";
18+
import { Link } from "components/Link/Link";
19+
import {
20+
MultiSelectCombobox,
21+
type Option,
22+
} from "components/MultiSelectCombobox/MultiSelectCombobox";
23+
import { Switch } from "components/Switch/Switch";
24+
import { useFormik } from "formik";
25+
import { Plus, Trash } from "lucide-react";
26+
import { type FC, useState } from "react";
27+
import { docs } from "utils/docs";
28+
import * as Yup from "yup";
29+
import { ExportPolicyButton } from "./ExportPolicyButton";
30+
import { IdpMappingTable } from "./IdpMappingTable";
31+
import { IdpPillList } from "./IdpPillList";
32+
33+
interface IdpGroupSyncFormProps {
34+
groupSyncSettings: GroupSyncSettings;
35+
groupsMap: Map<string, string>;
36+
groups: Group[];
37+
groupMappingCount: number;
38+
legacyGroupMappingCount: number;
39+
organization: Organization;
40+
onSubmit: (data: GroupSyncSettings) => void;
41+
}
42+
43+
const groupSyncValidationSchema = Yup.object({
44+
field: Yup.string().trim(),
45+
regex_filter: Yup.string().trim(),
46+
auto_create_missing_groups: Yup.boolean(),
47+
mapping: Yup.object().shape({
48+
[`${String}`]: Yup.array().of(Yup.string()),
49+
}),
50+
});
51+
52+
export const IdpGroupSyncForm = ({
53+
groupSyncSettings,
54+
groupMappingCount,
55+
legacyGroupMappingCount,
56+
groups,
57+
groupsMap,
58+
organization,
59+
onSubmit,
60+
}: IdpGroupSyncFormProps) => {
61+
const form = useFormik<GroupSyncSettings>({
62+
initialValues: {
63+
field: groupSyncSettings?.field ?? "",
64+
regex_filter: groupSyncSettings?.regex_filter ?? "",
65+
auto_create_missing_groups:
66+
groupSyncSettings?.auto_create_missing_groups ?? false,
67+
mapping: groupSyncSettings?.mapping ?? {},
68+
},
69+
validationSchema: groupSyncValidationSchema,
70+
onSubmit,
71+
enableReinitialize: Boolean(groupSyncSettings),
72+
});
73+
const [idpGroupName, setIdpGroupName] = useState("");
74+
const [coderGroups, setCoderGroups] = useState<Option[]>([]);
75+
76+
const getGroupNames = (groupIds: readonly string[]) => {
77+
return groupIds.map((groupId) => groupsMap.get(groupId) || groupId);
78+
};
79+
80+
const handleDelete = async (idpOrg: string) => {
81+
const newMapping = Object.fromEntries(
82+
Object.entries(form.values.mapping || {}).filter(
83+
([key]) => key !== idpOrg,
84+
),
85+
);
86+
const newSyncSettings = {
87+
...form.values,
88+
mapping: newMapping,
89+
};
90+
void form.setFieldValue("mapping", newSyncSettings.mapping);
91+
form.handleSubmit();
92+
};
93+
94+
const SYNC_FIELD_ID = "sync-field";
95+
const REGEX_FILTER_ID = "regex-filter";
96+
const AUTO_CREATE_MISSING_GROUPS_ID = "auto-create-missing-groups";
97+
const IDP_GROUP_NAME_ID = "idp-group-name";
98+
99+
return (
100+
<form onSubmit={form.handleSubmit}>
101+
<fieldset
102+
disabled={form.isSubmitting}
103+
className="flex flex-col border-none gap-8 pt-2"
104+
>
105+
<div className="flex justify-end">
106+
<ExportPolicyButton
107+
syncSettings={groupSyncSettings}
108+
organization={organization}
109+
type="groups"
110+
/>
111+
</div>
112+
<div className="grid items-center gap-3">
113+
<div className="flex flex-row items-center gap-5">
114+
<div className="grid grid-cols-2 gap-2 grid-rows-[20px_auto_20px]">
115+
<Label className="text-sm" htmlFor={SYNC_FIELD_ID}>
116+
Group sync field
117+
</Label>
118+
<Label className="text-sm" htmlFor={SYNC_FIELD_ID}>
119+
Regex filter
120+
</Label>
121+
<Input
122+
id={SYNC_FIELD_ID}
123+
value={form.values.field}
124+
onChange={async (event) => {
125+
void form.setFieldValue("field", event.target.value);
126+
}}
127+
className="min-w-72 w-72"
128+
/>
129+
<div className="flex flex-row gap-2">
130+
<Input
131+
id={REGEX_FILTER_ID}
132+
value={form.values.regex_filter ?? ""}
133+
onChange={async (event) => {
134+
void form.setFieldValue("regex_filter", event.target.value);
135+
}}
136+
className="min-w-40"
137+
/>
138+
<Button
139+
className="w-20"
140+
type="submit"
141+
disabled={form.isSubmitting || !form.dirty}
142+
onClick={(event) => {
143+
event.preventDefault();
144+
form.handleSubmit();
145+
}}
146+
>
147+
Save
148+
</Button>
149+
</div>
150+
<p className="text-content-secondary text-2xs m-0">
151+
If empty, group sync is deactivated
152+
</p>
153+
</div>
154+
</div>
155+
</div>
156+
<div className="flex flex-row items-center gap-3">
157+
<Switch
158+
id={AUTO_CREATE_MISSING_GROUPS_ID}
159+
checked={form.values.auto_create_missing_groups}
160+
onCheckedChange={async (checked) => {
161+
void form.setFieldValue("auto_create_missing_groups", checked);
162+
form.handleSubmit();
163+
}}
164+
/>
165+
<span className="flex flex-row items-center gap-1">
166+
<Label htmlFor={AUTO_CREATE_MISSING_GROUPS_ID}>
167+
Auto create missing groups
168+
</Label>
169+
<AutoCreateMissingGroupsHelpTooltip />
170+
</span>
171+
</div>
172+
<div className="flex flex-row gap-2 justify-between items-start">
173+
<div className="grid items-center gap-1">
174+
<Label className="text-sm" htmlFor={IDP_GROUP_NAME_ID}>
175+
IdP group name
176+
</Label>
177+
<Input
178+
id={IDP_GROUP_NAME_ID}
179+
value={idpGroupName}
180+
className="min-w-72 w-72"
181+
onChange={(event) => {
182+
setIdpGroupName(event.target.value);
183+
}}
184+
/>
185+
</div>
186+
<div className="grid items-center gap-1 flex-1">
187+
<Label className="text-sm" htmlFor=":r1d:">
188+
Coder group
189+
</Label>
190+
<MultiSelectCombobox
191+
className="min-w-60 max-w-3xl"
192+
value={coderGroups}
193+
onChange={setCoderGroups}
194+
defaultOptions={groups.map((group) => ({
195+
label: group.display_name || group.name,
196+
value: group.id,
197+
}))}
198+
hidePlaceholderWhenSelected
199+
placeholder="Select group"
200+
emptyIndicator={
201+
<p className="text-center text-md text-content-primary">
202+
All groups selected
203+
</p>
204+
}
205+
/>
206+
</div>
207+
<div className="grid grid-rows-[28px_auto]">
208+
&nbsp;
209+
<Button
210+
className="mb-px"
211+
type="submit"
212+
disabled={!idpGroupName || coderGroups.length === 0}
213+
onClick={async () => {
214+
const newSyncSettings = {
215+
...form.values,
216+
mapping: {
217+
...form.values.mapping,
218+
[idpGroupName]: coderGroups.map((role) => role.value),
219+
},
220+
};
221+
void form.setFieldValue("mapping", newSyncSettings.mapping);
222+
form.handleSubmit();
223+
setIdpGroupName("");
224+
setCoderGroups([]);
225+
}}
226+
>
227+
<Plus size={14} />
228+
Add IdP group
229+
</Button>
230+
</div>
231+
</div>
232+
233+
<div className="flex flex-col">
234+
<IdpMappingTable type="Group" rowCount={groupMappingCount}>
235+
{groupSyncSettings?.mapping &&
236+
Object.entries(groupSyncSettings.mapping)
237+
.sort()
238+
.map(([idpGroup, groups]) => (
239+
<GroupRow
240+
key={idpGroup}
241+
idpGroup={idpGroup}
242+
coderGroup={getGroupNames(groups)}
243+
onDelete={handleDelete}
244+
/>
245+
))}
246+
</IdpMappingTable>
247+
248+
{groupSyncSettings?.legacy_group_name_mapping && (
249+
<div>
250+
<LegacyGroupSyncHeader />
251+
<IdpMappingTable type="Group" rowCount={legacyGroupMappingCount}>
252+
{Object.entries(groupSyncSettings.legacy_group_name_mapping)
253+
.sort()
254+
.map(([idpGroup, groupId]) => (
255+
<GroupRow
256+
key={idpGroup}
257+
idpGroup={idpGroup}
258+
coderGroup={getGroupNames([groupId])}
259+
onDelete={handleDelete}
260+
/>
261+
))}
262+
</IdpMappingTable>
263+
</div>
264+
)}
265+
</div>
266+
</fieldset>
267+
</form>
268+
);
269+
};
270+
271+
interface GroupRowProps {
272+
idpGroup: string;
273+
coderGroup: readonly string[];
274+
onDelete: (idpOrg: string) => void;
275+
}
276+
277+
const GroupRow: FC<GroupRowProps> = ({ idpGroup, coderGroup, onDelete }) => {
278+
return (
279+
<TableRow data-testid={`group-${idpGroup}`}>
280+
<TableCell>{idpGroup}</TableCell>
281+
<TableCell>
282+
<IdpPillList roles={coderGroup} />
283+
</TableCell>
284+
<TableCell>
285+
<Button
286+
variant="outline"
287+
className="w-8 h-8 min-w-10 text-content-primary"
288+
aria-label="delete"
289+
onClick={() => onDelete(idpGroup)}
290+
>
291+
<Trash />
292+
<span className="sr-only">Delete IdP mapping</span>
293+
</Button>
294+
</TableCell>
295+
</TableRow>
296+
);
297+
};
298+
299+
const AutoCreateMissingGroupsHelpTooltip: FC = () => {
300+
return (
301+
<HelpTooltip>
302+
<HelpTooltipTrigger />
303+
<HelpTooltipContent>
304+
<HelpTooltipText>
305+
Enabling auto create missing groups will automatically create groups
306+
returned by the OIDC provider if they do not exist in Coder.
307+
</HelpTooltipText>
308+
</HelpTooltipContent>
309+
</HelpTooltip>
310+
);
311+
};
312+
313+
const LegacyGroupSyncHeader: FC = () => {
314+
return (
315+
<h4 className="text-xl font-medium">
316+
<div className="flex items-end gap-2">
317+
<span>Legacy group sync settings</span>
318+
<HelpTooltip>
319+
<HelpTooltipTrigger />
320+
<HelpTooltipContent>
321+
<HelpTooltipTitle>Legacy group sync settings</HelpTooltipTitle>
322+
<HelpTooltipText>
323+
These settings were configured using environment variables, and
324+
only apply to the default organization. It is now recommended to
325+
configure IdP sync via the CLI or the UI, which enables sync to be
326+
configured for any organization, and for those settings to be
327+
persisted without manually setting environment variables.{" "}
328+
<Link href={docs("/admin/users/idp-sync")}>
329+
Learn more&hellip;
330+
</Link>
331+
</HelpTooltipText>
332+
</HelpTooltipContent>
333+
</HelpTooltip>
334+
</div>
335+
</h4>
336+
);
337+
};

0 commit comments

Comments
 (0)