Added Ability to Rename Sites

This commit is contained in:
2025-09-28 02:24:17 -06:00
parent 3c0f283c25
commit b6e3781863
3 changed files with 128 additions and 1 deletions

View File

@@ -479,3 +479,36 @@ export function CreateSiteDialog({ open, onCancel, onCreate }) {
</Dialog> </Dialog>
); );
} }
export function RenameSiteDialog({ open, value, onChange, onCancel, onSave }) {
return (
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
<DialogTitle>Rename Site</DialogTitle>
<DialogContent>
<TextField
autoFocus
fullWidth
margin="dense"
label="Site Name"
variant="outlined"
value={value}
onChange={(e) => onChange(e.target.value)}
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: "#2a2a2a",
color: "#ccc",
"& fieldset": { borderColor: "#444" },
"&:hover fieldset": { borderColor: "#666" }
},
label: { color: "#aaa" },
mt: 1
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} sx={{ color: "#58a6ff" }}>Cancel</Button>
<Button onClick={onSave} sx={{ color: "#58a6ff" }}>Save</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -18,9 +18,10 @@ import {
} from "@mui/material"; } from "@mui/material";
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import DeleteIcon from "@mui/icons-material/DeleteOutline"; import DeleteIcon from "@mui/icons-material/DeleteOutline";
import EditIcon from "@mui/icons-material/Edit";
import FilterListIcon from "@mui/icons-material/FilterList"; import FilterListIcon from "@mui/icons-material/FilterList";
import ViewColumnIcon from "@mui/icons-material/ViewColumn"; import ViewColumnIcon from "@mui/icons-material/ViewColumn";
import { CreateSiteDialog, ConfirmDeleteDialog } from "../Dialogs.jsx"; import { CreateSiteDialog, ConfirmDeleteDialog, RenameSiteDialog } from "../Dialogs.jsx";
export default function SiteList({ onOpenDevicesForSite }) { export default function SiteList({ onOpenDevicesForSite }) {
const [rows, setRows] = useState([]); // {id, name, description, device_count} const [rows, setRows] = useState([]); // {id, name, description, device_count}
@@ -51,6 +52,8 @@ export default function SiteList({ onOpenDevicesForSite }) {
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [renameOpen, setRenameOpen] = useState(false);
const [renameValue, setRenameValue] = useState("");
const fetchSites = useCallback(async () => { const fetchSites = useCallback(async () => {
try { try {
@@ -148,6 +151,24 @@ export default function SiteList({ onOpenDevicesForSite }) {
<Box sx={{ p: 2, pb: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <Box sx={{ p: 2, pb: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}>Sites</Typography> <Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}>Sites</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
variant="outlined"
size="small"
startIcon={<EditIcon />}
disabled={selectedIds.size !== 1}
onClick={() => {
// Prefill with the currently selected site's name
const selId = selectedIds.size === 1 ? Array.from(selectedIds)[0] : null;
if (selId != null) {
const site = rows.find((r) => r.id === selId);
setRenameValue(site?.name || "");
setRenameOpen(true);
}
}}
sx={{ color: selectedIds.size === 1 ? '#58a6ff' : '#666', borderColor: selectedIds.size === 1 ? '#58a6ff' : '#333', textTransform: 'none' }}
>
Rename
</Button>
<Button <Button
variant="outlined" variant="outlined"
size="small" size="small"
@@ -329,6 +350,36 @@ export default function SiteList({ onOpenDevicesForSite }) {
await fetchSites(); await fetchSites();
}} }}
/> />
{/* Rename site dialog */}
<RenameSiteDialog
open={renameOpen}
value={renameValue}
onChange={setRenameValue}
onCancel={() => setRenameOpen(false)}
onSave={async () => {
const newName = (renameValue || '').trim();
if (!newName) return;
const selId = selectedIds.size === 1 ? Array.from(selectedIds)[0] : null;
if (selId == null) return;
try {
const res = await fetch('/api/sites/rename', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: selId, new_name: newName })
});
if (!res.ok) {
// Keep dialog open on error; optionally log
try { const err = await res.json(); console.warn('Rename failed', err); } catch {}
return;
}
setRenameOpen(false);
await fetchSites();
} catch (e) {
console.warn('Rename error', e);
}
}}
/>
</Paper> </Paper>
); );
} }

View File

@@ -1427,6 +1427,49 @@ def assign_devices_to_site():
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# Rename a site (update name only)
@app.route("/api/sites/rename", methods=["POST"])
def rename_site():
payload = request.get_json(silent=True) or {}
site_id = payload.get("id")
new_name = (payload.get("new_name") or "").strip()
try:
site_id = int(site_id)
except Exception:
return jsonify({"error": "invalid id"}), 400
if not new_name:
return jsonify({"error": "new_name is required"}), 400
try:
conn = _db_conn()
cur = conn.cursor()
cur.execute("UPDATE sites SET name = ? WHERE id = ?", (new_name, site_id))
if cur.rowcount == 0:
conn.close()
return jsonify({"error": "site not found"}), 404
conn.commit()
cur.execute(
"""
SELECT s.id, s.name, s.description, s.created_at,
COALESCE(ds.cnt, 0) AS device_count
FROM sites s
LEFT JOIN (
SELECT site_id, COUNT(*) AS cnt
FROM device_sites
GROUP BY site_id
) ds ON ds.site_id = s.id
WHERE s.id = ?
""",
(site_id,)
)
row = cur.fetchone()
conn.close()
return jsonify(_row_to_site(row))
except sqlite3.IntegrityError:
return jsonify({"error": "name already exists"}), 409
except Exception as e:
return jsonify({"error": str(e)}), 500
# --------------------------------------------- # ---------------------------------------------
# Global Search (suggestions) # Global Search (suggestions)
# --------------------------------------------- # ---------------------------------------------