mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 06:21:57 -06:00
Added Ability to Rename Sites
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user