import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Loader2, Search, Plus, CheckCircle2 } from 'lucide-react'; import { Button } from './Button'; import { api } from '../api/client'; import type { LDAPSearchResult, UserResponse } from '../api/client'; interface LdapUserPickerProps { onSuccess: (user: UserResponse) => void; } const SEARCH_DEBOUNCE_MS = 300; const MIN_QUERY_LENGTH = 2; export function LdapUserPicker({ onSuccess }: LdapUserPickerProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const [rawQuery, setRawQuery] = useState(''); const [debouncedQuery, setDebouncedQuery] = useState(''); const [selectedDn, setSelectedDn] = useState(null); const [errorMessage, setErrorMessage] = useState(null); // Debounce keystrokes — the search hits the directory and we don't want a // request per character. 300ms matches the debounce in other typeaheads in // this app (e.g. file manager). useEffect(() => { const trimmed = rawQuery.trim(); if (trimmed.length < MIN_QUERY_LENGTH) { setDebouncedQuery(''); return; } const id = setTimeout(() => setDebouncedQuery(trimmed), SEARCH_DEBOUNCE_MS); return () => clearTimeout(id); }, [rawQuery]); // Reset selection when the query changes so a stale selection from a previous // search can't be silently submitted. useEffect(() => { setSelectedDn(null); setErrorMessage(null); }, [debouncedQuery]); const searchQuery = useQuery({ queryKey: ['ldap-search', debouncedQuery], queryFn: () => api.searchLDAPDirectory(debouncedQuery), enabled: debouncedQuery.length >= MIN_QUERY_LENGTH, staleTime: 30_000, }); const provisionMutation = useMutation({ mutationFn: (username: string) => api.provisionLDAPUser(username), onSuccess: (user) => { queryClient.invalidateQueries({ queryKey: ['users'] }); onSuccess(user); }, onError: (error: Error) => { setErrorMessage(error.message || t('users.modal.ldapErrorProvision')); }, }); const selectedResult = useMemo( () => searchQuery.data?.find((r) => r.dn === selectedDn) ?? null, [searchQuery.data, selectedDn] ); const isShortQuery = rawQuery.trim().length > 0 && rawQuery.trim().length < MIN_QUERY_LENGTH; const isLoading = searchQuery.isFetching && debouncedQuery.length >= MIN_QUERY_LENGTH; const hasResults = !!searchQuery.data && searchQuery.data.length > 0; const showNoResults = !isLoading && !!searchQuery.data && searchQuery.data.length === 0 && debouncedQuery.length >= MIN_QUERY_LENGTH; const handleProvision = () => { if (!selectedResult || selectedResult.already_provisioned) return; setErrorMessage(null); provisionMutation.mutate(selectedResult.username); }; return (
{/* Search input */}
setRawQuery(e.target.value)} className="w-full pl-9 pr-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" placeholder={t('users.modal.ldapSearchPlaceholder')} autoComplete="off" />
{isShortQuery && (

{t('users.modal.ldapMinChars')}

)}
{/* Results panel */}
{isLoading && (
{t('users.modal.ldapSearching')}
)} {showNoResults && (
{t('users.modal.ldapNoResults')}
)} {searchQuery.isError && (
{searchQuery.error instanceof Error ? searchQuery.error.message : t('users.modal.ldapSearchError')}
)} {!isLoading && hasResults && (
    {searchQuery.data!.map((result) => ( setSelectedDn(result.dn)} /> ))}
)} {!isLoading && !searchQuery.data && !searchQuery.isError && (
{t('users.modal.ldapTypeToSearch')}
)}
{/* Selected user summary */} {selectedResult && (

{t('users.modal.ldapSelectedLabel')}: {selectedResult.username} {selectedResult.display_name && ( — {selectedResult.display_name} )}

{selectedResult.email && (

{selectedResult.email}

)}

{selectedResult.dn}

)} {/* Error from the provision mutation */} {errorMessage && (

{errorMessage}

)} {/* Submit button */}
); } interface LdapResultRowProps { result: LDAPSearchResult; selected: boolean; onSelect: () => void; } function LdapResultRow({ result, selected, onSelect }: LdapResultRowProps) { const { t } = useTranslation(); const disabled = result.already_provisioned; return (
  • ); }