AttributeMapping.tsx187 lines · main
1import { UseFormReturn } from 'react-hook-form'
2import { Button } from 'ui'
3import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
4import { SingleValueFieldArray } from 'ui-patterns/form/SingleValueFieldArray/SingleValueFieldArray'
5
6import type { SSOConfigFormSchema } from './SSOConfig'
7
8type ProviderAttribute = 'emailMapping' | 'userNameMapping' | 'firstNameMapping' | 'lastNameMapping'
9
10type ProviderPreset = {
11 name: string
12 attributeMapping: {
13 keys: {
14 email?: { name: string }
15 user_name?: { name: string }
16 first_name?: { name: string }
17 last_name?: { name: string }
18 name_identifier?: { name: string }
19 }
20 }
21}
22
23const PROVIDER_PRESETS: ProviderPreset[] = [
24 {
25 name: 'GSuite',
26 attributeMapping: {
27 keys: {
28 email: {
29 name: 'email',
30 },
31 user_name: {
32 name: 'user_name',
33 },
34 first_name: {
35 name: 'first_name',
36 },
37 last_name: {
38 name: 'last_name',
39 },
40 },
41 },
42 },
43 {
44 name: 'Azure',
45 attributeMapping: {
46 keys: {
47 email: {
48 name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
49 },
50 name_identifier: {
51 name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier',
52 },
53 first_name: {
54 name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
55 },
56 last_name: {
57 name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
58 },
59 },
60 },
61 },
62 {
63 name: 'Okta',
64 attributeMapping: {
65 keys: {
66 email: {
67 name: 'email',
68 },
69 user_name: {
70 name: 'user_name',
71 },
72 first_name: {
73 name: 'first_name',
74 },
75 last_name: {
76 name: 'last_name',
77 },
78 },
79 },
80 },
81] as const
82
83export const AttributeMapping = ({
84 form,
85 emailField,
86 userNameField,
87 firstNameField,
88 lastNameField,
89}: {
90 form: UseFormReturn<SSOConfigFormSchema>
91 emailField: ProviderAttribute
92 userNameField: ProviderAttribute
93 firstNameField: ProviderAttribute
94 lastNameField: ProviderAttribute
95}) => {
96 // Helper to apply a preset
97 function applyPreset(preset: ProviderPreset) {
98 const keys = preset.attributeMapping.keys
99 // Set each field if present in the preset, otherwise clear
100 form.setValue(emailField, [{ value: keys.email?.name ?? '' }], { shouldDirty: true })
101 form.setValue(userNameField, [{ value: keys.user_name?.name ?? '' }], { shouldDirty: true })
102 form.setValue(firstNameField, [{ value: keys.first_name?.name ?? '' }], { shouldDirty: true })
103 form.setValue(lastNameField, [{ value: keys.last_name?.name ?? '' }], { shouldDirty: true })
104 }
105
106 return (
107 <FormItemLayout
108 label="Attribute mapping"
109 layout="flex-row-reverse"
110 description={
111 <div>
112 <p>Map SSO attributes to user fields</p>
113 <div className="flex flex-col gap-y-2 my-2 mt-4">
114 <p>Presets for supported identity providers:</p>
115 <div className="flex flex-wrap items-center gap-2">
116 {PROVIDER_PRESETS.map((preset) => (
117 <Button key={preset.name} type="outline" onClick={() => applyPreset(preset)}>
118 {preset.name}
119 </Button>
120 ))}
121 </div>
122 </div>
123 </div>
124 }
125 className="gap-1"
126 >
127 <div className="grid w-full max-w-sm min-w-0 gap-3">
128 <MappingFieldArray form={form} fieldName={emailField} label="email" required />
129 <MappingFieldArray
130 form={form}
131 fieldName={userNameField}
132 label="user_name"
133 required={false}
134 />
135 <MappingFieldArray
136 form={form}
137 fieldName={firstNameField}
138 label="first_name"
139 required={false}
140 />
141 <MappingFieldArray
142 form={form}
143 fieldName={lastNameField}
144 label="last_name"
145 required={false}
146 />
147 </div>
148 </FormItemLayout>
149 )
150}
151
152const MappingFieldArray = ({
153 form,
154 fieldName,
155 label,
156 required,
157 placeholder,
158}: {
159 form: UseFormReturn<SSOConfigFormSchema>
160 fieldName: ProviderAttribute
161 label: string
162 required: boolean
163 placeholder?: string
164}) => {
165 return (
166 <div className="w-full min-w-0 space-y-1">
167 <div className="flex items-center justify-between gap-2">
168 <span className="text-foreground-light">{label}</span>
169 {!required ? <span className="text-foreground-muted">Optional</span> : null}
170 </div>
171 <SingleValueFieldArray
172 control={form.control}
173 name={fieldName}
174 valueFieldName="value"
175 createEmptyRow={() => ({ value: '' })}
176 placeholder={placeholder ?? ''}
177 addLabel="Add another"
178 removeLabel={`Remove ${label} mapping`}
179 minimumRows={1}
180 inputAutoComplete="off"
181 rowsClassName="grid gap-2 w-full"
182 addButtonType="default"
183 addButtonClassName="w-auto self-start justify-self-start"
184 />
185 </div>
186 )
187}