|
| 1 | +importTableCellfrom"@mui/material/TableCell"; |
| 2 | +importTableRowfrom"@mui/material/TableRow"; |
| 3 | +importtype{ |
| 4 | +Group, |
| 5 | +GroupSyncSettings, |
| 6 | +Organization, |
| 7 | +}from"api/typesGenerated"; |
| 8 | +import{Button}from"components/Button/Button"; |
| 9 | +import{ |
| 10 | +HelpTooltip, |
| 11 | +HelpTooltipContent, |
| 12 | +HelpTooltipText, |
| 13 | +HelpTooltipTitle, |
| 14 | +HelpTooltipTrigger, |
| 15 | +}from"components/HelpTooltip/HelpTooltip"; |
| 16 | +import{Input}from"components/Input/Input"; |
| 17 | +import{Label}from"components/Label/Label"; |
| 18 | +import{Link}from"components/Link/Link"; |
| 19 | +import{ |
| 20 | +MultiSelectCombobox, |
| 21 | +typeOption, |
| 22 | +}from"components/MultiSelectCombobox/MultiSelectCombobox"; |
| 23 | +import{Switch}from"components/Switch/Switch"; |
| 24 | +import{useFormik}from"formik"; |
| 25 | +import{Plus,Trash}from"lucide-react"; |
| 26 | +import{typeFC,useState}from"react"; |
| 27 | +import{docs}from"utils/docs"; |
| 28 | +import*asYupfrom"yup"; |
| 29 | +import{ExportPolicyButton}from"./ExportPolicyButton"; |
| 30 | +import{IdpMappingTable}from"./IdpMappingTable"; |
| 31 | +import{IdpPillList}from"./IdpPillList"; |
| 32 | + |
| 33 | +interfaceIdpGroupSyncFormProps{ |
| 34 | +groupSyncSettings:GroupSyncSettings; |
| 35 | +groupsMap:Map<string,string>; |
| 36 | +groups:Group[]; |
| 37 | +groupMappingCount:number; |
| 38 | +legacyGroupMappingCount:number; |
| 39 | +organization:Organization; |
| 40 | +onSubmit:(data:GroupSyncSettings)=>void; |
| 41 | +} |
| 42 | + |
| 43 | +constgroupSyncValidationSchema=Yup.object({ |
| 44 | +field:Yup.string().trim(), |
| 45 | +regex_filter:Yup.string().trim(), |
| 46 | +auto_create_missing_groups:Yup.boolean(), |
| 47 | +mapping:Yup.object().shape({ |
| 48 | +[`${String}`]:Yup.array().of(Yup.string()), |
| 49 | +}), |
| 50 | +}); |
| 51 | + |
| 52 | +exportconstIdpGroupSyncForm=({ |
| 53 | +groupSyncSettings, |
| 54 | +groupMappingCount, |
| 55 | +legacyGroupMappingCount, |
| 56 | +groups, |
| 57 | +groupsMap, |
| 58 | +organization, |
| 59 | +onSubmit, |
| 60 | +}:IdpGroupSyncFormProps)=>{ |
| 61 | +constform=useFormik<GroupSyncSettings>({ |
| 62 | +initialValues:{ |
| 63 | +field:groupSyncSettings?.field??"", |
| 64 | +regex_filter:groupSyncSettings?.regex_filter??"", |
| 65 | +auto_create_missing_groups: |
| 66 | +groupSyncSettings?.auto_create_missing_groups??false, |
| 67 | +mapping:groupSyncSettings?.mapping??{}, |
| 68 | +}, |
| 69 | +validationSchema:groupSyncValidationSchema, |
| 70 | +onSubmit, |
| 71 | +enableReinitialize:Boolean(groupSyncSettings), |
| 72 | +}); |
| 73 | +const[idpGroupName,setIdpGroupName]=useState(""); |
| 74 | +const[coderGroups,setCoderGroups]=useState<Option[]>([]); |
| 75 | + |
| 76 | +constgetGroupNames=(groupIds:readonlystring[])=>{ |
| 77 | +returngroupIds.map((groupId)=>groupsMap.get(groupId)||groupId); |
| 78 | +}; |
| 79 | + |
| 80 | +consthandleDelete=async(idpOrg:string)=>{ |
| 81 | +constnewMapping=Object.fromEntries( |
| 82 | +Object.entries(form.values.mapping||{}).filter( |
| 83 | +([key])=>key!==idpOrg, |
| 84 | +), |
| 85 | +); |
| 86 | +constnewSyncSettings={ |
| 87 | +...form.values, |
| 88 | +mapping:newMapping, |
| 89 | +}; |
| 90 | +voidform.setFieldValue("mapping",newSyncSettings.mapping); |
| 91 | +form.handleSubmit(); |
| 92 | +}; |
| 93 | + |
| 94 | +constSYNC_FIELD_ID="sync-field"; |
| 95 | +constREGEX_FILTER_ID="regex-filter"; |
| 96 | +constAUTO_CREATE_MISSING_GROUPS_ID="auto-create-missing-groups"; |
| 97 | +constIDP_GROUP_NAME_ID="idp-group-name"; |
| 98 | + |
| 99 | +return( |
| 100 | +<formonSubmit={form.handleSubmit}> |
| 101 | +<fieldset |
| 102 | +disabled={form.isSubmitting} |
| 103 | +className="flex flex-col border-none gap-8 pt-2" |
| 104 | +> |
| 105 | +<divclassName="flex justify-end"> |
| 106 | +<ExportPolicyButton |
| 107 | +syncSettings={groupSyncSettings} |
| 108 | +organization={organization} |
| 109 | +type="groups" |
| 110 | +/> |
| 111 | +</div> |
| 112 | +<divclassName="grid items-center gap-3"> |
| 113 | +<divclassName="flex flex-row items-center gap-5"> |
| 114 | +<divclassName="grid grid-cols-2 gap-2 grid-rows-[20px_auto_20px]"> |
| 115 | +<LabelclassName="text-sm"htmlFor={SYNC_FIELD_ID}> |
| 116 | +Group sync field |
| 117 | +</Label> |
| 118 | +<LabelclassName="text-sm"htmlFor={SYNC_FIELD_ID}> |
| 119 | +Regex filter |
| 120 | +</Label> |
| 121 | +<Input |
| 122 | +id={SYNC_FIELD_ID} |
| 123 | +value={form.values.field} |
| 124 | +onChange={async(event)=>{ |
| 125 | +voidform.setFieldValue("field",event.target.value); |
| 126 | +}} |
| 127 | +className="min-w-72 w-72" |
| 128 | +/> |
| 129 | +<divclassName="flex flex-row gap-2"> |
| 130 | +<Input |
| 131 | +id={REGEX_FILTER_ID} |
| 132 | +value={form.values.regex_filter??""} |
| 133 | +onChange={async(event)=>{ |
| 134 | +voidform.setFieldValue("regex_filter",event.target.value); |
| 135 | +}} |
| 136 | +className="min-w-40" |
| 137 | +/> |
| 138 | +<Button |
| 139 | +className="w-20" |
| 140 | +type="submit" |
| 141 | +disabled={form.isSubmitting||!form.dirty} |
| 142 | +onClick={(event)=>{ |
| 143 | +event.preventDefault(); |
| 144 | +form.handleSubmit(); |
| 145 | +}} |
| 146 | +> |
| 147 | +Save |
| 148 | +</Button> |
| 149 | +</div> |
| 150 | +<pclassName="text-content-secondary text-2xs m-0"> |
| 151 | +If empty, group sync is deactivated |
| 152 | +</p> |
| 153 | +</div> |
| 154 | +</div> |
| 155 | +</div> |
| 156 | +<divclassName="flex flex-row items-center gap-3"> |
| 157 | +<Switch |
| 158 | +id={AUTO_CREATE_MISSING_GROUPS_ID} |
| 159 | +checked={form.values.auto_create_missing_groups} |
| 160 | +onCheckedChange={async(checked)=>{ |
| 161 | +voidform.setFieldValue("auto_create_missing_groups",checked); |
| 162 | +form.handleSubmit(); |
| 163 | +}} |
| 164 | +/> |
| 165 | +<spanclassName="flex flex-row items-center gap-1"> |
| 166 | +<LabelhtmlFor={AUTO_CREATE_MISSING_GROUPS_ID}> |
| 167 | +Auto create missing groups |
| 168 | +</Label> |
| 169 | +<AutoCreateMissingGroupsHelpTooltip/> |
| 170 | +</span> |
| 171 | +</div> |
| 172 | +<divclassName="flex flex-row gap-2 justify-between items-start"> |
| 173 | +<divclassName="grid items-center gap-1"> |
| 174 | +<LabelclassName="text-sm"htmlFor={IDP_GROUP_NAME_ID}> |
| 175 | +IdP group name |
| 176 | +</Label> |
| 177 | +<Input |
| 178 | +id={IDP_GROUP_NAME_ID} |
| 179 | +value={idpGroupName} |
| 180 | +className="min-w-72 w-72" |
| 181 | +onChange={(event)=>{ |
| 182 | +setIdpGroupName(event.target.value); |
| 183 | +}} |
| 184 | +/> |
| 185 | +</div> |
| 186 | +<divclassName="grid items-center gap-1 flex-1"> |
| 187 | +<LabelclassName="text-sm"htmlFor=":r1d:"> |
| 188 | +Coder group |
| 189 | +</Label> |
| 190 | +<MultiSelectCombobox |
| 191 | +className="min-w-60 max-w-3xl" |
| 192 | +value={coderGroups} |
| 193 | +onChange={setCoderGroups} |
| 194 | +defaultOptions={groups.map((group)=>({ |
| 195 | +label:group.display_name||group.name, |
| 196 | +value:group.id, |
| 197 | +}))} |
| 198 | +hidePlaceholderWhenSelected |
| 199 | +placeholder="Select group" |
| 200 | +emptyIndicator={ |
| 201 | +<pclassName="text-center text-md text-content-primary"> |
| 202 | +All groups selected |
| 203 | +</p> |
| 204 | +} |
| 205 | +/> |
| 206 | +</div> |
| 207 | +<divclassName="grid grid-rows-[28px_auto]"> |
| 208 | + |
| 209 | +<Button |
| 210 | +className="mb-px" |
| 211 | +type="submit" |
| 212 | +disabled={!idpGroupName||coderGroups.length===0} |
| 213 | +onClick={async()=>{ |
| 214 | +constnewSyncSettings={ |
| 215 | +...form.values, |
| 216 | +mapping:{ |
| 217 | +...form.values.mapping, |
| 218 | +[idpGroupName]:coderGroups.map((role)=>role.value), |
| 219 | +}, |
| 220 | +}; |
| 221 | +voidform.setFieldValue("mapping",newSyncSettings.mapping); |
| 222 | +form.handleSubmit(); |
| 223 | +setIdpGroupName(""); |
| 224 | +setCoderGroups([]); |
| 225 | +}} |
| 226 | +> |
| 227 | +<Plussize={14}/> |
| 228 | +Add IdP group |
| 229 | +</Button> |
| 230 | +</div> |
| 231 | +</div> |
| 232 | + |
| 233 | +<divclassName="flex flex-col"> |
| 234 | +<IdpMappingTabletype="Group"rowCount={groupMappingCount}> |
| 235 | +{groupSyncSettings?.mapping&& |
| 236 | +Object.entries(groupSyncSettings.mapping) |
| 237 | +.sort() |
| 238 | +.map(([idpGroup,groups])=>( |
| 239 | +<GroupRow |
| 240 | +key={idpGroup} |
| 241 | +idpGroup={idpGroup} |
| 242 | +coderGroup={getGroupNames(groups)} |
| 243 | +onDelete={handleDelete} |
| 244 | +/> |
| 245 | +))} |
| 246 | +</IdpMappingTable> |
| 247 | + |
| 248 | +{groupSyncSettings?.legacy_group_name_mapping&&( |
| 249 | +<div> |
| 250 | +<LegacyGroupSyncHeader/> |
| 251 | +<IdpMappingTabletype="Group"rowCount={legacyGroupMappingCount}> |
| 252 | +{Object.entries(groupSyncSettings.legacy_group_name_mapping) |
| 253 | +.sort() |
| 254 | +.map(([idpGroup,groupId])=>( |
| 255 | +<GroupRow |
| 256 | +key={idpGroup} |
| 257 | +idpGroup={idpGroup} |
| 258 | +coderGroup={getGroupNames([groupId])} |
| 259 | +onDelete={handleDelete} |
| 260 | +/> |
| 261 | +))} |
| 262 | +</IdpMappingTable> |
| 263 | +</div> |
| 264 | +)} |
| 265 | +</div> |
| 266 | +</fieldset> |
| 267 | +</form> |
| 268 | +); |
| 269 | +}; |
| 270 | + |
| 271 | +interfaceGroupRowProps{ |
| 272 | +idpGroup:string; |
| 273 | +coderGroup:readonlystring[]; |
| 274 | +onDelete:(idpOrg:string)=>void; |
| 275 | +} |
| 276 | + |
| 277 | +constGroupRow:FC<GroupRowProps>=({ idpGroup, coderGroup, onDelete})=>{ |
| 278 | +return( |
| 279 | +<TableRowdata-testid={`group-${idpGroup}`}> |
| 280 | +<TableCell>{idpGroup}</TableCell> |
| 281 | +<TableCell> |
| 282 | +<IdpPillListroles={coderGroup}/> |
| 283 | +</TableCell> |
| 284 | +<TableCell> |
| 285 | +<Button |
| 286 | +variant="outline" |
| 287 | +className="w-8 h-8 min-w-10 text-content-primary" |
| 288 | +aria-label="delete" |
| 289 | +onClick={()=>onDelete(idpGroup)} |
| 290 | +> |
| 291 | +<Trash/> |
| 292 | +<spanclassName="sr-only">Delete IdP mapping</span> |
| 293 | +</Button> |
| 294 | +</TableCell> |
| 295 | +</TableRow> |
| 296 | +); |
| 297 | +}; |
| 298 | + |
| 299 | +constAutoCreateMissingGroupsHelpTooltip:FC=()=>{ |
| 300 | +return( |
| 301 | +<HelpTooltip> |
| 302 | +<HelpTooltipTrigger/> |
| 303 | +<HelpTooltipContent> |
| 304 | +<HelpTooltipText> |
| 305 | +Enabling auto create missing groups will automatically create groups |
| 306 | +returned by the OIDC provider if they do not exist in Coder. |
| 307 | +</HelpTooltipText> |
| 308 | +</HelpTooltipContent> |
| 309 | +</HelpTooltip> |
| 310 | +); |
| 311 | +}; |
| 312 | + |
| 313 | +constLegacyGroupSyncHeader:FC=()=>{ |
| 314 | +return( |
| 315 | +<h4className="text-xl font-medium"> |
| 316 | +<divclassName="flex items-end gap-2"> |
| 317 | +<span>Legacy group sync settings</span> |
| 318 | +<HelpTooltip> |
| 319 | +<HelpTooltipTrigger/> |
| 320 | +<HelpTooltipContent> |
| 321 | +<HelpTooltipTitle>Legacy group sync settings</HelpTooltipTitle> |
| 322 | +<HelpTooltipText> |
| 323 | +These settings were configured using environment variables, and |
| 324 | +only apply to the default organization. It is now recommended to |
| 325 | +configure IdP sync via the CLI or the UI, which enables sync to be |
| 326 | +configured for any organization, and for those settings to be |
| 327 | +persisted without manually setting environment variables.{" "} |
| 328 | +<Linkhref={docs("/admin/users/idp-sync")}> |
| 329 | +Learn more… |
| 330 | +</Link> |
| 331 | +</HelpTooltipText> |
| 332 | +</HelpTooltipContent> |
| 333 | +</HelpTooltip> |
| 334 | +</div> |
| 335 | +</h4> |
| 336 | +); |
| 337 | +}; |