infor annotations
This commit is contained in:
@@ -21,7 +21,7 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
|
||||
const [endDeviceId, setEndDeviceId] = useState<string | null>(null);
|
||||
|
||||
const isCableTool = ['fiber', 'cat6', 'cat6_poe'].includes(activeTool);
|
||||
const isDeviceTool = ['switch', 'indoor_ap', 'outdoor_ap'].includes(activeTool);
|
||||
const isDeviceTool = ['switch', 'indoor_ap', 'outdoor_ap', 'info'].includes(activeTool);
|
||||
const isWirelessTool = activeTool === 'wireless_mesh';
|
||||
|
||||
// Load all map items for snapping
|
||||
@@ -37,7 +37,7 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
|
||||
loadItems();
|
||||
}, [mapId]);
|
||||
|
||||
// Find nearby device for snapping
|
||||
// Find nearby device for snapping (exclude info markers)
|
||||
const findNearbyDevice = (lat: number, lng: number, radiusMeters = 5): any | null => {
|
||||
const devices = allItems.filter(item =>
|
||||
['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type) &&
|
||||
@@ -109,17 +109,25 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
|
||||
// Device placement - single click
|
||||
if (isDeviceTool) {
|
||||
try {
|
||||
const properties: any = {
|
||||
name: activeTool === 'info'
|
||||
? `Info at ${lat.toFixed(4)}, ${lng.toFixed(4)}`
|
||||
: `${activeTool} at ${lat.toFixed(4)}, ${lng.toFixed(4)}`,
|
||||
};
|
||||
|
||||
// Only add port_count and connections if it's not an info marker
|
||||
if (activeTool !== 'info') {
|
||||
properties.port_count = activeTool === 'switch' ? 5 : activeTool === 'outdoor_ap' ? 1 : 4;
|
||||
properties.connections = [];
|
||||
}
|
||||
|
||||
await mapItemService.createMapItem(mapId, {
|
||||
type: activeTool as any,
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [lng, lat],
|
||||
},
|
||||
properties: {
|
||||
name: `${activeTool} at ${lat.toFixed(4)}, ${lng.toFixed(4)}`,
|
||||
port_count: activeTool === 'switch' ? 5 : activeTool === 'outdoor_ap' ? 1 : 4,
|
||||
connections: [],
|
||||
},
|
||||
properties,
|
||||
});
|
||||
onItemCreated();
|
||||
// Reload items for snapping
|
||||
|
||||
@@ -86,6 +86,19 @@ const outdoorApIcon = new L.DivIcon({
|
||||
iconAnchor: [20, 40],
|
||||
});
|
||||
|
||||
const infoIcon = new L.DivIcon({
|
||||
html: `<div class="info-marker-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2"/>
|
||||
<path d="M12 16v-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||
<circle cx="12" cy="8" r="1.2"/>
|
||||
</svg>
|
||||
</div>`,
|
||||
className: 'custom-info-marker',
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 20],
|
||||
});
|
||||
|
||||
export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapItemsLayerProps) {
|
||||
const [items, setItems] = useState<MapItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -137,6 +150,8 @@ export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapIt
|
||||
return indoorApIcon;
|
||||
case 'outdoor_ap':
|
||||
return outdoorApIcon;
|
||||
case 'info':
|
||||
return infoIcon;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -343,9 +358,9 @@ export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapIt
|
||||
);
|
||||
}
|
||||
|
||||
// Render devices
|
||||
// Render devices and info markers
|
||||
if (
|
||||
['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type) &&
|
||||
['switch', 'indoor_ap', 'outdoor_ap', 'info'].includes(item.type) &&
|
||||
item.geometry.type === 'Point'
|
||||
) {
|
||||
const [lng, lat] = item.geometry.coordinates;
|
||||
@@ -373,7 +388,9 @@ export function MapItemsLayer({ mapId, refreshTrigger, readOnly = false }: MapIt
|
||||
<Popup>
|
||||
<div className="text-sm dark:bg-gray-800 dark:text-white" style={{ minWidth: '200px' }}>
|
||||
<div className="font-semibold text-gray-900 dark:text-white">{item.properties.name || item.type}</div>
|
||||
{item.type !== 'info' && (
|
||||
<div className="text-gray-600 dark:text-gray-400">Type: {item.type}</div>
|
||||
)}
|
||||
{item.properties.port_count && (
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
Ports: {item.properties.connections?.length || 0} / {item.properties.port_count}
|
||||
|
||||
@@ -74,6 +74,13 @@ const DEVICE_TOOLS: ToolButton[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const INFO_TOOL: ToolButton = {
|
||||
id: 'info',
|
||||
label: 'Info',
|
||||
icon: 'info',
|
||||
description: 'Information Marker',
|
||||
};
|
||||
|
||||
export function Toolbar({ mapId, readOnly = false }: ToolbarProps) {
|
||||
const { activeTool, setActiveTool } = useDrawingStore();
|
||||
|
||||
@@ -122,6 +129,17 @@ export function Toolbar({ mapId, readOnly = false }: ToolbarProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (tool.icon === 'info') {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" style={{ color: '#6366F1' }}>
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2"/>
|
||||
<path d="M12 16v-4"/>
|
||||
<circle cx="12" cy="8" r="1"/>
|
||||
<line x1="12" y1="12" x2="12" y2="16" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className="text-lg"
|
||||
@@ -180,6 +198,12 @@ export function Toolbar({ mapId, readOnly = false }: ToolbarProps) {
|
||||
{/* Devices section */}
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 px-1 mb-2">Devices</h3>
|
||||
{DEVICE_TOOLS.map(renderToolButton)}
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 my-2"></div>
|
||||
|
||||
{/* Info section */}
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 px-1 mb-2">Annotations</h3>
|
||||
{renderToolButton(INFO_TOOL)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,6 +125,30 @@
|
||||
color: #F59E0B;
|
||||
}
|
||||
|
||||
/* Info marker icon */
|
||||
.custom-info-marker {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.info-marker-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid #6366F1;
|
||||
color: #6366F1;
|
||||
}
|
||||
|
||||
.dark .info-marker-icon {
|
||||
background: rgb(31, 41, 55);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* Distance labels on map */
|
||||
.distance-label {
|
||||
background-color: rgba(255, 255, 255, 0.95) !important;
|
||||
|
||||
@@ -63,7 +63,7 @@ export function Dashboard() {
|
||||
<div
|
||||
className={`
|
||||
flex flex-col bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transition-all duration-300 ease-in-out
|
||||
fixed md:relative h-full w-80
|
||||
fixed md:relative h-full w-80 overflow-y-auto
|
||||
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
|
||||
`}
|
||||
style={{ zIndex: 9999 }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type MapItemType = 'cable' | 'switch' | 'indoor_ap' | 'outdoor_ap' | 'wireless_mesh';
|
||||
export type MapItemType = 'cable' | 'switch' | 'indoor_ap' | 'outdoor_ap' | 'wireless_mesh' | 'info';
|
||||
|
||||
export type CableType = 'fiber' | 'cat6' | 'cat6_poe';
|
||||
|
||||
@@ -60,7 +60,8 @@ export type DrawingTool =
|
||||
| 'switch'
|
||||
| 'indoor_ap'
|
||||
| 'outdoor_ap'
|
||||
| 'wireless_mesh';
|
||||
| 'wireless_mesh'
|
||||
| 'info';
|
||||
|
||||
export const CABLE_COLORS: Record<CableType, string> = {
|
||||
fiber: '#3B82F6', // Blue
|
||||
|
||||
Reference in New Issue
Block a user