11import { useCallback , useEffect , useState } from 'react' ;
2+ import { Link } from 'react-router-dom' ;
23import { Settings , FileText , Server , Users , Save , Plus , Pencil , Trash2 , X , Check , Search , Key , Shield , Database , RefreshCw , AlertCircle , CheckCircle2 , Clock , ChevronUp , ChevronDown , ChevronLeft , ChevronRight , ClipboardCheck , AlertTriangle , RotateCcw } from 'lucide-react' ;
34import { ConfirmModal } from '../components/ConfirmModal' ;
45import {
@@ -12,9 +13,6 @@ import {
1213 createServerGroup ,
1314 updateServerGroup ,
1415 deleteServerGroup ,
15- getServerGroupMembers ,
16- addServerGroupMember ,
17- removeServerGroupMember ,
1816 getServers ,
1917 deleteServer ,
2018 // New imports for agent-based architecture
@@ -591,35 +589,24 @@ function ReasonTemplatesTab() {
591589// Server Groups Tab
592590function ServerGroupsTab ( ) {
593591 const [ groups , setGroups ] = useState < ServerGroup [ ] > ( [ ] ) ;
594- const [ allServers , setAllServers ] = useState < ServerType [ ] > ( [ ] ) ;
595592 const [ loading , setLoading ] = useState ( true ) ;
596593 const [ error , setError ] = useState < string | null > ( null ) ;
597594 const [ showNewForm , setShowNewForm ] = useState ( false ) ;
598595 const [ newForm , setNewForm ] = useState ( { name : '' , description : '' , color : '#4ade80' } ) ;
599596 const [ editingId , setEditingId ] = useState < number | null > ( null ) ;
600597 const [ editForm , setEditForm ] = useState ( { name : '' , description : '' , color : '#4ade80' } ) ;
601598 const [ deleteModal , setDeleteModal ] = useState < { isOpen : boolean ; group : ServerGroup | null } > ( { isOpen : false , group : null } ) ;
602-
603- // Member management modal
604- const [ managingGroup , setManagingGroup ] = useState < ServerGroup | null > ( null ) ;
605- const [ groupMembers , setGroupMembers ] = useState < ServerType [ ] > ( [ ] ) ;
606- const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
607- const [ loadingMembers , setLoadingMembers ] = useState ( false ) ;
608599
609600 useEffect ( ( ) => {
610601 fetchData ( ) ;
611602 } , [ ] ) ;
612603
613604 async function fetchData ( ) {
614605 try {
615- const [ groupsData , serversData ] = await Promise . all ( [
616- getServerGroups ( ) ,
617- getServers ( ) ,
618- ] ) ;
606+ const groupsData = await getServerGroups ( ) ;
619607 setGroups ( groupsData . groups || [ ] ) ;
620- setAllServers ( serversData . servers || [ ] ) ;
621608 } catch ( err ) {
622- setError ( err instanceof Error ? err . message : 'Failed to load data ' ) ;
609+ setError ( err instanceof Error ? err . message : 'Failed to load groups ' ) ;
623610 } finally {
624611 setLoading ( false ) ;
625612 }
@@ -671,58 +658,6 @@ function ServerGroupsTab() {
671658 } ) ;
672659 }
673660
674- async function openMemberManagement ( group : ServerGroup ) {
675- setManagingGroup ( group ) ;
676- setSearchQuery ( '' ) ;
677- setLoadingMembers ( true ) ;
678- try {
679- const data = await getServerGroupMembers ( group . id ) ;
680- setGroupMembers ( data . servers || [ ] ) ;
681- } catch ( err ) {
682- setError ( err instanceof Error ? err . message : 'Failed to load group members' ) ;
683- } finally {
684- setLoadingMembers ( false ) ;
685- }
686- }
687-
688- async function handleAddServer ( serverId : number ) {
689- if ( ! managingGroup ) return ;
690- try {
691- await addServerGroupMember ( managingGroup . id , serverId ) ;
692- // Refresh members
693- const data = await getServerGroupMembers ( managingGroup . id ) ;
694- setGroupMembers ( data . servers || [ ] ) ;
695- fetchData ( ) ; // Refresh group counts
696- } catch ( err ) {
697- setError ( err instanceof Error ? err . message : 'Failed to add server to group' ) ;
698- }
699- }
700-
701- async function handleRemoveServer ( serverId : number ) {
702- if ( ! managingGroup ) return ;
703- try {
704- await removeServerGroupMember ( managingGroup . id , serverId ) ;
705- setGroupMembers ( groupMembers . filter ( s => s . id !== serverId ) ) ;
706- fetchData ( ) ; // Refresh group counts
707- } catch ( err ) {
708- setError ( err instanceof Error ? err . message : 'Failed to remove server from group' ) ;
709- }
710- }
711-
712- // Fuzzy search filter for servers not already in the group
713- const memberIds = new Set ( groupMembers . map ( s => s . id ) ) ;
714- const availableServers = allServers . filter ( s => ! memberIds . has ( s . id ) ) ;
715- const filteredServers = searchQuery . trim ( )
716- ? availableServers . filter ( s => {
717- const query = searchQuery . toLowerCase ( ) ;
718- return (
719- s . name . toLowerCase ( ) . includes ( query ) ||
720- s . osFamily ?. toLowerCase ( ) . includes ( query ) ||
721- s . ipv4Addrs ?. some ( ip => ip . includes ( query ) )
722- ) ;
723- } )
724- : [ ] ;
725-
726661 if ( loading ) {
727662 return < div className = "text-[#a5d6a7]" > Loading server groups...</ div > ;
728663 }
@@ -861,21 +796,21 @@ function ServerGroupsTab() {
861796 < td className = "py-3 px-4 text-[#e8f5e9] font-medium" > { group . name } </ td >
862797 < td className = "py-3 px-4 text-[#a5d6a7]" > { group . description || '-' } </ td >
863798 < td className = "py-3 px-4" >
864- < button
865- onClick = { ( ) => openMemberManagement ( group ) }
799+ < Link
800+ to = { `/admin/server-groups/ ${ group . id } /members` }
866801 className = "text-[#4ade80] hover:underline"
867802 >
868803 { group . serverCount ?? 0 } servers
869- </ button >
804+ </ Link >
870805 </ td >
871806 < td className = "py-3 px-4 text-right" >
872- < button
873- onClick = { ( ) => openMemberManagement ( group ) }
874- className = "p-1 text-[#6b7280] hover:text-[#4ade80]"
807+ < Link
808+ to = { `/admin/server-groups/ ${ group . id } /members` }
809+ className = "inline-block p-1 text-[#6b7280] hover:text-[#4ade80]"
875810 title = "Manage members"
876811 >
877812 < Server className = "w-4 h-4" />
878- </ button >
813+ </ Link >
879814 < button onClick = { ( ) => startEdit ( group ) } className = "p-1 text-[#6b7280] hover:text-[#4ade80] ml-1" >
880815 < Pencil className = "w-4 h-4" />
881816 </ button >
@@ -899,134 +834,6 @@ function ServerGroupsTab() {
899834 </ div >
900835 </ div >
901836
902- { /* Member Management Modal */ }
903- { managingGroup && (
904- < div className = "fixed inset-0 bg-black/50 flex items-center justify-center z-50" >
905- < div className = "bg-[#0a0f0d] border border-[#2d3f36] rounded-xl w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col" >
906- { /* Modal Header */ }
907- < div className = "flex items-center justify-between p-4 border-b border-[#2d3f36]" >
908- < div className = "flex items-center gap-3" >
909- < div
910- className = "w-4 h-4 rounded"
911- style = { { backgroundColor : managingGroup . color } }
912- />
913- < h3 className = "text-lg font-semibold text-[#e8f5e9]" >
914- Manage Members: { managingGroup . name }
915- </ h3 >
916- </ div >
917- < button
918- onClick = { ( ) => setManagingGroup ( null ) }
919- className = "p-1 text-[#6b7280] hover:text-[#e8f5e9]"
920- >
921- < X className = "w-5 h-5" />
922- </ button >
923- </ div >
924-
925- { /* Modal Content */ }
926- < div className = "flex-1 overflow-auto p-4 space-y-4" >
927- { /* Add Server Search */ }
928- < div >
929- < label className = "block text-sm font-medium text-[#a5d6a7] mb-2" >
930- Add Server
931- </ label >
932- < div className = "relative" >
933- < Search className = "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#6b7280]" />
934- < input
935- type = "text"
936- value = { searchQuery }
937- onChange = { ( e ) => setSearchQuery ( e . target . value ) }
938- placeholder = "Search servers by name, OS, or IP..."
939- className = "w-full pl-10 pr-4 py-2 bg-[#1a2420] border border-[#2d3f36] rounded-lg text-[#e8f5e9] placeholder-[#6b7280] focus:outline-none focus:border-[#4ade80]"
940- />
941- </ div >
942-
943- { /* Search Results */ }
944- { searchQuery . trim ( ) && (
945- < div className = "mt-2 border border-[#2d3f36] rounded-lg max-h-48 overflow-y-auto" >
946- { filteredServers . length > 0 ? (
947- filteredServers . slice ( 0 , 10 ) . map ( ( server ) => (
948- < button
949- key = { server . id }
950- onClick = { ( ) => {
951- handleAddServer ( server . id ) ;
952- setSearchQuery ( '' ) ;
953- } }
954- className = "w-full flex items-center justify-between px-4 py-2 hover:bg-[#1a2420] text-left border-b border-[#2d3f36] last:border-b-0"
955- >
956- < div >
957- < div className = "text-[#e8f5e9] font-medium" > { server . name } </ div >
958- < div className = "text-xs text-[#6b7280]" >
959- { server . osFamily } { server . osRelease } • { server . ipv4Addrs ?. [ 0 ] || 'No IP' }
960- </ div >
961- </ div >
962- < Plus className = "w-4 h-4 text-[#4ade80]" />
963- </ button >
964- ) )
965- ) : (
966- < div className = "px-4 py-3 text-[#6b7280] text-sm" >
967- No servers found matching "{ searchQuery } "
968- </ div >
969- ) }
970- { filteredServers . length > 10 && (
971- < div className = "px-4 py-2 text-[#6b7280] text-xs bg-[#1a2420]" >
972- Showing 10 of { filteredServers . length } results. Refine your search.
973- </ div >
974- ) }
975- </ div >
976- ) }
977- </ div >
978-
979- { /* Current Members */ }
980- < div >
981- < label className = "block text-sm font-medium text-[#a5d6a7] mb-2" >
982- Current Members ({ groupMembers . length } )
983- </ label >
984- { loadingMembers ? (
985- < div className = "text-[#6b7280]" > Loading...</ div >
986- ) : groupMembers . length > 0 ? (
987- < div className = "border border-[#2d3f36] rounded-lg divide-y divide-[#2d3f36]" >
988- { groupMembers . map ( ( server ) => (
989- < div
990- key = { server . id }
991- className = "flex items-center justify-between px-4 py-2"
992- >
993- < div >
994- < div className = "text-[#e8f5e9] font-medium" > { server . name } </ div >
995- < div className = "text-xs text-[#6b7280]" >
996- { server . osFamily } { server . osRelease } • { server . ipv4Addrs ?. [ 0 ] || 'No IP' }
997- </ div >
998- </ div >
999- < button
1000- onClick = { ( ) => handleRemoveServer ( server . id ) }
1001- className = "p-1 text-[#6b7280] hover:text-red-400"
1002- title = "Remove from group"
1003- >
1004- < X className = "w-4 h-4" />
1005- </ button >
1006- </ div >
1007- ) ) }
1008- </ div >
1009- ) : (
1010- < div className = "text-[#6b7280] text-sm py-4 text-center border border-[#2d3f36] rounded-lg" >
1011- No servers in this group yet. Use the search above to add servers.
1012- </ div >
1013- ) }
1014- </ div >
1015- </ div >
1016-
1017- { /* Modal Footer */ }
1018- < div className = "p-4 border-t border-[#2d3f36]" >
1019- < button
1020- onClick = { ( ) => setManagingGroup ( null ) }
1021- className = "btn btn-secondary w-full"
1022- >
1023- Close
1024- </ button >
1025- </ div >
1026- </ div >
1027- </ div >
1028- ) }
1029-
1030837 { /* Delete Confirmation Modal */ }
1031838 < ConfirmModal
1032839 isOpen = { deleteModal . isOpen }
0 commit comments