mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 05:41:58 -06:00
Additional Changes to Ansible Logic
This commit is contained in:
@@ -35,7 +35,9 @@ import Login from "./Login.jsx";
|
||||
import SiteList from "./Sites/Site_List";
|
||||
import DeviceList from "./Devices/Device_List";
|
||||
import DeviceDetails from "./Devices/Device_Details";
|
||||
import AgentDevices from "./Devices/Agent_Devices.jsx";
|
||||
import SSHDevices from "./Devices/SSH_Devices.jsx";
|
||||
import WinRMDevices from "./Devices/WinRM_Devices.jsx";
|
||||
import AssemblyList from "./Assemblies/Assembly_List";
|
||||
import AssemblyEditor from "./Assemblies/Assembly_Editor";
|
||||
import ScheduledJobsList from "./Scheduling/Scheduled_Jobs_List";
|
||||
@@ -160,8 +162,8 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
items.push({ label: "Site List", page: "sites" });
|
||||
break;
|
||||
case "devices":
|
||||
items.push({ label: "Inventory", page: "devices" });
|
||||
items.push({ label: "Devices", page: "devices" });
|
||||
items.push({ label: "Device List", page: "devices" });
|
||||
break;
|
||||
case "device_details":
|
||||
items.push({ label: "Devices", page: "devices" });
|
||||
@@ -203,14 +205,21 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
items.push({ label: "Automation", page: "jobs" });
|
||||
items.push({ label: "Community Content", page: "community" });
|
||||
break;
|
||||
case "devices":
|
||||
case "agent_devices":
|
||||
items.push({ label: "Inventory", page: "devices" });
|
||||
items.push({ label: "Devices", page: "devices" });
|
||||
items.push({ label: "Agent Devices", page: "agent_devices" });
|
||||
break;
|
||||
case "ssh_devices":
|
||||
items.push({ label: "Inventory", page: "devices" });
|
||||
items.push({ label: "Devices", page: "devices" });
|
||||
items.push({ label: "SSH Devices", page: "ssh_devices" });
|
||||
break;
|
||||
case "winrm_devices":
|
||||
items.push({ label: "Inventory", page: "devices" });
|
||||
items.push({ label: "Devices", page: "devices" });
|
||||
items.push({ label: "WinRM Devices", page: "winrm_devices" });
|
||||
break;
|
||||
case "access_credentials":
|
||||
items.push({ label: "Access Management", page: "access_credentials" });
|
||||
items.push({ label: "Credentials", page: "access_credentials" });
|
||||
@@ -565,7 +574,13 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
const isAdmin = (String(userRole || '').toLowerCase() === 'admin');
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin && (currentPage === 'server_info' || currentPage === 'access_credentials' || currentPage === 'access_users' || currentPage === 'ssh_devices')) {
|
||||
const requiresAdmin = currentPage === 'server_info'
|
||||
|| currentPage === 'access_credentials'
|
||||
|| currentPage === 'access_users'
|
||||
|| currentPage === 'ssh_devices'
|
||||
|| currentPage === 'winrm_devices'
|
||||
|| currentPage === 'agent_devices';
|
||||
if (!isAdmin && requiresAdmin) {
|
||||
setNotAuthorizedOpen(true);
|
||||
setCurrentPage('devices');
|
||||
}
|
||||
@@ -593,8 +608,19 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "agent_devices":
|
||||
return (
|
||||
<AgentDevices
|
||||
onSelectDevice={(d) => {
|
||||
setSelectedDevice(d);
|
||||
setCurrentPage("device_details");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "ssh_devices":
|
||||
return <SSHDevices />;
|
||||
case "winrm_devices":
|
||||
return <WinRMDevices />;
|
||||
|
||||
case "device_details":
|
||||
return (
|
||||
|
||||
219
Data/Server/WebUI/src/Devices/Add_Device.jsx
Normal file
219
Data/Server/WebUI/src/Devices/Add_Device.jsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Button,
|
||||
MenuItem,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
|
||||
const TYPE_OPTIONS = [
|
||||
{ value: "ssh", label: "SSH" },
|
||||
{ value: "winrm", label: "WinRM" }
|
||||
];
|
||||
|
||||
const initialForm = {
|
||||
hostname: "",
|
||||
address: "",
|
||||
description: "",
|
||||
operating_system: ""
|
||||
};
|
||||
|
||||
export default function AddDevice({
|
||||
open,
|
||||
onClose,
|
||||
defaultType = null,
|
||||
onCreated
|
||||
}) {
|
||||
const [type, setType] = useState(defaultType || "ssh");
|
||||
const [form, setForm] = useState(initialForm);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setType(defaultType || "ssh");
|
||||
setForm(initialForm);
|
||||
setError("");
|
||||
}
|
||||
}, [open, defaultType]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (submitting) return;
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const handleChange = (field) => (event) => {
|
||||
const value = event.target.value;
|
||||
setForm((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (submitting) return;
|
||||
const trimmedHostname = form.hostname.trim();
|
||||
const trimmedAddress = form.address.trim();
|
||||
if (!trimmedHostname) {
|
||||
setError("Hostname is required.");
|
||||
return;
|
||||
}
|
||||
if (!type) {
|
||||
setError("Select a device type.");
|
||||
return;
|
||||
}
|
||||
if (!trimmedAddress) {
|
||||
setError("Address is required.");
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
setError("");
|
||||
const payload = {
|
||||
hostname: trimmedHostname,
|
||||
address: trimmedAddress,
|
||||
description: form.description.trim(),
|
||||
operating_system: form.operating_system.trim()
|
||||
};
|
||||
const apiBase = type === "winrm" ? "/api/winrm_devices" : "/api/ssh_devices";
|
||||
try {
|
||||
const resp = await fetch(apiBase, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
onCreated && onCreated(data.device || null);
|
||||
onClose && onClose();
|
||||
} catch (err) {
|
||||
setError(String(err.message || err));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const dialogTitle = defaultType
|
||||
? `Add ${defaultType.toUpperCase()} Device`
|
||||
: "Add Device";
|
||||
|
||||
const typeLabel = (TYPE_OPTIONS.find((opt) => opt.value === type) || TYPE_OPTIONS[0]).label;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}
|
||||
>
|
||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
||||
<DialogContent sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
|
||||
{!defaultType && (
|
||||
<TextField
|
||||
select
|
||||
label="Device Type"
|
||||
size="small"
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
backgroundColor: "#1f1f1f",
|
||||
color: "#fff",
|
||||
"& fieldset": { borderColor: "#555" },
|
||||
"&:hover fieldset": { borderColor: "#888" }
|
||||
},
|
||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
||||
}}
|
||||
>
|
||||
{TYPE_OPTIONS.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
)}
|
||||
<TextField
|
||||
label="Hostname"
|
||||
value={form.hostname}
|
||||
onChange={handleChange("hostname")}
|
||||
size="small"
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
backgroundColor: "#1f1f1f",
|
||||
color: "#fff",
|
||||
"& fieldset": { borderColor: "#555" },
|
||||
"&:hover fieldset": { borderColor: "#888" }
|
||||
},
|
||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
||||
}}
|
||||
helperText="Name used inside Borealis."
|
||||
/>
|
||||
<TextField
|
||||
label={`${typeLabel} Address`}
|
||||
value={form.address}
|
||||
onChange={handleChange("address")}
|
||||
size="small"
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
backgroundColor: "#1f1f1f",
|
||||
color: "#fff",
|
||||
"& fieldset": { borderColor: "#555" },
|
||||
"&:hover fieldset": { borderColor: "#888" }
|
||||
},
|
||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
||||
}}
|
||||
helperText="IP or FQDN reachable from the Borealis server."
|
||||
/>
|
||||
<TextField
|
||||
label="Description"
|
||||
value={form.description}
|
||||
onChange={handleChange("description")}
|
||||
size="small"
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
backgroundColor: "#1f1f1f",
|
||||
color: "#fff",
|
||||
"& fieldset": { borderColor: "#555" },
|
||||
"&:hover fieldset": { borderColor: "#888" }
|
||||
},
|
||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Operating System"
|
||||
value={form.operating_system}
|
||||
onChange={handleChange("operating_system")}
|
||||
size="small"
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
backgroundColor: "#1f1f1f",
|
||||
color: "#fff",
|
||||
"& fieldset": { borderColor: "#555" },
|
||||
"&:hover fieldset": { borderColor: "#888" }
|
||||
},
|
||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
||||
}}
|
||||
/>
|
||||
{error && (
|
||||
<Typography variant="body2" sx={{ color: "#ff8080" }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={handleClose} sx={{ color: "#58a6ff" }} disabled={submitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="outlined"
|
||||
sx={{ color: "#58a6ff", borderColor: "#58a6ff" }}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
13
Data/Server/WebUI/src/Devices/Agent_Devices.jsx
Normal file
13
Data/Server/WebUI/src/Devices/Agent_Devices.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import DeviceList from "./Device_List.jsx";
|
||||
|
||||
export default function AgentDevices(props) {
|
||||
return (
|
||||
<DeviceList
|
||||
{...props}
|
||||
filterMode="agent"
|
||||
title="Agent Devices"
|
||||
showAddButton={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import AddIcon from "@mui/icons-material/Add";
|
||||
import CachedIcon from "@mui/icons-material/Cached";
|
||||
import { DeleteDeviceDialog, CreateCustomViewDialog, RenameCustomViewDialog } from "../Dialogs.jsx";
|
||||
import QuickJob from "../Scheduling/Quick_Job.jsx";
|
||||
import AddDevice from "./Add_Device.jsx";
|
||||
|
||||
function formatLastSeen(tsSec, offlineAfter = 300) {
|
||||
if (!tsSec) return "unknown";
|
||||
@@ -66,7 +67,14 @@ function formatUptime(seconds) {
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
export default function DeviceList({ onSelectDevice }) {
|
||||
export default function DeviceList({
|
||||
onSelectDevice,
|
||||
filterMode = "all",
|
||||
title,
|
||||
showAddButton,
|
||||
addButtonLabel,
|
||||
defaultAddType,
|
||||
}) {
|
||||
const [rows, setRows] = useState([]);
|
||||
const [orderBy, setOrderBy] = useState("status");
|
||||
const [order, setOrder] = useState("desc");
|
||||
@@ -76,6 +84,36 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
// Track selection by agent id to avoid duplicate hostname collisions
|
||||
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
||||
const [quickJobOpen, setQuickJobOpen] = useState(false);
|
||||
const [addDeviceOpen, setAddDeviceOpen] = useState(false);
|
||||
const [addDeviceType, setAddDeviceType] = useState(null);
|
||||
const computedTitle = useMemo(() => {
|
||||
if (title) return title;
|
||||
switch (filterMode) {
|
||||
case "agent":
|
||||
return "Agent Devices";
|
||||
case "ssh":
|
||||
return "SSH Devices";
|
||||
case "winrm":
|
||||
return "WinRM Devices";
|
||||
default:
|
||||
return "Device Inventory";
|
||||
}
|
||||
}, [filterMode, title]);
|
||||
const derivedDefaultType = useMemo(() => {
|
||||
if (defaultAddType !== undefined) return defaultAddType;
|
||||
if (filterMode === "ssh" || filterMode === "winrm") return filterMode;
|
||||
return null;
|
||||
}, [defaultAddType, filterMode]);
|
||||
const derivedAddLabel = useMemo(() => {
|
||||
if (addButtonLabel) return addButtonLabel;
|
||||
if (filterMode === "ssh") return "Add SSH Device";
|
||||
if (filterMode === "winrm") return "Add WinRM Device";
|
||||
return "Add Device";
|
||||
}, [addButtonLabel, filterMode]);
|
||||
const derivedShowAddButton = useMemo(() => {
|
||||
if (typeof showAddButton === "boolean") return showAddButton;
|
||||
return filterMode !== "agent";
|
||||
}, [showAddButton, filterMode]);
|
||||
|
||||
// Saved custom views (from server)
|
||||
const [views, setViews] = useState([]); // [{id, name, columns:[id], filters:{}}]
|
||||
@@ -305,6 +343,7 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
0
|
||||
) || 0;
|
||||
const connectionType = (device.connection_type || summary.connection_type || '').trim().toLowerCase();
|
||||
const connectionLabel = connectionType === 'ssh' ? 'SSH' : connectionType === 'winrm' ? 'WinRM' : '';
|
||||
const connectionEndpoint = (device.connection_endpoint || summary.connection_endpoint || '').trim();
|
||||
|
||||
const memoryList = Array.isArray(device.memory) ? device.memory : [];
|
||||
@@ -329,7 +368,7 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
lastSeenDisplay: formatLastSeen(lastSeen),
|
||||
os: osName,
|
||||
lastUser,
|
||||
type,
|
||||
type: type || connectionLabel || '',
|
||||
site: device.site_name || 'Not Configured',
|
||||
siteId: device.site_id || null,
|
||||
siteDescription: device.site_description || '',
|
||||
@@ -360,17 +399,27 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
summary,
|
||||
details: device.details || {},
|
||||
connectionType,
|
||||
connectionLabel,
|
||||
connectionEndpoint,
|
||||
isRemote: connectionType === 'ssh',
|
||||
isRemote: Boolean(connectionLabel),
|
||||
};
|
||||
});
|
||||
|
||||
setRows(normalized);
|
||||
let filtered = normalized;
|
||||
if (filterMode === "agent") {
|
||||
filtered = normalized.filter((row) => !row.connectionType);
|
||||
} else if (filterMode === "ssh") {
|
||||
filtered = normalized.filter((row) => row.connectionType === "ssh");
|
||||
} else if (filterMode === "winrm") {
|
||||
filtered = normalized.filter((row) => row.connectionType === "winrm");
|
||||
}
|
||||
|
||||
setRows(filtered);
|
||||
} catch (e) {
|
||||
console.warn('Failed to load devices:', e);
|
||||
setRows([]);
|
||||
}
|
||||
}, [repoHash, fetchLatestRepoHash, computeAgentVersion]);
|
||||
}, [repoHash, fetchLatestRepoHash, computeAgentVersion, filterMode]);
|
||||
|
||||
const fetchViews = useCallback(async () => {
|
||||
try {
|
||||
@@ -653,7 +702,7 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
<Box sx={{ p: 2, pb: 1, display: "flex", flexDirection: 'column', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}>
|
||||
Device Inventory
|
||||
{computedTitle}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{/* Views dropdown + add button */}
|
||||
@@ -753,6 +802,20 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
<ViewColumnIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{derivedShowAddButton && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
sx={{ bgcolor: "#58a6ff", color: "#0b0f19" }}
|
||||
onClick={() => {
|
||||
setAddDeviceType(derivedDefaultType ?? null);
|
||||
setAddDeviceOpen(true);
|
||||
}}
|
||||
>
|
||||
{derivedAddLabel}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{/* Second row: Quick Job button aligned under header title */}
|
||||
@@ -1224,6 +1287,19 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
</Box>
|
||||
</Popover>
|
||||
)}
|
||||
<AddDevice
|
||||
open={addDeviceOpen}
|
||||
defaultType={addDeviceType}
|
||||
onClose={() => {
|
||||
setAddDeviceOpen(false);
|
||||
setAddDeviceType(derivedDefaultType ?? null);
|
||||
}}
|
||||
onCreated={() => {
|
||||
setAddDeviceOpen(false);
|
||||
setAddDeviceType(derivedDefaultType ?? null);
|
||||
fetchDevices({ refreshRepo: true });
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import EditIcon from "@mui/icons-material/Edit";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import { ConfirmDeleteDialog } from "../Dialogs.jsx";
|
||||
import AddDevice from "./Add_Device.jsx";
|
||||
|
||||
const tableStyles = {
|
||||
"& th, & td": {
|
||||
@@ -45,7 +46,19 @@ const defaultForm = {
|
||||
operating_system: ""
|
||||
};
|
||||
|
||||
export default function SSHDevices() {
|
||||
export default function SSHDevices({ type = "ssh" }) {
|
||||
const typeLabel = type === "winrm" ? "WinRM" : "SSH";
|
||||
const apiBase = type === "winrm" ? "/api/winrm_devices" : "/api/ssh_devices";
|
||||
const pageTitle = `${typeLabel} Devices`;
|
||||
const addButtonLabel = `Add ${typeLabel} Device`;
|
||||
const addressLabel = `${typeLabel} Address`;
|
||||
const loadingLabel = `Loading ${typeLabel} devices…`;
|
||||
const emptyLabel = `No ${typeLabel} devices have been added yet.`;
|
||||
const descriptionText = type === "winrm"
|
||||
? "Manage remote endpoints reachable via WinRM for playbook execution."
|
||||
: "Manage remote endpoints reachable via SSH for playbook execution.";
|
||||
const editDialogTitle = `Edit ${typeLabel} Device`;
|
||||
const newDialogTitle = `New ${typeLabel} Device`;
|
||||
const [rows, setRows] = useState([]);
|
||||
const [orderBy, setOrderBy] = useState("hostname");
|
||||
const [order, setOrder] = useState("asc");
|
||||
@@ -58,6 +71,7 @@ export default function SSHDevices() {
|
||||
const [editTarget, setEditTarget] = useState(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||
const [addDeviceOpen, setAddDeviceOpen] = useState(false);
|
||||
|
||||
const isEdit = Boolean(editTarget);
|
||||
|
||||
@@ -65,7 +79,7 @@ export default function SSHDevices() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const resp = await fetch("/api/ssh_devices");
|
||||
const resp = await fetch(apiBase);
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
@@ -79,7 +93,7 @@ export default function SSHDevices() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [apiBase]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDevices();
|
||||
@@ -119,9 +133,7 @@ export default function SSHDevices() {
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
setEditTarget(null);
|
||||
setForm(defaultForm);
|
||||
setDialogOpen(true);
|
||||
setAddDeviceOpen(true);
|
||||
setFormError("");
|
||||
};
|
||||
|
||||
@@ -164,16 +176,14 @@ export default function SSHDevices() {
|
||||
setSubmitting(true);
|
||||
setFormError("");
|
||||
try {
|
||||
const resp = await fetch(
|
||||
isEdit
|
||||
? `/api/ssh_devices/${encodeURIComponent(editTarget.hostname)}`
|
||||
: "/api/ssh_devices",
|
||||
{
|
||||
method: isEdit ? "PUT" : "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
);
|
||||
const endpoint = isEdit
|
||||
? `${apiBase}/${encodeURIComponent(editTarget.hostname)}`
|
||||
: apiBase;
|
||||
const resp = await fetch(endpoint, {
|
||||
method: isEdit ? "PUT" : "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
@@ -205,7 +215,7 @@ export default function SSHDevices() {
|
||||
if (!deleteTarget) return;
|
||||
setDeleteBusy(true);
|
||||
try {
|
||||
const resp = await fetch(`/api/ssh_devices/${encodeURIComponent(deleteTarget.hostname)}`, {
|
||||
const resp = await fetch(`${apiBase}/${encodeURIComponent(deleteTarget.hostname)}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
@@ -232,10 +242,10 @@ export default function SSHDevices() {
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}>
|
||||
SSH Devices
|
||||
{pageTitle}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "#aaa" }}>
|
||||
Manage remote endpoints reachable via SSH for playbook execution.
|
||||
{descriptionText}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
@@ -256,7 +266,7 @@ export default function SSHDevices() {
|
||||
sx={{ bgcolor: "#58a6ff", color: "#0b0f19" }}
|
||||
onClick={openCreate}
|
||||
>
|
||||
New SSH Device
|
||||
{addButtonLabel}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -269,7 +279,7 @@ export default function SSHDevices() {
|
||||
{loading && (
|
||||
<Box sx={{ px: 2, py: 1.5, display: "flex", alignItems: "center", gap: 1, color: "#7db7ff" }}>
|
||||
<CircularProgress size={18} sx={{ color: "#58a6ff" }} />
|
||||
<Typography variant="body2">Loading SSH devices…</Typography>
|
||||
<Typography variant="body2">{loadingLabel}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -291,7 +301,7 @@ export default function SSHDevices() {
|
||||
direction={orderBy === "address" ? order : "asc"}
|
||||
onClick={handleSort("address")}
|
||||
>
|
||||
SSH Address
|
||||
{addressLabel}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell sortDirection={orderBy === "description" ? order : false}>
|
||||
@@ -341,7 +351,7 @@ export default function SSHDevices() {
|
||||
{!sortedRows.length && !loading && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} sx={{ textAlign: "center", color: "#888" }}>
|
||||
No SSH devices have been added yet.
|
||||
{emptyLabel}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -355,7 +365,7 @@ export default function SSHDevices() {
|
||||
maxWidth="sm"
|
||||
PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}
|
||||
>
|
||||
<DialogTitle>{isEdit ? "Edit SSH Device" : "New SSH Device"}</DialogTitle>
|
||||
<DialogTitle>{isEdit ? editDialogTitle : newDialogTitle}</DialogTitle>
|
||||
<DialogContent sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
|
||||
<TextField
|
||||
label="Hostname"
|
||||
@@ -376,7 +386,7 @@ export default function SSHDevices() {
|
||||
helperText="Hostname used within Borealis (unique)."
|
||||
/>
|
||||
<TextField
|
||||
label="SSH Address"
|
||||
label={addressLabel}
|
||||
value={form.address}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, address: e.target.value }))}
|
||||
fullWidth
|
||||
@@ -390,7 +400,7 @@ export default function SSHDevices() {
|
||||
},
|
||||
"& .MuiInputLabel-root": { color: "#aaa" }
|
||||
}}
|
||||
helperText="IP or FQDN Borealis can reach over SSH."
|
||||
helperText={`IP or FQDN Borealis can reach over ${typeLabel}.`}
|
||||
/>
|
||||
<TextField
|
||||
label="Description"
|
||||
@@ -449,13 +459,22 @@ export default function SSHDevices() {
|
||||
open={Boolean(deleteTarget)}
|
||||
message={
|
||||
deleteTarget
|
||||
? `Remove SSH device '${deleteTarget.hostname}' from inventory?`
|
||||
? `Remove ${typeLabel} device '${deleteTarget.hostname}' from inventory?`
|
||||
: ""
|
||||
}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onConfirm={handleDelete}
|
||||
confirmDisabled={deleteBusy}
|
||||
/>
|
||||
<AddDevice
|
||||
open={addDeviceOpen}
|
||||
defaultType={type}
|
||||
onClose={() => setAddDeviceOpen(false)}
|
||||
onCreated={() => {
|
||||
setAddDeviceOpen(false);
|
||||
loadDevices();
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
6
Data/Server/WebUI/src/Devices/WinRM_Devices.jsx
Normal file
6
Data/Server/WebUI/src/Devices/WinRM_Devices.jsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
import SSHDevices from "./SSH_Devices.jsx";
|
||||
|
||||
export default function WinRMDevices(props) {
|
||||
return <SSHDevices {...props} type="winrm" />;
|
||||
}
|
||||
@@ -154,7 +154,7 @@ function NavigationSidebar({ currentPage, onNavigate, isAdmin = false }) {
|
||||
})()}
|
||||
{/* Inventory */}
|
||||
{(() => {
|
||||
const groupActive = currentPage === "devices" || currentPage === "ssh_devices";
|
||||
const groupActive = ["devices", "ssh_devices", "winrm_devices", "agent_devices"].includes(currentPage);
|
||||
return (
|
||||
<Accordion
|
||||
expanded={expandedNav.devices}
|
||||
@@ -192,7 +192,9 @@ function NavigationSidebar({ currentPage, onNavigate, isAdmin = false }) {
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}>
|
||||
<NavItem icon={<DevicesIcon fontSize="small" />} label="Devices" pageKey="devices" />
|
||||
<NavItem icon={<DevicesIcon fontSize="small" />} label="Agent Devices" pageKey="agent_devices" indent />
|
||||
<NavItem icon={<DevicesIcon fontSize="small" />} label="SSH Devices" pageKey="ssh_devices" indent />
|
||||
<NavItem icon={<DevicesIcon fontSize="small" />} label="WinRM Devices" pageKey="winrm_devices" indent />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user