mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 10:52:22 +00:00
Map storage devices to their PCI storage controller
Each disk in /api/hardware now carries a controller_slot, resolved from the disk's sysfs device path: the deepest PCI address in that path is the controller (bridges sit above it), normalised to lspci's slot format so it matches pci_devices[].slot directly. NVMe falls back to the controller's own sysfs link when the namespace resolves to a subsystem with no PCI parent; zvols and other virtual devices get None. Hardware page: - The disk dialog shows its storage controller (click through to the controller), and the PCI device dialog lists the disks attached to it. - Storage controllers in the PCI Devices list get a badge with the number of attached disks. Storage view: - /api/storage returns controller_slot on each physical disk plus a controllers map (slot -> class, vendor, device, subsystem name, driver), built from the cached `lspci -vmm` output so the Storage view doesn't need the heavier /api/hardware enumeration. - "Physical Disks & SMART Status" can group disks under their storage controller, with a header showing the controller name, slot, driver and disk count. Groups follow PCI slot order; disks with no resolvable controller are listed last. USB disks keep their own section. - The disk details dialog shows the storage controller on the Overview tab. - Grouping is opt-in via a new Storage Display card in Settings, stored per browser (localStorage) and synced across open views, following the existing nav-tab-order preference pattern. Off by default. Also: - Treat "IDE interface" as a storage controller, so SATA controllers left in BIOS IDE mode (e.g. Intel 7 Series, ata_piix) appear in pci_devices and disks behind them can be mapped. - lspci prints "Device xxxx" when it has no subsystem name; that placeholder is ignored so the chip name is shown instead. - Add the missing sdevice field to the PCIDevice type. Tests cover the sysfs path -> controller slot rule against real paths from two PVE 9 hosts (SAS HBA, AHCI SATA, SATA in BIOS IDE mode, USB behind a hub, NVMe behind chipset bridges), the non-zero PCI domain case (Intel VMD), the NVMe multipath fallback, and get_storage_controllers' lspci parsing including the "Device xxxx" placeholder and driver lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Switch } from "./ui/switch"
|
||||
import { Check, HardDrive, Settings2 } from "lucide-react"
|
||||
import { useT } from "../lib/i18n/provider"
|
||||
import { useGroupDisksByController } from "../lib/storage-display"
|
||||
|
||||
export function StorageDisplayCard() {
|
||||
const t = useT()
|
||||
const [groupByController, setGroupByController] = useGroupDisksByController()
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
const [pending, setPending] = useState<boolean | null>(null)
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
const handleCancel = () => {
|
||||
setPending(null)
|
||||
setEditMode(false)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (pending !== null && pending !== groupByController) {
|
||||
setGroupByController(pending)
|
||||
}
|
||||
setPending(null)
|
||||
setEditMode(false)
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="h-5 w-5 text-blue-500" />
|
||||
<CardTitle>{t("settings.storageDisplay.title")}</CardTitle>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{saved && (
|
||||
<span className="flex items-center gap-1 text-xs text-green-500">
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
{t("status.saved")}
|
||||
</span>
|
||||
)}
|
||||
{editMode ? (
|
||||
<>
|
||||
<button
|
||||
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t("actions.cancel")}
|
||||
</button>
|
||||
<button
|
||||
className="h-7 px-3 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50 flex items-center gap-1.5"
|
||||
onClick={handleSave}
|
||||
disabled={pending === null || pending === groupByController}
|
||||
>
|
||||
<Check className="h-3 w-3" />
|
||||
{t("actions.save")}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors flex items-center gap-1.5"
|
||||
onClick={() => setEditMode(true)}
|
||||
>
|
||||
<Settings2 className="h-3 w-3" />
|
||||
{t("actions.edit")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription>{t("settings.storageDisplay.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className={editMode ? "bg-accent" : undefined}>
|
||||
<div className="text-foreground flex items-center justify-between gap-4">
|
||||
<div className="flex items-center">{t("settings.storageDisplay.groupByController")}</div>
|
||||
<Switch
|
||||
checked={pending ?? groupByController}
|
||||
disabled={!editMode}
|
||||
onCheckedChange={(checked) => setPending(checked)}
|
||||
className={`data-[state=checked]:bg-blue-600 data-[state=unchecked]:bg-input border border-border ${!editMode ? "opacity-60" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user