mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
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>
91 lines
3.4 KiB
TypeScript
91 lines
3.4 KiB
TypeScript
"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>
|
|
)
|
|
}
|