Feature: ZFS Pool-Aktionen (Scrub, Resilver, Clear Errors) + Product-Spalte fix
- Backend: neue Endpoints POST /api/pools/{name}/clear und /resilver
- Frontend: Pool ⋮-Menü mit Scrub, Resilver, Clear Errors
- Product-Spalte im Status-Tab bricht jetzt korrekt um statt abgeschnitten zu werden
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,7 +79,7 @@ function VdevTree({ vdevs, depth = 0 }: { vdevs: any[]; depth?: number }) {
|
||||
<td className="px-4 py-2 text-xs text-center">{v.write ?? 0}</td>
|
||||
<td className="px-4 py-2 text-xs text-center">{v.cksum ?? 0}</td>
|
||||
<td className="px-4 py-2 text-xs text-muted-foreground">{v.message || ""}</td>
|
||||
<td className="px-4 py-2 text-xs text-muted-foreground">{v.product || ""}</td>
|
||||
<td className="px-4 py-2 text-xs text-muted-foreground break-all">{v.product || ""}</td>
|
||||
<td className="px-4 py-2 text-xs">
|
||||
<button className="p-1 rounded hover:bg-muted">
|
||||
<MoreVertical className="w-3 h-3" />
|
||||
@@ -97,6 +97,9 @@ export default function ZfsPage() {
|
||||
const [lastRefresh, setLastRefresh] = useState<Date | null>(null)
|
||||
const [poolStates, setPoolStates] = useState<Map<string, PoolRowState>>(new Map())
|
||||
|
||||
// Pool context menu
|
||||
const [poolMenu, setPoolMenu] = useState<{ pool: Pool; x: number; y: number } | null>(null)
|
||||
|
||||
// Dialogs
|
||||
const [showCreateFilesystem, setShowCreateFilesystem] = useState(false)
|
||||
const [createFsPool, setCreateFsPool] = useState("")
|
||||
@@ -250,6 +253,20 @@ export default function ZfsPage() {
|
||||
setCloneTarget(null)
|
||||
}
|
||||
|
||||
const handlePoolAction = async (action: string, pool: Pool) => {
|
||||
setPoolMenu(null)
|
||||
if (action === "scrub") {
|
||||
await api.startScrub(pool.name)
|
||||
refreshPoolStatus(pool.name)
|
||||
} else if (action === "clear") {
|
||||
await api.clearPoolErrors(pool.name)
|
||||
refreshPoolStatus(pool.name)
|
||||
} else if (action === "resilver") {
|
||||
await api.resilverPool(pool.name)
|
||||
refreshPoolStatus(pool.name)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateFilesystem = async () => {
|
||||
if (!newFsName.trim()) return
|
||||
const fullName = createFsPool ? `${createFsPool}/${newFsName.trim()}` : newFsName.trim()
|
||||
@@ -359,7 +376,10 @@ export default function ZfsPage() {
|
||||
<UsageBar alloc={pool.alloc} size={pool.size} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="p-1 rounded hover:bg-muted">
|
||||
<button
|
||||
className="p-1 rounded hover:bg-muted"
|
||||
onClick={(e) => setPoolMenu({ pool, x: e.clientX, y: e.clientY })}
|
||||
>
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
@@ -696,6 +716,31 @@ export default function ZfsPage() {
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{/* Pool Context Menu */}
|
||||
{poolMenu && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setPoolMenu(null)} />
|
||||
<div
|
||||
className="fixed z-50 bg-background border border-border rounded-md shadow-lg py-1 min-w-[220px]"
|
||||
style={{ top: poolMenu.y, left: poolMenu.x }}
|
||||
>
|
||||
{[
|
||||
{ action: "scrub", label: "Scrub Storage Pool" },
|
||||
{ action: "resilver", label: "Resilver Storage Pool" },
|
||||
{ action: "clear", label: "Clear Storage Pool Errors" },
|
||||
].map(({ action, label }) => (
|
||||
<button
|
||||
key={action}
|
||||
className="w-full text-left px-4 py-2 text-sm hover:bg-muted"
|
||||
onClick={() => handlePoolAction(action, poolMenu.pool)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Snapshot Context Menu */}
|
||||
{snapContextMenu && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user