fixes and improvements
This commit is contained in:
@@ -48,15 +48,12 @@ function formatRate(bytes: number): string {
|
||||
return `${formatBytes(bytes)}/s`;
|
||||
}
|
||||
|
||||
function formatTime(epochSeconds: number | null): string {
|
||||
function formatAge(epochSeconds: number | null): string {
|
||||
if (!epochSeconds) return "-";
|
||||
const date = new Date(epochSeconds * 1000);
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(date.getDate()).padStart(2, "0");
|
||||
const hh = String(date.getHours()).padStart(2, "0");
|
||||
const min = String(date.getMinutes()).padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd} ${hh}:${min}`;
|
||||
const diff = Date.now() / 1000 - epochSeconds;
|
||||
if (diff < 60) return `${Math.max(0, Math.round(diff))}s ago`;
|
||||
if (diff < 3600) return `${Math.round(diff / 60)}m ago`;
|
||||
return `${Math.round(diff / 3600)}h ago`;
|
||||
}
|
||||
|
||||
function formatReadableTime(epochSeconds: number | null): string {
|
||||
@@ -89,22 +86,15 @@ function formatUpdatedDetails(epochSeconds: number | null): [string, string] {
|
||||
];
|
||||
}
|
||||
|
||||
function formatAge(epochSeconds: number | null): string {
|
||||
if (!epochSeconds) return "-";
|
||||
const diff = Date.now() / 1000 - epochSeconds;
|
||||
if (diff < 60) return `${Math.max(0, Math.round(diff))}s ago`;
|
||||
if (diff < 3600) return `${Math.round(diff / 60)}m ago`;
|
||||
return `${Math.round(diff / 3600)}h ago`;
|
||||
}
|
||||
|
||||
function formatSummary(
|
||||
summary: { avg: number; min: number; max: number } | null,
|
||||
formatter: (value: number) => string,
|
||||
) {
|
||||
if (!summary) return { value: "-", subtext: "" };
|
||||
if (!summary) return { value: "-", min: "", max: "" };
|
||||
return {
|
||||
value: formatter(summary.avg),
|
||||
subtext: `Low ${formatter(summary.min)}\nHigh ${formatter(summary.max)}`,
|
||||
min: formatter(summary.min),
|
||||
max: formatter(summary.max),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,19 +136,25 @@ function metricSortValue(
|
||||
}
|
||||
}
|
||||
|
||||
function metricCell(value: string, subtext?: string) {
|
||||
const [minLine, maxLine] = (subtext || "").split("\n");
|
||||
const min = minLine?.replace(/^Low\s+/, "").trim();
|
||||
const max = maxLine?.replace(/^High\s+/, "").trim();
|
||||
function MetricCell({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
}: {
|
||||
value: string;
|
||||
min?: string;
|
||||
max?: string;
|
||||
}) {
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
width: "100%",
|
||||
minWidth: 110,
|
||||
minWidth: 0,
|
||||
height: "100%",
|
||||
minHeight: 96,
|
||||
minHeight: 118,
|
||||
textAlign: "center",
|
||||
py: 0.4,
|
||||
justifyContent: "space-between",
|
||||
py: 0.5,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -168,16 +164,17 @@ function metricCell(value: string, subtext?: string) {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
px: 1.5,
|
||||
py: 1.15,
|
||||
py: 1.25,
|
||||
minHeight: 68,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
>
|
||||
<Stack spacing={0.1} sx={{ alignItems: "center" }}>
|
||||
<Stack spacing={0.25} sx={{ alignItems: "center" }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontSize: "0.62rem",
|
||||
fontSize: "0.6rem",
|
||||
lineHeight: 1,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.04em",
|
||||
@@ -189,7 +186,7 @@ function metricCell(value: string, subtext?: string) {
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 900,
|
||||
fontSize: "1.14rem",
|
||||
fontSize: "1.12rem",
|
||||
lineHeight: 1,
|
||||
textAlign: "center",
|
||||
whiteSpace: "nowrap",
|
||||
@@ -200,46 +197,48 @@ function metricCell(value: string, subtext?: string) {
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack
|
||||
spacing={0.25}
|
||||
sx={{
|
||||
width: "100%",
|
||||
pt: 0.1,
|
||||
}}
|
||||
>
|
||||
<Stack spacing={0.25} sx={{ width: "100%" }}>
|
||||
{min ? (
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
label={`Min ${min}`}
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 18,
|
||||
"& .MuiChip-label": {
|
||||
px: 0.5,
|
||||
py: 0,
|
||||
fontSize: "0.6rem",
|
||||
lineHeight: 1,
|
||||
},
|
||||
border: 1,
|
||||
borderColor: "divider",
|
||||
borderRadius: 999,
|
||||
px: 0.75,
|
||||
py: 0.15,
|
||||
fontSize: "0.6rem",
|
||||
lineHeight: 1.2,
|
||||
color: "text.secondary",
|
||||
textAlign: "center",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Min {min}
|
||||
</Box>
|
||||
) : null}
|
||||
{max ? (
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
label={`Max ${max}`}
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 18,
|
||||
"& .MuiChip-label": {
|
||||
px: 0.5,
|
||||
py: 0,
|
||||
fontSize: "0.6rem",
|
||||
lineHeight: 1,
|
||||
},
|
||||
border: 1,
|
||||
borderColor: "divider",
|
||||
borderRadius: 999,
|
||||
px: 0.75,
|
||||
py: 0.15,
|
||||
fontSize: "0.6rem",
|
||||
lineHeight: 1.2,
|
||||
color: "text.secondary",
|
||||
textAlign: "center",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Max {max}
|
||||
</Box>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -302,8 +301,11 @@ export function MonitoringOverviewTable({
|
||||
label={`Machines: ${overview?.enabled ?? 0}/${overview?.total ?? 0}`}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Last success: {formatTime(poller?.last_success_at ?? null)} · last
|
||||
run: {formatAge(poller?.last_run_at ?? null)}
|
||||
Last success:{" "}
|
||||
{poller?.last_success_at
|
||||
? new Date(poller.last_success_at * 1000).toLocaleString()
|
||||
: "-"}{" "}
|
||||
· last run: {formatAge(poller?.last_run_at ?? null)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : null}
|
||||
@@ -326,13 +328,16 @@ export function MonitoringOverviewTable({
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden",
|
||||
}
|
||||
: undefined
|
||||
: {
|
||||
maxWidth: "100%",
|
||||
overflowX: "auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Table
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: 1280,
|
||||
minWidth: 1500,
|
||||
tableLayout: "fixed",
|
||||
"& .MuiTableCell-root": {
|
||||
px: 1.1,
|
||||
@@ -350,6 +355,7 @@ export function MonitoringOverviewTable({
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ width: 260 }}
|
||||
sortDirection={sortKey === "machine" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
@@ -361,6 +367,7 @@ export function MonitoringOverviewTable({
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ width: 90 }}
|
||||
sortDirection={sortKey === "mode" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
@@ -372,6 +379,7 @@ export function MonitoringOverviewTable({
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ width: 120 }}
|
||||
sortDirection={sortKey === "status" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
@@ -382,103 +390,33 @@ export function MonitoringOverviewTable({
|
||||
Status
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sortDirection={sortKey === "cpu" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={sortKey === "cpu"}
|
||||
direction={sortKey === "cpu" ? sortDirection : "asc"}
|
||||
onClick={() => setSort("cpu")}
|
||||
{[
|
||||
["cpu", "CPU"],
|
||||
["iowait", "IO wait"],
|
||||
["mem", "RAM"],
|
||||
["net_rx", "Net down"],
|
||||
["net_tx", "Net up"],
|
||||
["disk_read", "Disk read"],
|
||||
["disk_write", "Disk write"],
|
||||
["disk_used", "Disk used"],
|
||||
].map(([key, label]) => (
|
||||
<TableCell
|
||||
key={key}
|
||||
align="center"
|
||||
sx={{ width: 150 }}
|
||||
sortDirection={sortKey === key ? sortDirection : false}
|
||||
>
|
||||
CPU
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sortDirection={sortKey === "iowait" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={sortKey === "iowait"}
|
||||
direction={sortKey === "iowait" ? sortDirection : "asc"}
|
||||
onClick={() => setSort("iowait")}
|
||||
>
|
||||
IO wait
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sortDirection={sortKey === "mem" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={sortKey === "mem"}
|
||||
direction={sortKey === "mem" ? sortDirection : "asc"}
|
||||
onClick={() => setSort("mem")}
|
||||
>
|
||||
RAM
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sortDirection={sortKey === "net_rx" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={sortKey === "net_rx"}
|
||||
direction={sortKey === "net_rx" ? sortDirection : "asc"}
|
||||
onClick={() => setSort("net_rx")}
|
||||
>
|
||||
Net down
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sortDirection={sortKey === "net_tx" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={sortKey === "net_tx"}
|
||||
direction={sortKey === "net_tx" ? sortDirection : "asc"}
|
||||
onClick={() => setSort("net_tx")}
|
||||
>
|
||||
Net up
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sortDirection={sortKey === "disk_read" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={sortKey === "disk_read"}
|
||||
direction={sortKey === "disk_read" ? sortDirection : "asc"}
|
||||
onClick={() => setSort("disk_read")}
|
||||
>
|
||||
Disk read
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sortDirection={sortKey === "disk_write" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={sortKey === "disk_write"}
|
||||
direction={sortKey === "disk_write" ? sortDirection : "asc"}
|
||||
onClick={() => setSort("disk_write")}
|
||||
>
|
||||
Disk write
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sortDirection={sortKey === "disk_used" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={sortKey === "disk_used"}
|
||||
direction={sortKey === "disk_used" ? sortDirection : "asc"}
|
||||
onClick={() => setSort("disk_used")}
|
||||
>
|
||||
Disk used
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableSortLabel
|
||||
active={sortKey === key}
|
||||
direction={sortKey === key ? sortDirection : "asc"}
|
||||
onClick={() => setSort(key as SortKey)}
|
||||
>
|
||||
{label}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell
|
||||
sx={{ width: 180 }}
|
||||
sortDirection={sortKey === "updated" ? sortDirection : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
@@ -526,16 +464,39 @@ export function MonitoringOverviewTable({
|
||||
row.disk_write_summary,
|
||||
formatRate,
|
||||
);
|
||||
const disk = row.disk
|
||||
? {
|
||||
value: row.disk.used_pct,
|
||||
min: `Used ${formatBytes(row.disk.used)}`,
|
||||
max: `Avail ${formatBytes(row.disk.available)}`,
|
||||
}
|
||||
: null;
|
||||
const updated = formatUpdatedDetails(
|
||||
row.latest_sample?.ts ?? null,
|
||||
);
|
||||
return (
|
||||
<TableRow key={machine.id}>
|
||||
<TableCell>
|
||||
<Stack spacing={0.25}>
|
||||
<TableCell sx={{ width: 260 }}>
|
||||
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={0.75}
|
||||
sx={{ alignItems: "center", flexWrap: "wrap" }}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
flexWrap: "nowrap",
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{machine.name}
|
||||
</Typography>
|
||||
{!machine.enabled && (
|
||||
@@ -546,15 +507,24 @@ export function MonitoringOverviewTable({
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{machine.mode === "local"
|
||||
? "Local API host"
|
||||
: `${machine.username || "user"}@${machine.host || "host"}:${machine.port}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>{machine.mode}</TableCell>
|
||||
<TableCell>
|
||||
<TableCell sx={{ width: 90 }}>{machine.mode}</TableCell>
|
||||
<TableCell sx={{ width: 120 }}>
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@@ -563,39 +533,68 @@ export function MonitoringOverviewTable({
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{metricCell(cpu.value, cpu.subtext)}
|
||||
<MetricCell
|
||||
value={cpu.value}
|
||||
min={cpu.min}
|
||||
max={cpu.max}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{metricCell(iowait.value, iowait.subtext)}
|
||||
<MetricCell
|
||||
value={iowait.value}
|
||||
min={iowait.min}
|
||||
max={iowait.max}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{metricCell(mem.value, mem.subtext)}
|
||||
<MetricCell
|
||||
value={mem.value}
|
||||
min={mem.min}
|
||||
max={mem.max}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{metricCell(netDown.value, netDown.subtext)}
|
||||
<MetricCell
|
||||
value={netDown.value}
|
||||
min={netDown.min}
|
||||
max={netDown.max}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{metricCell(netUp.value, netUp.subtext)}
|
||||
<MetricCell
|
||||
value={netUp.value}
|
||||
min={netUp.min}
|
||||
max={netUp.max}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{metricCell(diskRead.value, diskRead.subtext)}
|
||||
<MetricCell
|
||||
value={diskRead.value}
|
||||
min={diskRead.min}
|
||||
max={diskRead.max}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{metricCell(diskWrite.value, diskWrite.subtext)}
|
||||
<MetricCell
|
||||
value={diskWrite.value}
|
||||
min={diskWrite.min}
|
||||
max={diskWrite.max}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{row.disk
|
||||
? metricCell(
|
||||
row.disk.used_pct,
|
||||
`Used ${formatBytes(row.disk.used)}\nAvail ${formatBytes(row.disk.available)}`,
|
||||
)
|
||||
: "-"}
|
||||
{disk ? (
|
||||
<MetricCell
|
||||
value={disk.value}
|
||||
min={disk.min}
|
||||
max={disk.max}
|
||||
/>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack spacing={0.15} sx={{ alignItems: "center" }}>
|
||||
{formatUpdatedDetails(
|
||||
row.latest_sample?.ts ?? null,
|
||||
).map((line) => (
|
||||
<TableCell sx={{ width: 180 }}>
|
||||
<Stack spacing={0.1} sx={{ alignItems: "center" }}>
|
||||
{updated.map((line) => (
|
||||
<Typography
|
||||
key={line}
|
||||
variant="caption"
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
Stack,
|
||||
MenuItem,
|
||||
Tab,
|
||||
TextField,
|
||||
Typography,
|
||||
@@ -33,7 +34,6 @@ import { HoverEditButton } from "../components/HoverEditButton";
|
||||
import { SectionCard } from "../components/SectionCard";
|
||||
import { SelectionRailCard } from "../components/SelectionRailCard";
|
||||
import { TabbedCard } from "../components/TabbedCard";
|
||||
|
||||
const SERVICE_OPTIONS = [
|
||||
{ value: "monitoring", label: "Monitoring" },
|
||||
{ value: "files", label: "Files" },
|
||||
@@ -41,9 +41,7 @@ const SERVICE_OPTIONS = [
|
||||
{ value: "jellyseerr", label: "Jellyseerr" },
|
||||
{ value: "nextcloud", label: "Nextcloud" },
|
||||
];
|
||||
|
||||
type SettingsTab = "machines" | "ssh-keys" | "danger";
|
||||
|
||||
function emptyMachine(
|
||||
mode: MonitoringMachineInput["mode"] = "local",
|
||||
): MonitoringMachineInput {
|
||||
@@ -72,7 +70,6 @@ function emptyMachine(
|
||||
notes: "",
|
||||
};
|
||||
}
|
||||
|
||||
function MachineEditor({
|
||||
title,
|
||||
hint,
|
||||
@@ -97,7 +94,6 @@ function MachineEditor({
|
||||
const enabledServices = draft.services.length;
|
||||
const hasJellyfin = draft.services.includes("jellyfin");
|
||||
const hasJellyseerr = draft.services.includes("jellyseerr");
|
||||
|
||||
return (
|
||||
<Card variant="outlined">
|
||||
<CardContent sx={{ p: 1.5 }}>
|
||||
@@ -139,7 +135,6 @@ function MachineEditor({
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Grid container spacing={1.25}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
@@ -239,11 +234,12 @@ function MachineEditor({
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
size="small"
|
||||
label="SSH key id"
|
||||
label="SSH key"
|
||||
value={draft.ssh_key_id}
|
||||
onChange={(e) =>
|
||||
setDraft((current) => ({
|
||||
@@ -251,7 +247,19 @@ function MachineEditor({
|
||||
ssh_key_id: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
helperText={
|
||||
sshKeys.length > 0
|
||||
? "Select a saved SSH key."
|
||||
: "No SSH keys are saved yet. Add one in the SSH Keys tab."
|
||||
}
|
||||
>
|
||||
<MenuItem value="">No key selected</MenuItem>
|
||||
{sshKeys.map((key) => (
|
||||
<MenuItem key={key.id} value={key.id}>
|
||||
{key.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
@@ -387,15 +395,23 @@ function MachineEditor({
|
||||
/>
|
||||
</Grid>{" "}
|
||||
</Grid>
|
||||
|
||||
{selectedSSHKey && (
|
||||
{selectedSSHKey ? (
|
||||
<Alert severity="info">
|
||||
Selected key: {selectedSSHKey.name}
|
||||
{selectedSSHKey.fingerprint
|
||||
? ` · ${selectedSSHKey.fingerprint}`
|
||||
: ""}
|
||||
</Alert>
|
||||
)}
|
||||
) : !isLocal && sshKeys.length === 0 ? (
|
||||
<Alert severity="warning">
|
||||
No SSH keys have been saved yet. Add one before configuring SSH
|
||||
machines.
|
||||
</Alert>
|
||||
) : draft.ssh_key_id ? (
|
||||
<Alert severity="warning">
|
||||
The selected SSH key was not found.
|
||||
</Alert>
|
||||
) : null}
|
||||
{!isLocal && !hasJellyfin && (
|
||||
<Alert severity="warning">
|
||||
SSH machines usually need monitoring or files enabled.
|
||||
@@ -411,7 +427,6 @@ function MachineEditor({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function SSHKeyManager({ sshKeys }: { sshKeys: SSHKey[] }) {
|
||||
const saveKey = useSaveSSHKey();
|
||||
const generateKey = useGenerateSSHKey();
|
||||
@@ -420,20 +435,22 @@ function SSHKeyManager({ sshKeys }: { sshKeys: SSHKey[] }) {
|
||||
id: null,
|
||||
name: "",
|
||||
private_key: "",
|
||||
public_key: "",
|
||||
fingerprint: "",
|
||||
passphrase: "",
|
||||
notes: "",
|
||||
});
|
||||
const editing = Boolean(draft.id);
|
||||
|
||||
const clear = () =>
|
||||
setDraft({
|
||||
id: null,
|
||||
name: "",
|
||||
private_key: "",
|
||||
passphrase: "",
|
||||
public_key: "",
|
||||
fingerprint: "",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
return (
|
||||
<Card variant="outlined">
|
||||
<CardContent sx={{ p: 1.5 }}>
|
||||
@@ -461,7 +478,6 @@ function SSHKeyManager({ sshKeys }: { sshKeys: SSHKey[] }) {
|
||||
label={`${sshKeys.length} saved`}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Grid container spacing={1.25}>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
@@ -524,7 +540,6 @@ function SSHKeyManager({ sshKeys }: { sshKeys: SSHKey[] }) {
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Stack direction="row" spacing={1} sx={{ flexWrap: "wrap" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -552,6 +567,8 @@ function SSHKeyManager({ sshKeys }: { sshKeys: SSHKey[] }) {
|
||||
name: generated.name || current.name,
|
||||
private_key: generated.private_key,
|
||||
passphrase: generated.passphrase,
|
||||
public_key: generated.public_key,
|
||||
fingerprint: generated.fingerprint,
|
||||
notes: generated.notes,
|
||||
}));
|
||||
}}
|
||||
@@ -562,11 +579,9 @@ function SSHKeyManager({ sshKeys }: { sshKeys: SSHKey[] }) {
|
||||
Clear
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{saveKey.error && (
|
||||
<Alert severity="error">{String(saveKey.error)}</Alert>
|
||||
)}
|
||||
|
||||
{sshKeys.length > 0 ? (
|
||||
<Grid container spacing={1.25}>
|
||||
{sshKeys.map((key) => (
|
||||
@@ -658,6 +673,8 @@ function SSHKeyManager({ sshKeys }: { sshKeys: SSHKey[] }) {
|
||||
name: key.name,
|
||||
private_key: "",
|
||||
passphrase: "",
|
||||
public_key: key.public_key,
|
||||
fingerprint: key.fingerprint,
|
||||
notes: key.notes,
|
||||
})
|
||||
}
|
||||
@@ -686,7 +703,6 @@ function SSHKeyManager({ sshKeys }: { sshKeys: SSHKey[] }) {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ResetLocalDatabaseCard() {
|
||||
const resetDatabase = useResetLocalDatabase();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -699,7 +715,6 @@ function ResetLocalDatabaseCard() {
|
||||
ackSettings &&
|
||||
ackIndex &&
|
||||
ackIrreversible;
|
||||
|
||||
const close = () => {
|
||||
setOpen(false);
|
||||
setPhrase("");
|
||||
@@ -707,7 +722,6 @@ function ResetLocalDatabaseCard() {
|
||||
setAckIndex(false);
|
||||
setAckIrreversible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card variant="outlined">
|
||||
@@ -745,7 +759,6 @@ function ResetLocalDatabaseCard() {
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={open} onClose={close} fullWidth maxWidth="sm">
|
||||
<DialogTitle>Reset local database</DialogTitle>
|
||||
<DialogContent>
|
||||
@@ -810,7 +823,6 @@ function ResetLocalDatabaseCard() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Settings() {
|
||||
const { data: machines, error } = useMonitoringSettings();
|
||||
const { data: sshKeys = [] } = useSSHKeys();
|
||||
@@ -822,7 +834,6 @@ export function Settings() {
|
||||
emptyMachine(),
|
||||
);
|
||||
const [selectedMachineId, setSelectedMachineId] = useState("");
|
||||
|
||||
const orderedMachines = useMemo(() => machines ?? [], [machines]);
|
||||
const selectedMachine = useMemo(
|
||||
() =>
|
||||
@@ -837,7 +848,6 @@ export function Settings() {
|
||||
const sshMachines = orderedMachines.filter(
|
||||
(machine) => machine.mode === "ssh",
|
||||
);
|
||||
|
||||
const beginLocal = () => {
|
||||
setMachineDraft(emptyMachine("local"));
|
||||
setMachineDialogOpen(true);
|
||||
@@ -858,7 +868,6 @@ export function Settings() {
|
||||
setMachineDialogOpen(false);
|
||||
setMachineDraft(emptyMachine(draft.mode));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={2.25}>
|
||||
<Stack spacing={0.5}>
|
||||
@@ -870,7 +879,6 @@ export function Settings() {
|
||||
tabbed admin workspace.
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{error && <Alert severity="error">{String(error)}</Alert>}
|
||||
{saveMachine.error && (
|
||||
<Alert severity="error">{String(saveMachine.error)}</Alert>
|
||||
@@ -878,7 +886,6 @@ export function Settings() {
|
||||
{deleteMachine.error && (
|
||||
<Alert severity="error">{String(deleteMachine.error)}</Alert>
|
||||
)}
|
||||
|
||||
<TabbedCard
|
||||
value={tab}
|
||||
onChange={(value) => setTab(value as SettingsTab)}
|
||||
@@ -951,7 +958,6 @@ export function Settings() {
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{orderedMachines.length > 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -1014,15 +1020,15 @@ export function Settings() {
|
||||
host: machine.host,
|
||||
port: machine.port,
|
||||
username: machine.username,
|
||||
key_directory: "",
|
||||
key_name: "",
|
||||
path_prefix: "",
|
||||
ssh_key_id: machine.ssh_key_id,
|
||||
key_directory: "",
|
||||
key_name: "",
|
||||
path_prefix: "",
|
||||
ssh_key_id: machine.ssh_key_id,
|
||||
ssh_private_key: "",
|
||||
ssh_private_key_passphrase: "",
|
||||
password: "",
|
||||
media_root: machine.media_root,
|
||||
jellyfin_url: machine.jellyfin_url,
|
||||
jellyfin_url: machine.jellyfin_url,
|
||||
jellyfin_user_id: machine.jellyfin_user_id,
|
||||
jellyfin_api_key: "",
|
||||
jellyseerr_url: machine.jellyseerr_url,
|
||||
@@ -1036,7 +1042,6 @@ export function Settings() {
|
||||
})}
|
||||
</Box>
|
||||
</SelectionRailCard>
|
||||
|
||||
<SectionCard
|
||||
title={selectedMachine?.name || "No machine selected"}
|
||||
description={
|
||||
@@ -1140,15 +1145,15 @@ export function Settings() {
|
||||
host: selectedMachine.host,
|
||||
port: selectedMachine.port,
|
||||
username: selectedMachine.username,
|
||||
key_directory: "",
|
||||
key_name: "",
|
||||
path_prefix: "",
|
||||
ssh_key_id: selectedMachine.ssh_key_id,
|
||||
key_directory: "",
|
||||
key_name: "",
|
||||
path_prefix: "",
|
||||
ssh_key_id: selectedMachine.ssh_key_id,
|
||||
ssh_private_key: "",
|
||||
ssh_private_key_passphrase: "",
|
||||
password: "",
|
||||
media_root: selectedMachine.media_root,
|
||||
jellyfin_url: selectedMachine.jellyfin_url,
|
||||
jellyfin_url: selectedMachine.jellyfin_url,
|
||||
jellyfin_user_id:
|
||||
selectedMachine.jellyfin_user_id,
|
||||
jellyfin_api_key: "",
|
||||
@@ -1179,11 +1184,9 @@ export function Settings() {
|
||||
) : null}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{tab === "ssh-keys" && <SSHKeyManager sshKeys={sshKeys} />}
|
||||
{tab === "danger" && <ResetLocalDatabaseCard />}
|
||||
</TabbedCard>
|
||||
|
||||
<Dialog
|
||||
open={machineDialogOpen}
|
||||
onClose={closeMachineDialog}
|
||||
|
||||
@@ -105,6 +105,8 @@ export interface SSHKeyInput {
|
||||
name: string;
|
||||
private_key: string;
|
||||
passphrase: string;
|
||||
public_key: string;
|
||||
fingerprint: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user