feat: cascade-delete hosts when removing a network
When a VLAN/LAN is deleted, all non-gateway, non-livebox devices with an interface in that network are deleted automatically. Gateway and livebox devices are preserved; their interface is unlinked (vlan_id set to NULL). The confirmation dialog now shows the exact count of devices that will be deleted (all three locales: fr/en/es). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,21 @@ def delete_vlan(vlan_pk: int, db: Session = Depends(get_db)):
|
|||||||
db_vlan = db.query(models.Vlan).filter(models.Vlan.id == vlan_pk).first()
|
db_vlan = db.query(models.Vlan).filter(models.Vlan.id == vlan_pk).first()
|
||||||
if not db_vlan:
|
if not db_vlan:
|
||||||
raise HTTPException(status_code=404, detail="VLAN introuvable")
|
raise HTTPException(status_code=404, detail="VLAN introuvable")
|
||||||
|
|
||||||
|
# Collect devices with an interface in this VLAN
|
||||||
|
ifaces = (
|
||||||
|
db.query(models.DeviceInterface)
|
||||||
|
.filter(models.DeviceInterface.vlan_id == vlan_pk)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
device_ids = {i.device_id for i in ifaces}
|
||||||
|
|
||||||
|
for device_id in device_ids:
|
||||||
|
device = db.query(models.Device).filter(models.Device.id == device_id).first()
|
||||||
|
if device and not device.is_gateway and not device.is_livebox:
|
||||||
|
db.delete(device) # cascade deletes all its interfaces
|
||||||
|
|
||||||
|
# Gateway/livebox interfaces in this VLAN will be SET NULL by SQLAlchemy
|
||||||
db.delete(db_vlan)
|
db.delete(db_vlan)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
:devices="devices"
|
:devices="devices"
|
||||||
:vlans="vlans"
|
:vlans="vlans"
|
||||||
/>
|
/>
|
||||||
<VlanManager v-if="view === 'vlans'" :vlans="vlans" @refresh="loadAll" />
|
<VlanManager v-if="view === 'vlans'" :vlans="vlans" :devices="devices" @refresh="loadAll" />
|
||||||
<DeviceManager v-if="view === 'devices'" :devices="devices" :vlans="vlans" @refresh="loadAll" />
|
<DeviceManager v-if="view === 'devices'" :devices="devices" :vlans="vlans" @refresh="loadAll" />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ import { ref, reactive } from 'vue'
|
|||||||
import { vlansApi } from '../api.js'
|
import { vlansApi } from '../api.js'
|
||||||
import { t, tFmt } from '../i18n.js'
|
import { t, tFmt } from '../i18n.js'
|
||||||
|
|
||||||
const props = defineProps({ vlans: Array })
|
const props = defineProps({ vlans: Array, devices: { type: Array, default: () => [] } })
|
||||||
const emit = defineEmits(['refresh'])
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
const showForm = ref(false)
|
const showForm = ref(false)
|
||||||
@@ -145,9 +145,18 @@ async function save() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _affectedCount(vlan) {
|
||||||
|
return props.devices.filter(
|
||||||
|
d => !d.is_gateway && !d.is_livebox && d.interfaces.some(i => i.vlan_id === vlan.id)
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
|
||||||
async function remove(vlan) {
|
async function remove(vlan) {
|
||||||
const label = vlan.vlan_id != null ? `VLAN ${vlan.vlan_id} — ${vlan.name}` : `LAN ${vlan.name}`
|
const label = vlan.vlan_id != null ? `VLAN ${vlan.vlan_id} — ${vlan.name}` : `LAN ${vlan.name}`
|
||||||
if (!confirm(`Supprimer ${tFmt('confirmDeleteNetwork', label)}`)) return
|
const count = _affectedCount(vlan)
|
||||||
|
let msg = `Supprimer ${tFmt('confirmDeleteNetwork', label)}`
|
||||||
|
if (count > 0) msg += '\n' + tFmt('confirmDeleteNetworkHosts', count)
|
||||||
|
if (!confirm(msg)) return
|
||||||
try {
|
try {
|
||||||
await vlansApi.remove(vlan.id)
|
await vlansApi.remove(vlan.id)
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const LANGS = {
|
|||||||
badgeLivebox: 'Livebox',
|
badgeLivebox: 'Livebox',
|
||||||
confirmDeleteDevice: '{0} et tous ses liens ?',
|
confirmDeleteDevice: '{0} et tous ses liens ?',
|
||||||
confirmDeleteNetwork: '{0} ?',
|
confirmDeleteNetwork: '{0} ?',
|
||||||
|
confirmDeleteNetworkHosts: '{0} équipement(s) hors passerelle seront également supprimés.',
|
||||||
saveError: 'Erreur lors de la sauvegarde',
|
saveError: 'Erreur lors de la sauvegarde',
|
||||||
deleteError: 'Erreur lors de la suppression',
|
deleteError: 'Erreur lors de la suppression',
|
||||||
descPlaceholder: 'Rôle, OS, notes…',
|
descPlaceholder: 'Rôle, OS, notes…',
|
||||||
@@ -229,6 +230,7 @@ const LANGS = {
|
|||||||
badgeLivebox: 'ISP Box',
|
badgeLivebox: 'ISP Box',
|
||||||
confirmDeleteDevice: '{0} and all its links?',
|
confirmDeleteDevice: '{0} and all its links?',
|
||||||
confirmDeleteNetwork: '{0}?',
|
confirmDeleteNetwork: '{0}?',
|
||||||
|
confirmDeleteNetworkHosts: '{0} non-gateway device(s) will also be deleted.',
|
||||||
saveError: 'Error while saving',
|
saveError: 'Error while saving',
|
||||||
deleteError: 'Error while deleting',
|
deleteError: 'Error while deleting',
|
||||||
descPlaceholder: 'Role, OS, notes…',
|
descPlaceholder: 'Role, OS, notes…',
|
||||||
@@ -384,6 +386,7 @@ const LANGS = {
|
|||||||
badgeLivebox: 'Router ISP',
|
badgeLivebox: 'Router ISP',
|
||||||
confirmDeleteDevice: '{0} y todos sus enlaces?',
|
confirmDeleteDevice: '{0} y todos sus enlaces?',
|
||||||
confirmDeleteNetwork: '{0}?',
|
confirmDeleteNetwork: '{0}?',
|
||||||
|
confirmDeleteNetworkHosts: '{0} equipo(s) no gateway también serán eliminados.',
|
||||||
saveError: 'Error al guardar',
|
saveError: 'Error al guardar',
|
||||||
deleteError: 'Error al eliminar',
|
deleteError: 'Error al eliminar',
|
||||||
descPlaceholder: 'Rol, SO, notas…',
|
descPlaceholder: 'Rol, SO, notas…',
|
||||||
|
|||||||
Reference in New Issue
Block a user