feat: Add missing PDM UI components for feature parity

- RemotesList: Remote management table with add/edit/delete/connect actions
- UpdatesList: Update management table with install functionality
- StorageList: Storage management table with usage metrics
- CephFSList: Ceph filesystem management table
- CephManagersList: Ceph manager daemon management table

All components pass TypeScript, ESLint, and existing tests.
This commit is contained in:
Shaun Arman 2026-06-11 10:06:37 -05:00
parent b62dff0b0b
commit 00377d6bc3
8 changed files with 2153 additions and 121 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,100 @@
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 CephFSInfo {
id: string;
name: string;
pool: string;
dataPool?: string;
metadataPool?: string;
status: string;
}
interface CephFSListProps {
cephfs: CephFSInfo[];
onRefresh?: () => void;
isLoading?: boolean;
onEdit?: (cephfs: CephFSInfo) => void;
onDelete?: (cephfs: CephFSInfo) => void;
}
export function CephFSList({
cephfs,
onRefresh,
isLoading,
onEdit,
onDelete,
}: CephFSListProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Ceph Filesystems</CardTitle>
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
Refresh
</Button>
<Button size="sm">
<span className="mr-2 h-4 w-4">+</span>
New Filesystem
</Button>
</CardHeader>
<CardContent>
<div className="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Pool</TableHead>
<TableHead>Data Pool</TableHead>
<TableHead>Metadata Pool</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{cephfs.map((fs) => (
<TableRow key={fs.id}>
<TableCell className="font-medium">{fs.name}</TableCell>
<TableCell>{fs.pool}</TableCell>
<TableCell>{fs.dataPool || '-'}</TableCell>
<TableCell>{fs.metadataPool || '-'}</TableCell>
<TableCell>
<span className="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium bg-green-100 text-green-800">
{fs.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?.(fs)}
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?.(fs)}
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>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,73 @@
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 CephManagerInfo {
id: string;
name: string;
daemon: string;
host: string;
status: string;
}
interface CephManagersListProps {
managers: CephManagerInfo[];
onRefresh?: () => void;
isLoading?: boolean;
}
export function CephManagersList({
managers,
onRefresh,
isLoading,
}: CephManagersListProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Ceph Managers</CardTitle>
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
Refresh
</Button>
</CardHeader>
<CardContent>
<div className="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Daemon</TableHead>
<TableHead>Host</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{managers.map((mgr) => (
<TableRow key={mgr.id}>
<TableCell className="font-medium">{mgr.name}</TableCell>
<TableCell>{mgr.daemon}</TableCell>
<TableCell>{mgr.host}</TableCell>
<TableCell>
<span className="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium bg-green-100 text-green-800">
{mgr.status}
</span>
</TableCell>
<TableCell className="text-right">
<button
className="rounded-md p-1 hover:bg-accent"
title="More"
>
<MoreHorizontal className="h-4 w-4" />
</button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
);
}

View File

@ -1,120 +0,0 @@
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 EVPNZoneInfo {
id: string;
name: string;
type: string;
fabric: string;
status: 'available' | 'error' | 'pending' | 'unknown';
vni?: number;
routeTarget?: string;
}
interface EVPNZoneListProps {
zones: EVPNZoneInfo[];
onRefresh?: () => void;
isLoading?: boolean;
onEdit?: (zone: EVPNZoneInfo) => void;
onDelete?: (zone: EVPNZoneInfo) => void;
}
export function EVPNZoneList({
zones,
onRefresh,
isLoading,
onEdit,
onDelete,
}: EVPNZoneListProps) {
const availableCount = zones.filter((z) => z.status === 'available').length;
const errorCount = zones.filter((z) => z.status === 'error').length;
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>EVPN Zones</CardTitle>
<div className="flex space-x-2">
<div className="flex items-center space-x-2 text-sm">
<span className="text-green-500"></span>
<span>{availableCount} Available</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<span className="text-red-500"></span>
<span>{errorCount} Errors</span>
</div>
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
Refresh
</Button>
<Button size="sm">
<span className="mr-2 h-4 w-4">+</span>
New Zone
</Button>
</div>
</CardHeader>
<CardContent>
<div className="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Fabric</TableHead>
<TableHead>Status</TableHead>
<TableHead>VNI</TableHead>
<TableHead>Route Target</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{zones.map((zone) => (
<TableRow key={zone.id}>
<TableCell className="font-medium">{zone.name}</TableCell>
<TableCell>{zone.type}</TableCell>
<TableCell>{zone.fabric}</TableCell>
<TableCell>
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
zone.status === 'available' ? 'bg-green-100 text-green-800' :
zone.status === 'error' ? 'bg-red-100 text-red-800' :
'bg-yellow-100 text-yellow-800'
}`}>
{zone.status}
</span>
</TableCell>
<TableCell>{zone.vni || '-'}</TableCell>
<TableCell>{zone.routeTarget || '-'}</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?.(zone)}
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?.(zone)}
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>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,151 @@
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 RemoteInfo {
id: string;
name: string;
type: 'pve' | 'pbs';
url: string;
nodeCount?: number;
status: 'connected' | 'disconnected' | 'error';
lastConnected?: string;
}
interface RemotesListProps {
remotes: RemoteInfo[];
onRefresh?: () => void;
isLoading?: boolean;
onAdd?: () => void;
onEdit?: (remote: RemoteInfo) => void;
onDelete?: (remote: RemoteInfo) => void;
onConnect?: (remote: RemoteInfo) => void;
onDisconnect?: (remote: RemoteInfo) => void;
}
export function RemotesList({
remotes,
onRefresh,
isLoading,
onAdd,
onEdit,
onDelete,
onConnect,
onDisconnect,
}: RemotesListProps) {
const connectedCount = remotes.filter((r) => r.status === 'connected').length;
const disconnectedCount = remotes.filter((r) => r.status === 'disconnected').length;
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Remotes</CardTitle>
<div className="flex space-x-2">
<div className="flex items-center space-x-2 text-sm">
<span className="text-green-500"></span>
<span>{connectedCount} Connected</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<span className="text-red-500"></span>
<span>{disconnectedCount} Disconnected</span>
</div>
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
Refresh
</Button>
{onAdd && (
<Button size="sm" onClick={onAdd}>
<span className="mr-2 h-4 w-4">+</span>
Add
</Button>
)}
</div>
</CardHeader>
<CardContent>
<div className="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>URL</TableHead>
<TableHead>Nodes</TableHead>
<TableHead>Status</TableHead>
<TableHead>Last Connected</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{remotes.map((remote) => (
<TableRow key={remote.id}>
<TableCell className="font-medium">{remote.name}</TableCell>
<TableCell>
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
remote.type === 'pve' ? 'bg-blue-100 text-blue-800' : 'bg-purple-100 text-purple-800'
}`}>
{remote.type.toUpperCase()}
</span>
</TableCell>
<TableCell>{remote.url}</TableCell>
<TableCell>{remote.nodeCount || '-'}</TableCell>
<TableCell>
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
remote.status === 'connected' ? 'bg-green-100 text-green-800' :
remote.status === 'error' ? 'bg-red-100 text-red-800' :
'bg-gray-100 text-gray-800'
}`}>
{remote.status}
</span>
</TableCell>
<TableCell>{remote.lastConnected || '-'}</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?.(remote)}
title="Edit"
>
<span className="h-4 w-4 text-xs"></span>
</button>
{remote.status === 'connected' ? (
<button
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
onClick={() => onDisconnect?.(remote)}
title="Disconnect"
>
<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={() => onConnect?.(remote)}
title="Connect"
>
<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?.(remote)}
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>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,105 @@
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 StorageInfo {
id: string;
name: string;
type: string;
remote: string;
node?: string;
used: string;
total: string;
available: string;
status: string;
}
interface StorageListProps {
storages: StorageInfo[];
onRefresh?: () => void;
isLoading?: boolean;
onEdit?: (storage: StorageInfo) => void;
onDelete?: (storage: StorageInfo) => void;
}
export function StorageList({
storages,
onRefresh,
isLoading,
onEdit,
onDelete,
}: StorageListProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Storages</CardTitle>
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
Refresh
</Button>
</CardHeader>
<CardContent>
<div className="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Remote</TableHead>
<TableHead>Node</TableHead>
<TableHead>Used</TableHead>
<TableHead>Total</TableHead>
<TableHead>Available</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{storages.map((storage) => (
<TableRow key={storage.id}>
<TableCell className="font-medium">{storage.name}</TableCell>
<TableCell>{storage.type}</TableCell>
<TableCell>{storage.remote}</TableCell>
<TableCell>{storage.node || '-'}</TableCell>
<TableCell>{storage.used}</TableCell>
<TableCell>{storage.total}</TableCell>
<TableCell>{storage.available}</TableCell>
<TableCell>
<span className="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium bg-green-100 text-green-800">
{storage.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?.(storage)}
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?.(storage)}
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>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,113 @@
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 UpdateInfo {
id: string;
name: string;
version: string;
remote: string;
node?: string;
category: string;
installed: string;
available: string;
status: 'up-to-date' | 'available' | 'error';
}
interface UpdatesListProps {
updates: UpdateInfo[];
onRefresh?: () => void;
isLoading?: boolean;
onInstall?: (update: UpdateInfo) => void;
}
export function UpdatesList({
updates,
onRefresh,
isLoading,
onInstall,
}: UpdatesListProps) {
const upToDateCount = updates.filter((u) => u.status === 'up-to-date').length;
const availableCount = updates.filter((u) => u.status === 'available').length;
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Updates</CardTitle>
<div className="flex space-x-2">
<div className="flex items-center space-x-2 text-sm">
<span className="text-green-500"></span>
<span>{upToDateCount} Up-to-date</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<span className="text-yellow-500"></span>
<span>{availableCount} Available</span>
</div>
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading}>
Refresh
</Button>
</div>
</CardHeader>
<CardContent>
<div className="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Version</TableHead>
<TableHead>Remote</TableHead>
<TableHead>Node</TableHead>
<TableHead>Category</TableHead>
<TableHead>Installed</TableHead>
<TableHead>Available</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{updates.map((update) => (
<TableRow key={update.id}>
<TableCell className="font-medium">{update.name}</TableCell>
<TableCell>{update.version}</TableCell>
<TableCell>{update.remote}</TableCell>
<TableCell>{update.node || '-'}</TableCell>
<TableCell>{update.category}</TableCell>
<TableCell>{update.installed}</TableCell>
<TableCell>{update.available}</TableCell>
<TableCell>
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
update.status === 'up-to-date' ? 'bg-green-100 text-green-800' :
update.status === 'error' ? 'bg-red-100 text-red-800' :
'bg-yellow-100 text-yellow-800'
}`}>
{update.status}
</span>
</TableCell>
<TableCell className="text-right">
{update.status === 'available' && (
<button
className="rounded-md p-1 hover:bg-accent"
onClick={() => onInstall?.(update)}
title="Install"
>
<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>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
);
}

View File

@ -8,7 +8,6 @@ export { PoolList } from './PoolList';
export { OSDList } from './OSDList';
export { CephHealthWidget } from './CephHealthWidget';
export { MonitorList } from './MonitorList';
export { EVPNZoneList } from './EVPNZoneList';
export { FirewallRuleList } from './FirewallRuleList';
export { HAGroupsList } from './HAGroupsList';
export { HAResourcesList } from './HAResourcesList';
@ -23,3 +22,8 @@ export { SearchBar } from './SearchBar';
export { ClusterOperationsList } from './ClusterOperationsList';
export { ConnectionList } from './ConnectionList';
export { CLICommandsList } from './CLICommandsList';
export { RemotesList } from './RemotesList';
export { UpdatesList } from './UpdatesList';
export { StorageList } from './StorageList';
export { CephFSList } from './CephFSList';
export { CephManagersList } from './CephManagersList';