Merge remote-tracking branch 'origin/main' into wilm
# Conflicts: # shopdb/__init__.py
This commit is contained in:
@@ -47,7 +47,7 @@ def _upsert_primary_ip(asset, ip):
|
||||
# =============================================================================
|
||||
|
||||
@network_bp.route('/types', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def list_network_device_types():
|
||||
"""List all network device types."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -69,7 +69,7 @@ def list_network_device_types():
|
||||
|
||||
|
||||
@network_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def get_network_device_type(type_id: int):
|
||||
"""Get a single network device type."""
|
||||
t = db.session.get(NetworkDeviceType, type_id)
|
||||
@@ -85,7 +85,6 @@ def get_network_device_type(type_id: int):
|
||||
|
||||
|
||||
@network_bp.route('/types', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('network.create')
|
||||
def create_network_device_type():
|
||||
"""Create a new network device type."""
|
||||
@@ -123,7 +122,6 @@ def create_network_device_type():
|
||||
|
||||
|
||||
@network_bp.route('/types/<int:type_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('network.edit')
|
||||
def update_network_device_type(type_id: int):
|
||||
"""Update a network device type."""
|
||||
@@ -158,7 +156,6 @@ def update_network_device_type(type_id: int):
|
||||
|
||||
|
||||
@network_bp.route('/types/<int:type_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('network.delete')
|
||||
def delete_network_device_type(type_id: int):
|
||||
"""Delete a network device type. Refused if any device still uses it."""
|
||||
@@ -179,7 +176,7 @@ def delete_network_device_type(type_id: int):
|
||||
# =============================================================================
|
||||
|
||||
@network_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def list_network_devices():
|
||||
"""
|
||||
List all network devices with filtering and pagination.
|
||||
@@ -287,7 +284,7 @@ def list_network_devices():
|
||||
|
||||
|
||||
@network_bp.route('/<int:device_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def get_network_device(device_id: int):
|
||||
"""Get a single network device with full details."""
|
||||
netdev = db.session.get(NetworkDevice, device_id)
|
||||
@@ -307,7 +304,7 @@ def get_network_device(device_id: int):
|
||||
|
||||
|
||||
@network_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def get_network_device_by_asset(asset_id: int):
|
||||
"""Get network device data by asset ID."""
|
||||
netdev = NetworkDevice.query.filter_by(assetid=asset_id).first()
|
||||
@@ -327,7 +324,7 @@ def get_network_device_by_asset(asset_id: int):
|
||||
|
||||
|
||||
@network_bp.route('/by-hostname/<hostname>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def get_network_device_by_hostname(hostname: str):
|
||||
"""Get network device by hostname."""
|
||||
netdev = NetworkDevice.query.filter_by(hostname=hostname).first()
|
||||
@@ -347,7 +344,6 @@ def get_network_device_by_hostname(hostname: str):
|
||||
|
||||
|
||||
@network_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('network.create')
|
||||
def create_network_device():
|
||||
"""
|
||||
@@ -464,7 +460,6 @@ def create_network_device():
|
||||
|
||||
|
||||
@network_bp.route('/<int:device_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('network.edit')
|
||||
def update_network_device(device_id: int):
|
||||
"""Update network device (both Asset and NetworkDevice records)."""
|
||||
@@ -559,7 +554,6 @@ def update_network_device(device_id: int):
|
||||
|
||||
|
||||
@network_bp.route('/<int:device_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('network.delete')
|
||||
def delete_network_device(device_id: int):
|
||||
"""Delete (soft delete) network device."""
|
||||
@@ -589,7 +583,7 @@ def delete_network_device(device_id: int):
|
||||
# =============================================================================
|
||||
|
||||
@network_bp.route('/dashboard/summary', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def dashboard_summary():
|
||||
"""Get network device dashboard summary data."""
|
||||
# Total active network devices
|
||||
@@ -637,7 +631,7 @@ def dashboard_summary():
|
||||
# =============================================================================
|
||||
|
||||
@network_bp.route('/vlans', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def list_vlans():
|
||||
"""List all VLANs with filtering and pagination."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -670,7 +664,7 @@ def list_vlans():
|
||||
|
||||
|
||||
@network_bp.route('/vlans/<int:vlan_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def get_vlan(vlan_id: int):
|
||||
"""Get a single VLAN with its subnets."""
|
||||
vlan = db.session.get(VLAN, vlan_id)
|
||||
@@ -690,7 +684,6 @@ def get_vlan(vlan_id: int):
|
||||
|
||||
|
||||
@network_bp.route('/vlans', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('network.create')
|
||||
def create_vlan():
|
||||
"""Create a new VLAN."""
|
||||
@@ -732,7 +725,6 @@ def create_vlan():
|
||||
|
||||
|
||||
@network_bp.route('/vlans/<int:vlan_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('network.edit')
|
||||
def update_vlan(vlan_id: int):
|
||||
"""Update a VLAN."""
|
||||
@@ -778,7 +770,6 @@ def update_vlan(vlan_id: int):
|
||||
|
||||
|
||||
@network_bp.route('/vlans/<int:vlan_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('network.delete')
|
||||
def delete_vlan(vlan_id: int):
|
||||
"""Delete (soft delete) a VLAN."""
|
||||
@@ -815,7 +806,7 @@ def delete_vlan(vlan_id: int):
|
||||
# =============================================================================
|
||||
|
||||
@network_bp.route('/subnets', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def list_subnets():
|
||||
"""List all subnets with filtering and pagination."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -856,7 +847,7 @@ def list_subnets():
|
||||
|
||||
|
||||
@network_bp.route('/subnets/<int:subnet_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('network.view')
|
||||
def get_subnet(subnet_id: int):
|
||||
"""Get a single subnet plus the network devices whose primary IP falls in
|
||||
its CIDR range."""
|
||||
@@ -932,7 +923,6 @@ def _asset_detail_url(assettype, assetid):
|
||||
|
||||
|
||||
@network_bp.route('/subnets', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('network.create')
|
||||
def create_subnet():
|
||||
"""Create a new subnet."""
|
||||
@@ -998,7 +988,6 @@ def create_subnet():
|
||||
|
||||
|
||||
@network_bp.route('/subnets/<int:subnet_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('network.edit')
|
||||
def update_subnet(subnet_id: int):
|
||||
"""Update a subnet."""
|
||||
@@ -1049,7 +1038,6 @@ def update_subnet(subnet_id: int):
|
||||
|
||||
|
||||
@network_bp.route('/subnets/<int:subnet_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('network.delete')
|
||||
def delete_subnet(subnet_id: int):
|
||||
"""Delete (soft delete) a subnet."""
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
<template>
|
||||
<div class="detail-page" v-if="device">
|
||||
<!--
|
||||
The same page-header + header-actions block every other asset detail page
|
||||
uses. This page had no header at all: Edit was tucked inside the hero
|
||||
title row and there was no Print Label, no Retire and no way back to the
|
||||
list, so managing a network device meant looking somewhere different from
|
||||
every other asset type.
|
||||
-->
|
||||
<div class="page-header">
|
||||
<h2>Network Device Details</h2>
|
||||
<div class="header-actions">
|
||||
<router-link :to="`/print/asset-label/network_device/${deviceId}`"
|
||||
class="btn btn-secondary" target="_blank">Print Label</router-link>
|
||||
<router-link v-if="authStore.isAuthenticated" :to="`/network/${deviceId}/edit`"
|
||||
class="btn btn-primary">Edit</router-link>
|
||||
<button v-if="authStore.isAuthenticated" @click="confirmRetire"
|
||||
class="btn btn-danger">Retire</button>
|
||||
<router-link to="/network" class="btn btn-secondary">Back to List</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-card">
|
||||
<div class="hero-image">
|
||||
<img v-if="device.networkdevice?.imageurl" :src="withBase(device.networkdevice.imageurl)" alt="Model photo" />
|
||||
@@ -10,13 +30,6 @@
|
||||
<div class="hero-content">
|
||||
<div class="hero-title-row">
|
||||
<h1 class="hero-title">{{ device.networkdevice?.hostname || device.name || device.assetnumber }}</h1>
|
||||
<router-link
|
||||
v-if="authStore.isAuthenticated"
|
||||
:to="`/network/${deviceId}/edit`"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Edit
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span class="badge" :style="colorStyle(device.statuscolor)">
|
||||
@@ -184,18 +197,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="action-bar" v-if="authStore.isAuthenticated">
|
||||
<router-link :to="`/print/asset-label/network_device/${deviceId}`" class="btn btn-secondary" target="_blank">
|
||||
Print Label
|
||||
</router-link>
|
||||
<router-link :to="`/network/${deviceId}/edit`" class="btn btn-primary">
|
||||
Edit Device
|
||||
</router-link>
|
||||
<button @click="confirmDelete" class="btn btn-danger">
|
||||
Delete Device
|
||||
</button>
|
||||
</div>
|
||||
<!-- The manage actions live in the page header, where every other asset
|
||||
detail page puts them. They used to sit down here instead, which is
|
||||
why finding them on a network device meant looking somewhere else. -->
|
||||
|
||||
<!-- Audit Footer -->
|
||||
<div class="audit-footer">
|
||||
@@ -228,6 +232,7 @@ import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
|
||||
import { useWarrantyBadge } from '@/composables/warrantyBadge'
|
||||
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
||||
import { useToast } from '@/composables/toast'
|
||||
import { apiError } from '@/utils/apiError'
|
||||
import { setRecordTitle } from '@/composables/pageTitle'
|
||||
const toast = useToast()
|
||||
|
||||
@@ -293,17 +298,24 @@ function formatDate(dateStr) {
|
||||
})
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (confirm(`Are you sure you want to delete ${device.value.networkdevice?.hostname || device.value.assetnumber}?`)) {
|
||||
try {
|
||||
await networkApi.delete(deviceId)
|
||||
router.push('/network')
|
||||
} catch (error) {
|
||||
console.error('Error deleting device:', error)
|
||||
toast.error('Failed to delete device')
|
||||
}
|
||||
// Soft delete, like every other asset type: the record leaves the lists while
|
||||
// its history and relationships stay put, and it can be restored by setting
|
||||
// isactive back. Called Retire rather than Delete for exactly that reason.
|
||||
async function confirmRetire () {
|
||||
const label = device.value?.networkdevice?.hostname
|
||||
|| device.value?.name || device.value?.assetnumber || 'this device'
|
||||
if (!confirm(`Retire ${label}?\n\nIt will no longer appear in ShopDB. `
|
||||
+ `History and relationships are kept, and it can be restored.`)) return
|
||||
try {
|
||||
await networkApi.delete(deviceId)
|
||||
toast.success(`Retired ${label}`)
|
||||
router.push('/network')
|
||||
} catch (error) {
|
||||
console.error('Error retiring network device:', error)
|
||||
toast.error(apiError(error, 'Failed to retire network device'))
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -207,7 +207,9 @@ class NetworkPlugin(BasePlugin):
|
||||
'name': 'Network',
|
||||
'icon': 'network-wired',
|
||||
'route': '/network',
|
||||
'permission': 'network.view',
|
||||
'position': 18,
|
||||
'section': 'assets',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user