feat(storage): rename snapraid_content to snapraid_conf; clarify conf vs content file; add auto-detect and conf viewer
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
BINARY=nasctl
|
BINARY=nasctl
|
||||||
VERSION?=0.7.6
|
VERSION?=0.8.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
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Rename snapraid_content to snapraid_conf to reflect that this field
|
||||||
|
-- holds the path to the snapraid.conf TEXT configuration file,
|
||||||
|
-- not the binary snapraid.content database file.
|
||||||
|
ALTER TABLE storage_config RENAME COLUMN snapraid_content TO snapraid_conf;
|
||||||
@@ -97,7 +97,7 @@ type StorageConfig struct {
|
|||||||
MoverInplace bool `json:"mover_inplace"`
|
MoverInplace bool `json:"mover_inplace"`
|
||||||
MoverRsyncOptions string `json:"mover_rsync_options"`
|
MoverRsyncOptions string `json:"mover_rsync_options"`
|
||||||
MoverWarningThreshold int `json:"mover_warning_threshold"`
|
MoverWarningThreshold int `json:"mover_warning_threshold"`
|
||||||
SnapraidContent string `json:"snapraid_content"`
|
SnapraidConf string `json:"snapraid_conf"`
|
||||||
SnapraidDataDirs string `json:"snapraid_data_dirs"`
|
SnapraidDataDirs string `json:"snapraid_data_dirs"`
|
||||||
SnapraidParityDir string `json:"snapraid_parity_dir"`
|
SnapraidParityDir string `json:"snapraid_parity_dir"`
|
||||||
SnapraidScrubPlan int `json:"snapraid_scrub_plan"`
|
SnapraidScrubPlan int `json:"snapraid_scrub_plan"`
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func scanStorageConfig(row interface {
|
|||||||
&removeSource,
|
&removeSource,
|
||||||
&inplace,
|
&inplace,
|
||||||
&c.MoverRsyncOptions,
|
&c.MoverRsyncOptions,
|
||||||
&c.SnapraidContent,
|
&c.SnapraidConf,
|
||||||
&c.SnapraidDataDirs,
|
&c.SnapraidDataDirs,
|
||||||
&c.SnapraidParityDir,
|
&c.SnapraidParityDir,
|
||||||
&c.SnapraidScrubPlan,
|
&c.SnapraidScrubPlan,
|
||||||
@@ -36,7 +36,7 @@ func scanStorageConfig(row interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) GetStorageConfig() (StorageConfig, error) {
|
func (d *DB) GetStorageConfig() (StorageConfig, error) {
|
||||||
row := d.conn.QueryRow(`SELECT id, mover_source, mover_dest, mover_clean_macos, mover_remove_source, mover_inplace, mover_rsync_options, snapraid_content, snapraid_data_dirs, snapraid_parity_dir, snapraid_scrub_plan, mover_warning_threshold, updated_at FROM storage_config WHERE id = 1`)
|
row := d.conn.QueryRow(`SELECT id, mover_source, mover_dest, mover_clean_macos, mover_remove_source, mover_inplace, mover_rsync_options, snapraid_conf, snapraid_data_dirs, snapraid_parity_dir, snapraid_scrub_plan, mover_warning_threshold, updated_at FROM storage_config WHERE id = 1`)
|
||||||
c, err := scanStorageConfig(row)
|
c, err := scanStorageConfig(row)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return StorageConfig{ID: 1, MoverCleanMacOS: true, MoverRemoveSource: true, MoverInplace: true, SnapraidScrubPlan: 8}, nil
|
return StorageConfig{ID: 1, MoverCleanMacOS: true, MoverRemoveSource: true, MoverInplace: true, SnapraidScrubPlan: 8}, nil
|
||||||
@@ -51,11 +51,11 @@ func (d *DB) UpdateStorageConfig(c StorageConfig) error {
|
|||||||
_, err := d.conn.Exec(`
|
_, err := d.conn.Exec(`
|
||||||
UPDATE storage_config
|
UPDATE storage_config
|
||||||
SET mover_source=?, mover_dest=?, mover_clean_macos=?, mover_remove_source=?, mover_inplace=?,
|
SET mover_source=?, mover_dest=?, mover_clean_macos=?, mover_remove_source=?, mover_inplace=?,
|
||||||
mover_rsync_options=?, snapraid_content=?, snapraid_data_dirs=?, snapraid_parity_dir=?,
|
mover_rsync_options=?, snapraid_conf=?, snapraid_data_dirs=?, snapraid_parity_dir=?,
|
||||||
snapraid_scrub_plan=?, mover_warning_threshold=?, updated_at=datetime('now')
|
snapraid_scrub_plan=?, mover_warning_threshold=?, updated_at=datetime('now')
|
||||||
WHERE id=1`,
|
WHERE id=1`,
|
||||||
c.MoverSource, c.MoverDest, toInt(c.MoverCleanMacOS), toInt(c.MoverRemoveSource), toInt(c.MoverInplace),
|
c.MoverSource, c.MoverDest, toInt(c.MoverCleanMacOS), toInt(c.MoverRemoveSource), toInt(c.MoverInplace),
|
||||||
c.MoverRsyncOptions, c.SnapraidContent, c.SnapraidDataDirs, c.SnapraidParityDir,
|
c.MoverRsyncOptions, c.SnapraidConf, c.SnapraidDataDirs, c.SnapraidParityDir,
|
||||||
c.SnapraidScrubPlan, c.MoverWarningThreshold, c.ID,
|
c.SnapraidScrubPlan, c.MoverWarningThreshold, c.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -170,13 +170,13 @@ func (jm *JobManager) runJob(job db.StorageJob) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
sc := SnapraidConfig{
|
sc := SnapraidConfig{
|
||||||
Content: envcfg.SnapraidContent,
|
Conf: envcfg.SnapraidConf,
|
||||||
DataDirs: ParseSnapraidDataDirs(envcfg.SnapraidDataDirs),
|
DataDirs: ParseSnapraidDataDirs(envcfg.SnapraidDataDirs),
|
||||||
ParityDir: envcfg.SnapraidParityDir,
|
ParityDir: envcfg.SnapraidParityDir,
|
||||||
ScrubPlan: envcfg.SnapraidScrubPlan,
|
ScrubPlan: envcfg.SnapraidScrubPlan,
|
||||||
}
|
}
|
||||||
if err := ValidateSnapraidContent(sc.Content); err != nil {
|
if err := ValidateSnapraidConf(sc.Conf); err != nil {
|
||||||
jm.failJob(job.ID, -1, fmt.Sprintf("content validation: %v", err))
|
jm.failJob(job.ID, -1, fmt.Sprintf("conf validation: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if job.Kind == "snapraid_scrub" {
|
if job.Kind == "snapraid_scrub" {
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SnapraidConfig struct {
|
type SnapraidConfig struct {
|
||||||
Content string
|
Conf string
|
||||||
DataDirs []string
|
DataDirs []string
|
||||||
ParityDir string
|
ParityDir string
|
||||||
ScrubPlan int
|
ScrubPlan int
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildSnapraidArgs(kind string, cfg SnapraidConfig) ([]string, error) {
|
func BuildSnapraidArgs(kind string, cfg SnapraidConfig) ([]string, error) {
|
||||||
if cfg.Content == "" {
|
if cfg.Conf == "" {
|
||||||
return nil, fmt.Errorf("snapraid content file is required")
|
return nil, fmt.Errorf("snapraid conf file is required")
|
||||||
}
|
}
|
||||||
args := []string{"snapraid", "-c", cfg.Content}
|
args := []string{"snapraid", "-c", cfg.Conf}
|
||||||
switch kind {
|
switch kind {
|
||||||
case "snapraid_diff":
|
case "snapraid_diff":
|
||||||
args = append(args, "diff")
|
args = append(args, "diff")
|
||||||
@@ -34,16 +34,16 @@ func BuildSnapraidArgs(kind string, cfg SnapraidConfig) ([]string, error) {
|
|||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateSnapraidContent(content string) error {
|
func ValidateSnapraidConf(conf string) error {
|
||||||
if content == "" {
|
if conf == "" {
|
||||||
return fmt.Errorf("snapraid content file is required")
|
return fmt.Errorf("snapraid conf file is required")
|
||||||
}
|
}
|
||||||
info, err := os.Stat(content)
|
info, err := os.Stat(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("snapraid content %s: %w", content, err)
|
return fmt.Errorf("snapraid conf %s: %w", conf, err)
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return fmt.Errorf("snapraid content %s: is a directory, not a file", content)
|
return fmt.Errorf("snapraid conf %s: is a directory, not a file", conf)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,23 +10,23 @@ func TestBuildSnapraidArgs(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"snapraid_diff",
|
"snapraid_diff",
|
||||||
SnapraidConfig{Content: "/pool/snapraid.content"},
|
SnapraidConfig{Conf: "/etc/snapraid.conf"},
|
||||||
[]string{"snapraid", "-c", "/pool/snapraid.content", "diff"},
|
[]string{"snapraid", "-c", "/etc/snapraid.conf", "diff"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"snapraid_sync",
|
"snapraid_sync",
|
||||||
SnapraidConfig{Content: "/pool/snapraid.content"},
|
SnapraidConfig{Conf: "/etc/snapraid.conf"},
|
||||||
[]string{"snapraid", "-c", "/pool/snapraid.content", "sync"},
|
[]string{"snapraid", "-c", "/etc/snapraid.conf", "sync"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"snapraid_check",
|
"snapraid_check",
|
||||||
SnapraidConfig{Content: "/pool/snapraid.content"},
|
SnapraidConfig{Conf: "/etc/snapraid.conf"},
|
||||||
[]string{"snapraid", "-c", "/pool/snapraid.content", "check"},
|
[]string{"snapraid", "-c", "/etc/snapraid.conf", "check"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"snapraid_scrub",
|
"snapraid_scrub",
|
||||||
SnapraidConfig{Content: "/pool/snapraid.content", ScrubPlan: 8},
|
SnapraidConfig{Conf: "/etc/snapraid.conf", ScrubPlan: 8},
|
||||||
[]string{"snapraid", "-c", "/pool/snapraid.content", "scrub", "-p", "8"},
|
[]string{"snapraid", "-c", "/etc/snapraid.conf", "scrub", "-p", "8"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -48,10 +48,10 @@ func TestBuildSnapraidArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildSnapraidArgsNoContent(t *testing.T) {
|
func TestBuildSnapraidArgsNoConf(t *testing.T) {
|
||||||
_, err := BuildSnapraidArgs("snapraid_sync", SnapraidConfig{Content: ""})
|
_, err := BuildSnapraidArgs("snapraid_sync", SnapraidConfig{Conf: ""})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error for empty content")
|
t.Error("expected error for empty conf")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -31,9 +32,36 @@ func (s *Server) handleStorageGetConfig(w http.ResponseWriter, r *http.Request)
|
|||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if cfg.SnapraidConf == "" {
|
||||||
|
for _, path := range []string{"/etc/snapraid.conf", "/usr/local/etc/snapraid.conf"} {
|
||||||
|
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
||||||
|
cfg.SnapraidConf = path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusOK, cfg)
|
writeJSON(w, http.StatusOK, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleStorageGetConfFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg, err := s.DB.GetStorageConfig()
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cfg.SnapraidConf == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "snapraid conf path not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(cfg.SnapraidConf)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("read %s: %v", cfg.SnapraidConf, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleStorageUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleStorageUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
var cfg db.StorageConfig
|
var cfg db.StorageConfig
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
@@ -67,9 +95,9 @@ func (s *Server) handleStorageUpdateConfig(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg.SnapraidContent != "" {
|
if cfg.SnapraidConf != "" {
|
||||||
if err := validateAbsPath(cfg.SnapraidContent); err != nil {
|
if err := validateAbsPath(cfg.SnapraidConf); err != nil {
|
||||||
writeError(w, http.StatusBadRequest, fmt.Sprintf("snapraid_content: %v", err))
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("snapraid_conf: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ func NewRouter(s *Server) chi.Router {
|
|||||||
storageRouter.Get("/disk-usage", s.handleStorageDiskUsage)
|
storageRouter.Get("/disk-usage", s.handleStorageDiskUsage)
|
||||||
storageRouter.Get("/config", s.handleStorageGetConfig)
|
storageRouter.Get("/config", s.handleStorageGetConfig)
|
||||||
storageRouter.Put("/config", s.handleStorageUpdateConfig)
|
storageRouter.Put("/config", s.handleStorageUpdateConfig)
|
||||||
|
storageRouter.Get("/conf-file", s.handleStorageGetConfFile)
|
||||||
storageRouter.Route("/jobs", func(j chi.Router) {
|
storageRouter.Route("/jobs", func(j chi.Router) {
|
||||||
j.Get("/", s.handleStorageListJobs)
|
j.Get("/", s.handleStorageListJobs)
|
||||||
j.Post("/", s.handleStorageStartJob)
|
j.Post("/", s.handleStorageStartJob)
|
||||||
|
|||||||
+5
-1
@@ -170,7 +170,7 @@ export interface StorageConfig {
|
|||||||
mover_inplace: boolean;
|
mover_inplace: boolean;
|
||||||
mover_rsync_options: string;
|
mover_rsync_options: string;
|
||||||
mover_warning_threshold: number;
|
mover_warning_threshold: number;
|
||||||
snapraid_content: string;
|
snapraid_conf: string;
|
||||||
snapraid_data_dirs: string;
|
snapraid_data_dirs: string;
|
||||||
snapraid_parity_dir: string;
|
snapraid_parity_dir: string;
|
||||||
snapraid_scrub_plan: number;
|
snapraid_scrub_plan: number;
|
||||||
@@ -322,6 +322,10 @@ export const api = {
|
|||||||
request<StorageJob>("POST", "/storage/jobs", { kind, args }),
|
request<StorageJob>("POST", "/storage/jobs", { kind, args }),
|
||||||
cancelStorageJob: (id: number) => request<{ ok: boolean }>("POST", `/storage/jobs/${id}/cancel`),
|
cancelStorageJob: (id: number) => request<{ ok: boolean }>("POST", `/storage/jobs/${id}/cancel`),
|
||||||
storageJobStreamUrl: (id: number) => `/api/storage/jobs/${id}/stream`,
|
storageJobStreamUrl: (id: number) => `/api/storage/jobs/${id}/stream`,
|
||||||
|
getStorageConfFile: () => fetch(`/api/storage/conf-file`).then(r => {
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
|
||||||
|
return r.text();
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function formatBytes(bytes: number): string {
|
export function formatBytes(bytes: number): string {
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ export default function Storage() {
|
|||||||
const [scrubPlanInput, setScrubPlanInput] = useState("8");
|
const [scrubPlanInput, setScrubPlanInput] = useState("8");
|
||||||
const [dirtyConfig, setDirtyConfig] = useState(false);
|
const [dirtyConfig, setDirtyConfig] = useState(false);
|
||||||
const [pendingConfig, setPendingConfig] = useState<Partial<StorageConfig>>({});
|
const [pendingConfig, setPendingConfig] = useState<Partial<StorageConfig>>({});
|
||||||
const outputRef = useRef<HTMLPreElement>(null);
|
const [confModalOpen, setConfModalOpen] = useState(false);
|
||||||
|
const [confModalContent, setConfModalContent] = useState("");
|
||||||
|
const [confModalError, setConfModalError] = useState("");
|
||||||
|
const outputRef = useRef<HTMLPreElement | null>(null);
|
||||||
const esRef = useRef<EventSource | null>(null);
|
const esRef = useRef<EventSource | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -116,6 +119,18 @@ export default function Storage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openConfModal() {
|
||||||
|
setConfModalOpen(true);
|
||||||
|
setConfModalContent("");
|
||||||
|
setConfModalError("");
|
||||||
|
try {
|
||||||
|
const content = await api.getStorageConfFile();
|
||||||
|
setConfModalContent(content);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setConfModalError(e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function startJob(kind: JobKind) {
|
async function startJob(kind: JobKind) {
|
||||||
try {
|
try {
|
||||||
const job = await api.startStorageJob(kind);
|
const job = await api.startStorageJob(kind);
|
||||||
@@ -311,12 +326,35 @@ export default function Storage() {
|
|||||||
|
|
||||||
<fieldset className="space-y-3">
|
<fieldset className="space-y-3">
|
||||||
<legend className="text-sm font-medium text-slate-300">SnapRAID</legend>
|
<legend className="text-sm font-medium text-slate-300">SnapRAID</legend>
|
||||||
<Field
|
<div>
|
||||||
label="Content file"
|
<Field
|
||||||
value={mergedConfig.snapraid_content ?? ""}
|
label="Archivo de configuración"
|
||||||
onChange={(v) => handleConfigChange("snapraid_content", v)}
|
value={mergedConfig.snapraid_conf ?? ""}
|
||||||
placeholder="/mnt/pool/snapraid.content"
|
onChange={(v) => handleConfigChange("snapraid_conf", v)}
|
||||||
/>
|
placeholder="/etc/snapraid.conf"
|
||||||
|
/>
|
||||||
|
<div className="mt-1 flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-xs text-slate-500 underline hover:text-slate-300 disabled:opacity-40"
|
||||||
|
onClick={openConfModal}
|
||||||
|
disabled={!mergedConfig.snapraid_conf}
|
||||||
|
>
|
||||||
|
Ver contenido
|
||||||
|
</button>
|
||||||
|
{!mergedConfig.snapraid_conf && (
|
||||||
|
<span className="text-xs text-amber-400">
|
||||||
|
No configurado. Busca en{" "}
|
||||||
|
<code className="text-amber-300">/etc/snapraid.conf</code> o{" "}
|
||||||
|
<code className="text-amber-300">/usr/local/etc/snapraid.conf</code>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-slate-500">
|
||||||
|
Ruta al archivo <code className="text-slate-400">snapraid.conf</code>. Este es el archivo de configuración de texto,{" "}
|
||||||
|
<span className="text-amber-400">no el archivo binario .content</span>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<Field
|
<Field
|
||||||
label="Directorios de datos (CSV)"
|
label="Directorios de datos (CSV)"
|
||||||
value={mergedConfig.snapraid_data_dirs ?? ""}
|
value={mergedConfig.snapraid_data_dirs ?? ""}
|
||||||
@@ -503,6 +541,32 @@ export default function Storage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Conf file viewer modal */}
|
||||||
|
{confModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||||
|
<div className="w-full max-w-2xl rounded-lg border border-slate-600 bg-slate-800 p-4 shadow-xl">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium text-slate-200">
|
||||||
|
{mergedConfig.snapraid_conf ?? "snapraid.conf"}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
className="text-slate-400 hover:text-white"
|
||||||
|
onClick={() => setConfModalOpen(false)}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{confModalError ? (
|
||||||
|
<p className="text-sm text-red-400">{confModalError}</p>
|
||||||
|
) : (
|
||||||
|
<pre className="max-h-96 overflow-auto rounded bg-slate-900 p-3 text-xs text-slate-300 font-mono">
|
||||||
|
{confModalContent || "Cargando..."}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user