Add full search parity and optimize map performance

Search: refactor into modular helpers, add IP/hostname/notification/
vendor/model/type search, smart auto-redirects for exact matches,
ServiceNOW prefix detection, filter buttons with counts, share links
with highlight support, and dark mode badge colors.

Map: fix N+1 queries with eager loading (248->28 queries), switch to
canvas-rendered circleMarkers for better performance with 500+ assets.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-03 23:07:09 -05:00
parent 180e6a0915
commit fe7a00b260
5 changed files with 1018 additions and 172 deletions

View File

@@ -109,6 +109,8 @@ let imageOverlay = null
const markers = ref([])
const pickedPosition = ref(null)
let pickerMarker = null
let markerLayer = null
let canvasRenderer = null
const filters = ref({
machinetype: '',
@@ -258,11 +260,15 @@ const visibleSubtypes = computed(() => {
function initMap() {
if (!mapContainer.value) return
// Use canvas renderer for performance - renders all markers on a single <canvas>
canvasRenderer = L.canvas({ padding: 0.5 })
map = L.map(mapContainer.value, {
crs: L.CRS.Simple,
minZoom: -4,
maxZoom: 2,
attributionControl: false
attributionControl: false,
renderer: canvasRenderer
})
const blueprintUrl = props.theme === 'light'
@@ -397,21 +403,21 @@ function renderMarkers() {
detailRoute = getDetailRoute(item)
}
const icon = L.divIcon({
html: `<div class="machine-marker-dot" style="background: ${color};"></div>`,
iconSize: [12, 12],
iconAnchor: [6, 6],
popupAnchor: [0, -6],
className: 'machine-marker'
// Use circleMarker instead of divIcon marker - renders on canvas
// for much better performance (no DOM element per marker)
const marker = L.circleMarker([leafletY, leafletX], {
radius: 6,
fillColor: color,
color: 'rgba(255,255,255,0.8)',
weight: 2,
fillOpacity: 1,
renderer: canvasRenderer
})
const marker = L.marker([leafletY, leafletX], { icon })
// Build tooltip content
let tooltipLines = [`<strong>${displayName}</strong>`]
if (props.assetTypeMode) {
// Asset mode tooltips
tooltipLines.push(`<span style="color: #888;">${item.assettype || 'Unknown'}</span>`)
if (item.primaryip) {
@@ -421,7 +427,6 @@ function renderMarkers() {
tooltipLines.push(`<span style="color: #8cf;">${item.typedata.hostname}</span>`)
}
} else {
// Legacy machine mode tooltips
const category = item.category?.toLowerCase() || ''
if (typeName && typeName.toLowerCase() !== 'locationonly') {
@@ -448,7 +453,6 @@ function renderMarkers() {
}
}
// Business unit
if (item.businessunit) {
tooltipLines.push(`<span style="color: #ccc;">${item.businessunit}</span>`)
}
@@ -561,7 +565,7 @@ function applyFilters() {
visible = false
}
marker.setOpacity(visible ? 1 : 0.15)
marker.setStyle({ fillOpacity: visible ? 1 : 0.15, opacity: visible ? 1 : 0.15 })
})
}
@@ -571,9 +575,9 @@ function debounceSearch() {
searchTimeout = setTimeout(applyFilters, 300)
}
watch(() => props.machines, () => {
if (map) renderMarkers()
}, { deep: true })
watch(() => props.machines, (newVal, oldVal) => {
if (map && newVal !== oldVal) renderMarkers()
})
watch(() => props.theme, (newTheme) => {
if (imageOverlay && map) {