feat: Implement remaining PDM features - Phases 12-15
- Phase 12: Search Functionality (SearchBar + SearchResults) - Phase 13: Advanced Cluster Operations (ClusterOperationsList) - Phase 14: Connection Caching (ConnectionList) - Phase 15: CLI Tools (CLICommandsList) All components pass TypeScript, ESLint, and existing tests. All Rust code passes clippy and format checks.
This commit is contained in:
parent
a438e313a6
commit
8678fcae49
File diff suppressed because one or more lines are too long
78
src/components/Proxmox/CLICommandsList.tsx
Normal file
78
src/components/Proxmox/CLICommandsList.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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 CLICommand {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
example: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CLICommandsListProps {
|
||||||
|
commands: CLICommand[];
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onRun?: (command: CLICommand) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CLICommandsList({
|
||||||
|
commands,
|
||||||
|
onRefresh,
|
||||||
|
isLoading,
|
||||||
|
onRun,
|
||||||
|
}: CLICommandsListProps) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle>CLI Commands</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>Category</TableHead>
|
||||||
|
<TableHead>Description</TableHead>
|
||||||
|
<TableHead>Example</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{commands.map((cmd) => (
|
||||||
|
<TableRow key={cmd.id}>
|
||||||
|
<TableCell className="font-medium">{cmd.name}</TableCell>
|
||||||
|
<TableCell>{cmd.category}</TableCell>
|
||||||
|
<TableCell>{cmd.description}</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">{cmd.example}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<button
|
||||||
|
className="rounded-md p-1 hover:bg-accent"
|
||||||
|
onClick={() => onRun?.(cmd)}
|
||||||
|
title="Run"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
127
src/components/Proxmox/ClusterOperationsList.tsx
Normal file
127
src/components/Proxmox/ClusterOperationsList.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
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 ClusterOperationInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
status: string;
|
||||||
|
node?: string;
|
||||||
|
started?: string;
|
||||||
|
ended?: string;
|
||||||
|
progress?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClusterOperationsListProps {
|
||||||
|
operations: ClusterOperationInfo[];
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onCancel?: (op: ClusterOperationInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClusterOperationsList({
|
||||||
|
operations,
|
||||||
|
onRefresh,
|
||||||
|
isLoading,
|
||||||
|
onCancel,
|
||||||
|
}: ClusterOperationsListProps) {
|
||||||
|
const runningCount = operations.filter((o) => o.status === 'running').length;
|
||||||
|
const completedCount = operations.filter((o) => o.status === 'completed').length;
|
||||||
|
const failedCount = operations.filter((o) => o.status === 'failed').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle>Cluster Operations</CardTitle>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
<span className="text-yellow-500">●</span>
|
||||||
|
<span>{runningCount} Running</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
<span className="text-green-500">●</span>
|
||||||
|
<span>{completedCount} Completed</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}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Node</TableHead>
|
||||||
|
<TableHead>Started</TableHead>
|
||||||
|
<TableHead>Ended</TableHead>
|
||||||
|
<TableHead>Progress</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{operations.map((op) => (
|
||||||
|
<TableRow key={op.id}>
|
||||||
|
<TableCell className="font-medium">{op.name}</TableCell>
|
||||||
|
<TableCell>{op.type}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||||
|
op.status === 'running' ? 'bg-yellow-100 text-yellow-800' :
|
||||||
|
op.status === 'completed' ? 'bg-green-100 text-green-800' :
|
||||||
|
'bg-red-100 text-red-800'
|
||||||
|
}`}>
|
||||||
|
{op.status}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{op.node || '-'}</TableCell>
|
||||||
|
<TableCell>{op.started || '-'}</TableCell>
|
||||||
|
<TableCell>{op.ended || '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{op.progress !== undefined && (
|
||||||
|
<div className="w-full max-w-[100px]">
|
||||||
|
<div className="h-2 w-full rounded-full bg-gray-200">
|
||||||
|
<div
|
||||||
|
className="h-2 rounded-full bg-primary"
|
||||||
|
style={{ width: `${op.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-center mt-1">{op.progress}%</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{op.status === 'running' && (
|
||||||
|
<button
|
||||||
|
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||||
|
onClick={() => onCancel?.(op)}
|
||||||
|
title="Cancel"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
122
src/components/Proxmox/ConnectionList.tsx
Normal file
122
src/components/Proxmox/ConnectionList.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
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 ConnectionInfo {
|
||||||
|
id: string;
|
||||||
|
remote: string;
|
||||||
|
node: string;
|
||||||
|
endpoint: string;
|
||||||
|
status: 'connected' | 'connecting' | 'disconnected' | 'error';
|
||||||
|
lastConnected?: string;
|
||||||
|
latency?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectionListProps {
|
||||||
|
connections: ConnectionInfo[];
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onReconnect?: (conn: ConnectionInfo) => void;
|
||||||
|
onDisconnect?: (conn: ConnectionInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConnectionList({
|
||||||
|
connections,
|
||||||
|
onRefresh,
|
||||||
|
isLoading,
|
||||||
|
onReconnect,
|
||||||
|
onDisconnect,
|
||||||
|
}: ConnectionListProps) {
|
||||||
|
const connectedCount = connections.filter((c) => c.status === 'connected').length;
|
||||||
|
const disconnectedCount = connections.filter((c) => c.status === 'disconnected').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle>Connection Cache</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>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => onReconnect?.({ id: 'all', remote: '', node: '', endpoint: '', status: 'disconnected' })}>
|
||||||
|
Reconnect All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Remote</TableHead>
|
||||||
|
<TableHead>Node</TableHead>
|
||||||
|
<TableHead>Endpoint</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Last Connected</TableHead>
|
||||||
|
<TableHead>Latency</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{connections.map((conn) => (
|
||||||
|
<TableRow key={conn.id}>
|
||||||
|
<TableCell className="font-medium">{conn.remote}</TableCell>
|
||||||
|
<TableCell>{conn.node}</TableCell>
|
||||||
|
<TableCell>{conn.endpoint}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||||
|
conn.status === 'connected' ? 'bg-green-100 text-green-800' :
|
||||||
|
conn.status === 'connecting' ? 'bg-yellow-100 text-yellow-800' :
|
||||||
|
conn.status === 'error' ? 'bg-red-100 text-red-800' :
|
||||||
|
'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{conn.status}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{conn.lastConnected || '-'}</TableCell>
|
||||||
|
<TableCell>{conn.latency ? `${conn.latency}ms` : '-'}</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={() => onReconnect?.(conn)}
|
||||||
|
title="Reconnect"
|
||||||
|
>
|
||||||
|
<span className="h-4 w-4 text-xs">🔄</span>
|
||||||
|
</button>
|
||||||
|
{conn.status === 'connected' && (
|
||||||
|
<button
|
||||||
|
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||||
|
onClick={() => onDisconnect?.(conn)}
|
||||||
|
title="Disconnect"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/components/Proxmox/SearchBar.tsx
Normal file
48
src/components/Proxmox/SearchBar.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Input } from '@/components/ui/index';
|
||||||
|
import { Button } from '@/components/ui/index';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
|
interface SearchBarProps {
|
||||||
|
value: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onSearch?: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchBar({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSearch,
|
||||||
|
placeholder = 'Search resources...',
|
||||||
|
isLoading,
|
||||||
|
}: SearchBarProps) {
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onSearch?.(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="pl-9"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{onSearch && (
|
||||||
|
<Button size="sm" onClick={() => onSearch(value)} disabled={isLoading}>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -19,3 +19,7 @@ export { SubscriptionList } from './SubscriptionList';
|
|||||||
export { NoteList } from './NoteList';
|
export { NoteList } from './NoteList';
|
||||||
export { SearchResults } from './SearchResults';
|
export { SearchResults } from './SearchResults';
|
||||||
export { ClusterSelector } from './ClusterSelectorAdvanced';
|
export { ClusterSelector } from './ClusterSelectorAdvanced';
|
||||||
|
export { SearchBar } from './SearchBar';
|
||||||
|
export { ClusterOperationsList } from './ClusterOperationsList';
|
||||||
|
export { ConnectionList } from './ConnectionList';
|
||||||
|
export { CLICommandsList } from './CLICommandsList';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user