fixes and improvements
This commit is contained in:
@@ -1,88 +1,37 @@
|
||||
import { Box, Divider, Grid, Stack, Typography } from "@mui/material";
|
||||
import { Stack } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
useCounts,
|
||||
useLibraries,
|
||||
useActivity,
|
||||
useMonitoringOverview,
|
||||
} from "../hooks/useDashboard";
|
||||
import { useActivity, useMonitoringOverview } from "../hooks/useDashboard";
|
||||
import { NowPlaying } from "../components/NowPlaying";
|
||||
import { MetricCard } from "../components/MetricCard";
|
||||
import { LibraryOverview } from "../components/LibraryOverview";
|
||||
import { MonitoringOverviewTable } from "../components/MonitoringOverviewTable";
|
||||
import { SectionCard } from "../components/SectionCard";
|
||||
|
||||
export function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
const { data: counts } = useCounts();
|
||||
const { data: libraries } = useLibraries();
|
||||
const { data: activity } = useActivity();
|
||||
const { data: monitoringOverview } = useMonitoringOverview();
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ mb: 1.5 }}>
|
||||
Activity
|
||||
</Typography>
|
||||
{activity && (
|
||||
<Stack spacing={2.25}>
|
||||
<SectionCard
|
||||
title="Jellyfin activity"
|
||||
description="Live sessions and idle users from Jellyfin."
|
||||
>
|
||||
{activity ? (
|
||||
<NowPlaying
|
||||
sessions={activity}
|
||||
onSelectSession={(session) =>
|
||||
navigate(`/users?user=${encodeURIComponent(session.user)}`)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
) : null}
|
||||
</SectionCard>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ mb: 1.5 }}>
|
||||
Monitoring Overview
|
||||
</Typography>
|
||||
<MonitoringOverviewTable overview={monitoringOverview} />
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ mb: 1.5 }}>
|
||||
Library Stats
|
||||
</Typography>
|
||||
{counts && (
|
||||
<Grid container spacing={2} sx={{ mb: 2, alignItems: "stretch" }}>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
label="Total"
|
||||
value={(
|
||||
counts.movies +
|
||||
counts.series +
|
||||
counts.episodes
|
||||
).toLocaleString()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
label="Movies"
|
||||
value={counts.movies.toLocaleString()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
label="Series"
|
||||
value={counts.series.toLocaleString()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
label="Episodes"
|
||||
value={counts.episodes.toLocaleString()}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{libraries && <LibraryOverview libraries={libraries} />}
|
||||
</Box>
|
||||
<SectionCard
|
||||
title="Monitoring overview"
|
||||
description="10-minute averages and fleet status across all configured machines."
|
||||
>
|
||||
<MonitoringOverviewTable overview={monitoringOverview} embedded />
|
||||
</SectionCard>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,80 +1,108 @@
|
||||
import { Alert, Button, Chip, Stack, Typography } from "@mui/material";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Alert, Button, Stack, Tab, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
useMonitoringMachines,
|
||||
useMonitoringPoller,
|
||||
} from "../hooks/useMonitoring";
|
||||
import { useMonitoringOverview } from "../hooks/useDashboard";
|
||||
import { MachineMonitoringSection } from "../components/MachineMonitoringSection";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { MonitoringOverviewTable } from "../components/MonitoringOverviewTable";
|
||||
import { SectionCard } from "../components/SectionCard";
|
||||
import { TabbedCard } from "../components/TabbedCard";
|
||||
|
||||
type MonitoringTab = "overview" | string;
|
||||
|
||||
export function Monitoring() {
|
||||
const { data: machines, isLoading, error } = useMonitoringMachines();
|
||||
const { data: machines = [], isLoading, error } = useMonitoringMachines();
|
||||
const { data: poller } = useMonitoringPoller();
|
||||
const { data: overview } = useMonitoringOverview();
|
||||
const navigate = useNavigate();
|
||||
const [tab, setTab] = useState<MonitoringTab>("overview");
|
||||
|
||||
const selectedMachine = useMemo(
|
||||
() => machines.find((machine) => machine.id === tab) ?? null,
|
||||
[machines, tab],
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={2.25}>
|
||||
<Stack spacing={0.5}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{ alignItems: "center", flexWrap: "wrap" }}
|
||||
>
|
||||
<Typography variant="h5">Monitoring</Typography>
|
||||
{poller && (
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color={poller.worker_running ? "success" : "default"}
|
||||
label={
|
||||
poller.worker_running
|
||||
? `Poller running · ${poller.interval_seconds}s`
|
||||
: "Poller stopped"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800 }}>
|
||||
Monitoring
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Each machine below is monitored with its own settings and collector
|
||||
state, and recent activity is gathered automatically by the backend.
|
||||
Review fleet health first, then switch to a machine-specific tab.
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{error && <Alert severity="error">{String(error)}</Alert>}
|
||||
{isLoading && (
|
||||
{error ? <Alert severity="error">{String(error)}</Alert> : null}
|
||||
{isLoading ? (
|
||||
<Alert severity="info">Loading monitoring machines...</Alert>
|
||||
)}
|
||||
{!isLoading && (machines?.length ?? 0) === 0 && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
action={
|
||||
<Button
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
Open Settings
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
No monitoring machines are configured yet.
|
||||
</Alert>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{machines?.map((machine) => (
|
||||
<Stack
|
||||
key={machine.id}
|
||||
spacing={2}
|
||||
sx={{
|
||||
p: 2,
|
||||
border: 1,
|
||||
borderColor: "divider",
|
||||
borderRadius: 2,
|
||||
bgcolor: "background.paper",
|
||||
}}
|
||||
>
|
||||
<MachineMonitoringSection machine={machine} />
|
||||
</Stack>
|
||||
))}
|
||||
<TabbedCard
|
||||
value={tab}
|
||||
onChange={setTab}
|
||||
tabs={[
|
||||
<Tab key="overview" value="overview" label="Overview" />,
|
||||
...machines.map((machine) => (
|
||||
<Tab key={machine.id} value={machine.id} label={machine.name} />
|
||||
)),
|
||||
]}
|
||||
contentSx={{ p: 1.5 }}
|
||||
>
|
||||
{tab === "overview" ? (
|
||||
<Stack spacing={1.5}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{ alignItems: "center", flexWrap: "wrap" }}
|
||||
>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
Fleet overview
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{poller?.worker_running
|
||||
? `Poller running · ${poller.interval_seconds}s`
|
||||
: "Poller stopped"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{!isLoading && machines.length === 0 ? (
|
||||
<Alert
|
||||
severity="warning"
|
||||
action={
|
||||
<Button
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
Open Settings
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
No monitoring machines are configured yet.
|
||||
</Alert>
|
||||
) : overview ? (
|
||||
<MonitoringOverviewTable overview={overview} embedded />
|
||||
) : (
|
||||
<Alert severity="info">Loading fleet overview...</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
) : selectedMachine ? (
|
||||
<SectionCard
|
||||
title={selectedMachine.name}
|
||||
description={
|
||||
selectedMachine.mode === "local"
|
||||
? "Local API host"
|
||||
: `${selectedMachine.username || "user"}@${selectedMachine.host || "host"}:${selectedMachine.port}`
|
||||
}
|
||||
>
|
||||
<MachineMonitoringSection machine={selectedMachine} />
|
||||
</SectionCard>
|
||||
) : null}
|
||||
</TabbedCard>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user