fixes and improvements

This commit is contained in:
2026-05-06 23:37:19 +02:00
parent b0d84399ab
commit b789034bbe
6 changed files with 349 additions and 311 deletions
+219 -172
View File
@@ -1,5 +1,5 @@
import { useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router-dom";
import { DataGrid } from "@mui/x-data-grid";
import type { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
import {
@@ -15,6 +15,7 @@ import {
MenuItem,
Select,
Stack,
Tab,
TextField,
Typography,
useMediaQuery,
@@ -27,6 +28,8 @@ import {
} from "../hooks/useFiles";
import { usePersistentState } from "../hooks/usePersistentState";
import { useMonitoringSettings } from "../hooks/useSettings";
import { SectionCard } from "../components/SectionCard";
import { TabbedCard } from "../components/TabbedCard";
interface DisplayRow {
id: string;
@@ -596,6 +599,7 @@ export function FileBrowser() {
);
const { currentDir, pathInput, selectedPath, selectedJob } = browserState;
const selectedMachineId = searchParams.get("machine_id") || initialMachineId;
const navigateToSettings = useNavigate();
const updateBrowserState = (patch: Partial<FileBrowserState>) =>
setBrowserState((current) => ({ ...current, ...patch }));
@@ -685,194 +689,237 @@ export function FileBrowser() {
const selectedTemplate = templates?.find((t) => t.key === selectedJob);
return (
<Stack spacing={2}>
<Stack spacing={2.25}>
<Stack
direction="row"
spacing={1}
sx={{ alignItems: "center", flexWrap: "wrap" }}
>
<Typography variant="h5">File Browser</Typography>
<FormControl size="small" sx={{ minWidth: 220 }}>
<InputLabel>Machine</InputLabel>
<Select
label="Machine"
value={selectedMachineId}
onChange={(e) => setMachine(String(e.target.value))}
>
{fileMachines.map((machine) => (
<MenuItem key={machine.id} value={machine.id}>
{machine.name} · {machine.mode}
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
<Stack direction={isMobile ? "column" : "row"} spacing={1}>
<TextField
fullWidth
size="small"
label="Remote path"
value={pathInput}
onChange={(e) => updateBrowserState({ pathInput: e.target.value })}
onKeyDown={handlePathSubmit}
/>
<Button
fullWidth={isMobile}
variant="outlined"
onClick={() => navigate(pathInput || "/")}
>
Open
</Button>
<Button
fullWidth={isMobile}
variant="outlined"
onClick={() => refetch()}
>
Refresh
</Button>
</Stack>
<Typography variant="caption" color="text.secondary">
Current: {currentDir}{" "}
{selectedPath ? `| Selected: ${selectedPath}` : ""}{" "}
{listing ? `| Entries: ${listing.count}` : ""}
</Typography>
{error && <Alert severity="error">{String(error)}</Alert>}
<Box
sx={{
height: 420,
bgcolor: "background.paper",
border: 1,
borderColor: "divider",
borderRadius: 2,
}}
>
<DataGrid
rows={rows}
columns={columns}
loading={isLoading}
rowSelectionModel={rowSelectionModel}
columnVisibilityModel={
isMobile ? { ext: false, modified: false } : undefined
<Chip
label={
fileMachines.length
? `${fileMachines.length} machine${fileMachines.length === 1 ? "" : "s"}`
: "No file machines"
}
hideFooter
sx={{
"& .MuiDataGrid-columnHeaders": {
fontWeight: 700,
backgroundColor: "action.hover",
},
}}
onRowClick={(params) => {
const row = params.row as DisplayRow;
if (row.type === "dir" || row.type === "up") navigate(row.path);
else
updateBrowserState({
selectedPath: row.path,
currentDir,
pathInput: row.path,
});
}}
variant="outlined"
size="small"
/>
</Box>
</Stack>
{selectedPath && isVideoFile(selectedPath) && (
<Card variant="outlined">
<CardContent>
{ffprobeError ? (
<Alert severity="error" sx={{ mb: 2 }}>
{String(ffprobeError)}
</Alert>
) : ffprobeLoading && !ffprobeData ? (
<Typography color="text.secondary">
Loading ffprobe data...
</Typography>
) : ffprobeData ? (
<FfprobeDetails
path={selectedPath}
data={ffprobeData as FfprobeData}
/>
) : (
<Typography color="text.secondary">
No ffprobe data available.
</Typography>
)}
</CardContent>
</Card>
)}
{selectedPath && templates && templates.length > 0 && (
<Card variant="outlined">
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 1 }}>
Jobs
</Typography>
<Grid container spacing={1.5}>
<Grid size={{ xs: 12, md: 4 }}>
<FormControl fullWidth size="small">
<InputLabel>Job template</InputLabel>
<Select
label="Job template"
value={selectedJob}
onChange={(e) =>
updateBrowserState({ selectedJob: e.target.value })
}
>
{templates.map((tpl) => (
<MenuItem key={tpl.key} value={tpl.key}>
{tpl.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid size={{ xs: 12, md: 8 }}>
<TabbedCard
value={fileMachines.length > 0 ? selectedMachineId : ""}
onChange={setMachine}
tabs={fileMachines.map((machine) => (
<Tab key={machine.id} value={machine.id} label={`${machine.name} · ${machine.mode}`} />
))}
>
{fileMachines.length > 0 ? (
<Stack spacing={2}>
<SectionCard
title="Browser"
description="Read-only listing with explicit open/select actions."
>
<Stack spacing={1.5}>
<Stack direction={isMobile ? "column" : "row"} spacing={1}>
<Button
variant="contained"
disabled={!selectedJob || runJob.isPending}
onClick={() =>
runJob.mutate({ jobKey: selectedJob, path: selectedPath })
<TextField
fullWidth
size="small"
label="Remote path"
value={pathInput}
onChange={(e) =>
updateBrowserState({ pathInput: e.target.value })
}
onKeyDown={handlePathSubmit}
/>
<Button
fullWidth={isMobile}
variant="outlined"
onClick={() => navigate(pathInput || "/")}
>
Run job
Open
</Button>
{selectedTemplate && (
<Typography
variant="body2"
color="text.secondary"
sx={{ alignSelf: "center" }}
<Button
fullWidth={isMobile}
variant="outlined"
onClick={() => refetch()}
>
Refresh
</Button>
</Stack>
<Typography variant="caption" color="text.secondary">
Current: {currentDir}{" "}
{selectedPath ? `| Selected: ${selectedPath}` : ""}{" "}
{listing ? `| Entries: ${listing.count}` : ""}
</Typography>
{error && <Alert severity="error">{String(error)}</Alert>}
<Box
sx={{
height: 420,
bgcolor: "background.paper",
border: 1,
borderColor: "divider",
borderRadius: 2,
}}
>
<DataGrid
rows={rows}
columns={columns}
loading={isLoading}
rowSelectionModel={rowSelectionModel}
columnVisibilityModel={
isMobile ? { ext: false, modified: false } : undefined
}
hideFooter
sx={{
"& .MuiDataGrid-columnHeaders": {
fontWeight: 700,
backgroundColor: "action.hover",
},
}}
onRowClick={(params) => {
const row = params.row as DisplayRow;
if (row.type === "dir" || row.type === "up")
navigate(row.path);
else
updateBrowserState({
selectedPath: row.path,
currentDir,
pathInput: row.path,
});
}}
/>
</Box>
</Stack>
</SectionCard>
<SectionCard
title="Media info"
description="ffprobe metadata for the selected media file."
>
{selectedPath ? (
isVideoFile(selectedPath) ? (
ffprobeError ? (
<Alert severity="error">{String(ffprobeError)}</Alert>
) : ffprobeLoading && !ffprobeData ? (
<Alert severity="info">Loading ffprobe data...</Alert>
) : ffprobeData ? (
<FfprobeDetails
path={selectedPath}
data={ffprobeData as FfprobeData}
/>
) : (
<Alert severity="info">No ffprobe data available.</Alert>
)
) : (
<Alert severity="info">
Select a video file to view ffprobe details.
</Alert>
)
) : (
<Alert severity="info">
Select a file in Browser to view ffprobe details.
</Alert>
)}
</SectionCard>
<SectionCard
title="Jobs"
description="Run predefined safe jobs against the selected file."
>
{selectedPath && templates && templates.length > 0 ? (
<Stack spacing={1.5}>
<Grid container spacing={1.5}>
<Grid size={{ xs: 12, md: 4 }}>
<FormControl fullWidth size="small">
<InputLabel>Job template</InputLabel>
<Select
label="Job template"
value={selectedJob}
onChange={(e) =>
updateBrowserState({ selectedJob: e.target.value })
}
>
{templates.map((tpl) => (
<MenuItem key={tpl.key} value={tpl.key}>
{tpl.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid size={{ xs: 12, md: 8 }}>
<Stack
direction={isMobile ? "column" : "row"}
spacing={1}
>
<Button
variant="contained"
disabled={!selectedJob || runJob.isPending}
onClick={() =>
runJob.mutate({
jobKey: selectedJob,
path: selectedPath,
})
}
>
Run job
</Button>
{selectedTemplate && (
<Typography
variant="body2"
color="text.secondary"
sx={{ alignSelf: "center" }}
>
{selectedTemplate.description}
</Typography>
)}
</Stack>
</Grid>
</Grid>
{runJob.data && (
<Box
component="pre"
sx={{
p: 1.5,
bgcolor: "action.hover",
overflow: "auto",
maxHeight: 260,
fontSize: 12,
borderRadius: 1,
}}
>
{selectedTemplate.description}
</Typography>
Exit: {runJob.data.exit_status}
{"\n"}
{runJob.data.stdout}
{runJob.data.stderr && `\nSTDERR: ${runJob.data.stderr}`}
</Box>
)}
</Stack>
</Grid>
</Grid>
{runJob.data && (
<Box
component="pre"
sx={{
mt: 1.5,
p: 1.5,
bgcolor: "action.hover",
overflow: "auto",
maxHeight: 260,
fontSize: 12,
}}
) : (
<Alert severity="info">
Select a file in Browser to run jobs.
</Alert>
)}
</SectionCard>
</Stack>
) : (
<Alert
severity="warning"
action={
<Button
color="inherit"
size="small"
onClick={() => navigateToSettings("/settings")}
>
Exit: {runJob.data.exit_status}
{"\n"}
{runJob.data.stdout}
{runJob.data.stderr && `\nSTDERR: ${runJob.data.stderr}`}
</Box>
)}
</CardContent>
</Card>
)}
Open Settings
</Button>
}
>
No file-capable machines are configured yet.
</Alert>
)}
</TabbedCard>
</Stack>
);
}