AttributeMapping.tsx 5.0 KB

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