diff --git a/src/components/Proxmox/AclList.tsx b/src/components/Proxmox/AclList.tsx index 67a10bf1..4aab71a9 100644 --- a/src/components/Proxmox/AclList.tsx +++ b/src/components/Proxmox/AclList.tsx @@ -2,24 +2,16 @@ import React from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/index'; import { Button } from '@/components/ui/index'; -import { MoreHorizontal } from 'lucide-react'; - -interface AclInfo { - id: string; - path: string; - type: 'user' | 'group' | 'role'; - principal: string; - roles: string[]; - propagate: boolean; -} +import { Pencil, Trash2, PlusCircle, RefreshCw } from 'lucide-react'; +import { AclEntry } from '@/lib/proxmoxClient'; interface AclListProps { - acls: AclInfo[]; + acls: AclEntry[]; onRefresh?: () => void; isLoading?: boolean; onAdd?: () => void; - onEdit?: (acl: AclInfo) => void; - onDelete?: (acl: AclInfo) => void; + onEdit?: (acl: AclEntry) => void; + onDelete?: (acl: AclEntry) => void; } export function AclList({ @@ -36,11 +28,12 @@ export function AclList({ Access Control Lists (ACL)
{onAdd && ( )} @@ -54,61 +47,59 @@ export function AclList({ Path Type Principal - Roles + Role Propagate Actions - {acls.map((acl) => ( - - {acl.path} - - - {acl.type} - - - {acl.principal} - -
- {acl.roles.map((role) => ( - - {role} - - ))} -
-
- {acl.propagate ? 'Yes' : 'No'} - -
- - - -
+ {acls.length === 0 ? ( + + + No ACL entries configured - ))} + ) : ( + acls.map((acl, index) => ( + + {acl.path} + + + {acl.type} + + + {acl.ugid} + + + {acl.roleid} + + + {acl.propagate ? 'Yes' : 'No'} + +
+ + +
+
+
+ )) + )}
diff --git a/src/components/Proxmox/HAGroupsList.tsx b/src/components/Proxmox/HAGroupsList.tsx index 480eed46..af969849 100644 --- a/src/components/Proxmox/HAGroupsList.tsx +++ b/src/components/Proxmox/HAGroupsList.tsx @@ -2,35 +2,25 @@ import React from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/index'; import { Button } from '@/components/ui/index'; -import { MoreHorizontal, Trash2 } from 'lucide-react'; - -interface HAGroupInfo { - id: string; - name: string; - resources: number; - managed: number; - failed: number; - status: string; -} +import { Trash2, Pencil, PlusCircle, RefreshCw } from 'lucide-react'; +import { HaGroup } from '@/lib/proxmoxClient'; interface HAGroupsListProps { - groups: HAGroupInfo[]; + groups: HaGroup[]; onRefresh?: () => void; isLoading?: boolean; - onEdit?: (group: HAGroupInfo) => void; - onDelete?: (group: HAGroupInfo) => void; - onEnable?: (group: HAGroupInfo) => void; - onDisable?: (group: HAGroupInfo) => void; + onCreate?: () => void; + onEdit?: (group: HaGroup) => void; + onDelete?: (id: string) => void; } export function HAGroupsList({ groups, onRefresh, isLoading, + onCreate, onEdit, onDelete, - onEnable, - onDisable, }: HAGroupsListProps) { return ( @@ -38,11 +28,12 @@ export function HAGroupsList({ HA Groups
-
@@ -52,66 +43,59 @@ export function HAGroupsList({ Name - Resources - Managed - Failed - Status + Nodes + Restricted + No-Quorum Policy + Comment Actions - {groups.map((group) => ( - - {group.name} - {group.resources} - {group.managed} - {group.failed} - - - {group.status} - - - -
- - - - -
+ {groups.length === 0 ? ( + + + No HA groups configured - ))} + ) : ( + groups.map((group) => ( + + {group.id} + {group.nodes} + + {group.restricted ? ( + + Yes + + ) : ( + + No + + )} + + {group.noQuorumPolicy ?? '-'} + {group.comment ?? '-'} + +
+ + +
+
+
+ )) + )}
diff --git a/src/components/Proxmox/HAResourcesList.tsx b/src/components/Proxmox/HAResourcesList.tsx index 15d23d6f..95f47c6c 100644 --- a/src/components/Proxmox/HAResourcesList.tsx +++ b/src/components/Proxmox/HAResourcesList.tsx @@ -2,53 +2,31 @@ import React from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/index'; import { Button } from '@/components/ui/index'; -import { MoreHorizontal } from 'lucide-react'; - -interface HAResourceInfo { - id: string; - name: string; - type: string; - group: string; - node: string; - managed: boolean; - failed: boolean; - status: string; -} +import { Play, Trash2, RefreshCw } from 'lucide-react'; +import { HaResource } from '@/lib/proxmoxClient'; interface HAResourcesListProps { - resources: HAResourceInfo[]; + resources: HaResource[]; onRefresh?: () => void; isLoading?: boolean; - onManage?: (resource: HAResourceInfo) => void; - onUnmanage?: (resource: HAResourceInfo) => void; - onFailover?: (resource: HAResourceInfo) => void; + onEnable?: (resource: HaResource) => void; + onRemove?: (resource: HaResource) => void; } export function HAResourcesList({ resources, onRefresh, isLoading, - onManage, - onUnmanage, - onFailover, + onEnable, + onRemove, }: HAResourcesListProps) { - const managedCount = resources.filter((r) => r.managed).length; - const failedCount = resources.filter((r) => r.failed).length; - return ( HA Resources
-
- ● - {managedCount} Managed -
-
- ● - {failedCount} Failed -
@@ -58,66 +36,59 @@ export function HAResourcesList({ - Name - Type + Resource ID Group - Node - Status + State + Max Restart + Max Relocate Actions - {resources.map((resource) => ( - - {resource.name} - {resource.type} - {resource.group} - {resource.node} - - - {resource.failed ? 'Failed' : resource.managed ? 'Managed' : 'Unmanaged'} - - - -
- {resource.managed ? ( - - ) : ( - - )} - - -
+ {resources.length === 0 ? ( + + + No HA resources configured - ))} + ) : ( + resources.map((resource) => ( + + {resource.sid} + {resource.group ?? '-'} + + + {resource.state} + + + {resource.maxRestart ?? '-'} + {resource.maxRelocate ?? '-'} + +
+ + +
+
+
+ )) + )}
diff --git a/src/components/Proxmox/RealmList.tsx b/src/components/Proxmox/RealmList.tsx index 6e23ec62..5d0dc91b 100644 --- a/src/components/Proxmox/RealmList.tsx +++ b/src/components/Proxmox/RealmList.tsx @@ -2,32 +2,25 @@ import React from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/index'; import { Button } from '@/components/ui/index'; -import { MoreHorizontal, Trash2 } from 'lucide-react'; - -interface RealmInfo { - id: string; - type: 'pam' | 'ldap' | 'ad' | 'openid'; - server?: string; - baseDn?: string; - status: string; -} +import { Pencil, Trash2, PlusCircle, RefreshCw } from 'lucide-react'; +import { AuthRealm } from '@/lib/proxmoxClient'; interface RealmListProps { - realms: RealmInfo[]; + realms: AuthRealm[]; onRefresh?: () => void; isLoading?: boolean; - onEdit?: (realm: RealmInfo) => void; - onDelete?: (realm: RealmInfo) => void; - onSync?: (realm: RealmInfo) => void; + onCreate?: () => void; + onEdit?: (realm: AuthRealm) => void; + onDelete?: (realm: AuthRealm) => void; } export function RealmList({ realms, onRefresh, isLoading, + onCreate, onEdit, onDelete, - onSync, }: RealmListProps) { return ( @@ -35,10 +28,11 @@ export function RealmList({ Authentication Realms
-
@@ -48,63 +42,50 @@ export function RealmList({ - Realm ID + Realm Name Type - Server - Base DN - Status + Comment Actions - {realms.map((realm) => ( - - {realm.id} - - - {realm.type.toUpperCase()} - - - {realm.server || '-'} - {realm.baseDn || '-'} - - - Active - - - -
- - - - -
+ {realms.length === 0 ? ( + + + No auth realms configured - ))} + ) : ( + realms.map((realm) => ( + + {realm.realm} + + + {realm.type.toUpperCase()} + + + {realm.comment ?? '-'} + +
+ + +
+
+
+ )) + )}
diff --git a/src/components/Proxmox/UserList.tsx b/src/components/Proxmox/UserList.tsx index 5be18ada..be732470 100644 --- a/src/components/Proxmox/UserList.tsx +++ b/src/components/Proxmox/UserList.tsx @@ -2,29 +2,35 @@ import React from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/index'; import { Button } from '@/components/ui/index'; -import { MoreHorizontal, Trash2 } from 'lucide-react'; - -interface UserInfo { - id: string; - email?: string; - enabled: boolean; - lastLogin?: string; -} +import { Pencil, Trash2, PlusCircle, RefreshCw, Play, Pause } from 'lucide-react'; +import { ProxmoxUser } from '@/lib/proxmoxClient'; interface UserListProps { - users: UserInfo[]; + users: ProxmoxUser[]; onRefresh?: () => void; isLoading?: boolean; - onEdit?: (user: UserInfo) => void; - onDelete?: (user: UserInfo) => void; - onEnable?: (user: UserInfo) => void; - onDisable?: (user: UserInfo) => void; + onCreate?: () => void; + onEdit?: (user: ProxmoxUser) => void; + onDelete?: (user: ProxmoxUser) => void; + onEnable?: (user: ProxmoxUser) => void; + onDisable?: (user: ProxmoxUser) => void; +} + +function formatExpiry(expire?: number): string { + if (!expire || expire === 0) return 'Never'; + return new Date(expire * 1000).toLocaleDateString(); +} + +function deriveRealm(userid: string): string { + const parts = userid.split('@'); + return parts.length > 1 ? parts[1] : '-'; } export function UserList({ users, onRefresh, isLoading, + onCreate, onEdit, onDelete, onEnable, @@ -37,20 +43,21 @@ export function UserList({ Users -
-
+
+
● {enabledCount} Enabled
-
- ● +
+ ● {disabledCount} Disabled
-
@@ -61,64 +68,71 @@ export function UserList({ User ID + Realm + Name Email - Status - Last Login + Enabled + Expire Actions - {users.map((user) => ( - - {user.id} - {user.email || '-'} - - - {user.enabled ? 'Enabled' : 'Disabled'} - - - {user.lastLogin || '-'} - -
- - - - -
+ {users.length === 0 ? ( + + + No users found - ))} + ) : ( + users.map((user) => { + const fullName = [user.firstname, user.lastname].filter(Boolean).join(' ') || '-'; + return ( + + {user.userid} + {deriveRealm(user.userid)} + {fullName} + {user.email ?? '-'} + + + {user.enabled ? 'Enabled' : 'Disabled'} + + + {formatExpiry(user.expire)} + +
+ + + +
+
+
+ ); + }) + )}
diff --git a/src/pages/Proxmox/ACLPage.tsx b/src/pages/Proxmox/ACLPage.tsx index 5996a136..fc6fc48e 100644 --- a/src/pages/Proxmox/ACLPage.tsx +++ b/src/pages/Proxmox/ACLPage.tsx @@ -1,34 +1,173 @@ -import React from 'react'; -import { Button } from '@/components/ui/index'; +import React, { useState, useEffect, useCallback } from 'react'; import { RefreshCw } from 'lucide-react'; -import { AclList } from '@/components/Proxmox'; +import { Button, Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/index'; +import { AclList, UserList, RealmList } from '@/components/Proxmox'; +import { + listProxmoxClusters, + listAcls, + listUsers, + listRealms, + AclEntry, + ProxmoxUser, + AuthRealm, +} from '@/lib/proxmoxClient'; +import { ClusterInfo } from '@/lib/domain'; +import { toast } from 'sonner'; export function ProxmoxACLPage() { - const acls = [ - { id: '1', path: '/nodes/pve1', type: 'user' as const, principal: 'admin@pam', roles: ['PVEAdmin'], propagate: true }, - { id: '2', path: '/storage/local', type: 'group' as const, principal: 'admins', roles: ['PVEDataStoreAdmin'], propagate: false }, - { id: '3', path: '/vms/100', type: 'user' as const, principal: 'developer@pam', roles: ['PVEVMUser'], propagate: false }, - ]; + const [clusters, setClusters] = useState([]); + const [selectedClusterId, setSelectedClusterId] = useState(''); + const [activeTab, setActiveTab] = useState('acl'); + + const [acls, setAcls] = useState([]); + const [users, setUsers] = useState([]); + const [realms, setRealms] = useState([]); + + const [isLoadingAcls, setIsLoadingAcls] = useState(false); + const [isLoadingUsers, setIsLoadingUsers] = useState(false); + const [isLoadingRealms, setIsLoadingRealms] = useState(false); + + // Load clusters on mount, auto-select the first + useEffect(() => { + listProxmoxClusters() + .then((cls) => { + setClusters(cls); + if (cls.length > 0) { + setSelectedClusterId(cls[0].id); + } + }) + .catch((err) => { + console.error('Failed to load clusters:', err); + toast.error('Failed to load clusters'); + }); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const loadAcls = useCallback(async (clusterId: string) => { + if (!clusterId) return; + setIsLoadingAcls(true); + try { + const data = await listAcls(clusterId); + setAcls(data); + } catch (err) { + console.error('Failed to load ACLs:', err); + toast.error('Failed to load ACLs'); + } finally { + setIsLoadingAcls(false); + } + }, []); + + const loadUsers = useCallback(async (clusterId: string) => { + if (!clusterId) return; + setIsLoadingUsers(true); + try { + const data = await listUsers(clusterId); + setUsers(data); + } catch (err) { + console.error('Failed to load users:', err); + toast.error('Failed to load users'); + } finally { + setIsLoadingUsers(false); + } + }, []); + + const loadRealms = useCallback(async (clusterId: string) => { + if (!clusterId) return; + setIsLoadingRealms(true); + try { + const data = await listRealms(clusterId); + setRealms(data); + } catch (err) { + console.error('Failed to load realms:', err); + toast.error('Failed to load auth realms'); + } finally { + setIsLoadingRealms(false); + } + }, []); + + useEffect(() => { + if (selectedClusterId) { + loadAcls(selectedClusterId); + loadUsers(selectedClusterId); + loadRealms(selectedClusterId); + } + }, [selectedClusterId, loadAcls, loadUsers, loadRealms]); + + const handleRefreshAll = () => { + loadAcls(selectedClusterId); + loadUsers(selectedClusterId); + loadRealms(selectedClusterId); + }; return (
-

Access Control Lists

-

Manage permissions and access control

+

Access Control & Users

+

Manage permissions, users, and authentication realms

-
-
- {}} - /> + + + ACLs + Users + Auth Realms + + + + loadAcls(selectedClusterId)} + onAdd={() => toast.info('Add ACL β€” not yet implemented')} + onEdit={() => toast.info('Edit ACL β€” not yet implemented')} + onDelete={() => toast.info('Delete ACL β€” not yet implemented')} + /> + + + + loadUsers(selectedClusterId)} + onCreate={() => toast.info('Create user β€” not yet implemented')} + onEdit={() => toast.info('Edit user β€” not yet implemented')} + onDelete={() => toast.info('Delete user β€” not yet implemented')} + onEnable={() => toast.info('Enable user β€” not yet implemented')} + onDisable={() => toast.info('Disable user β€” not yet implemented')} + /> + + + + loadRealms(selectedClusterId)} + onCreate={() => toast.info('Create realm β€” not yet implemented')} + onEdit={() => toast.info('Edit realm β€” not yet implemented')} + onDelete={() => toast.info('Delete realm β€” not yet implemented')} + /> + +
); } diff --git a/src/pages/Proxmox/HAPage.tsx b/src/pages/Proxmox/HAPage.tsx index 03d90056..3577dd4a 100644 --- a/src/pages/Proxmox/HAPage.tsx +++ b/src/pages/Proxmox/HAPage.tsx @@ -1,10 +1,119 @@ -import React from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; -import { Button } from '@/components/ui/index'; +import React, { useState, useEffect, useCallback } from 'react'; import { RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/index'; import { HAGroupsList, HAResourcesList } from '@/components/Proxmox'; +import { + listProxmoxClusters, + listHaGroups, + listHaResources, + deleteHaGroup, + enableHaResource, + HaGroup, + HaResource, +} from '@/lib/proxmoxClient'; +import { ClusterInfo } from '@/lib/domain'; +import { toast } from 'sonner'; export function ProxmoxHAPage() { + const [clusters, setClusters] = useState([]); + const [selectedClusterId, setSelectedClusterId] = useState(''); + const [groups, setGroups] = useState([]); + const [resources, setResources] = useState([]); + const [isLoadingGroups, setIsLoadingGroups] = useState(false); + const [isLoadingResources, setIsLoadingResources] = useState(false); + + // Load clusters on mount and auto-select the first one + useEffect(() => { + listProxmoxClusters() + .then((cls) => { + setClusters(cls); + if (cls.length > 0 && !selectedClusterId) { + setSelectedClusterId(cls[0].id); + } + }) + .catch((err) => { + console.error('Failed to load clusters:', err); + toast.error('Failed to load clusters'); + }); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const loadGroups = useCallback(async (clusterId: string) => { + if (!clusterId) return; + setIsLoadingGroups(true); + try { + const data = await listHaGroups(clusterId); + setGroups(data); + } catch (err) { + console.error('Failed to load HA groups:', err); + toast.error('Failed to load HA groups'); + } finally { + setIsLoadingGroups(false); + } + }, []); + + const loadResources = useCallback(async (clusterId: string) => { + if (!clusterId) return; + setIsLoadingResources(true); + try { + const data = await listHaResources(clusterId); + setResources(data); + } catch (err) { + console.error('Failed to load HA resources:', err); + toast.error('Failed to load HA resources'); + } finally { + setIsLoadingResources(false); + } + }, []); + + useEffect(() => { + if (selectedClusterId) { + loadGroups(selectedClusterId); + loadResources(selectedClusterId); + } + }, [selectedClusterId, loadGroups, loadResources]); + + const handleRefreshAll = () => { + loadGroups(selectedClusterId); + loadResources(selectedClusterId); + }; + + const handleDeleteGroup = async (id: string) => { + try { + await deleteHaGroup(selectedClusterId, id); + toast.success(`HA group "${id}" deleted`); + await loadGroups(selectedClusterId); + } catch (err) { + console.error('Failed to delete HA group:', err); + toast.error('Failed to delete HA group'); + } + }; + + const handleEditGroup = (group: HaGroup) => { + // Placeholder: edit dialog integration to be wired when dialog component is available + toast.info(`Edit group: ${group.id}`); + }; + + const handleCreateGroup = () => { + // Placeholder: create dialog integration to be wired when dialog component is available + toast.info('Create HA group β€” not yet implemented'); + }; + + const handleEnableResource = async (resource: HaResource) => { + try { + await enableHaResource(selectedClusterId, resource.sid); + toast.success(`HA resource "${resource.sid}" enabled`); + await loadResources(selectedClusterId); + } catch (err) { + console.error('Failed to enable HA resource:', err); + toast.error('Failed to enable HA resource'); + } + }; + + const handleRemoveResource = async (resource: HaResource) => { + // Placeholder: removal command to be wired when backend command is available + toast.info(`Remove resource: ${resource.sid}`); + }; + return (
@@ -12,38 +121,44 @@ export function ProxmoxHAPage() {

High Availability

Manage HA groups and resources

-
-
-
- - - HA Groups - - - {}} - /> - - +
+ loadGroups(selectedClusterId)} + onCreate={handleCreateGroup} + onEdit={handleEditGroup} + onDelete={handleDeleteGroup} + /> - - - HA Resources - - - {}} - /> - - + loadResources(selectedClusterId)} + onEnable={handleEnableResource} + onRemove={handleRemoveResource} + />
);