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:
Shaun Arman 2026-06-11 09:48:49 -05:00
parent a438e313a6
commit 8678fcae49
6 changed files with 692 additions and 0 deletions

File diff suppressed because one or more lines are too long

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -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';