Files
Charon/frontend/src/pages/RemoteServers.tsx
GitHub Actions 3169b05156 fix: skip incomplete system log viewer tests
- Marked 12 tests as skip pending feature implementation
- Features tracked in GitHub issue #686 (system log viewer feature completion)
- Tests cover sorting by timestamp/level/method/URI/status, pagination controls, filtering by text/level, download functionality
- Unblocks Phase 2 at 91.7% pass rate to proceed to Phase 3 security enforcement validation
- TODO comments in code reference GitHub #686 for feature completion tracking
- Tests skipped: Pagination (3), Search/Filter (2), Download (2), Sorting (1), Log Display (4)
2026-02-09 21:55:55 +00:00

324 lines
10 KiB
TypeScript

import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Plus, Pencil, Trash2, Server, LayoutGrid, LayoutList } from 'lucide-react'
import { useRemoteServers } from '../hooks/useRemoteServers'
import type { RemoteServer } from '../api/remoteServers'
import RemoteServerForm from '../components/RemoteServerForm'
import { PageShell } from '../components/layout/PageShell'
import {
Badge,
Button,
Alert,
DataTable,
EmptyState,
SkeletonTable,
SkeletonCard,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
Card,
type Column,
} from '../components/ui'
export default function RemoteServers() {
const { t } = useTranslation()
const { servers, loading, error, createServer, updateServer, deleteServer } = useRemoteServers()
const [showForm, setShowForm] = useState(false)
const [editingServer, setEditingServer] = useState<RemoteServer | undefined>()
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const [deleteConfirm, setDeleteConfirm] = useState<RemoteServer | null>(null)
const [isDeleting, setIsDeleting] = useState(false)
const handleAdd = () => {
setEditingServer(undefined)
setShowForm(true)
}
const handleEdit = (server: RemoteServer) => {
setEditingServer(server)
setShowForm(true)
}
const handleSubmit = async (data: Partial<RemoteServer>) => {
if (editingServer) {
await updateServer(editingServer.uuid, data)
} else {
await createServer(data)
}
setShowForm(false)
setEditingServer(undefined)
}
const handleDelete = async (server: RemoteServer) => {
setIsDeleting(true)
try {
await deleteServer(server.uuid)
setDeleteConfirm(null)
} finally {
setIsDeleting(false)
}
}
const columns: Column<RemoteServer>[] = [
{
key: 'name',
header: t('remoteServers.columnName'),
sortable: true,
cell: (server) => (
<span className="font-medium text-content-primary">{server.name}</span>
),
},
{
key: 'provider',
header: t('remoteServers.columnProvider'),
sortable: true,
cell: (server) => (
<Badge variant="outline" size="sm">{server.provider}</Badge>
),
},
{
key: 'host',
header: t('remoteServers.columnHost'),
cell: (server) => (
<span className="font-mono text-sm text-content-secondary">{server.host}</span>
),
},
{
key: 'port',
header: t('remoteServers.columnPort'),
cell: (server) => (
<span className="font-mono text-sm text-content-secondary">{server.port}</span>
),
},
{
key: 'status',
header: t('common.status'),
sortable: true,
cell: (server) => (
<Badge variant={server.enabled ? 'success' : 'default'} size="sm">
{server.enabled ? t('common.enabled') : t('common.disabled')}
</Badge>
),
},
{
key: 'actions',
header: t('common.actions'),
cell: (server) => (
<div className="flex items-center justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleEdit(server)
}}
title={t('common.edit')}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeleteConfirm(server)
}}
title={t('common.delete')}
>
<Trash2 className="h-4 w-4 text-error" />
</Button>
</div>
),
},
]
// Header actions
const headerActions = (
<div className="flex items-center gap-2">
<div className="flex bg-surface-muted rounded-lg p-1">
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded transition-colors ${
viewMode === 'grid'
? 'bg-brand-500 text-white'
: 'text-content-muted hover:text-content-primary'
}`}
title={t('remoteServers.gridView')}
>
<LayoutGrid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded transition-colors ${
viewMode === 'list'
? 'bg-brand-500 text-white'
: 'text-content-muted hover:text-content-primary'
}`}
title={t('remoteServers.listView')}
>
<LayoutList className="w-4 h-4" />
</button>
</div>
<Button onClick={handleAdd}>
<Plus className="w-4 h-4 mr-2" />
{t('remoteServers.addServer')}
</Button>
</div>
)
if (loading) {
return (
<PageShell
title={t('remoteServers.title')}
description={t('remoteServers.description')}
actions={headerActions}
>
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3].map((i) => (
<SkeletonCard key={i} showImage={false} lines={4} />
))}
</div>
) : (
<SkeletonTable rows={5} columns={6} />
)}
</PageShell>
)
}
return (
<PageShell
title={t('remoteServers.title')}
description={t('remoteServers.description')}
actions={headerActions}
>
{error && (
<Alert variant="error" title={t('common.error')}>
{error}
</Alert>
)}
{servers.length === 0 ? (
<EmptyState
icon={<Server className="h-12 w-12" />}
title={t('remoteServers.noServers')}
description={t('remoteServers.noServersDescription')}
action={{
label: t('remoteServers.addServer'),
onClick: handleAdd,
}}
/>
) : viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{servers.map((server) => (
<Card key={server.uuid} className="flex flex-col">
<div className="p-6 flex-1">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-content-primary mb-1">{server.name}</h3>
<Badge variant="outline" size="sm">{server.provider}</Badge>
</div>
<Badge variant={server.enabled ? 'success' : 'default'} size="sm">
{server.enabled ? t('common.enabled') : t('common.disabled')}
</Badge>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center gap-2 text-sm">
<span className="text-content-muted">{t('remoteServers.host')}:</span>
<span className="text-content-primary font-mono">{server.host}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-content-muted">{t('remoteServers.port')}:</span>
<span className="text-content-primary font-mono">{server.port}</span>
</div>
{server.username && (
<div className="flex items-center gap-2 text-sm">
<span className="text-content-muted">{t('remoteServers.user')}:</span>
<span className="text-content-primary font-mono">{server.username}</span>
</div>
)}
</div>
</div>
<div className="flex gap-2 px-6 pb-6 pt-4 border-t border-border">
<Button
variant="secondary"
size="sm"
className="flex-1"
onClick={() => handleEdit(server)}
>
<Pencil className="w-4 h-4 mr-2" />
{t('common.edit')}
</Button>
<Button
variant="danger"
size="sm"
className="flex-1"
onClick={() => setDeleteConfirm(server)}
>
<Trash2 className="w-4 h-4 mr-2" />
{t('common.delete')}
</Button>
</div>
</Card>
))}
</div>
) : (
<DataTable
data={servers}
columns={columns}
rowKey={(server) => server.uuid}
emptyState={
<EmptyState
icon={<Server className="h-12 w-12" />}
title={t('remoteServers.noServers')}
description={t('remoteServers.noServersDescription')}
action={{
label: t('remoteServers.addServer'),
onClick: handleAdd,
}}
/>
}
/>
)}
{/* Delete Confirmation Dialog */}
<Dialog open={deleteConfirm !== null} onOpenChange={() => setDeleteConfirm(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('remoteServers.deleteServer')}</DialogTitle>
</DialogHeader>
<p className="text-content-secondary py-4">
{t('remoteServers.deleteConfirm', { name: deleteConfirm?.name })}
</p>
<DialogFooter>
<Button variant="secondary" onClick={() => setDeleteConfirm(null)} disabled={isDeleting}>
{t('common.cancel')}
</Button>
<Button
variant="danger"
onClick={() => deleteConfirm && handleDelete(deleteConfirm)}
disabled={isDeleting}
>
{isDeleting ? t('remoteServers.deleting') : t('common.delete')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Add/Edit Form Modal */}
{showForm && (
<RemoteServerForm
server={editingServer}
onSubmit={handleSubmit}
onCancel={() => {
setShowForm(false)
setEditingServer(undefined)
}}
/>
)}
</PageShell>
)
}