feat: auto-detect OS from description; expand OS_LIST to 51 distros
- Remove os field from Device API and form — OS is now detected automatically from name/description via detectOs() in brandIcons.js - Expand OS_LIST from 20 to 51 entries covering all major distros (Debian/Ubuntu flavours, Red Hat, SUSE, Arch, BSD, security distros, Windows/macOS/iOS/Android, generic Linux/BSD catch-alls) - Display detected OS icon in IpAddressing.vue description column - Fix virt_type validator to normalise empty string to null Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,19 @@ def _migrate_users():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_device_os():
|
||||||
|
"""Ajoute la colonne os sur devices si absente."""
|
||||||
|
with engine.connect() as conn:
|
||||||
|
if not conn.execute(text(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='devices'"
|
||||||
|
)).fetchone():
|
||||||
|
return
|
||||||
|
cols = [row[1] for row in conn.execute(text("PRAGMA table_info(devices)")).fetchall()]
|
||||||
|
if 'os' not in cols:
|
||||||
|
conn.execute(text("ALTER TABLE devices ADD COLUMN os VARCHAR"))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def _migrate_drop_links_table():
|
def _migrate_drop_links_table():
|
||||||
"""Supprime la table links (fonctionnalité retirée en phase 3). Idempotent."""
|
"""Supprime la table links (fonctionnalité retirée en phase 3). Idempotent."""
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
@@ -162,6 +175,7 @@ def _migrate_drop_links_table():
|
|||||||
_migrate_vlan_nullable()
|
_migrate_vlan_nullable()
|
||||||
_migrate_device_virt_type()
|
_migrate_device_virt_type()
|
||||||
_migrate_device_url()
|
_migrate_device_url()
|
||||||
|
_migrate_device_os()
|
||||||
_migrate_users_must_change_password()
|
_migrate_users_must_change_password()
|
||||||
_migrate_users_token_version()
|
_migrate_users_token_version()
|
||||||
_migrate_force_admin_password_change()
|
_migrate_force_admin_password_change()
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class Device(Base):
|
|||||||
is_livebox = Column(Boolean, default=False)
|
is_livebox = Column(Boolean, default=False)
|
||||||
virt_type = Column(String, nullable=True)
|
virt_type = Column(String, nullable=True)
|
||||||
url = Column(String, nullable=True)
|
url = Column(String, nullable=True)
|
||||||
|
os = Column(String, nullable=True)
|
||||||
|
|
||||||
interfaces = relationship(
|
interfaces = relationship(
|
||||||
"DeviceInterface", back_populates="device", cascade="all, delete-orphan"
|
"DeviceInterface", back_populates="device", cascade="all, delete-orphan"
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ class DeviceCreate(BaseModel):
|
|||||||
@field_validator("virt_type")
|
@field_validator("virt_type")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _virt_type(cls, v: Optional[str]) -> Optional[str]:
|
def _virt_type(cls, v: Optional[str]) -> Optional[str]:
|
||||||
|
if not v:
|
||||||
|
return None
|
||||||
if v not in _VALID_VIRT_TYPES:
|
if v not in _VALID_VIRT_TYPES:
|
||||||
raise ValueError(f"Invalid virt_type: {v!r}. Must be one of: baremetal, lxc, qemu")
|
raise ValueError(f"Invalid virt_type: {v!r}. Must be one of: baremetal, lxc, qemu")
|
||||||
return v
|
return v
|
||||||
@@ -104,7 +106,6 @@ class DeviceCreate(BaseModel):
|
|||||||
raise ValueError("url must be a valid http or https URL")
|
raise ValueError("url must be a valid http or https URL")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class DeviceOut(BaseModel):
|
class DeviceOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "network-topology",
|
"name": "network-topology",
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
/>
|
/>
|
||||||
<VlanManager v-if="view === 'vlans'" :vlans="vlans" :devices="devices" @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" />
|
||||||
|
<IpAddressing v-if="view === 'addressing'" :devices="devices" :vlans="vlans" @refresh="loadAll" />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,6 +125,7 @@ import { isAuthenticated, currentUsername, mustChangePassword, setAuth, clearAut
|
|||||||
import TopologyGraph from './components/TopologyGraph.vue'
|
import TopologyGraph from './components/TopologyGraph.vue'
|
||||||
import VlanManager from './components/VlanManager.vue'
|
import VlanManager from './components/VlanManager.vue'
|
||||||
import DeviceManager from './components/DeviceManager.vue'
|
import DeviceManager from './components/DeviceManager.vue'
|
||||||
|
import IpAddressing from './components/IpAddressing.vue'
|
||||||
import DiscoveryModal from './components/DiscoveryModal.vue'
|
import DiscoveryModal from './components/DiscoveryModal.vue'
|
||||||
import LoginPage from './components/LoginPage.vue'
|
import LoginPage from './components/LoginPage.vue'
|
||||||
import AccountModal from './components/AccountModal.vue'
|
import AccountModal from './components/AccountModal.vue'
|
||||||
@@ -139,9 +141,10 @@ const showAccount = ref(false)
|
|||||||
const langs = ['fr', 'en', 'es']
|
const langs = ['fr', 'en', 'es']
|
||||||
|
|
||||||
const tabs = computed(() => [
|
const tabs = computed(() => [
|
||||||
{ id: 'topology', label: t('tabTopology'), icon: '■' },
|
{ id: 'topology', label: t('tabTopology'), icon: '■' },
|
||||||
{ id: 'vlans', label: t('tabNetworks'), icon: '◆' },
|
{ id: 'vlans', label: t('tabNetworks'), icon: '◆' },
|
||||||
{ id: 'devices', label: t('tabDevices'), icon: '▣' },
|
{ id: 'devices', label: t('tabDevices'), icon: '▣' },
|
||||||
|
{ id: 'addressing', label: t('tabAddressing'), icon: '⊞' },
|
||||||
])
|
])
|
||||||
|
|
||||||
function onLogin({ token, username, mustChangePassword: mcp }) {
|
function onLogin({ token, username, mustChangePassword: mcp }) {
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ import {
|
|||||||
siElastic, siKibana, siLogstash, siSplunk, siGraylog, siJaeger, siOpentelemetry,
|
siElastic, siKibana, siLogstash, siSplunk, siGraylog, siJaeger, siOpentelemetry,
|
||||||
siApple,
|
siApple,
|
||||||
siDebian, siUbuntu, siFirefox,
|
siDebian, siUbuntu, siFirefox,
|
||||||
|
siCentos, siRedhat, siFedora, siAlpinelinux, siArchlinux, siOpensuse, siFreebsd,
|
||||||
|
siAlmalinux, siRockylinux, siNixos, siGentoo, siVoidlinux, siSlackware,
|
||||||
|
siSuse, siManjaro, siLinuxmint, siZorin, siPopos, siDeepin, siElementary,
|
||||||
|
siMxlinux, siSolus, siEndeavouros, siArtixlinux, siAsahilinux,
|
||||||
|
siKubuntu, siLubuntu, siXubuntu, siUbuntumate,
|
||||||
|
siBsd, siOpenbsd, siNetbsd,
|
||||||
|
siKalilinux, siParrotsecurity, siTails, siQubesos, siReactos,
|
||||||
|
siMacos, siIos, siLinux,
|
||||||
|
siAndroid,
|
||||||
siAnsible,
|
siAnsible,
|
||||||
siDell, siHp,
|
siDell, siHp,
|
||||||
siRaspberrypi, siArduino,
|
siRaspberrypi, siArduino,
|
||||||
@@ -22,7 +31,7 @@ import {
|
|||||||
siWordpress, siGhost, siGrav, siJekyll, siHugo, siHexo, siDrupal, siJoomla, siTypo3, siOctobercms, siTextpattern,
|
siWordpress, siGhost, siGrav, siJekyll, siHugo, siHexo, siDrupal, siJoomla, siTypo3, siOctobercms, siTextpattern,
|
||||||
siMatomo, siPlausibleanalytics,
|
siMatomo, siPlausibleanalytics,
|
||||||
siSamsung, siLg, siSony, siPanasonic, siSharp, siToshiba, siVestel,
|
siSamsung, siLg, siSony, siPanasonic, siSharp, siToshiba, siVestel,
|
||||||
siChromecast, siAndroid, siAppletv, siAmazonfiretv, siRoku, siKodi,
|
siChromecast, siAppletv, siAmazonfiretv, siRoku, siKodi,
|
||||||
siJellyfin, siHomeassistant, siPhilipshue, siXiaomi,
|
siJellyfin, siHomeassistant, siPhilipshue, siXiaomi,
|
||||||
siRadarr, siSonarr, siTransmission,
|
siRadarr, siSonarr, siTransmission,
|
||||||
siExcalidraw,
|
siExcalidraw,
|
||||||
@@ -295,6 +304,84 @@ const BRANDS = [
|
|||||||
{ kw: ['xiaomi', 'mi home', 'yeelight'], icon: siXiaomi },
|
{ kw: ['xiaomi', 'mi home', 'yeelight'], icon: siXiaomi },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const OS_LIST = [
|
||||||
|
// Infrastructure
|
||||||
|
{ value: 'proxmox', label: 'Proxmox VE', kw: ['proxmox', 'pve'], icon: siProxmox },
|
||||||
|
{ value: 'truenas', label: 'TrueNAS', kw: ['truenas', 'freenas'], icon: siTruenas },
|
||||||
|
{ value: 'synology', label: 'Synology DSM', kw: ['synology', 'dsm'], icon: siSynology },
|
||||||
|
// Réseau / pare-feu
|
||||||
|
{ value: 'openwrt', label: 'OpenWrt', kw: ['openwrt'], icon: siOpenwrt },
|
||||||
|
{ value: 'pfsense', label: 'pfSense', kw: ['pfsense'], icon: siPfsense },
|
||||||
|
{ value: 'opnsense', label: 'OPNsense', kw: ['opnsense'], icon: siOpnsense },
|
||||||
|
// SBC
|
||||||
|
{ value: 'raspbian', label: 'Raspberry Pi OS', kw: ['raspbian', 'raspberry pi os', 'pi os'], icon: siRaspberrypi },
|
||||||
|
// Debian / Ubuntu
|
||||||
|
{ value: 'ubuntu', label: 'Ubuntu', kw: ['ubuntu'], icon: siUbuntu },
|
||||||
|
{ value: 'kubuntu', label: 'Kubuntu', kw: ['kubuntu'], icon: siKubuntu },
|
||||||
|
{ value: 'lubuntu', label: 'Lubuntu', kw: ['lubuntu'], icon: siLubuntu },
|
||||||
|
{ value: 'xubuntu', label: 'Xubuntu', kw: ['xubuntu'], icon: siXubuntu },
|
||||||
|
{ value: 'ubuntumate', label: 'Ubuntu MATE', kw: ['ubuntu mate', 'ubuntumate'], icon: siUbuntumate },
|
||||||
|
{ value: 'debian', label: 'Debian', kw: ['debian'], icon: siDebian },
|
||||||
|
// Red Hat / CentOS
|
||||||
|
{ value: 'redhat', label: 'Red Hat', kw: ['red hat', 'redhat', 'rhel'], icon: siRedhat },
|
||||||
|
{ value: 'centos', label: 'CentOS', kw: ['centos'], icon: siCentos },
|
||||||
|
{ value: 'almalinux', label: 'AlmaLinux', kw: ['almalinux', 'alma linux'], icon: { ...siAlmalinux, hex: '4d4d4d' } },
|
||||||
|
{ value: 'rockylinux', label: 'Rocky Linux', kw: ['rockylinux', 'rocky linux', 'rocky'], icon: siRockylinux },
|
||||||
|
{ value: 'fedora', label: 'Fedora', kw: ['fedora'], icon: siFedora },
|
||||||
|
// SUSE
|
||||||
|
{ value: 'opensuse', label: 'openSUSE', kw: ['opensuse', 'open suse'], icon: siOpensuse },
|
||||||
|
{ value: 'suse', label: 'SUSE Linux Enterprise', kw: ['sles', 'suse linux enterprise'], icon: { ...siSuse, hex: '73BA25' } },
|
||||||
|
// Arch
|
||||||
|
{ value: 'arch', label: 'Arch Linux', kw: ['arch linux', 'archlinux'], icon: siArchlinux },
|
||||||
|
{ value: 'manjaro', label: 'Manjaro', kw: ['manjaro'], icon: siManjaro },
|
||||||
|
{ value: 'endeavouros', label: 'EndeavourOS', kw: ['endeavouros', 'endeavour os'], icon: siEndeavouros },
|
||||||
|
{ value: 'artixlinux', label: 'Artix Linux', kw: ['artixlinux', 'artix linux', 'artix'], icon: siArtixlinux },
|
||||||
|
// Alpine
|
||||||
|
{ value: 'alpine', label: 'Alpine Linux', kw: ['alpine linux', 'alpine'], icon: siAlpinelinux },
|
||||||
|
// Divers
|
||||||
|
{ value: 'nixos', label: 'NixOS', kw: ['nixos', 'nix os'], icon: siNixos },
|
||||||
|
{ value: 'gentoo', label: 'Gentoo', kw: ['gentoo'], icon: siGentoo },
|
||||||
|
{ value: 'voidlinux', label: 'Void Linux', kw: ['voidlinux', 'void linux'], icon: siVoidlinux },
|
||||||
|
{ value: 'slackware', label: 'Slackware', kw: ['slackware'], icon: { ...siSlackware, hex: '4d4d4d' } },
|
||||||
|
{ value: 'asahilinux', label: 'Asahi Linux', kw: ['asahi linux', 'asahi'], icon: siAsahilinux },
|
||||||
|
// Bureau
|
||||||
|
{ value: 'linuxmint', label: 'Linux Mint', kw: ['linux mint', 'linuxmint'], icon: siLinuxmint },
|
||||||
|
{ value: 'popos', label: 'Pop!_OS', kw: ["pop!_os", 'popos', 'pop os'], icon: siPopos },
|
||||||
|
{ value: 'zorin', label: 'Zorin OS', kw: ['zorin'], icon: siZorin },
|
||||||
|
{ value: 'elementary', label: 'elementary OS', kw: ['elementary os', 'elementaryos'], icon: siElementary },
|
||||||
|
{ value: 'deepin', label: 'deepin', kw: ['deepin'], icon: siDeepin },
|
||||||
|
{ value: 'mxlinux', label: 'MX Linux', kw: ['mx linux', 'mxlinux'], icon: { ...siMxlinux, hex: '4d4d4d' } },
|
||||||
|
{ value: 'solus', label: 'Solus', kw: ['solus'], icon: siSolus },
|
||||||
|
// BSD
|
||||||
|
{ value: 'freebsd', label: 'FreeBSD', kw: ['freebsd', 'free bsd'], icon: siFreebsd },
|
||||||
|
{ value: 'openbsd', label: 'OpenBSD', kw: ['openbsd'], icon: siOpenbsd },
|
||||||
|
{ value: 'netbsd', label: 'NetBSD', kw: ['netbsd'], icon: siNetbsd },
|
||||||
|
// Sécurité
|
||||||
|
{ value: 'kali', label: 'Kali Linux', kw: ['kali linux', 'kalilinux'], icon: siKalilinux },
|
||||||
|
{ value: 'parrot', label: 'Parrot Security', kw: ['parrot security', 'parrotsec', 'parrot os'], icon: siParrotsecurity },
|
||||||
|
{ value: 'tails', label: 'Tails', kw: ['tails os', 'tails linux'], icon: siTails },
|
||||||
|
{ value: 'qubes', label: 'Qubes OS', kw: ['qubes os', 'qubesos'], icon: siQubesos },
|
||||||
|
// Windows / macOS / mobile
|
||||||
|
{ value: 'windows', label: 'Windows', kw: ['windows', 'win10', 'win11', 'winserver', 'windows server'], icon: ICON_WINDOWS },
|
||||||
|
{ value: 'macos', label: 'macOS', kw: ['macos', 'mac os'], icon: { ...siMacos, hex: '555555' } },
|
||||||
|
{ value: 'ios', label: 'iOS / iPadOS', kw: ['ios', 'ipados'], icon: { ...siIos, hex: '555555' } },
|
||||||
|
{ value: 'android', label: 'Android', kw: ['android'], icon: siAndroid },
|
||||||
|
// Autres
|
||||||
|
{ value: 'reactos', label: 'ReactOS', kw: ['reactos'], icon: siReactos },
|
||||||
|
{ value: 'linux', label: 'Linux (générique)', kw: ['linux'], icon: siLinux },
|
||||||
|
{ value: 'bsd', label: 'BSD (générique)', kw: ['bsd'], icon: siBsd },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function osIcon(value) {
|
||||||
|
return OS_LIST.find(o => o.value === value) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectOs(name, description) {
|
||||||
|
const text = ((name || '') + ' ' + (description || '')).toLowerCase()
|
||||||
|
if (!text.trim()) return null
|
||||||
|
return OS_LIST.find(o => o.kw.some(kw => text.includes(kw))) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
export function detectBrands(name, description) {
|
export function detectBrands(name, description) {
|
||||||
const text = ((name || '') + ' ' + (description || '')).toLowerCase()
|
const text = ((name || '') + ' ' + (description || '')).toLowerCase()
|
||||||
if (!text.trim()) return []
|
if (!text.trim()) return []
|
||||||
|
|||||||
@@ -149,90 +149,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="showForm" class="modal-overlay" @click.self="showForm = false">
|
<DeviceFormModal
|
||||||
<div class="modal">
|
v-if="showForm"
|
||||||
<h2>{{ editing ? t('editDevice') : t('newDevice') }}</h2>
|
:device="editing"
|
||||||
<form @submit.prevent="save">
|
:vlans="props.vlans"
|
||||||
<div class="fields-row">
|
@close="showForm = false"
|
||||||
<div class="field">
|
@saved="emit('refresh')"
|
||||||
<label>{{ t('fieldName') }}</label>
|
/>
|
||||||
<input v-model="form.name" type="text" required :placeholder="t('deviceNamePlaceholder')" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>{{ t('fieldType') }}</label>
|
|
||||||
<select v-model="form.type">
|
|
||||||
<option v-for="dtype in deviceTypes" :key="dtype.value" :value="dtype.value">{{ dtype.label }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>{{ t('fieldDescription') }}</label>
|
|
||||||
<textarea v-model="form.description" rows="2" :placeholder="t('descPlaceholder')"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="checkboxes">
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" v-model="form.is_gateway" />
|
|
||||||
<span>{{ t('isGateway') }}</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" v-model="form.is_livebox" />
|
|
||||||
<span>{{ t('isLivebox') }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div v-if="!['desktop','laptop'].includes(form.type)" class="field">
|
|
||||||
<label>{{ t('accessUrl') }}</label>
|
|
||||||
<input v-model="form.url" type="url" placeholder="https://192.168.1.1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label>{{ t('runtimeType') }}</label>
|
|
||||||
<select v-model="form.virt_type">
|
|
||||||
<option :value="null">{{ t('notSpecified') }}</option>
|
|
||||||
<option value="baremetal">{{ t('baremetal') }}</option>
|
|
||||||
<option value="lxc">{{ t('lxcContainer') }}</option>
|
|
||||||
<option value="qemu">{{ t('vmQemu') }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="interfaces-section">
|
|
||||||
<div class="interfaces-header">
|
|
||||||
<strong>{{ t('networkInterfaces') }}</strong>
|
|
||||||
<button type="button" class="btn-add-iface" @click="addInterface">{{ t('addInterface') }}</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="form.interfaces.length === 0" class="iface-empty">
|
|
||||||
{{ t('noInterface') }}
|
|
||||||
</div>
|
|
||||||
<div v-for="(iface, idx) in form.interfaces" :key="idx" class="iface-row">
|
|
||||||
<input v-model="iface.name" type="text" placeholder="eth0" class="iface-input" />
|
|
||||||
<input v-model="iface.ip_address" type="text" placeholder="IP" class="iface-input wide" />
|
|
||||||
<select v-model="iface.vlan_id" class="iface-select">
|
|
||||||
<option :value="null">— VLAN</option>
|
|
||||||
<option v-for="v in props.vlans" :key="v.id" :value="v.id">
|
|
||||||
{{ v.vlan_id != null ? 'VLAN ' + v.vlan_id : 'LAN' }} — {{ v.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<label class="iface-upstream">
|
|
||||||
<input type="checkbox" v-model="iface.is_upstream" />
|
|
||||||
<span>{{ t('wanLabel') }}</span>
|
|
||||||
</label>
|
|
||||||
<button type="button" class="btn-rm-iface" @click="removeInterface(idx)">✕</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="button" class="btn-secondary" @click="showForm = false">{{ t('cancel') }}</button>
|
|
||||||
<button type="submit" class="btn-primary">{{ editing ? t('save') : t('create') }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { devicesApi } from '../api.js'
|
import { devicesApi } from '../api.js'
|
||||||
import DeviceIcon from './DeviceIcon.vue'
|
import DeviceIcon from './DeviceIcon.vue'
|
||||||
|
import DeviceFormModal from './DeviceFormModal.vue'
|
||||||
import { detectBrands } from '../brandIcons.js'
|
import { detectBrands } from '../brandIcons.js'
|
||||||
import { t, tFmt } from '../i18n.js'
|
import { t, tFmt } from '../i18n.js'
|
||||||
|
|
||||||
@@ -360,10 +291,6 @@ const emit = defineEmits(['refresh'])
|
|||||||
|
|
||||||
const showForm = ref(false)
|
const showForm = ref(false)
|
||||||
const editing = ref(null)
|
const editing = ref(null)
|
||||||
const form = reactive({
|
|
||||||
name: '', type: 'server', description: '',
|
|
||||||
is_gateway: false, is_livebox: false, virt_type: null, url: null, interfaces: []
|
|
||||||
})
|
|
||||||
|
|
||||||
function typeLabel(type) {
|
function typeLabel(type) {
|
||||||
return deviceTypes.value.find(d => d.value === type)?.label || type
|
return deviceTypes.value.find(d => d.value === type)?.label || type
|
||||||
@@ -392,47 +319,14 @@ function vlanLabel(vlanId) {
|
|||||||
|
|
||||||
function openAdd() {
|
function openAdd() {
|
||||||
editing.value = null
|
editing.value = null
|
||||||
Object.assign(form, { name: '', type: 'server', description: '', is_gateway: false, is_livebox: false, virt_type: null, url: null, interfaces: [] })
|
|
||||||
showForm.value = true
|
showForm.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(device) {
|
function openEdit(device) {
|
||||||
editing.value = device
|
editing.value = device
|
||||||
Object.assign(form, {
|
|
||||||
name: device.name,
|
|
||||||
type: device.type,
|
|
||||||
description: device.description,
|
|
||||||
is_gateway: device.is_gateway,
|
|
||||||
is_livebox: device.is_livebox,
|
|
||||||
virt_type: device.virt_type || null,
|
|
||||||
url: device.url || null,
|
|
||||||
interfaces: device.interfaces.map(i => ({ ...i }))
|
|
||||||
})
|
|
||||||
showForm.value = true
|
showForm.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function addInterface() {
|
|
||||||
form.interfaces.push({ name: 'eth' + form.interfaces.length, ip_address: '', vlan_id: null, is_upstream: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeInterface(idx) {
|
|
||||||
form.interfaces.splice(idx, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save() {
|
|
||||||
try {
|
|
||||||
if (editing.value) {
|
|
||||||
await devicesApi.update(editing.value.id, form)
|
|
||||||
} else {
|
|
||||||
await devicesApi.create(form)
|
|
||||||
}
|
|
||||||
showForm.value = false
|
|
||||||
emit('refresh')
|
|
||||||
} catch (e) {
|
|
||||||
alert(e.response?.data?.detail || t('saveError'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function remove(device) {
|
async function remove(device) {
|
||||||
if (!confirm(tFmt('confirmDeleteDevice', device.name))) return
|
if (!confirm(tFmt('confirmDeleteDevice', device.name))) return
|
||||||
try {
|
try {
|
||||||
@@ -641,58 +535,4 @@ h1 { font-size: 22px; font-weight: 700; color: var(--text-primary); }
|
|||||||
}
|
}
|
||||||
.iface-upstream { color: #F97316; font-weight: 600; font-size: 11px; }
|
.iface-upstream { color: #F97316; font-weight: 600; font-size: 11px; }
|
||||||
|
|
||||||
/* ── Modal ────────────────────────────────────────────────────────────────── */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed; inset: 0; background: var(--modal-overlay);
|
|
||||||
display: flex; align-items: center; justify-content: center; z-index: 100;
|
|
||||||
}
|
|
||||||
.modal {
|
|
||||||
background: var(--bg-card); border-radius: 16px; padding: 28px;
|
|
||||||
width: 600px; max-width: 95vw; max-height: 90vh; overflow-y: auto;
|
|
||||||
box-shadow: var(--shadow-modal);
|
|
||||||
}
|
|
||||||
.modal h2 { font-size: 18px; font-weight: 700; color: var(--text-primary); margin-bottom: 20px; }
|
|
||||||
|
|
||||||
.fields-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
||||||
.field { margin-bottom: 14px; }
|
|
||||||
.field label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 5px; }
|
|
||||||
.field input[type="text"],
|
|
||||||
.field input[type="url"],
|
|
||||||
.field input[type="number"],
|
|
||||||
.field select,
|
|
||||||
.field textarea {
|
|
||||||
width: 100%; padding: 9px 12px; border: 1.5px solid var(--border);
|
|
||||||
border-radius: 8px; font-size: 14px; color: var(--text-primary);
|
|
||||||
background: var(--bg-input); box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.field input:focus, .field select:focus, .field textarea:focus {
|
|
||||||
outline: none; border-color: #3B82F6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxes { display: flex; gap: 20px; margin-bottom: 16px; }
|
|
||||||
.checkbox { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; color: var(--text-secondary); }
|
|
||||||
.checkbox input { width: 16px; height: 16px; cursor: pointer; }
|
|
||||||
|
|
||||||
.interfaces-section { border: 1.5px solid var(--border); border-radius: 10px; padding: 14px; margin-bottom: 14px; }
|
|
||||||
.interfaces-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
|
||||||
.interfaces-header strong { font-size: 13px; color: var(--text-secondary); }
|
|
||||||
.btn-add-iface {
|
|
||||||
padding: 5px 12px; background: #EFF6FF; color: #3B82F6;
|
|
||||||
border: 1.5px solid #BFDBFE; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn-add-iface:hover { background: #DBEAFE; }
|
|
||||||
.iface-empty { font-size: 13px; color: var(--text-faint); text-align: center; padding: 8px 0; }
|
|
||||||
|
|
||||||
.iface-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
|
|
||||||
.iface-input { padding: 7px 10px; border: 1.5px solid var(--border); border-radius: 6px; font-size: 13px; width: 80px; background: var(--bg-input); color: var(--text-primary); }
|
|
||||||
.iface-input.wide { width: 130px; }
|
|
||||||
.iface-select { padding: 7px 10px; border: 1.5px solid var(--border); border-radius: 6px; font-size: 13px; background: var(--bg-input); color: var(--text-primary); }
|
|
||||||
.iface-upstream { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--text-muted); cursor: pointer; }
|
|
||||||
.btn-rm-iface {
|
|
||||||
width: 26px; height: 26px; border: none; border-radius: 6px;
|
|
||||||
background: #FEE2E2; color: #EF4444; font-size: 14px; cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn-rm-iface:hover { background: #FECACA; }
|
|
||||||
|
|
||||||
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ const LANGS = {
|
|||||||
tabTopology: 'Topologie',
|
tabTopology: 'Topologie',
|
||||||
tabNetworks: 'Réseaux',
|
tabNetworks: 'Réseaux',
|
||||||
tabDevices: 'Équipements',
|
tabDevices: 'Équipements',
|
||||||
|
tabAddressing: 'Adressage IP',
|
||||||
|
ipAddressing: 'Adressage IP',
|
||||||
|
colHostname: "Nom d'hôte",
|
||||||
|
colOs: 'OS',
|
||||||
|
fieldOs: "Système d'exploitation",
|
||||||
|
noIpConfigured: 'Aucune adresse IP configurée.',
|
||||||
discovery: '🔍 Découverte auto',
|
discovery: '🔍 Découverte auto',
|
||||||
statsNetwork: 'Réseau',
|
statsNetwork: 'Réseau',
|
||||||
statsNetworks: 'Réseaux',
|
statsNetworks: 'Réseaux',
|
||||||
@@ -187,6 +193,12 @@ const LANGS = {
|
|||||||
tabTopology: 'Topology',
|
tabTopology: 'Topology',
|
||||||
tabNetworks: 'Networks',
|
tabNetworks: 'Networks',
|
||||||
tabDevices: 'Devices',
|
tabDevices: 'Devices',
|
||||||
|
tabAddressing: 'IP Addressing',
|
||||||
|
ipAddressing: 'IP Addressing',
|
||||||
|
colHostname: 'Hostname',
|
||||||
|
colOs: 'OS',
|
||||||
|
fieldOs: 'Operating system',
|
||||||
|
noIpConfigured: 'No IP address configured.',
|
||||||
discovery: '🔍 Auto discovery',
|
discovery: '🔍 Auto discovery',
|
||||||
statsNetwork: 'Network',
|
statsNetwork: 'Network',
|
||||||
statsNetworks: 'Networks',
|
statsNetworks: 'Networks',
|
||||||
@@ -355,6 +367,12 @@ const LANGS = {
|
|||||||
tabTopology: 'Topología',
|
tabTopology: 'Topología',
|
||||||
tabNetworks: 'Redes',
|
tabNetworks: 'Redes',
|
||||||
tabDevices: 'Equipos',
|
tabDevices: 'Equipos',
|
||||||
|
tabAddressing: 'Direccionamiento IP',
|
||||||
|
ipAddressing: 'Direccionamiento IP',
|
||||||
|
colHostname: 'Nombre de host',
|
||||||
|
colOs: 'SO',
|
||||||
|
fieldOs: 'Sistema operativo',
|
||||||
|
noIpConfigured: 'No hay ninguna dirección IP configurada.',
|
||||||
discovery: '🔍 Descubrimiento auto',
|
discovery: '🔍 Descubrimiento auto',
|
||||||
statsNetwork: 'Red',
|
statsNetwork: 'Red',
|
||||||
statsNetworks: 'Redes',
|
statsNetworks: 'Redes',
|
||||||
|
|||||||
Reference in New Issue
Block a user