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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy