Add print badges, pagination, route splitting, JWT auth fixes, and list page alignment
- Fix equipment badge barcode not rendering (loading race condition) - Fix printer QR code not rendering on initial load (same race condition) - Add model image to equipment badge via imageurl from Model table - Fix white-on-white machine number text on badge, tighten barcode spacing - Add PaginationBar component used across all list pages - Split monolithic router into per-plugin route modules - Fix 25 GET API endpoints returning 401 (jwt_required -> optional=True) - Align list page columns across Equipment, PCs, and Network pages - Add print views: EquipmentBadge, PrinterQRSingle, PrinterQRBatch, USBLabelBatch - Add PC Relationships report, migration docs, and CLAUDE.md project guide - Various plugin model, API, and frontend refinements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,12 @@
|
||||
<div class="hero-card">
|
||||
<div class="hero-image">
|
||||
<div class="device-icon">
|
||||
<span class="icon">{{ getDeviceIcon() }}</span>
|
||||
<span class="icon"><component :is="getDeviceIcon()" :size="24" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="hero-title-row">
|
||||
<h1 class="hero-title">{{ device.network_device?.hostname || device.name || device.assetnumber }}</h1>
|
||||
<h1 class="hero-title">{{ device.networkdevice?.hostname || device.name || device.assetnumber }}</h1>
|
||||
<router-link
|
||||
v-if="authStore.isAuthenticated"
|
||||
:to="`/network/${deviceId}/edit`"
|
||||
@@ -18,14 +18,14 @@
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span class="badge" :class="getStatusClass(device.status_name)">
|
||||
{{ device.status_name || 'Unknown' }}
|
||||
<span class="badge" :class="getStatusClass(device.statusname)">
|
||||
{{ device.statusname || 'Unknown' }}
|
||||
</span>
|
||||
<span v-if="device.network_device?.networkdevicetype_name" class="meta-item">
|
||||
{{ device.network_device.networkdevicetype_name }}
|
||||
<span v-if="device.networkdevice?.networkdevicetypename" class="meta-item">
|
||||
{{ device.networkdevice.networkdevicetypename }}
|
||||
</span>
|
||||
<span v-if="device.network_device?.vendor_name" class="meta-item">
|
||||
{{ device.network_device.vendor_name }}
|
||||
<span v-if="device.networkdevice?.vendorname" class="meta-item">
|
||||
{{ device.networkdevice.vendorname }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="hero-details">
|
||||
@@ -37,19 +37,19 @@
|
||||
<span class="label">Serial</span>
|
||||
<span class="value mono">{{ device.serialnumber }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="device.location_name">
|
||||
<div class="detail-item" v-if="device.locationname">
|
||||
<span class="label">Location</span>
|
||||
<span class="value">{{ device.location_name }}</span>
|
||||
<span class="value">{{ device.locationname }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="device.businessunit_name">
|
||||
<div class="detail-item" v-if="device.businessunitname">
|
||||
<span class="label">Business Unit</span>
|
||||
<span class="value">{{ device.businessunit_name }}</span>
|
||||
<span class="value">{{ device.businessunitname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-features" v-if="device.network_device">
|
||||
<span v-if="device.network_device.ispoe" class="feature-badge poe">PoE</span>
|
||||
<span v-if="device.network_device.ismanaged" class="feature-badge managed">Managed</span>
|
||||
<span v-if="device.network_device.portcount" class="feature-badge ports">{{ device.network_device.portcount }} Ports</span>
|
||||
<div class="hero-features" v-if="device.networkdevice">
|
||||
<span v-if="device.networkdevice.ispoe" class="feature-badge poe">PoE</span>
|
||||
<span v-if="device.networkdevice.ismanaged" class="feature-badge managed">Managed</span>
|
||||
<span v-if="device.networkdevice.portcount" class="feature-badge ports">{{ device.networkdevice.portcount }} Ports</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,27 +62,27 @@
|
||||
<div class="info-list">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Hostname</span>
|
||||
<span class="info-value mono">{{ device.network_device?.hostname || '-' }}</span>
|
||||
<span class="info-value mono">{{ device.networkdevice?.hostname || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Firmware Version</span>
|
||||
<span class="info-value">{{ device.network_device?.firmwareversion || '-' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.firmwareversion || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Port Count</span>
|
||||
<span class="info-value">{{ device.network_device?.portcount || '-' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.portcount || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Rack Unit</span>
|
||||
<span class="info-value">{{ device.network_device?.rackunit || '-' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.rackunit || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">PoE Capable</span>
|
||||
<span class="info-value">{{ device.network_device?.ispoe ? 'Yes' : 'No' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.ispoe ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Managed Device</span>
|
||||
<span class="info-value">{{ device.network_device?.ismanaged ? 'Yes' : 'No' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.ismanaged ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,17 +113,17 @@
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Vendor</span>
|
||||
<span class="info-value">{{ device.network_device?.vendor_name || '-' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.vendorname || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Device Type</span>
|
||||
<span class="info-value">{{ device.network_device?.networkdevicetype_name || '-' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.networkdevicetypename || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Status</span>
|
||||
<span class="info-value">
|
||||
<span class="badge" :class="getStatusClass(device.status_name)">
|
||||
{{ device.status_name || 'Unknown' }}
|
||||
<span class="badge" :class="getStatusClass(device.statusname)">
|
||||
{{ device.statusname || 'Unknown' }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -177,6 +177,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Network, Router, Shield, Wifi, Camera, Server, Server as Rack, Globe } from 'lucide-vue-next'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { networkApi } from '../../api'
|
||||
import AssetRelationships from '../../components/AssetRelationships.vue'
|
||||
@@ -207,15 +208,15 @@ async function loadDevice() {
|
||||
}
|
||||
|
||||
function getDeviceIcon() {
|
||||
const type = device.value?.network_device?.networkdevicetype_name?.toLowerCase() || ''
|
||||
if (type.includes('switch')) return '⏛'
|
||||
if (type.includes('router')) return '⇌'
|
||||
if (type.includes('firewall')) return '🛡'
|
||||
if (type.includes('access point') || type.includes('ap')) return '📶'
|
||||
if (type.includes('camera')) return '📷'
|
||||
if (type.includes('server')) return '🖥'
|
||||
if (type.includes('idf') || type.includes('closet')) return '🗄'
|
||||
return '🌐'
|
||||
const type = device.value?.networkdevice?.networkdevicetypename?.toLowerCase() || ''
|
||||
if (type.includes('switch')) return Network
|
||||
if (type.includes('router')) return Router
|
||||
if (type.includes('firewall')) return Shield
|
||||
if (type.includes('access point') || type.includes('ap')) return Wifi
|
||||
if (type.includes('camera')) return Camera
|
||||
if (type.includes('server')) return Server
|
||||
if (type.includes('idf') || type.includes('closet')) return Rack
|
||||
return Globe
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
@@ -240,7 +241,7 @@ function formatDate(dateStr) {
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (confirm(`Are you sure you want to delete ${device.value.network_device?.hostname || device.value.assetnumber}?`)) {
|
||||
if (confirm(`Are you sure you want to delete ${device.value.networkdevice?.hostname || device.value.assetnumber}?`)) {
|
||||
try {
|
||||
await networkApi.delete(deviceId)
|
||||
router.push('/network')
|
||||
|
||||
@@ -280,7 +280,7 @@ onMounted(async () => {
|
||||
|
||||
async function loadDeviceTypes() {
|
||||
try {
|
||||
const response = await networkApi.types.list({ per_page: 100 })
|
||||
const response = await networkApi.types.list({ perpage: 100 })
|
||||
deviceTypes.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading device types:', err)
|
||||
@@ -289,7 +289,7 @@ async function loadDeviceTypes() {
|
||||
|
||||
async function loadVendors() {
|
||||
try {
|
||||
const response = await vendorsApi.list({ per_page: 100 })
|
||||
const response = await vendorsApi.list({ perpage: 100 })
|
||||
vendors.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading vendors:', err)
|
||||
@@ -298,7 +298,7 @@ async function loadVendors() {
|
||||
|
||||
async function loadLocations() {
|
||||
try {
|
||||
const response = await locationsApi.list({ per_page: 100 })
|
||||
const response = await locationsApi.list({ perpage: 100 })
|
||||
locations.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading locations:', err)
|
||||
@@ -307,7 +307,7 @@ async function loadLocations() {
|
||||
|
||||
async function loadStatuses() {
|
||||
try {
|
||||
const response = await statusesApi.list({ per_page: 100 })
|
||||
const response = await statusesApi.list({ perpage: 100 })
|
||||
statuses.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading statuses:', err)
|
||||
@@ -316,7 +316,7 @@ async function loadStatuses() {
|
||||
|
||||
async function loadBusinessUnits() {
|
||||
try {
|
||||
const response = await businessunitsApi.list({ per_page: 100 })
|
||||
const response = await businessunitsApi.list({ perpage: 100 })
|
||||
businessUnits.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading business units:', err)
|
||||
@@ -340,15 +340,15 @@ async function loadDevice() {
|
||||
form.value.notes = data.notes || ''
|
||||
|
||||
// Network device specific
|
||||
if (data.network_device) {
|
||||
form.value.hostname = data.network_device.hostname || ''
|
||||
form.value.networkdevicetypeid = data.network_device.networkdevicetypeid || ''
|
||||
form.value.vendorid = data.network_device.vendorid || ''
|
||||
form.value.firmwareversion = data.network_device.firmwareversion || ''
|
||||
form.value.portcount = data.network_device.portcount
|
||||
form.value.rackunit = data.network_device.rackunit || ''
|
||||
form.value.ispoe = data.network_device.ispoe || false
|
||||
form.value.ismanaged = data.network_device.ismanaged || false
|
||||
if (data.networkdevice) {
|
||||
form.value.hostname = data.networkdevice.hostname || ''
|
||||
form.value.networkdevicetypeid = data.networkdevice.networkdevicetypeid || ''
|
||||
form.value.vendorid = data.networkdevice.vendorid || ''
|
||||
form.value.firmwareversion = data.networkdevice.firmwareversion || ''
|
||||
form.value.portcount = data.networkdevice.portcount
|
||||
form.value.rackunit = data.networkdevice.rackunit || ''
|
||||
form.value.ispoe = data.networkdevice.ispoe || false
|
||||
form.value.ismanaged = data.networkdevice.ismanaged || false
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading device:', err)
|
||||
@@ -386,7 +386,7 @@ async function submitForm() {
|
||||
router.push(`/network/${deviceId}`)
|
||||
} else {
|
||||
const response = await networkApi.create(payload)
|
||||
const newId = response.data.data?.network_device?.networkdeviceid
|
||||
const newId = response.data.data?.networkdevice?.networkdeviceid
|
||||
router.push(newId ? `/network/${newId}` : '/network')
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -54,37 +54,39 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Asset #</th>
|
||||
<th>Hostname</th>
|
||||
<th>Serial Number</th>
|
||||
<th>Type</th>
|
||||
<th>Vendor</th>
|
||||
<th>Features</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
<th>Location</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="device in devices" :key="device.network_device?.networkdeviceid || device.assetid">
|
||||
<td class="mono">{{ device.network_device?.hostname || '-' }}</td>
|
||||
<tr v-for="device in devices" :key="device.networkdevice?.networkdeviceid || device.assetid">
|
||||
<td>{{ device.assetnumber }}</td>
|
||||
<td>{{ device.network_device?.networkdevicetype_name || '-' }}</td>
|
||||
<td>{{ device.network_device?.vendor_name || '-' }}</td>
|
||||
<td class="mono">{{ device.networkdevice?.hostname || '-' }}</td>
|
||||
<td class="mono">{{ device.serialnumber || '-' }}</td>
|
||||
<td>{{ device.networkdevice?.networkdevicetypename || '-' }}</td>
|
||||
<td>{{ device.networkdevice?.vendorname || '-' }}</td>
|
||||
<td class="features">
|
||||
<span v-if="device.network_device?.ispoe" class="feature-tag poe">PoE</span>
|
||||
<span v-if="device.network_device?.ismanaged" class="feature-tag managed">Managed</span>
|
||||
<span v-if="device.network_device?.portcount" class="feature-tag ports">{{ device.network_device.portcount }} ports</span>
|
||||
<span v-if="!device.network_device?.ispoe && !device.network_device?.ismanaged && !device.network_device?.portcount">-</span>
|
||||
<span v-if="device.networkdevice?.ispoe" class="feature-tag poe">PoE</span>
|
||||
<span v-if="device.networkdevice?.ismanaged" class="feature-tag managed">Managed</span>
|
||||
<span v-if="device.networkdevice?.portcount" class="feature-tag ports">{{ device.networkdevice.portcount }} ports</span>
|
||||
<span v-if="!device.networkdevice?.ispoe && !device.networkdevice?.ismanaged && !device.networkdevice?.portcount">-</span>
|
||||
</td>
|
||||
<td>{{ device.location_name || '-' }}</td>
|
||||
<td>
|
||||
<span class="badge" :class="getStatusClass(device.status_name)">
|
||||
{{ device.status_name || 'Unknown' }}
|
||||
<span class="badge" :class="getStatusClass(device.statusname)">
|
||||
{{ device.statusname || 'Unknown' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ device.locationname || '-' }}</td>
|
||||
<td class="actions">
|
||||
<router-link
|
||||
:to="`/network/${device.network_device?.networkdeviceid}`"
|
||||
:to="`/network/${device.networkdevice?.networkdeviceid}`"
|
||||
class="btn btn-secondary btn-sm"
|
||||
>
|
||||
View
|
||||
@@ -92,7 +94,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="devices.length === 0">
|
||||
<td colspan="8" style="text-align: center; color: var(--text-light);">
|
||||
<td colspan="9" style="text-align: center; color: var(--text-light);">
|
||||
No network devices found
|
||||
</td>
|
||||
</tr>
|
||||
@@ -101,36 +103,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button
|
||||
:disabled="page === 1"
|
||||
@click="goToPage(page - 1)"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<button
|
||||
v-for="p in visiblePages"
|
||||
:key="p"
|
||||
:class="{ active: p === page }"
|
||||
@click="goToPage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="page === totalPages"
|
||||
@click="goToPage(page + 1)"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<PaginationBar
|
||||
:page="page"
|
||||
:totalPages="totalPages"
|
||||
:perPage="perPage"
|
||||
@update:page="goToPage"
|
||||
@update:perPage="changePerPage"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { networkApi, vendorsApi, locationsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const devices = ref([])
|
||||
const deviceTypes = ref([])
|
||||
@@ -143,20 +131,11 @@ const vendorFilter = ref('')
|
||||
const locationFilter = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(25)
|
||||
const totalCount = ref(0)
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const pages = []
|
||||
const start = Math.max(1, page.value - 2)
|
||||
const end = Math.min(totalPages.value, page.value + 2)
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
return pages
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadDeviceTypes(),
|
||||
@@ -168,7 +147,7 @@ onMounted(async () => {
|
||||
|
||||
async function loadDeviceTypes() {
|
||||
try {
|
||||
const response = await networkApi.types.list({ per_page: 100 })
|
||||
const response = await networkApi.types.list({ perpage: 100 })
|
||||
deviceTypes.value = response.data.data || []
|
||||
// Get counts for each type
|
||||
await updateTypeCounts()
|
||||
@@ -181,7 +160,7 @@ async function updateTypeCounts() {
|
||||
// Get summary for type counts
|
||||
try {
|
||||
const response = await networkApi.dashboardSummary()
|
||||
const byType = response.data.data?.by_type || []
|
||||
const byType = response.data.data?.bytype || response.data.data?.by_type || []
|
||||
totalCount.value = response.data.data?.total || 0
|
||||
|
||||
// Map counts to types
|
||||
@@ -196,7 +175,7 @@ async function updateTypeCounts() {
|
||||
|
||||
async function loadVendors() {
|
||||
try {
|
||||
const response = await vendorsApi.list({ per_page: 100 })
|
||||
const response = await vendorsApi.list({ perpage: 100 })
|
||||
vendors.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error loading vendors:', error)
|
||||
@@ -205,7 +184,7 @@ async function loadVendors() {
|
||||
|
||||
async function loadLocations() {
|
||||
try {
|
||||
const response = await locationsApi.list({ per_page: 100 })
|
||||
const response = await locationsApi.list({ perpage: 100 })
|
||||
locations.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error loading locations:', error)
|
||||
@@ -217,7 +196,7 @@ async function loadDevices() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
per_page: 25
|
||||
perpage: perPage.value
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
if (selectedType.value) params.type_id = selectedType.value
|
||||
@@ -226,7 +205,7 @@ async function loadDevices() {
|
||||
|
||||
const response = await networkApi.list(params)
|
||||
devices.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pagination?.total_pages || 1
|
||||
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
||||
} catch (error) {
|
||||
console.error('Error loading network devices:', error)
|
||||
} finally {
|
||||
@@ -255,6 +234,12 @@ function goToPage(p) {
|
||||
}
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
if (!status) return 'badge-info'
|
||||
const s = status.toLowerCase()
|
||||
|
||||
Reference in New Issue
Block a user