@@ -3,7 +3,8 @@ import { useQueryClient } from "@tanstack/react-query";
33import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations" ;
44import { Accordion , AccordionBody , AccordionHeader , SelectItem , TextInput } from "@tremor/react" ;
55import { Alert , Button , Form , Input , Modal , Select , Select as Select2 , Space , Tooltip , Typography } from "antd" ;
6- import React , { useEffect , useMemo , useState } from "react" ;
6+ import debounce from "lodash/debounce" ;
7+ import React , { useCallback , useEffect , useMemo , useState } from "react" ;
78import BulkCreateUsers from "./bulk_create_users_button" ;
89import TeamDropdown from "./common_components/team_dropdown" ;
910import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key" ;
@@ -14,6 +15,7 @@ import {
1415 invitationCreateCall ,
1516 modelAvailableCall ,
1617 userCreateCall ,
18+ userFilterUICall ,
1719} from "./networking" ;
1820import OnboardingModal , { InvitationLink } from "./onboarding_link" ;
1921const { Option } = Select ;
@@ -66,6 +68,33 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
6668 const [ invitationLinkData , setInvitationLinkData ] = useState < InvitationLink | null > ( null ) ;
6769 const [ baseUrl , setBaseUrl ] = useState < string | null > ( null ) ;
6870 const { data : organizations = [ ] } = useOrganizations ( ) ;
71+ const [ ownerOptions , setOwnerOptions ] = useState < { label : string ; value : string } [ ] > ( [ ] ) ;
72+ const [ ownerSearchLoading , setOwnerSearchLoading ] = useState ( false ) ;
73+
74+ const fetchOwners = async ( searchText : string ) => {
75+ if ( ! searchText ) {
76+ setOwnerOptions ( [ ] ) ;
77+ return ;
78+ }
79+ setOwnerSearchLoading ( true ) ;
80+ try {
81+ const params = new URLSearchParams ( ) ;
82+ params . append ( "user_email" , searchText ) ;
83+ const results = await userFilterUICall ( accessToken , params ) ;
84+ setOwnerOptions (
85+ results . map ( ( u : { user_email : string ; user_id : string } ) => ( {
86+ label : `${ u . user_email } (${ u . user_id } )` ,
87+ value : u . user_id ,
88+ } ) )
89+ ) ;
90+ } catch {
91+ // silently ignore search errors
92+ } finally {
93+ setOwnerSearchLoading ( false ) ;
94+ }
95+ } ;
96+
97+ const debouncedOwnerSearch = useCallback ( debounce ( fetchOwners , 300 ) , [ accessToken ] ) ;
6998
7099 // Derive teams from the user's organizations, falling back to the teams prop
71100 const availableTeams = useMemo ( ( ) => {
@@ -113,6 +142,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
113142 user_role : string ;
114143 organization_ids ?: string [ ] ;
115144 organizations ?: string [ ] ;
145+ owner_ids ?: string [ ] ;
116146 } ) => {
117147 try {
118148 NotificationsManager . info ( "Making API Call" ) ;
@@ -213,6 +243,41 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
213243 < TeamDropdown />
214244 </ Form . Item >
215245
246+ < Form . Item
247+ label = {
248+ < span >
249+ Owners{ " " }
250+ < Tooltip title = "At least 2 users who own this service account" >
251+ < InfoCircleOutlined style = { { marginLeft : "4px" } } />
252+ </ Tooltip >
253+ </ span >
254+ }
255+ name = "owner_ids"
256+ required = { false }
257+ rules = { [
258+ {
259+ validator : async ( _ , value ) => {
260+ if ( value && value . length > 0 && value . length < 2 ) {
261+ throw new Error ( "If specifying owners, please select at least 2" ) ;
262+ }
263+ } ,
264+ } ,
265+ ] }
266+ help = "optional — if provided, minimum 2 owners"
267+ >
268+ < Select2
269+ mode = "multiple"
270+ showSearch
271+ placeholder = "Search by email to add owners"
272+ filterOption = { false }
273+ onSearch = { ( v ) => debouncedOwnerSearch ( v ) }
274+ loading = { ownerSearchLoading }
275+ options = { ownerOptions }
276+ notFoundContent = { ownerSearchLoading ? "Searching..." : "No users found" }
277+ style = { { width : "100%" } }
278+ />
279+ </ Form . Item >
280+
216281 < Form . Item label = "Metadata" name = "metadata" >
217282 < Input . TextArea rows = { 4 } placeholder = "Enter metadata as JSON" />
218283 </ Form . Item >
@@ -309,6 +374,41 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
309374 </ Select >
310375 </ Form . Item >
311376
377+ < Form . Item
378+ label = {
379+ < span >
380+ Owners{ " " }
381+ < Tooltip title = "At least 2 users who own this service account" >
382+ < InfoCircleOutlined style = { { marginLeft : "4px" } } />
383+ </ Tooltip >
384+ </ span >
385+ }
386+ name = "owner_ids"
387+ required = { false }
388+ rules = { [
389+ {
390+ validator : async ( _ , value ) => {
391+ if ( value && value . length > 0 && value . length < 2 ) {
392+ throw new Error ( "If specifying owners, please select at least 2" ) ;
393+ }
394+ } ,
395+ } ,
396+ ] }
397+ help = "optional — if provided, minimum 2 owners"
398+ >
399+ < Select2
400+ mode = "multiple"
401+ showSearch
402+ placeholder = "Search by email to add owners"
403+ filterOption = { false }
404+ onSearch = { ( v ) => debouncedOwnerSearch ( v ) }
405+ loading = { ownerSearchLoading }
406+ options = { ownerOptions }
407+ notFoundContent = { ownerSearchLoading ? "Searching..." : "No users found" }
408+ style = { { width : "100%" } }
409+ />
410+ </ Form . Item >
411+
312412 < Form . Item label = "Metadata" name = "metadata" >
313413 < Input . TextArea rows = { 4 } placeholder = "Enter metadata as JSON" />
314414 </ Form . Item >
0 commit comments