feat: multi-source disk entries with chips display
diskUsage now carries Sources []string instead of a single Source. collectDiskUsage accumulates all sources (samba, nfs, manual) per path. NFS no longer adds a redundant label to used_by. Dashboard and Settings render one chip per source. When a path is shared via SMB and NFS, both chips appear. API breaking: disks[].source replaced by disks[].sources[]. Version: 0.2.1 -> 0.3.0
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
BINARY=nasctl
|
BINARY=nasctl
|
||||||
VERSION?=0.2.1
|
VERSION?=0.3.0
|
||||||
GO?=go
|
GO?=go
|
||||||
LDFLAGS=-s -w -X github.com/darroyo/nasctl/internal/web.Version=$(VERSION) -X github.com/darroyo/nasctl/internal/web.Commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
LDFLAGS=-s -w -X github.com/darroyo/nasctl/internal/web.Version=$(VERSION) -X github.com/darroyo/nasctl/internal/web.Commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
BUILD_FLAGS=CGO_ENABLED=0
|
BUILD_FLAGS=CGO_ENABLED=0
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ import (
|
|||||||
|
|
||||||
type diskUsage struct {
|
type diskUsage struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
TotalBytes uint64 `json:"total_bytes"`
|
TotalBytes uint64 `json:"total_bytes"`
|
||||||
FreeBytes uint64 `json:"free_bytes"`
|
FreeBytes uint64 `json:"free_bytes"`
|
||||||
UsedBytes uint64 `json:"used_bytes"`
|
UsedBytes uint64 `json:"used_bytes"`
|
||||||
UsedPercent float64 `json:"used_percent"`
|
UsedPercent float64 `json:"used_percent"`
|
||||||
Source string `json:"source"`
|
Sources []string `json:"sources"`
|
||||||
UsedBy []string `json:"used_by,omitempty"`
|
UsedBy []string `json:"used_by,omitempty"`
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type serviceStatus struct {
|
type serviceStatus struct {
|
||||||
@@ -45,6 +45,15 @@ func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addSource(e *diskUsage, src string) {
|
||||||
|
for _, s := range e.Sources {
|
||||||
|
if s == src {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.Sources = append(e.Sources, src)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) collectDiskUsage() []diskUsage {
|
func (s *Server) collectDiskUsage() []diskUsage {
|
||||||
entries := make(map[string]*diskUsage)
|
entries := make(map[string]*diskUsage)
|
||||||
|
|
||||||
@@ -52,11 +61,13 @@ func (s *Server) collectDiskUsage() []diskUsage {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
for _, share := range shares {
|
for _, share := range shares {
|
||||||
key := share.Path
|
key := share.Path
|
||||||
if e, ok := entries[key]; ok {
|
e, ok := entries[key]
|
||||||
e.UsedBy = append(e.UsedBy, share.Name)
|
if !ok {
|
||||||
} else {
|
e = &diskUsage{Path: key, Sources: []string{}, Available: true}
|
||||||
entries[key] = &diskUsage{Path: key, Source: "samba", UsedBy: []string{share.Name}, Available: true}
|
entries[key] = e
|
||||||
}
|
}
|
||||||
|
addSource(e, "samba")
|
||||||
|
e.UsedBy = append(e.UsedBy, share.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,12 +75,12 @@ func (s *Server) collectDiskUsage() []diskUsage {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
for _, exp := range exports {
|
for _, exp := range exports {
|
||||||
key := exp.Path
|
key := exp.Path
|
||||||
label := "nfs:" + exp.Path
|
e, ok := entries[key]
|
||||||
if e, ok := entries[key]; ok {
|
if !ok {
|
||||||
e.UsedBy = append(e.UsedBy, label)
|
e = &diskUsage{Path: key, Sources: []string{}, Available: true}
|
||||||
} else {
|
entries[key] = e
|
||||||
entries[key] = &diskUsage{Path: key, Source: "nfs", UsedBy: []string{label}, Available: true}
|
|
||||||
}
|
}
|
||||||
|
addSource(e, "nfs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,21 +88,28 @@ func (s *Server) collectDiskUsage() []diskUsage {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
for _, m := range watched {
|
for _, m := range watched {
|
||||||
key := m.Path
|
key := m.Path
|
||||||
if _, ok := entries[key]; !ok {
|
e, ok := entries[key]
|
||||||
entries[key] = &diskUsage{Path: key, Source: "manual", Available: true}
|
if !ok {
|
||||||
|
e = &diskUsage{Path: key, Sources: []string{}, Available: true}
|
||||||
|
entries[key] = e
|
||||||
}
|
}
|
||||||
|
addSource(e, "manual")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []diskUsage
|
var result []diskUsage
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
|
sort.Strings(e.Sources)
|
||||||
fillDiskUsage(e)
|
fillDiskUsage(e)
|
||||||
result = append(result, *e)
|
result = append(result, *e)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(result, func(i, j int) bool {
|
sort.Slice(result, func(i, j int) bool {
|
||||||
if result[i].Source != result[j].Source {
|
if len(result[i].Sources) == 0 || len(result[j].Sources) == 0 {
|
||||||
return result[i].Source < result[j].Source
|
return len(result[i].Sources) > 0
|
||||||
|
}
|
||||||
|
if result[i].Sources[0] != result[j].Sources[0] {
|
||||||
|
return result[i].Sources[0] < result[j].Sources[0]
|
||||||
}
|
}
|
||||||
return result[i].Path < result[j].Path
|
return result[i].Path < result[j].Path
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ export interface DiskUsage {
|
|||||||
free_bytes: number;
|
free_bytes: number;
|
||||||
used_bytes: number;
|
used_bytes: number;
|
||||||
used_percent: number;
|
used_percent: number;
|
||||||
source: "system" | "mount" | "samba" | "nfs" | "manual";
|
sources: ("manual" | "samba" | "nfs")[];
|
||||||
used_by?: string[];
|
used_by?: string[];
|
||||||
available: boolean;
|
available: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import { useEffect, useState } from "react";
|
|||||||
import { api, formatBytes, SystemStatus } from "../api";
|
import { api, formatBytes, SystemStatus } from "../api";
|
||||||
import { useDirty } from "../DirtyContext";
|
import { useDirty } from "../DirtyContext";
|
||||||
|
|
||||||
|
const SOURCE_COLORS: Record<string, string> = {
|
||||||
|
samba: "bg-emerald-500/20 text-emerald-300",
|
||||||
|
nfs: "bg-amber-500/20 text-amber-300",
|
||||||
|
manual: "bg-cyan-500/20 text-cyan-300",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SOURCE_LABELS: Record<string, string> = {
|
||||||
|
samba: "SMB",
|
||||||
|
nfs: "NFS",
|
||||||
|
manual: "Vigilado",
|
||||||
|
};
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [status, setStatus] = useState<SystemStatus | null>(null);
|
const [status, setStatus] = useState<SystemStatus | null>(null);
|
||||||
const { modules } = useDirty();
|
const { modules } = useDirty();
|
||||||
@@ -11,7 +23,7 @@ export default function Dashboard() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const visibleDisks = status?.disks.filter(
|
const visibleDisks = status?.disks.filter(
|
||||||
(d) => d.source === "manual" || d.source === "samba" || d.source === "nfs"
|
(d) => d.sources.includes("manual") || d.sources.includes("samba") || d.sources.includes("nfs")
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,9 +39,22 @@ export default function Dashboard() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{visibleDisks.map((d) => (
|
{visibleDisks.map((d) => (
|
||||||
<div key={d.path}>
|
<div key={d.path}>
|
||||||
<div className="mb-1 flex justify-between text-sm">
|
<div className="mb-1 flex flex-wrap items-center justify-between gap-x-3 gap-y-1 text-sm">
|
||||||
<span className="text-slate-300">{d.path}</span>
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-slate-400">
|
<span className="text-slate-300">{d.path}</span>
|
||||||
|
{d.sources.map((s) => (
|
||||||
|
<span
|
||||||
|
key={s}
|
||||||
|
className={`rounded-full px-2 py-0.5 text-xs ${SOURCE_COLORS[s] ?? "bg-slate-700 text-slate-300"}`}
|
||||||
|
>
|
||||||
|
{SOURCE_LABELS[s] ?? s}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{d.used_by && d.used_by.length > 0 && (
|
||||||
|
<span className="text-xs text-slate-500">{d.used_by.join(", ")}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-slate-400 text-xs">
|
||||||
{formatBytes(d.used_bytes)} / {formatBytes(d.total_bytes)}
|
{formatBytes(d.used_bytes)} / {formatBytes(d.total_bytes)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,9 +23,14 @@ function DiskUsageItem({ disk }: { disk: SystemStatus["disks"][number] }) {
|
|||||||
<div className="mb-1 flex flex-wrap items-center justify-between gap-x-3 gap-y-1 text-sm">
|
<div className="mb-1 flex flex-wrap items-center justify-between gap-x-3 gap-y-1 text-sm">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-slate-300">{disk.path}</span>
|
<span className="text-slate-300">{disk.path}</span>
|
||||||
<span className={`rounded-full px-2 py-0.5 text-xs ${SOURCE_COLORS[disk.source] ?? "bg-slate-700 text-slate-300"}`}>
|
{disk.sources.map((s) => (
|
||||||
{SOURCE_LABELS[disk.source] ?? disk.source}
|
<span
|
||||||
</span>
|
key={s}
|
||||||
|
className={`rounded-full px-2 py-0.5 text-xs ${SOURCE_COLORS[s] ?? "bg-slate-700 text-slate-300"}`}
|
||||||
|
>
|
||||||
|
{SOURCE_LABELS[s] ?? s}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
{disk.used_by && disk.used_by.length > 0 && (
|
{disk.used_by && disk.used_by.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{disk.used_by.map((name) => (
|
{disk.used_by.map((name) => (
|
||||||
@@ -125,8 +130,8 @@ export default function Settings() {
|
|||||||
setWatched(prev => prev.filter(m => m.id !== id));
|
setWatched(prev => prev.filter(m => m.id !== id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const manualDisks = status?.disks.filter(d => d.source === "manual") ?? [];
|
const manualDisks = status?.disks.filter(d => d.sources.includes("manual")) ?? [];
|
||||||
const shareDisks = status?.disks.filter(d => d.source === "samba" || d.source === "nfs") ?? [];
|
const shareDisks = status?.disks.filter(d => d.sources.includes("samba") || d.sources.includes("nfs")) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user