feat: implement v1.2.1 fixes #95
@ -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({
|
||||
<CardTitle>Access Control Lists (ACL)</CardTitle>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
{onAdd && (
|
||||
<Button size="sm" onClick={onAdd}>
|
||||
<span className="mr-2 h-4 w-4">+</span>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
New ACL
|
||||
</Button>
|
||||
)}
|
||||
@ -54,61 +47,59 @@ export function AclList({
|
||||
<TableHead>Path</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Principal</TableHead>
|
||||
<TableHead>Roles</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Propagate</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{acls.map((acl) => (
|
||||
<TableRow key={acl.id}>
|
||||
<TableCell className="font-mono text-xs">{acl.path}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||
acl.type === 'user' ? 'bg-blue-100 text-blue-800' :
|
||||
acl.type === 'group' ? 'bg-purple-100 text-purple-800' :
|
||||
'bg-orange-100 text-orange-800'
|
||||
}`}>
|
||||
{acl.type}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{acl.principal}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{acl.roles.map((role) => (
|
||||
<span key={role} className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-800">
|
||||
{role}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{acl.propagate ? 'Yes' : 'No'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onEdit?.(acl)}
|
||||
title="Edit"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">✏️</span>
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onDelete?.(acl)}
|
||||
title="Delete"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">🗑️</span>
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
title="More"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{acls.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||
No ACL entries configured
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : (
|
||||
acls.map((acl, index) => (
|
||||
<TableRow key={`${acl.path}-${acl.ugid}-${acl.roleid}-${index}`}>
|
||||
<TableCell className="font-mono text-xs">{acl.path}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||
acl.type === 'user' ? 'bg-blue-100 text-blue-800' :
|
||||
acl.type === 'group' ? 'bg-purple-100 text-purple-800' :
|
||||
'bg-orange-100 text-orange-800'
|
||||
}`}>
|
||||
{acl.type}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{acl.ugid}</TableCell>
|
||||
<TableCell>
|
||||
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-800">
|
||||
{acl.roleid}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{acl.propagate ? 'Yes' : 'No'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onEdit?.(acl)}
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onDelete?.(acl)}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@ -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 (
|
||||
<Card>
|
||||
@ -38,11 +28,12 @@ export function HAGroupsList({
|
||||
<CardTitle>HA Groups</CardTitle>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button size="sm">
|
||||
<span className="mr-2 h-4 w-4">+</span>
|
||||
New Group
|
||||
<Button size="sm" onClick={onCreate}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
Add Group
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@ -52,66 +43,59 @@ export function HAGroupsList({
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Resources</TableHead>
|
||||
<TableHead>Managed</TableHead>
|
||||
<TableHead>Failed</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Nodes</TableHead>
|
||||
<TableHead>Restricted</TableHead>
|
||||
<TableHead>No-Quorum Policy</TableHead>
|
||||
<TableHead>Comment</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groups.map((group) => (
|
||||
<TableRow key={group.id}>
|
||||
<TableCell className="font-medium">{group.name}</TableCell>
|
||||
<TableCell>{group.resources}</TableCell>
|
||||
<TableCell>{group.managed}</TableCell>
|
||||
<TableCell>{group.failed}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||
group.status === 'healthy' ? 'bg-green-100 text-green-800' :
|
||||
group.status === 'error' ? 'bg-red-100 text-red-800' :
|
||||
'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{group.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onEdit?.(group)}
|
||||
title="Edit"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">✏️</span>
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => group.managed > 0 ? onDisable?.(group) : onEnable?.(group)}
|
||||
title={group.managed > 0 ? 'Disable' : 'Enable'}
|
||||
>
|
||||
{group.managed > 0 ? (
|
||||
<span className="h-4 w-4 text-xs">⏸️</span>
|
||||
) : (
|
||||
<span className="h-4 w-4 text-xs">▶️</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onDelete?.(group)}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
title="More"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{groups.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||
No HA groups configured
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : (
|
||||
groups.map((group) => (
|
||||
<TableRow key={group.id}>
|
||||
<TableCell className="font-medium">{group.id}</TableCell>
|
||||
<TableCell className="font-mono text-xs">{group.nodes}</TableCell>
|
||||
<TableCell>
|
||||
{group.restricted ? (
|
||||
<span className="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
Yes
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium bg-gray-100 text-gray-600">
|
||||
No
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{group.noQuorumPolicy ?? '-'}</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">{group.comment ?? '-'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onEdit?.(group)}
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onDelete?.(group.id)}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@ -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 (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle>HA Resources</CardTitle>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<span className="text-green-500">●</span>
|
||||
<span>{managedCount} Managed</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<span className="text-red-500">●</span>
|
||||
<span>{failedCount} Failed</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
@ -58,66 +36,59 @@ export function HAResourcesList({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Resource ID</TableHead>
|
||||
<TableHead>Group</TableHead>
|
||||
<TableHead>Node</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>State</TableHead>
|
||||
<TableHead>Max Restart</TableHead>
|
||||
<TableHead>Max Relocate</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{resources.map((resource) => (
|
||||
<TableRow key={resource.id}>
|
||||
<TableCell className="font-medium">{resource.name}</TableCell>
|
||||
<TableCell>{resource.type}</TableCell>
|
||||
<TableCell>{resource.group}</TableCell>
|
||||
<TableCell>{resource.node}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||
resource.failed ? 'bg-red-100 text-red-800' :
|
||||
resource.managed ? 'bg-green-100 text-green-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{resource.failed ? 'Failed' : resource.managed ? 'Managed' : 'Unmanaged'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
{resource.managed ? (
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onUnmanage?.(resource)}
|
||||
title="Unmanage"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">⏹️</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-green-100 hover:text-green-600"
|
||||
onClick={() => onManage?.(resource)}
|
||||
title="Manage"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">▶️</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onFailover?.(resource)}
|
||||
title="Failover"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">🔄</span>
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
title="More"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{resources.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||
No HA resources configured
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : (
|
||||
resources.map((resource) => (
|
||||
<TableRow key={resource.sid}>
|
||||
<TableCell className="font-medium font-mono text-xs">{resource.sid}</TableCell>
|
||||
<TableCell>{resource.group ?? '-'}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||
resource.state === 'started' ? 'bg-green-100 text-green-800' :
|
||||
resource.state === 'stopped' ? 'bg-gray-100 text-gray-600' :
|
||||
resource.state === 'error' ? 'bg-red-100 text-red-800' :
|
||||
'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{resource.state}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{resource.maxRestart ?? '-'}</TableCell>
|
||||
<TableCell>{resource.maxRelocate ?? '-'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-green-100 hover:text-green-600"
|
||||
onClick={() => onEnable?.(resource)}
|
||||
title="Enable"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onRemove?.(resource)}
|
||||
title="Remove"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@ -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 (
|
||||
<Card>
|
||||
@ -35,10 +28,11 @@ export function RealmList({
|
||||
<CardTitle>Authentication Realms</CardTitle>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button size="sm">
|
||||
<span className="mr-2 h-4 w-4">+</span>
|
||||
<Button size="sm" onClick={onCreate}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
New Realm
|
||||
</Button>
|
||||
</div>
|
||||
@ -48,63 +42,50 @@ export function RealmList({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Realm ID</TableHead>
|
||||
<TableHead>Realm Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Server</TableHead>
|
||||
<TableHead>Base DN</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Comment</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{realms.map((realm) => (
|
||||
<TableRow key={realm.id}>
|
||||
<TableCell className="font-medium">{realm.id}</TableCell>
|
||||
<TableCell>
|
||||
<span className="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{realm.type.toUpperCase()}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{realm.server || '-'}</TableCell>
|
||||
<TableCell>{realm.baseDn || '-'}</TableCell>
|
||||
<TableCell>
|
||||
<span className="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium bg-green-100 text-green-800">
|
||||
Active
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onEdit?.(realm)}
|
||||
title="Edit"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">✏️</span>
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onSync?.(realm)}
|
||||
title="Sync Users"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">🔄</span>
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onDelete?.(realm)}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
title="More"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{realms.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">
|
||||
No auth realms configured
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : (
|
||||
realms.map((realm) => (
|
||||
<TableRow key={realm.realm}>
|
||||
<TableCell className="font-medium">{realm.realm}</TableCell>
|
||||
<TableCell>
|
||||
<span className="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{realm.type.toUpperCase()}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">{realm.comment ?? '-'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onEdit?.(realm)}
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onDelete?.(realm)}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@ -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({
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle>Users</CardTitle>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-1 text-sm text-muted-foreground">
|
||||
<span className="text-green-500">●</span>
|
||||
<span>{enabledCount} Enabled</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<span className="text-gray-500">●</span>
|
||||
<div className="flex items-center space-x-1 text-sm text-muted-foreground">
|
||||
<span className="text-gray-400">●</span>
|
||||
<span>{disabledCount} Disabled</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button size="sm">
|
||||
<span className="mr-2 h-4 w-4">+</span>
|
||||
<Button size="sm" onClick={onCreate}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
New User
|
||||
</Button>
|
||||
</div>
|
||||
@ -61,64 +68,71 @@ export function UserList({
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User ID</TableHead>
|
||||
<TableHead>Realm</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Last Login</TableHead>
|
||||
<TableHead>Enabled</TableHead>
|
||||
<TableHead>Expire</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-medium">{user.id}</TableCell>
|
||||
<TableCell>{user.email || '-'}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||
user.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{user.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{user.lastLogin || '-'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onEdit?.(user)}
|
||||
title="Edit"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">✏️</span>
|
||||
</button>
|
||||
<button
|
||||
className={`rounded-md p-1 hover:bg-accent ${
|
||||
user.enabled ? 'text-green-600' : 'text-gray-600'
|
||||
}`}
|
||||
onClick={() => user.enabled ? onDisable?.(user) : onEnable?.(user)}
|
||||
title={user.enabled ? 'Disable' : 'Enable'}
|
||||
>
|
||||
{user.enabled ? (
|
||||
<span className="h-4 w-4 text-xs">⏸️</span>
|
||||
) : (
|
||||
<span className="h-4 w-4 text-xs">▶️</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onDelete?.(user)}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
title="More"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{users.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
|
||||
No users found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : (
|
||||
users.map((user) => {
|
||||
const fullName = [user.firstname, user.lastname].filter(Boolean).join(' ') || '-';
|
||||
return (
|
||||
<TableRow key={user.userid}>
|
||||
<TableCell className="font-medium font-mono text-xs">{user.userid}</TableCell>
|
||||
<TableCell>{deriveRealm(user.userid)}</TableCell>
|
||||
<TableCell>{fullName}</TableCell>
|
||||
<TableCell>{user.email ?? '-'}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||
user.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
{user.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{formatExpiry(user.expire)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onEdit?.(user)}
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => user.enabled ? onDisable?.(user) : onEnable?.(user)}
|
||||
title={user.enabled ? 'Disable' : 'Enable'}
|
||||
>
|
||||
{user.enabled ? (
|
||||
<Pause className="h-4 w-4 text-yellow-600" />
|
||||
) : (
|
||||
<Play className="h-4 w-4 text-green-600" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onDelete?.(user)}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@ -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<ClusterInfo[]>([]);
|
||||
const [selectedClusterId, setSelectedClusterId] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState<string>('acl');
|
||||
|
||||
const [acls, setAcls] = useState<AclEntry[]>([]);
|
||||
const [users, setUsers] = useState<ProxmoxUser[]>([]);
|
||||
const [realms, setRealms] = useState<AuthRealm[]>([]);
|
||||
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Access Control Lists</h1>
|
||||
<p className="text-muted-foreground">Manage permissions and access control</p>
|
||||
<h1 className="text-2xl font-bold">Access Control & Users</h1>
|
||||
<p className="text-muted-foreground">Manage permissions, users, and authentication realms</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
{clusters.length > 1 && (
|
||||
<select
|
||||
className="rounded-md border px-3 py-1.5 text-sm bg-background"
|
||||
value={selectedClusterId}
|
||||
onChange={(e) => setSelectedClusterId(e.target.value)}
|
||||
>
|
||||
{clusters.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={handleRefreshAll}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AclList
|
||||
acls={acls}
|
||||
onRefresh={() => {}}
|
||||
/>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="acl">ACLs</TabsTrigger>
|
||||
<TabsTrigger value="users">Users</TabsTrigger>
|
||||
<TabsTrigger value="realms">Auth Realms</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="acl">
|
||||
<AclList
|
||||
acls={acls}
|
||||
isLoading={isLoadingAcls}
|
||||
onRefresh={() => 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')}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="users">
|
||||
<UserList
|
||||
users={users}
|
||||
isLoading={isLoadingUsers}
|
||||
onRefresh={() => 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')}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="realms">
|
||||
<RealmList
|
||||
realms={realms}
|
||||
isLoading={isLoadingRealms}
|
||||
onRefresh={() => 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')}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<ClusterInfo[]>([]);
|
||||
const [selectedClusterId, setSelectedClusterId] = useState<string>('');
|
||||
const [groups, setGroups] = useState<HaGroup[]>([]);
|
||||
const [resources, setResources] = useState<HaResource[]>([]);
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -12,38 +121,44 @@ export function ProxmoxHAPage() {
|
||||
<h1 className="text-2xl font-bold">High Availability</h1>
|
||||
<p className="text-muted-foreground">Manage HA groups and resources</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
{clusters.length > 1 && (
|
||||
<select
|
||||
className="rounded-md border px-3 py-1.5 text-sm bg-background"
|
||||
value={selectedClusterId}
|
||||
onChange={(e) => setSelectedClusterId(e.target.value)}
|
||||
>
|
||||
{clusters.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={handleRefreshAll}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>HA Groups</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<HAGroupsList
|
||||
groups={[]}
|
||||
onRefresh={() => {}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<HAGroupsList
|
||||
groups={groups}
|
||||
isLoading={isLoadingGroups}
|
||||
onRefresh={() => loadGroups(selectedClusterId)}
|
||||
onCreate={handleCreateGroup}
|
||||
onEdit={handleEditGroup}
|
||||
onDelete={handleDeleteGroup}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>HA Resources</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<HAResourcesList
|
||||
resources={[]}
|
||||
onRefresh={() => {}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<HAResourcesList
|
||||
resources={resources}
|
||||
isLoading={isLoadingResources}
|
||||
onRefresh={() => loadResources(selectedClusterId)}
|
||||
onEnable={handleEnableResource}
|
||||
onRemove={handleRemoveResource}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user