11import { useState , useEffect } from 'react' ;
22import { useQuery } from '@tanstack/react-query' ;
3- import { Plus , Trash2 , Eye , EyeOff } from 'lucide-react' ;
3+ import { Plus , Trash2 , Eye , EyeOff , AlertCircle , CheckCircle2 , Info } from 'lucide-react' ;
44import {
55 Dialog ,
66 DialogContent ,
@@ -23,7 +23,16 @@ import {
2323} from '@/components/ui/select' ;
2424import { Tabs , TabsContent , TabsList , TabsTrigger } from '@/components/ui/tabs' ;
2525import { Badge } from '@/components/ui/badge' ;
26+ import { Alert , AlertDescription } from '@/components/ui/alert' ;
2627import { useToast } from '@/hooks/use-toast' ;
28+ import {
29+ validateAccessListName ,
30+ validateAccessListIp ,
31+ validateUsername ,
32+ validatePassword ,
33+ getAccessListHints ,
34+ getAccessListExample
35+ } from '@/utils/access-list-validators' ;
2736import {
2837 useCreateAccessList ,
2938 useUpdateAccessList ,
@@ -75,6 +84,20 @@ export function AccessListFormDialog({
7584 const [ selectedDomains , setSelectedDomains ] = useState < string [ ] > ( [ ] ) ;
7685 const [ originalDomainIds , setOriginalDomainIds ] = useState < string [ ] > ( [ ] ) ; // Track original domains for edit mode
7786
87+ // Validation states
88+ const [ nameValidation , setNameValidation ] = useState < { valid : boolean ; error ?: string } > ( { valid : true } ) ;
89+ const [ ipValidations , setIpValidations ] = useState < Record < number , { valid : boolean ; error ?: string } > > ( { } ) ;
90+ const [ userValidations , setUserValidations ] = useState < Record < number , { username : { valid : boolean ; error ?: string } ; password : { valid : boolean ; error ?: string } } > > ( { } ) ;
91+
92+ // Validate name in real-time
93+ useEffect ( ( ) => {
94+ if ( formData . name . trim ( ) . length > 0 ) {
95+ setNameValidation ( validateAccessListName ( formData . name ) ) ;
96+ } else {
97+ setNameValidation ( { valid : true } ) ;
98+ }
99+ } , [ formData . name ] ) ;
100+
78101 // Reset form when dialog opens or access list changes
79102 useEffect ( ( ) => {
80103 if ( open ) {
@@ -120,6 +143,10 @@ export function AccessListFormDialog({
120143 setSelectedDomains ( [ ] ) ;
121144 setOriginalDomainIds ( [ ] ) ; // Reset original domains
122145 }
146+ // Reset validations
147+ setNameValidation ( { valid : true } ) ;
148+ setIpValidations ( { } ) ;
149+ setUserValidations ( { } ) ;
123150 }
124151 } , [ open , accessList ] ) ;
125152
@@ -280,6 +307,18 @@ export function AccessListFormDialog({
280307 const newIps = [ ...allowedIps ] ;
281308 newIps [ index ] = value ;
282309 setAllowedIps ( newIps ) ;
310+
311+ // Validate IP in real-time
312+ if ( value . trim ( ) . length > 0 ) {
313+ const validation = validateAccessListIp ( value ) ;
314+ setIpValidations ( prev => ( { ...prev , [ index ] : validation } ) ) ;
315+ } else {
316+ setIpValidations ( prev => {
317+ const newValidations = { ...prev } ;
318+ delete newValidations [ index ] ;
319+ return newValidations ;
320+ } ) ;
321+ }
283322 } ;
284323
285324 const addAuthUser = ( ) => {
@@ -301,6 +340,31 @@ export function AccessListFormDialog({
301340 const newUsers = [ ...authUsers ] ;
302341 ( newUsers [ index ] as any ) [ field ] = value ;
303342 setAuthUsers ( newUsers ) ;
343+
344+ // Validate username/password in real-time
345+ if ( field === 'username' && typeof value === 'string' ) {
346+ if ( value . trim ( ) . length > 0 ) {
347+ const validation = validateUsername ( value ) ;
348+ setUserValidations ( prev => ( {
349+ ...prev ,
350+ [ index ] : {
351+ username : validation ,
352+ password : prev [ index ] ?. password || { valid : true }
353+ }
354+ } ) ) ;
355+ }
356+ } else if ( field === 'password' && typeof value === 'string' ) {
357+ if ( value . trim ( ) . length > 0 ) {
358+ const validation = validatePassword ( value , ! isEditMode ) ;
359+ setUserValidations ( prev => ( {
360+ ...prev ,
361+ [ index ] : {
362+ username : prev [ index ] ?. username || { valid : true } ,
363+ password : validation
364+ }
365+ } ) ) ;
366+ }
367+ }
304368 } ;
305369
306370 const toggleDomainSelection = ( domainId : string ) => {
@@ -338,16 +402,29 @@ export function AccessListFormDialog({
338402 < div className = "space-y-4" >
339403 < div >
340404 < Label htmlFor = "name" > Name *</ Label >
341- < Input
342- id = "name"
343- value = { formData . name }
344- onChange = { ( e ) =>
345- setFormData ( { ...formData , name : e . target . value } )
346- }
347- placeholder = "e.g., admin-panel-access"
348- disabled = { isPending }
349- required
350- />
405+ < div className = "relative" >
406+ < Input
407+ id = "name"
408+ value = { formData . name }
409+ onChange = { ( e ) =>
410+ setFormData ( { ...formData , name : e . target . value } )
411+ }
412+ placeholder = { getAccessListExample ( 'name' ) }
413+ disabled = { isPending }
414+ required
415+ className = { ! nameValidation . valid && formData . name . trim ( ) . length > 0 ? 'border-red-500' : nameValidation . valid && formData . name . trim ( ) . length > 0 ? 'border-green-500' : '' }
416+ />
417+ { nameValidation . valid && formData . name . trim ( ) . length > 0 && (
418+ < CheckCircle2 className = "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-green-500" />
419+ ) }
420+ { ! nameValidation . valid && formData . name . trim ( ) . length > 0 && (
421+ < AlertCircle className = "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-red-500" />
422+ ) }
423+ </ div >
424+ { ! nameValidation . valid && nameValidation . error && (
425+ < p className = "text-xs text-red-500 mt-1" > { nameValidation . error } </ p >
426+ ) }
427+ < p className = "text-xs text-muted-foreground mt-1" > { getAccessListHints ( 'name' ) } </ p >
351428 </ div >
352429
353430 < div >
@@ -430,29 +507,46 @@ export function AccessListFormDialog({
430507 </ div >
431508
432509 { allowedIps . map ( ( ip , index ) => (
433- < div key = { index } className = "flex gap-2" >
434- < Input
435- value = { ip }
436- onChange = { ( e ) => updateIpField ( index , e . target . value ) }
437- placeholder = "e.g., 192.168.1.1 or 10.0.0.0/24"
438- disabled = { isPending }
439- />
440- { allowedIps . length > 1 && (
441- < Button
442- type = "button"
443- variant = "outline"
444- size = "icon"
445- onClick = { ( ) => removeIpField ( index ) }
446- disabled = { isPending }
447- >
448- < Trash2 className = "h-4 w-4" />
449- </ Button >
510+ < div key = { index } className = "space-y-1" >
511+ < div className = "flex gap-2" >
512+ < div className = "relative flex-1" >
513+ < Input
514+ value = { ip }
515+ onChange = { ( e ) => updateIpField ( index , e . target . value ) }
516+ placeholder = { getAccessListExample ( 'ip' ) }
517+ disabled = { isPending }
518+ className = { ipValidations [ index ] && ! ipValidations [ index ] . valid ? 'border-red-500' : ipValidations [ index ] ?. valid ? 'border-green-500' : '' }
519+ />
520+ { ipValidations [ index ] ?. valid && ip . trim ( ) . length > 0 && (
521+ < CheckCircle2 className = "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-green-500" />
522+ ) }
523+ { ipValidations [ index ] && ! ipValidations [ index ] . valid && (
524+ < AlertCircle className = "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-red-500" />
525+ ) }
526+ </ div >
527+ { allowedIps . length > 1 && (
528+ < Button
529+ type = "button"
530+ variant = "outline"
531+ size = "icon"
532+ onClick = { ( ) => removeIpField ( index ) }
533+ disabled = { isPending }
534+ >
535+ < Trash2 className = "h-4 w-4" />
536+ </ Button >
537+ ) }
538+ </ div >
539+ { ipValidations [ index ] && ! ipValidations [ index ] . valid && ipValidations [ index ] . error && (
540+ < p className = "text-xs text-red-500" > { ipValidations [ index ] . error } </ p >
450541 ) }
451542 </ div >
452543 ) ) }
453- < p className = "text-xs text-muted-foreground" >
454- Enter IP addresses or CIDR notation (e.g., 192.168.1.0/24)
455- </ p >
544+ < Alert >
545+ < Info className = "h-4 w-4" />
546+ < AlertDescription >
547+ < strong > Hint:</ strong > { getAccessListHints ( 'ip' ) }
548+ </ AlertDescription >
549+ </ Alert >
456550 </ div >
457551 ) }
458552
@@ -496,15 +590,27 @@ export function AccessListFormDialog({
496590 < div className = "grid grid-cols-2 gap-2" >
497591 < div >
498592 < Label className = "text-xs" > Username (min 3 chars)</ Label >
499- < Input
500- value = { user . username }
501- onChange = { ( e ) =>
502- updateAuthUser ( index , 'username' , e . target . value )
503- }
504- placeholder = "username"
505- disabled = { isPending }
506- minLength = { 3 }
507- />
593+ < div className = "relative" >
594+ < Input
595+ value = { user . username }
596+ onChange = { ( e ) =>
597+ updateAuthUser ( index , 'username' , e . target . value )
598+ }
599+ placeholder = { getAccessListExample ( 'username' ) }
600+ disabled = { isPending }
601+ minLength = { 3 }
602+ className = { userValidations [ index ] ?. username && ! userValidations [ index ] . username . valid ? 'border-red-500' : userValidations [ index ] ?. username ?. valid ? 'border-green-500' : '' }
603+ />
604+ { userValidations [ index ] ?. username ?. valid && user . username . trim ( ) . length > 0 && (
605+ < CheckCircle2 className = "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-green-500" />
606+ ) }
607+ { userValidations [ index ] ?. username && ! userValidations [ index ] . username . valid && (
608+ < AlertCircle className = "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-red-500" />
609+ ) }
610+ </ div >
611+ { userValidations [ index ] ?. username && ! userValidations [ index ] . username . valid && userValidations [ index ] . username . error && (
612+ < p className = "text-xs text-red-500 mt-1" > { userValidations [ index ] . username . error } </ p >
613+ ) }
508614 </ div >
509615
510616 < div >
0 commit comments