Skip to content

Commit d9d4cc0

Browse files
committed
feat: add combobox using claim field values
1 parent 323559b commit d9d4cc0

File tree

4 files changed

+126
-21
lines changed

4 files changed

+126
-21
lines changed

site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
Organization,
77
} from "api/typesGenerated";
88
import { Button } from "components/Button/Button";
9+
import { Combobox } from "components/Combobox/Combobox";
910
import {
1011
HelpTooltip,
1112
HelpTooltipContent,
@@ -30,7 +31,7 @@ import {
3031
} from "components/Tooltip/Tooltip";
3132
import { useFormik } from "formik";
3233
import { Plus, Trash, TriangleAlert } from "lucide-react";
33-
import { type FC, useId, useState } from "react";
34+
import { type FC, useId, useState, type KeyboardEventHandler } from "react";
3435
import { docs } from "utils/docs";
3536
import { isUUID } from "utils/uuid";
3637
import * as Yup from "yup";
@@ -70,6 +71,7 @@ interface IdpGroupSyncFormProps {
7071
legacyGroupMappingCount: number;
7172
organization: Organization;
7273
onSubmit: (data: GroupSyncSettings) => void;
74+
onSyncFieldChange: (value: string) => void;
7375
}
7476

7577
export const IdpGroupSyncForm: FC<IdpGroupSyncFormProps> = ({
@@ -81,6 +83,7 @@ export const IdpGroupSyncForm: FC<IdpGroupSyncFormProps> = ({
8183
groupsMap,
8284
organization,
8385
onSubmit,
86+
onSyncFieldChange,
8487
}) => {
8588
const form = useFormik<GroupSyncSettings>({
8689
initialValues: {
@@ -97,6 +100,8 @@ export const IdpGroupSyncForm: FC<IdpGroupSyncFormProps> = ({
97100
const [idpGroupName, setIdpGroupName] = useState("");
98101
const [coderGroups, setCoderGroups] = useState<Option[]>([]);
99102
const id = useId();
103+
const [comboInputValue, setComboInputValue] = useState("");
104+
const [open, setOpen] = useState(false);
100105

101106
const getGroupNames = (groupIds: readonly string[]) => {
102107
return groupIds.map((groupId) => groupsMap.get(groupId) || groupId);
@@ -116,6 +121,19 @@ export const IdpGroupSyncForm: FC<IdpGroupSyncFormProps> = ({
116121
form.handleSubmit();
117122
};
118123

124+
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
125+
if (
126+
event.key === "Enter" &&
127+
comboInputValue &&
128+
!claimFieldValues?.some((value) => value === comboInputValue.toLowerCase())
129+
) {
130+
event.preventDefault();
131+
setIdpGroupName(comboInputValue);
132+
setComboInputValue("");
133+
setOpen(false);
134+
}
135+
};
136+
119137
return (
120138
<form onSubmit={form.handleSubmit}>
121139
<fieldset
@@ -143,6 +161,7 @@ export const IdpGroupSyncForm: FC<IdpGroupSyncFormProps> = ({
143161
value={form.values.field}
144162
onChange={(event) => {
145163
void form.setFieldValue("field", event.target.value);
164+
onSyncFieldChange(event.target.value);
146165
}}
147166
className="w-72"
148167
/>
@@ -202,14 +221,31 @@ export const IdpGroupSyncForm: FC<IdpGroupSyncFormProps> = ({
202221
<Label className="text-sm" htmlFor={`${id}-idp-group-name`}>
203222
IdP group name
204223
</Label>
205-
<Input
206-
id={`${id}-idp-group-name`}
207-
value={idpGroupName}
208-
className="w-72"
209-
onChange={(event) => {
210-
setIdpGroupName(event.target.value);
211-
}}
212-
/>
224+
{claimFieldValues ? (
225+
<Combobox
226+
value={idpGroupName}
227+
options={claimFieldValues}
228+
placeholder="Select IdP organization"
229+
open={open}
230+
onOpenChange={setOpen}
231+
inputValue={comboInputValue}
232+
onInputChange={setComboInputValue}
233+
onKeyDown={handleKeyDown}
234+
onSelect={(value: string) => {
235+
setIdpGroupName(value);
236+
setOpen(false);
237+
}}
238+
/>
239+
) : (
240+
<Input
241+
id={`${id}-idp-group-name`}
242+
value={idpGroupName}
243+
className="w-72"
244+
onChange={(event) => {
245+
setIdpGroupName(event.target.value);
246+
}}
247+
/>
248+
)}
213249
</div>
214250
<div className="grid items-center gap-1 flex-1">
215251
<Label className="text-sm" htmlFor={`${id}-coder-group`}>

site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import TableCell from "@mui/material/TableCell";
22
import TableRow from "@mui/material/TableRow";
33
import type { Organization, Role, RoleSyncSettings } from "api/typesGenerated";
44
import { Button } from "components/Button/Button";
5+
import { Combobox } from "components/Combobox/Combobox";
56
import { Input } from "components/Input/Input";
67
import { Label } from "components/Label/Label";
78
import {
@@ -17,7 +18,7 @@ import {
1718
} from "components/Tooltip/Tooltip";
1819
import { useFormik } from "formik";
1920
import { Plus, Trash, TriangleAlert } from "lucide-react";
20-
import { type FC, useId, useState } from "react";
21+
import { type FC, type KeyboardEventHandler, useId, useState } from "react";
2122
import * as Yup from "yup";
2223
import { ExportPolicyButton } from "./ExportPolicyButton";
2324
import { IdpMappingTable } from "./IdpMappingTable";
@@ -53,6 +54,7 @@ interface IdpRoleSyncFormProps {
5354
organization: Organization;
5455
roles: Role[];
5556
onSubmit: (data: RoleSyncSettings) => void;
57+
onSyncFieldChange: (value: string) => void;
5658
}
5759

5860
export const IdpRoleSyncForm: FC<IdpRoleSyncFormProps> = ({
@@ -62,6 +64,7 @@ export const IdpRoleSyncForm: FC<IdpRoleSyncFormProps> = ({
6264
organization,
6365
roles,
6466
onSubmit,
67+
onSyncFieldChange,
6568
}) => {
6669
const form = useFormik<RoleSyncSettings>({
6770
initialValues: {
@@ -75,6 +78,8 @@ export const IdpRoleSyncForm: FC<IdpRoleSyncFormProps> = ({
7578
const [idpRoleName, setIdpRoleName] = useState("");
7679
const [coderRoles, setCoderRoles] = useState<Option[]>([]);
7780
const id = useId();
81+
const [comboInputValue, setComboInputValue] = useState("");
82+
const [open, setOpen] = useState(false);
7883

7984
const handleDelete = async (idpOrg: string) => {
8085
const newMapping = Object.fromEntries(
@@ -90,6 +95,19 @@ export const IdpRoleSyncForm: FC<IdpRoleSyncFormProps> = ({
9095
form.handleSubmit();
9196
};
9297

98+
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
99+
if (
100+
event.key === "Enter" &&
101+
comboInputValue &&
102+
!claimFieldValues?.some((value) => value === comboInputValue.toLowerCase())
103+
) {
104+
event.preventDefault();
105+
setIdpRoleName(comboInputValue);
106+
setComboInputValue("");
107+
setOpen(false);
108+
}
109+
};
110+
93111
return (
94112
<form onSubmit={form.handleSubmit}>
95113
<fieldset
@@ -114,6 +132,7 @@ export const IdpRoleSyncForm: FC<IdpRoleSyncFormProps> = ({
114132
value={form.values.field}
115133
onChange={(event) => {
116134
void form.setFieldValue("field", event.target.value);
135+
onSyncFieldChange(event.target.value);
117136
}}
118137
className="w-72"
119138
/>
@@ -143,14 +162,31 @@ export const IdpRoleSyncForm: FC<IdpRoleSyncFormProps> = ({
143162
<Label className="text-sm" htmlFor={`${id}-idp-role-name`}>
144163
IdP role name
145164
</Label>
146-
<Input
147-
id={`${id}-idp-role-name`}
148-
value={idpRoleName}
149-
className="w-72"
150-
onChange={(event) => {
151-
setIdpRoleName(event.target.value);
152-
}}
153-
/>
165+
{claimFieldValues ? (
166+
<Combobox
167+
value={idpRoleName}
168+
options={claimFieldValues}
169+
placeholder="Select IdP organization"
170+
open={open}
171+
onOpenChange={setOpen}
172+
inputValue={comboInputValue}
173+
onInputChange={setComboInputValue}
174+
onKeyDown={handleKeyDown}
175+
onSelect={(value: string) => {
176+
setIdpRoleName(value);
177+
setOpen(false);
178+
}}
179+
/>
180+
) : (
181+
<Input
182+
id={`${id}-idp-role-name`}
183+
value={idpRoleName}
184+
className="w-72"
185+
onChange={(event) => {
186+
setIdpRoleName(event.target.value);
187+
}}
188+
/>
189+
)}
154190
</div>
155191
<div className="grid items-center gap-1 flex-1">
156192
<Label className="text-sm" htmlFor={`${id}-coder-role`}>

site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
roleIdpSyncSettings,
99
} from "api/queries/organizations";
1010
import { organizationRoles } from "api/queries/roles";
11+
import type { GroupSyncSettings, RoleSyncSettings } from "api/typesGenerated";
1112
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
1213
import { EmptyState } from "components/EmptyState/EmptyState";
1314
import { displayError } from "components/GlobalSnackbar/utils";
@@ -16,7 +17,7 @@ import { Link } from "components/Link/Link";
1617
import { Paywall } from "components/Paywall/Paywall";
1718
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
1819
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
19-
import type { FC } from "react";
20+
import { type FC, useState } from "react";
2021
import { Helmet } from "react-helmet-async";
2122
import { useMutation, useQueries, useQuery, useQueryClient } from "react-query";
2223
import { useParams, useSearchParams } from "react-router-dom";
@@ -29,6 +30,8 @@ export const IdpSyncPage: FC = () => {
2930
const { organization: organizationName } = useParams() as {
3031
organization: string;
3132
};
33+
const [groupClaimField, setGroupClaimField] = useState("");
34+
const [roleClaimField, setRoleClaimField] = useState("");
3235
// IdP sync does not have its own entitlement and is based on templace_rbac
3336
const { template_rbac: isIdpSyncEnabled } = useFeatureVisibility();
3437
const { organizations } = useOrganizationSettings();
@@ -41,8 +44,22 @@ export const IdpSyncPage: FC = () => {
4144
rolesQuery,
4245
] = useQueries({
4346
queries: [
44-
groupIdpSyncSettings(organizationName),
45-
roleIdpSyncSettings(organizationName),
47+
{
48+
...groupIdpSyncSettings(organizationName),
49+
onSuccess: (data: GroupSyncSettings) => {
50+
if (data?.field) {
51+
setGroupClaimField(data.field);
52+
}
53+
},
54+
},
55+
{
56+
...roleIdpSyncSettings(organizationName),
57+
onSuccess: (data: RoleSyncSettings) => {
58+
if (data?.field) {
59+
setRoleClaimField(data.field);
60+
}
61+
},
62+
},
4663
groupsByOrganization(organizationName),
4764
organizationRoles(organizationName),
4865
],
@@ -86,6 +103,14 @@ export const IdpSyncPage: FC = () => {
86103
}
87104
}
88105

106+
const handleGroupSyncFieldChange = (value: string) => {
107+
setGroupClaimField(value);
108+
};
109+
110+
const handleRoleSyncFieldChange = (value: string) => {
111+
setRoleClaimField(value);
112+
};
113+
89114
return (
90115
<>
91116
<Helmet>
@@ -121,6 +146,8 @@ export const IdpSyncPage: FC = () => {
121146
groupsMap={groupsMap}
122147
roles={rolesQuery.data}
123148
organization={organization}
149+
onGroupSyncFieldChange={handleGroupSyncFieldChange}
150+
onRoleSyncFieldChange={handleRoleSyncFieldChange}
124151
error={error}
125152
onSubmitGroupSyncSettings={async (data) => {
126153
try {

site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPageView.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ interface IdpSyncPageViewProps {
2121
groupsMap: Map<string, string>;
2222
roles: Role[] | undefined;
2323
organization: Organization;
24+
onGroupSyncFieldChange: (value: string) => void;
25+
onRoleSyncFieldChange: (value: string) => void;
2426
error?: unknown;
2527
onSubmitGroupSyncSettings: (data: GroupSyncSettings) => void;
2628
onSubmitRoleSyncSettings: (data: RoleSyncSettings) => void;
@@ -35,6 +37,8 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({
3537
groupsMap,
3638
roles,
3739
organization,
40+
onGroupSyncFieldChange,
41+
onRoleSyncFieldChange,
3842
error,
3943
onSubmitGroupSyncSettings,
4044
onSubmitRoleSyncSettings,
@@ -76,6 +80,7 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({
7680
groupsMap={groupsMap}
7781
organization={organization}
7882
onSubmit={onSubmitGroupSyncSettings}
83+
onSyncFieldChange={onGroupSyncFieldChange}
7984
/>
8085
) : (
8186
<IdpRoleSyncForm
@@ -85,6 +90,7 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({
8590
roles={roles || []}
8691
organization={organization}
8792
onSubmit={onSubmitRoleSyncSettings}
93+
onSyncFieldChange={onRoleSyncFieldChange}
8894
/>
8995
)}
9096
</div>

0 commit comments

Comments
 (0)