Network: consolidate into one tabbed hub; subnet devices span all asset types
Some checks failed
CI / backend (push) Successful in 1m39s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 8s

Replaces the two flat "Network Devices" + "View Networks" nav entries with a
single "Network" entry opening a tabbed hub: Devices | Networks | VLANs
(NetworkHub renders the existing device list, the subnet browse, and the VLAN
list; VLANs is now reachable outside Settings). /network -> hub; /networks
redirects to the Networks tab; subnet detail stays at /networks/:id.

Subnet "Devices on this network" now matches ANY asset whose primary IP falls in
the CIDR (PCs, printers, machines, measuring tools - not just network devices),
computed on the core Communication + Asset tables; each row links to its typed
detail (extension id resolved lazily/guarded per plugin). Fixes the empty list -
printers and PCs carry IPs and now appear (e.g. 35 devices on 10.80.92.0/24).

Also: subnet-browse search uses the standard form-control styling; dropped the
redundant per-tab page header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-13 14:15:51 -04:00
parent dd541fba0a
commit 8528617037
7 changed files with 129 additions and 38 deletions

View File

@@ -5,7 +5,7 @@ export default [
{
path: 'network',
name: 'network',
component: () => import('../../views/network/NetworkDevicesList.vue'),
component: () => import('../../views/network/NetworkHub.vue'),
meta: { plugin: 'network' }
},
{
@@ -27,10 +27,9 @@ export default [
meta: { requiresAuth: true, plugin: 'network' }
},
{
// Legacy path -> the Networks tab of the hub.
path: 'networks',
name: 'networks',
component: () => import('../../views/network/SubnetsBrowse.vue'),
meta: { plugin: 'network' }
redirect: { path: '/network', query: { tab: 'networks' } }
},
{
path: 'networks/:id',

View File

@@ -156,8 +156,7 @@ const defaultNav = [
{ name: 'Map', icon: 'map', route: '/map', position: 4 },
{ name: 'Machines', icon: 'cog', route: '/machines', position: 10 },
{ name: 'PCs', icon: 'desktop', route: '/pcs', position: 15 },
{ name: 'Network Devices', icon: 'network-wired', route: '/network', position: 18 },
{ name: 'View Networks', icon: 'globe', route: '/networks', position: 19 },
{ name: 'Network', icon: 'network-wired', route: '/network', position: 18 },
{ name: 'Printers', icon: 'printer', route: '/printers', position: 20 },
{ name: 'USB Devices', icon: 'usb', route: '/usb', position: 45 },
{ name: 'Applications', icon: 'app-window', route: '/applications', position: 30, section: 'information' },

View File

@@ -0,0 +1,72 @@
<template>
<div>
<div class="page-header">
<h1>Network</h1>
</div>
<div class="hub-tabs">
<button
v-for="tab in tabs"
:key="tab.key"
class="hub-tab"
:class="{ active: active === tab.key }"
@click="setTab(tab.key)"
>{{ tab.label }}</button>
</div>
<component :is="current" />
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import NetworkDevicesList from './NetworkDevicesList.vue'
import SubnetsBrowse from './SubnetsBrowse.vue'
import VLANsList from '../settings/VLANsList.vue'
const route = useRoute()
const router = useRouter()
const tabs = [
{ key: 'devices', label: 'Devices', comp: NetworkDevicesList },
{ key: 'networks', label: 'Networks', comp: SubnetsBrowse },
{ key: 'vlans', label: 'VLANs', comp: VLANsList },
]
const active = ref(tabs.some(t => t.key === route.query.tab) ? route.query.tab : 'devices')
const current = computed(() => (tabs.find(t => t.key === active.value) || tabs[0]).comp)
function setTab(key) {
active.value = key
router.replace({ query: { ...route.query, tab: key } })
}
watch(() => route.query.tab, (value) => {
if (value && value !== active.value && tabs.some(t => t.key === value)) {
active.value = value
}
})
</script>
<style scoped>
.hub-tabs {
display: flex;
gap: 0.25rem;
border-bottom: 1px solid var(--border);
margin-bottom: 1.25rem;
}
.hub-tab {
background: none;
border: none;
border-bottom: 2px solid transparent;
padding: 0.6rem 1rem;
cursor: pointer;
color: var(--text-light);
font-size: 0.95rem;
font-weight: 500;
}
.hub-tab:hover { color: var(--text); }
.hub-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
</style>

View File

@@ -37,14 +37,13 @@
<tr><th>Device</th><th>IP</th><th>Type</th></tr>
</thead>
<tbody>
<tr v-for="dev in devices" :key="dev.networkdeviceid">
<tr v-for="dev in devices" :key="dev.assetid">
<td>
<router-link :to="`/network/${dev.networkdeviceid}`">
{{ dev.name || dev.hostname || dev.assetnumber }}
</router-link>
<router-link v-if="dev.url" :to="dev.url">{{ dev.name || dev.assetnumber }}</router-link>
<span v-else>{{ dev.name || dev.assetnumber }}</span>
</td>
<td class="mono">{{ dev.ipaddress }}</td>
<td>{{ dev.networkdevicetypename || '-' }}</td>
<td>{{ typeLabel(dev.assettype) }}</td>
</tr>
</tbody>
</table>
@@ -67,6 +66,14 @@ const loading = ref(true)
const devices = computed(() => subnet.value?.devices || [])
const TYPE_LABELS = {
computer: 'PC', network_device: 'Network Device', printer: 'Printer',
machine: 'Machine', measuring_tool: 'Measuring Tool',
}
function typeLabel(type) {
return TYPE_LABELS[type] || type || '-'
}
async function load() {
loading.value = true
try {

View File

@@ -1,11 +1,7 @@
<template>
<div>
<div class="page-header">
<h1>Networks</h1>
</div>
<div class="filters">
<input v-model="search" type="text" placeholder="Search networks..." class="search-input" />
<input v-model="search" type="text" placeholder="Search networks..." class="form-control" />
</div>
<div class="card">