+
+
β
{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 || '-'}
-
-
- onEdit?.(user)}
- title="Edit"
- >
- βοΈ
-
- user.enabled ? onDisable?.(user) : onEnable?.(user)}
- title={user.enabled ? 'Disable' : 'Enable'}
- >
- {user.enabled ? (
- βΈοΈ
- ) : (
- βΆοΈ
- )}
-
- onDelete?.(user)}
- title="Delete"
- >
-
-
-
-
-
-
+ {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)}
+
+
+
onEdit?.(user)}
+ title="Edit"
+ >
+
+
+
user.enabled ? onDisable?.(user) : onEnable?.(user)}
+ title={user.enabled ? 'Disable' : 'Enable'}
+ >
+ {user.enabled ? (
+
+ ) : (
+
+ )}
+
+
onDelete?.(user)}
+ title="Delete"
+ >
+
+
+
+
+
+ );
+ })
+ )}
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
-
-
+
+ {clusters.length > 1 && (
+
+ )}
+
Refresh
-
{}}
- />
+
+
+ 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
-
-
+
+ {clusters.length > 1 && (
+
+ )}
+
Refresh
-
-
-
- HA Groups
-
-
- {}}
- />
-
-
+
+ loadGroups(selectedClusterId)}
+ onCreate={handleCreateGroup}
+ onEdit={handleEditGroup}
+ onDelete={handleDeleteGroup}
+ />
-
-
- HA Resources
-
-
- {}}
- />
-
-
+ loadResources(selectedClusterId)}
+ onEnable={handleEnableResource}
+ onRemove={handleRemoveResource}
+ />
);