Skip to content

Commit 33599ce

Browse files
committed
feat: add combobox using claim field values
1 parent b13cac9 commit 33599ce

File tree

5 files changed

+148
-30
lines changed

5 files changed

+148
-30
lines changed

site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@ import {
2828
MultiSelectCombobox,
2929
type Option,
3030
} from "components/MultiSelectCombobox/MultiSelectCombobox";
31-
import {
32-
Popover,
33-
PopoverContent,
34-
PopoverTrigger,
35-
} from "components/Popover/Popover";
3631
import { Spinner } from "components/Spinner/Spinner";
3732
import { Switch } from "components/Switch/Switch";
3833
import {
@@ -43,9 +38,8 @@ import {
4338
TableRow,
4439
} from "components/Table/Table";
4540
import { useFormik } from "formik";
46-
import { Check, ChevronDown, CornerDownLeft, Plus, Trash } from "lucide-react";
41+
import { Plus, Trash } from "lucide-react";
4742
import { type FC, type KeyboardEventHandler, useId, useState } from "react";
48-
import { cn } from "utils/cn";
4943
import { docs } from "utils/docs";
5044
import { isUUID } from "utils/uuid";
5145
import * as Yup from "yup";

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

Lines changed: 47 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,
@@ -24,7 +25,7 @@ import { Spinner } from "components/Spinner/Spinner";
2425
import { Switch } from "components/Switch/Switch";
2526
import { useFormik } from "formik";
2627
import { Plus, Trash } from "lucide-react";
27-
import { type FC, useId, useState } from "react";
28+
import { type FC, type KeyboardEventHandler, useId, useState } from "react";
2829
import { docs } from "utils/docs";
2930
import { isUUID } from "utils/uuid";
3031
import * as Yup from "yup";
@@ -39,7 +40,9 @@ interface IdpGroupSyncFormProps {
3940
groupMappingCount: number;
4041
legacyGroupMappingCount: number;
4142
organization: Organization;
43+
claimFieldValues: string[] | undefined;
4244
onSubmit: (data: GroupSyncSettings) => void;
45+
onSyncFieldChange: (value: string) => void;
4346
}
4447

4548
const groupSyncValidationSchema = Yup.object({
@@ -72,7 +75,9 @@ export const IdpGroupSyncForm = ({
7275
groups,
7376
groupsMap,
7477
organization,
78+
claimFieldValues,
7579
onSubmit,
80+
onSyncFieldChange,
7681
}: IdpGroupSyncFormProps) => {
7782
const form = useFormik<GroupSyncSettings>({
7883
initialValues: {
@@ -89,6 +94,8 @@ export const IdpGroupSyncForm = ({
8994
const [idpGroupName, setIdpGroupName] = useState("");
9095
const [coderGroups, setCoderGroups] = useState<Option[]>([]);
9196
const id = useId();
97+
const [comboInputValue, setComboInputValue] = useState("");
98+
const [open, setOpen] = useState(false);
9299

93100
const getGroupNames = (groupIds: readonly string[]) => {
94101
return groupIds.map((groupId) => groupsMap.get(groupId) || groupId);
@@ -108,6 +115,19 @@ export const IdpGroupSyncForm = ({
108115
form.handleSubmit();
109116
};
110117

118+
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
119+
if (
120+
event.key === "Enter" &&
121+
comboInputValue &&
122+
!claimFieldValues?.some((value) => value === comboInputValue.toLowerCase())
123+
) {
124+
event.preventDefault();
125+
setIdpGroupName(comboInputValue);
126+
setComboInputValue("");
127+
setOpen(false);
128+
}
129+
};
130+
111131
return (
112132
<form onSubmit={form.handleSubmit}>
113133
<fieldset
@@ -135,6 +155,7 @@ export const IdpGroupSyncForm = ({
135155
value={form.values.field}
136156
onChange={(event) => {
137157
void form.setFieldValue("field", event.target.value);
158+
onSyncFieldChange(event.target.value);
138159
}}
139160
className="w-72"
140161
/>
@@ -194,14 +215,31 @@ export const IdpGroupSyncForm = ({
194215
<Label className="text-sm" htmlFor={`${id}-idp-group-name`}>
195216
IdP group name
196217
</Label>
197-
<Input
198-
id={`${id}-idp-group-name`}
199-
value={idpGroupName}
200-
className="w-72"
201-
onChange={(event) => {
202-
setIdpGroupName(event.target.value);
203-
}}
204-
/>
218+
{claimFieldValues ? (
219+
<Combobox
220+
value={idpGroupName}
221+
options={claimFieldValues}
222+
placeholder="Select IdP organization"
223+
open={open}
224+
onOpenChange={setOpen}
225+
inputValue={comboInputValue}
226+
onInputChange={setComboInputValue}
227+
onKeyDown={handleKeyDown}
228+
onSelect={(value: string) => {
229+
setIdpGroupName(value);
230+
setOpen(false);
231+
}}
232+
/>
233+
) : (
234+
<Input
235+
id={`${id}-idp-group-name`}
236+
value={idpGroupName}
237+
className="w-72"
238+
onChange={(event) => {
239+
setIdpGroupName(event.target.value);
240+
}}
241+
/>
242+
)}
205243
</div>
206244
<div className="grid items-center gap-1 flex-1">
207245
<Label className="text-sm" htmlFor={`${id}-coder-group`}>

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

Lines changed: 47 additions & 10 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 {
@@ -11,18 +12,19 @@ import {
1112
import { Spinner } from "components/Spinner/Spinner";
1213
import { useFormik } from "formik";
1314
import { Plus, Trash } from "lucide-react";
14-
import { type FC, useId, useState } from "react";
15+
import { type FC, type KeyboardEventHandler, useId, useState } from "react";
1516
import * as Yup from "yup";
1617
import { ExportPolicyButton } from "./ExportPolicyButton";
1718
import { IdpMappingTable } from "./IdpMappingTable";
1819
import { IdpPillList } from "./IdpPillList";
19-
2020
interface IdpRoleSyncFormProps {
2121
roleSyncSettings: RoleSyncSettings;
2222
roleMappingCount: number;
2323
organization: Organization;
24+
claimFieldValues: string[] | undefined;
2425
roles: Role[];
2526
onSubmit: (data: RoleSyncSettings) => void;
27+
onSyncFieldChange: (value: string) => void;
2628
}
2729

2830
const roleSyncValidationSchema = Yup.object({
@@ -52,8 +54,10 @@ export const IdpRoleSyncForm = ({
5254
roleSyncSettings,
5355
roleMappingCount,
5456
organization,
57+
claimFieldValues,
5558
roles,
5659
onSubmit,
60+
onSyncFieldChange,
5761
}: IdpRoleSyncFormProps) => {
5862
const form = useFormik<RoleSyncSettings>({
5963
initialValues: {
@@ -67,6 +71,8 @@ export const IdpRoleSyncForm = ({
6771
const [idpRoleName, setIdpRoleName] = useState("");
6872
const [coderRoles, setCoderRoles] = useState<Option[]>([]);
6973
const id = useId();
74+
const [comboInputValue, setComboInputValue] = useState("");
75+
const [open, setOpen] = useState(false);
7076

7177
const handleDelete = async (idpOrg: string) => {
7278
const newMapping = Object.fromEntries(
@@ -82,6 +88,19 @@ export const IdpRoleSyncForm = ({
8288
form.handleSubmit();
8389
};
8490

91+
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
92+
if (
93+
event.key === "Enter" &&
94+
comboInputValue &&
95+
!claimFieldValues?.some((value) => value === comboInputValue.toLowerCase())
96+
) {
97+
event.preventDefault();
98+
setIdpRoleName(comboInputValue);
99+
setComboInputValue("");
100+
setOpen(false);
101+
}
102+
};
103+
85104
return (
86105
<form onSubmit={form.handleSubmit}>
87106
<fieldset
@@ -106,6 +125,7 @@ export const IdpRoleSyncForm = ({
106125
value={form.values.field}
107126
onChange={(event) => {
108127
void form.setFieldValue("field", event.target.value);
128+
onSyncFieldChange(event.target.value);
109129
}}
110130
className="w-72"
111131
/>
@@ -135,14 +155,31 @@ export const IdpRoleSyncForm = ({
135155
<Label className="text-sm" htmlFor={`${id}-idp-role-name`}>
136156
IdP role name
137157
</Label>
138-
<Input
139-
id={`${id}-idp-role-name`}
140-
value={idpRoleName}
141-
className="w-72"
142-
onChange={(event) => {
143-
setIdpRoleName(event.target.value);
144-
}}
145-
/>
158+
{claimFieldValues ? (
159+
<Combobox
160+
value={idpRoleName}
161+
options={claimFieldValues}
162+
placeholder="Select IdP organization"
163+
open={open}
164+
onOpenChange={setOpen}
165+
inputValue={comboInputValue}
166+
onInputChange={setComboInputValue}
167+
onKeyDown={handleKeyDown}
168+
onSelect={(value: string) => {
169+
setIdpRoleName(value);
170+
setOpen(false);
171+
}}
172+
/>
173+
) : (
174+
<Input
175+
id={`${id}-idp-role-name`}
176+
value={idpRoleName}
177+
className="w-72"
178+
onChange={(event) => {
179+
setIdpRoleName(event.target.value);
180+
}}
181+
/>
182+
)}
146183
</div>
147184
<div className="grid items-center gap-1 flex-1">
148185
<Label className="text-sm" htmlFor={`${id}-coder-role`}>

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

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
patchRoleSyncSettings,
77
roleIdpSyncSettings,
88
} from "api/queries/organizations";
9+
import { idpSyncClaimFieldValues } from "api/queries/organizations";
910
import { organizationRoles } from "api/queries/roles";
11+
import type { GroupSyncSettings, RoleSyncSettings } from "api/typesGenerated";
1012
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
1113
import { EmptyState } from "components/EmptyState/EmptyState";
1214
import { displayError } from "components/GlobalSnackbar/utils";
@@ -15,9 +17,9 @@ import { Link } from "components/Link/Link";
1517
import { Paywall } from "components/Paywall/Paywall";
1618
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
1719
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
18-
import type { FC } from "react";
20+
import { type FC, useState } from "react";
1921
import { Helmet } from "react-helmet-async";
20-
import { useMutation, useQueries, useQueryClient } from "react-query";
22+
import { useMutation, useQueries, useQuery, useQueryClient } from "react-query";
2123
import { useParams } from "react-router-dom";
2224
import { docs } from "utils/docs";
2325
import { pageTitle } from "utils/page";
@@ -28,6 +30,8 @@ export const IdpSyncPage: FC = () => {
2830
const { organization: organizationName } = useParams() as {
2931
organization: string;
3032
};
33+
const [groupClaimField, setGroupClaimField] = useState("");
34+
const [roleClaimField, setRoleClaimField] = useState("");
3135
// IdP sync does not have its own entitlement and is based on templace_rbac
3236
const { template_rbac: isIdpSyncEnabled } = useFeatureVisibility();
3337
const { organizations } = useOrganizationSettings();
@@ -40,13 +44,34 @@ export const IdpSyncPage: FC = () => {
4044
rolesQuery,
4145
] = useQueries({
4246
queries: [
43-
groupIdpSyncSettings(organizationName),
44-
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+
},
4563
groupsByOrganization(organizationName),
4664
organizationRoles(organizationName),
4765
],
4866
});
4967

68+
const { data: groupClaimFieldValues } = useQuery(
69+
idpSyncClaimFieldValues(groupClaimField),
70+
);
71+
const { data: roleClaimFieldValues } = useQuery(
72+
idpSyncClaimFieldValues(roleClaimField),
73+
);
74+
5075
if (!organization) {
5176
return <EmptyState message="Organization not found" />;
5277
}
@@ -72,6 +97,14 @@ export const IdpSyncPage: FC = () => {
7297
}
7398
}
7499

100+
const handleGroupSyncFieldChange = (value: string) => {
101+
setGroupClaimField(value);
102+
};
103+
104+
const handleRoleSyncFieldChange = (value: string) => {
105+
setRoleClaimField(value);
106+
};
107+
75108
return (
76109
<>
77110
<Helmet>
@@ -105,6 +138,10 @@ export const IdpSyncPage: FC = () => {
105138
groupsMap={groupsMap}
106139
roles={rolesQuery.data}
107140
organization={organization}
141+
onGroupSyncFieldChange={handleGroupSyncFieldChange}
142+
onRoleSyncFieldChange={handleRoleSyncFieldChange}
143+
groupClaimFieldValues={groupClaimFieldValues}
144+
roleClaimFieldValues={roleClaimFieldValues}
108145
error={error}
109146
onSubmitGroupSyncSettings={async (data) => {
110147
try {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ interface IdpSyncPageViewProps {
2020
groupsMap: Map<string, string>;
2121
roles: Role[] | undefined;
2222
organization: Organization;
23+
onGroupSyncFieldChange: (value: string) => void;
24+
onRoleSyncFieldChange: (value: string) => void;
25+
groupClaimFieldValues: string[] | undefined;
26+
roleClaimFieldValues: string[] | undefined;
2327
error?: unknown;
2428
onSubmitGroupSyncSettings: (data: GroupSyncSettings) => void;
2529
onSubmitRoleSyncSettings: (data: RoleSyncSettings) => void;
@@ -32,6 +36,10 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({
3236
groupsMap,
3337
roles,
3438
organization,
39+
onGroupSyncFieldChange,
40+
onRoleSyncFieldChange,
41+
groupClaimFieldValues,
42+
roleClaimFieldValues,
3543
error,
3644
onSubmitGroupSyncSettings,
3745
onSubmitRoleSyncSettings,
@@ -73,15 +81,19 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({
7381
groups={groups}
7482
groupsMap={groupsMap}
7583
organization={organization}
84+
claimFieldValues={groupClaimFieldValues}
7685
onSubmit={onSubmitGroupSyncSettings}
86+
onSyncFieldChange={onGroupSyncFieldChange}
7787
/>
7888
) : (
7989
<IdpRoleSyncForm
8090
roleSyncSettings={roleSyncSettings}
8191
roleMappingCount={roleMappingCount}
8292
roles={roles || []}
8393
organization={organization}
94+
claimFieldValues={roleClaimFieldValues}
8495
onSubmit={onSubmitRoleSyncSettings}
96+
onSyncFieldChange={onRoleSyncFieldChange}
8597
/>
8698
)}
8799
</div>

0 commit comments

Comments
 (0)