mirror of
https://github.com/MikroWizard/mikrofront.git
synced 2025-12-06 18:19:29 +00:00
feat: add network topology maps for pro users
- Add comprehensive network visualization module - Pro feature with advanced topology mapping - Interactive network device mapping - Integration with navigation and routing
This commit is contained in:
parent
996c189076
commit
b20a3d7826
8 changed files with 1286 additions and 0 deletions
|
|
@ -49,6 +49,11 @@ const routes: Routes = [
|
|||
loadChildren: () =>
|
||||
import('./views/devices_group/devgroup.module').then((m) => m.DevicesGroupModule)
|
||||
},
|
||||
{
|
||||
path: 'maps',
|
||||
loadChildren: () =>
|
||||
import('./views/maps/maps.module').then((m) => m.MapsModule)
|
||||
},
|
||||
{
|
||||
path: 'authlog',
|
||||
loadChildren: () =>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ export const navItems: INavData[] = [
|
|||
// linkProps: { fragment: 'someAnchor' },
|
||||
icon: 'fa-solid fa-layer-group'
|
||||
},
|
||||
{
|
||||
name: 'Network Maps',
|
||||
url: '/maps',
|
||||
icon:'fa-solid fa-map',
|
||||
attributes: { 'pro':true }
|
||||
},
|
||||
// {
|
||||
// name: 'Tools',
|
||||
// url: '/login',
|
||||
|
|
|
|||
377
src/app/views/maps/code-typescript.txt
Normal file
377
src/app/views/maps/code-typescript.txt
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import { Router } from "@angular/router";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import { Network } from 'vis-network/peer';
|
||||
import { DataSet } from 'vis-data';
|
||||
|
||||
@Component({
|
||||
templateUrl: "maps.component.html",
|
||||
styleUrls: ["maps.component.scss"],
|
||||
})
|
||||
export class MapsComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public ispro: boolean = false;
|
||||
public tz: string;
|
||||
public savedPositions: any = {};
|
||||
public savedPositionsKey = "network-layout";
|
||||
public selectedDevice: any = null;
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
private router: Router,
|
||||
private login_checker: loginChecker
|
||||
) {
|
||||
var _self = this;
|
||||
if (!this.login_checker.isLoggedIn()) {
|
||||
setTimeout(function () {
|
||||
_self.router.navigate(["login"]);
|
||||
}, 100);
|
||||
}
|
||||
this.data_provider.getSessionInfo().then((res) => {
|
||||
_self.uid = res.uid;
|
||||
_self.uname = res.name;
|
||||
_self.ispro = res.ISPRO;
|
||||
if (!_self.ispro)
|
||||
setTimeout(function () {
|
||||
_self.router.navigate(["dashboard"]);
|
||||
}, 100);
|
||||
_self.tz = res.tz;
|
||||
});
|
||||
}
|
||||
|
||||
@ViewChild('network', { static: true }) networkContainer: ElementRef | undefined;
|
||||
|
||||
mikrotikData: any[] = [];
|
||||
|
||||
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadFontAwesome();
|
||||
this.savedPositions = JSON.parse(localStorage.getItem(this.savedPositionsKey) || "{}");
|
||||
this.loadNetworkData();
|
||||
}
|
||||
|
||||
loadNetworkData(): void {
|
||||
this.data_provider.getNetworkMap().then((res) => {
|
||||
this.mikrotikData = res;
|
||||
console.dir(res);
|
||||
setTimeout(() => {
|
||||
this.createNetworkMap();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
loadFontAwesome() {
|
||||
if (!document.querySelector('link[href*="font-awesome"]')) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
createNetworkMap() {
|
||||
const container = this.networkContainer?.nativeElement;
|
||||
if (!container) return;
|
||||
|
||||
let nodes = new DataSet<any>([]);
|
||||
let edges = new DataSet<any>([]);
|
||||
let deviceMap: { [key: string]: string } = {}; // uniqueId to nodeId mapping
|
||||
let allDevices: { [key: string]: any } = {}; // uniqueId to device info mapping
|
||||
let macToDevice: { [key: string]: string } = {}; // MAC -> uniqueId mapping
|
||||
const hasSavedPositions = Object.keys(this.savedPositions).length > 0;
|
||||
let nodeIdCounter = 1;
|
||||
const getUniqueId = (obj: any): string => {
|
||||
if (obj.device_id) return `dev_${obj.device_id}`;
|
||||
if (obj.mac) return `mac_${obj.mac}`;
|
||||
if (obj.software_id) return `sw_${obj.software_id}`;
|
||||
if (obj.hostname) return `host_${obj.hostname}`;
|
||||
return `unknown_${obj.address || Math.random().toString(36).slice(2)}`;
|
||||
};
|
||||
// Collect all devices
|
||||
this.mikrotikData.forEach((device) => {
|
||||
const deviceId = device.device_id || `${device.name}_${Date.now()}`;
|
||||
|
||||
if (!allDevices[deviceId]) {
|
||||
allDevices[deviceId] = {
|
||||
name: device.name,
|
||||
type: 'Router',
|
||||
brand: 'MikroTik'
|
||||
};
|
||||
}
|
||||
|
||||
Object.entries(device.interfaces).forEach(([_, iface]: [string, any]) => {
|
||||
if (iface.mac) {
|
||||
macToDevice[iface.mac] = deviceId;
|
||||
}
|
||||
|
||||
iface.neighbors.forEach((neighbor: any) => {
|
||||
const neighborId =
|
||||
neighbor.device_id ||
|
||||
neighbor.software_id ||
|
||||
`${neighbor.hostname}_${neighbor.mac}_${neighbor.address || 'unknown'}`;
|
||||
|
||||
if (!allDevices[neighborId]) {
|
||||
allDevices[neighborId] = {
|
||||
name: neighbor.hostname || neighbor.mac || 'Unknown',
|
||||
type: neighbor.type || 'Router',
|
||||
brand: neighbor.brand || 'MikroTik'
|
||||
};
|
||||
}
|
||||
|
||||
if (neighbor.mac) {
|
||||
macToDevice[neighbor.mac] = neighborId;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Create nodes
|
||||
Object.entries(allDevices).forEach(([uniqueId, device]: [string, any]) => {
|
||||
const nodeId = `node_${nodeIdCounter++}`;
|
||||
deviceMap[uniqueId] = nodeId;
|
||||
|
||||
nodes.add({
|
||||
id: nodeId,
|
||||
label: device.name,
|
||||
shape: 'image',
|
||||
image: this.getDeviceIcon(device.type || 'Unknown', device.brand || 'Unknown'),
|
||||
size: 15,
|
||||
font: { size: 11, color: '#333', face: 'Arial, sans-serif' },
|
||||
...(hasSavedPositions && this.savedPositions[nodeId]
|
||||
? { x: this.savedPositions[nodeId].x, y: this.savedPositions[nodeId].y }
|
||||
: {})
|
||||
} as any);
|
||||
});
|
||||
|
||||
// Create edges
|
||||
this.mikrotikData.forEach((device) => {
|
||||
Object.entries(device.interfaces).forEach(([ifaceName, iface]: [string, any]) => {
|
||||
const sourceDeviceId = macToDevice[iface.mac];
|
||||
iface.neighbors.forEach((neighbor: any) => {
|
||||
const targetDeviceId = macToDevice[neighbor.mac];
|
||||
|
||||
if (deviceMap[sourceDeviceId] && deviceMap[targetDeviceId]) {
|
||||
const edgeId = `${sourceDeviceId}_${targetDeviceId}`;
|
||||
const reverseId = `${targetDeviceId}_${sourceDeviceId}`;
|
||||
|
||||
if (!edges.get().find(e => e.id === edgeId || e.id === reverseId)) {
|
||||
edges.add({
|
||||
id: edgeId,
|
||||
from: deviceMap[sourceDeviceId],
|
||||
to: deviceMap[targetDeviceId],
|
||||
label: ifaceName,
|
||||
color: { color: '#34495e', highlight: '#3498db' },
|
||||
width: 3,
|
||||
smooth: { type: 'continuous', roundness: 0.1 },
|
||||
font: {
|
||||
size: 12,
|
||||
color: '#2c3e50',
|
||||
face: 'Arial, sans-serif',
|
||||
strokeWidth: 1,
|
||||
strokeColor: '#ffffff',
|
||||
align: 'horizontal'
|
||||
}
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const data = { nodes, edges };
|
||||
const options = { physics: { enabled: true, stabilization: { iterations: 100 }, barnesHut: { gravitationalConstant: -8000, centralGravity: 0.3, springLength: 200, springConstant: 0.04, damping: 0.09 } }, interaction: { hover: true, dragNodes: true, dragView: true, zoomView: true, hoverConnectedEdges: false, selectConnectedEdges: false, navigationButtons: false, keyboard: false }, nodes: { borderWidth: 3, shadow: true }, edges: { shadow: true, smooth: true, length: 150 }, manipulation: { enabled: false } };
|
||||
const network = new Network(container, data, options);
|
||||
|
||||
// Keep your existing events (dragEnd, click, stabilization, etc.)
|
||||
// No changes needed below
|
||||
network.on('dragEnd', () => {
|
||||
const positions = network.getPositions();
|
||||
this.savedPositions = positions;
|
||||
localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions));
|
||||
});
|
||||
|
||||
network.on('click', (event: any) => {
|
||||
if (event.nodes[0]) {
|
||||
const clickedNode = nodes.get(event.nodes[0]);
|
||||
const canvasPosition = network.canvasToDOM(event.pointer.canvas);
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const mainContainer = document.querySelector('.main-container') as HTMLElement;
|
||||
const mainRect = mainContainer?.getBoundingClientRect() || containerRect;
|
||||
|
||||
let adjustedX = canvasPosition.x + containerRect.left - mainRect.left + 20;
|
||||
let adjustedY = canvasPosition.y + containerRect.top - mainRect.top - 50;
|
||||
|
||||
const popupWidth = 280;
|
||||
const popupHeight = 200;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
if (adjustedX + popupWidth > viewportWidth) adjustedX -= popupWidth + 40;
|
||||
if (adjustedY + popupHeight > viewportHeight) adjustedY -= popupHeight + 40;
|
||||
if (adjustedX < 20) adjustedX = 20;
|
||||
if (adjustedY < 20) adjustedY = 20;
|
||||
|
||||
this.handleNodeClick(clickedNode, { x: adjustedX, y: adjustedY });
|
||||
}
|
||||
});
|
||||
|
||||
network.on('stabilizationIterationsDone', () => {
|
||||
network.fit();
|
||||
if (!hasSavedPositions) {
|
||||
const positions = network.getPositions();
|
||||
this.savedPositions = positions;
|
||||
localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions));
|
||||
}
|
||||
});
|
||||
|
||||
if (hasSavedPositions) {
|
||||
setTimeout(() => network.fit(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
handleNodeClick(node: any, position?: { x: number, y: number }) {
|
||||
this.selectedDevice = node;
|
||||
if (position) {
|
||||
this.selectedDevice.popupPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
closeDeviceDetails() {
|
||||
this.selectedDevice = null;
|
||||
}
|
||||
|
||||
getDeviceInterfaces() {
|
||||
if (!this.selectedDevice) return [];
|
||||
|
||||
const device = this.mikrotikData.find(d => d.name === this.selectedDevice.label);
|
||||
if (!device) return [];
|
||||
|
||||
return Object.entries(device.interfaces).map(([name, data]: [string, any]) => ({
|
||||
name,
|
||||
address: data.address,
|
||||
mac: data.mac
|
||||
}));
|
||||
}
|
||||
|
||||
getNodeColor(deviceType: string): string {
|
||||
const colors = {
|
||||
'gateway': '#dc3545',
|
||||
'router': '#fd7e14',
|
||||
'switch': '#6f42c1',
|
||||
'ap': '#20c997',
|
||||
'cpe': '#0dcaf0'
|
||||
};
|
||||
return (colors as any)[deviceType] || '#6c757d';
|
||||
}
|
||||
|
||||
getNodeBorderColor(deviceType: string): string {
|
||||
const borderColors = {
|
||||
'gateway': '#b02a37',
|
||||
'router': '#e8681a',
|
||||
'switch': '#59359a',
|
||||
'ap': '#1aa179',
|
||||
'cpe': '#0baccc'
|
||||
};
|
||||
return (borderColors as any)[deviceType] || '#495057';
|
||||
}
|
||||
|
||||
getDeviceIcon(deviceType: string, brand: string): string {
|
||||
const basePath = './assets/Network-Icons-SVG/';
|
||||
const type = deviceType.toLowerCase();
|
||||
const brandName = brand.toLowerCase();
|
||||
|
||||
// MikroTik devices
|
||||
if (brandName === 'mikrotik') {
|
||||
if (type === 'switch') {
|
||||
return `${basePath}cumulus-switch-v2.svg`;
|
||||
}
|
||||
return `${basePath}cumulus-router-v2.svg`;
|
||||
}
|
||||
|
||||
// Cisco devices
|
||||
if (brandName === 'cisco') {
|
||||
if (type === 'switch') {
|
||||
return `${basePath}cisco-switch-l2.svg`;
|
||||
}
|
||||
return `${basePath}cisco-router.svg`;
|
||||
}
|
||||
|
||||
// Juniper devices
|
||||
if (brandName === 'juniper') {
|
||||
if (type === 'switch') {
|
||||
return `${basePath}juniper-switch-l2.svg`;
|
||||
}
|
||||
return `${basePath}juniper-router.svg`;
|
||||
}
|
||||
|
||||
// HPE/Aruba devices
|
||||
if (brandName === 'hpe/aruba' || brandName === 'aruba' || brandName === 'hpe') {
|
||||
if (type === 'server') {
|
||||
return `${basePath}generic-server-1.svg`;
|
||||
}
|
||||
return `${basePath}arista-switch.svg`;
|
||||
}
|
||||
|
||||
// Ubiquiti devices
|
||||
if (brandName === 'ubiquiti' || brandName === 'ubnt') {
|
||||
if (type === 'switch') {
|
||||
return `${basePath}generic-switch-l2-v1-colour.svg`;
|
||||
}
|
||||
return `${basePath}generic-router-colour.svg`;
|
||||
}
|
||||
|
||||
// Default icons by type
|
||||
const defaultIcons = {
|
||||
'switch': `${basePath}generic-switch-l2-v1-colour.svg`,
|
||||
'router': `${basePath}generic-router-colour.svg`,
|
||||
'router/switch': `${basePath}generic-router-colour.svg`,
|
||||
'server': `${basePath}generic-server-1.svg`,
|
||||
'unknown': `${basePath}generic-router-colour.svg`
|
||||
};
|
||||
|
||||
return (defaultIcons as any)[type] || `${basePath}generic-router-colour.svg`;
|
||||
}
|
||||
|
||||
getDefaultPosition(deviceName: string, index: number): { x: number, y: number } {
|
||||
const positions = {
|
||||
'Core Router': { x: 0, y: 0 },
|
||||
'Edge Router': { x: -200, y: -100 },
|
||||
'Distribution Switch': { x: 200, y: -100 },
|
||||
'Access Point 1': { x: 100, y: 100 },
|
||||
'Access Point 2': { x: 300, y: 100 },
|
||||
'Customer Router 1': { x: 0, y: 200 },
|
||||
'Customer Router 2': { x: 200, y: 200 }
|
||||
};
|
||||
return (positions as any)[deviceName] || { x: index * 100, y: index * 50 };
|
||||
}
|
||||
|
||||
webAccess() {
|
||||
if (!this.selectedDevice) return;
|
||||
const device = this.mikrotikData.find(d => d.name === this.selectedDevice.label);
|
||||
if (device) {
|
||||
const firstInterface = Object.values(device.interfaces)[0] as any;
|
||||
const ip = firstInterface.address.split('/')[0];
|
||||
window.open(`http://${ip}`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
showMoreInfo() {
|
||||
console.log('More info for:', this.selectedDevice);
|
||||
// Implement modal or detailed view
|
||||
}
|
||||
|
||||
pingDevice() {
|
||||
console.log('Ping device:', this.selectedDevice);
|
||||
// Implement ping functionality
|
||||
}
|
||||
|
||||
configureDevice() {
|
||||
console.log('Configure device:', this.selectedDevice);
|
||||
// Implement configuration interface
|
||||
}
|
||||
|
||||
}
|
||||
21
src/app/views/maps/maps-routing.module.ts
Normal file
21
src/app/views/maps/maps-routing.module.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { MapsComponent } from './maps.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: MapsComponent,
|
||||
data: {
|
||||
title: $localize`Maps Wall`
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class MapsRoutingModule {
|
||||
}
|
||||
82
src/app/views/maps/maps.component.html
Normal file
82
src/app/views/maps/maps.component.html
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<c-row class="network-container">
|
||||
<c-col [xs]="12" class="network-col">
|
||||
<c-card class="network-card">
|
||||
<button cButton color="primary" size="sm" (click)="refreshData()" class="refresh-btn">
|
||||
<i class="fas fa-sync-alt"></i> Refresh
|
||||
</button>
|
||||
<div #network class="network-canvas"></div>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
|
||||
<div *ngIf="selectedDevice" class="floating-sidebar" [ngStyle]="{
|
||||
'left.px': selectedDevice.popupPosition?.x || 20,
|
||||
'top.px': selectedDevice.popupPosition?.y || 20
|
||||
}">
|
||||
<div class="device-panel">
|
||||
<div class="panel-header">
|
||||
<span class="device-name">{{ selectedDevice.label }}</span>
|
||||
<button cButton variant="ghost" size="sm" (click)="closeDeviceDetails()" class="close-btn">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<div class="device-info">
|
||||
<div><small><strong>Type:</strong> {{getDeviceInfo()?.type}} ({{getDeviceInfo()?.brand}})</small></div>
|
||||
<div><small><strong>Board:</strong> {{getDeviceInfo()?.board}}</small></div>
|
||||
<div><small><strong>Version:</strong> {{getDeviceInfo()?.version}}</small></div>
|
||||
<div *ngIf="getDeviceInfo()?.systemDescription"><small><strong>System:</strong> {{getDeviceInfo()?.systemDescription}}</small></div>
|
||||
<div><small><strong>IP:</strong> {{getPrimaryIP()}}</small></div>
|
||||
<div><small><strong>Neighbors:</strong> {{getNeighborCount()}}</small></div>
|
||||
</div>
|
||||
|
||||
<div class="interfaces-section">
|
||||
<div *ngFor="let interface of getDeviceInterfaces()" class="interface-row">
|
||||
<div class="if-header">
|
||||
<span class="if-name">{{ interface.name }}</span>
|
||||
<span class="if-neighbors">({{ interface.neighbors }} neighbors)</span>
|
||||
</div>
|
||||
<span class="if-ip">{{ interface.address }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions-section">
|
||||
<button cButton color="primary" size="sm" (click)="webAccess()" class="compact-btn">
|
||||
<i class="fas fa-globe"></i> Web
|
||||
</button>
|
||||
<button *ngIf="getDeviceInfo()?.deviceId" cButton color="info" size="sm" (click)="showMoreInfo()" class="compact-btn">
|
||||
<i class="fas fa-info-circle"></i> Info
|
||||
</button>
|
||||
<!-- <button cButton color="success" size="sm" (click)="pingDevice(getDeviceInfo()?.deviceId)" class="compact-btn">
|
||||
<i class="fas fa-play"></i> Ping
|
||||
</button>
|
||||
<button cButton color="warning" size="sm" (click)="configureDevice(getDeviceInfo()?.deviceId)" class="compact-btn">
|
||||
<i class="fas fa-cog"></i> Config
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Web Access Modal -->
|
||||
<c-modal #WebAccessModal backdrop="static" [(visible)]="showWebAccessModal" id="WebAccessModal">
|
||||
<c-modal-header>
|
||||
<h6 cModalTitle>Web Access Options</h6>
|
||||
<button cButtonClose (click)="closeWebAccessModal()"></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<p>Choose how to access the device:</p>
|
||||
<div class="d-grid gap-2">
|
||||
<button *ngIf="ispro" cButton color="primary" (click)="openProxyAccess()">
|
||||
<i class="fas fa-shield-alt"></i> Proxy Access , through Mikrowizard server
|
||||
</button>
|
||||
<button cButton color="secondary" (click)="openDirectAccess()">
|
||||
<i class="fas fa-external-link-alt"></i> Direct Access
|
||||
</button>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton color="secondary" (click)="closeWebAccessModal()">Cancel</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
256
src/app/views/maps/maps.component.scss
Normal file
256
src/app/views/maps/maps.component.scss
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
:host {
|
||||
.network-container {
|
||||
height: calc(100vh - 165px);
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.network-col {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.network-card {
|
||||
height: 100%;
|
||||
border: none;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.network-canvas {
|
||||
width: 100%;
|
||||
height: calc(100vh - 200px);
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
|
||||
::ng-deep .vis-network {
|
||||
cursor: default;
|
||||
|
||||
.vis-item {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.floating-sidebar {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.device-panel {
|
||||
background: rgba(44, 62, 80, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(52, 73, 94, 0.8);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
width: 280px;
|
||||
max-height: calc(100vh - 40px);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(52, 73, 94, 0.5);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
color: #bdc3c7;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
/* Override global scrollbar styles */
|
||||
scrollbar-width: thin !important;
|
||||
scrollbar-color: #bdc3c7 rgba(52, 73, 94, 0.3) !important;
|
||||
|
||||
/* Custom scrollbar styling for webkit */
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px !important;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(52, 73, 94, 0.3) !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #bdc3c7 !important;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: #ecf0f1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.interfaces-section {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 3px;
|
||||
|
||||
.interface-row {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #95a5a6;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.interface-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1px 0;
|
||||
border-bottom: 1px solid rgba(52, 73, 94, 0.3);
|
||||
font-size: 12px;
|
||||
gap: 2px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.if-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.if-name {
|
||||
color: #3498db;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.if-ip {
|
||||
color: #ecf0f1;
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.if-neighbors {
|
||||
color: #95a5a6;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
&:has(button:only-child) {
|
||||
button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:has(button:only-child)) {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.compact-btn {
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mdc-line-ripple.mdc-line-ripple--deactivating.ng-star-inserted {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
::ng-deep .main-container{
|
||||
padding:0!important;
|
||||
margin-top:-10px;
|
||||
}
|
||||
|
||||
::ng-deep .header{
|
||||
margin-bottom: 0.9rem!important;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
:host .floating-sidebar {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
:host .device-panel {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
479
src/app/views/maps/maps.component.ts
Normal file
479
src/app/views/maps/maps.component.ts
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import { Router } from "@angular/router";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import { Network } from 'vis-network/peer';
|
||||
import { DataSet } from 'vis-data';
|
||||
|
||||
@Component({
|
||||
templateUrl: "maps.component.html",
|
||||
styleUrls: ["maps.component.scss"],
|
||||
})
|
||||
export class MapsComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public ispro: boolean = false;
|
||||
public tz: string;
|
||||
public savedPositions: any = {};
|
||||
public savedPositionsKey = "network-layout";
|
||||
public selectedDevice: any = null;
|
||||
public showWebAccessModal: boolean = false;
|
||||
public showMoreInfoModal: boolean = false;
|
||||
public currentDeviceInfo: any = null;
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
private router: Router,
|
||||
private login_checker: loginChecker
|
||||
) {
|
||||
var _self = this;
|
||||
if (!this.login_checker.isLoggedIn()) {
|
||||
setTimeout(function () {
|
||||
_self.router.navigate(["login"]);
|
||||
}, 100);
|
||||
}
|
||||
this.data_provider.getSessionInfo().then((res) => {
|
||||
_self.uid = res.uid;
|
||||
_self.uname = res.name;
|
||||
_self.ispro = res.ISPRO;
|
||||
if (!_self.ispro)
|
||||
setTimeout(function () {
|
||||
_self.router.navigate(["dashboard"]);
|
||||
}, 100);
|
||||
_self.tz = res.tz;
|
||||
});
|
||||
}
|
||||
|
||||
@ViewChild('network', { static: true }) networkContainer: ElementRef | undefined;
|
||||
|
||||
mikrotikData: any[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadFontAwesome();
|
||||
this.savedPositions = JSON.parse(localStorage.getItem(this.savedPositionsKey) || "{}");
|
||||
this.loadNetworkData();
|
||||
}
|
||||
|
||||
loadNetworkData(): void {
|
||||
this.data_provider.getNetworkMap().then((res) => {
|
||||
this.mikrotikData = res;
|
||||
console.dir(res);
|
||||
setTimeout(() => {
|
||||
this.createNetworkMap();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
refreshData(): void {
|
||||
this.selectedDevice = null;
|
||||
this.loadNetworkData();
|
||||
}
|
||||
|
||||
|
||||
|
||||
loadFontAwesome() {
|
||||
if (!document.querySelector('link[href*="font-awesome"]')) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
createNetworkMap() {
|
||||
const container = this.networkContainer?.nativeElement;
|
||||
if (!container) return;
|
||||
|
||||
let nodes = new DataSet<any>([]);
|
||||
let edges = new DataSet<any>([]);
|
||||
let deviceMap: { [key: string]: string } = {}; // uniqueId (hostname) to nodeId mapping
|
||||
let allDevices: { [key: string]: any } = {}; // uniqueId to device info mapping
|
||||
let macToDevice: { [key: string]: string } = {}; // MAC -> uniqueId (hostname) mapping
|
||||
const hasSavedPositions = Object.keys(this.savedPositions).length > 0;
|
||||
let nodeIdCounter = 1;
|
||||
|
||||
// Collect all devices using hostname as consistent unique ID
|
||||
this.mikrotikData.forEach((device) => {
|
||||
const deviceId = device.name; // Use name (hostname) as unique ID
|
||||
|
||||
if (!allDevices[deviceId]) {
|
||||
allDevices[deviceId] = {
|
||||
name: device.name,
|
||||
type: 'Router',
|
||||
brand: 'MikroTik'
|
||||
};
|
||||
}
|
||||
|
||||
Object.entries(device.interfaces).forEach(([_, iface]: [string, any]) => {
|
||||
if (iface.mac) {
|
||||
macToDevice[iface.mac] = deviceId; // Map to hostname
|
||||
}
|
||||
|
||||
if (iface.neighbors && Array.isArray(iface.neighbors)) {
|
||||
iface.neighbors.forEach((neighbor: any) => {
|
||||
const neighborId = neighbor.hostname || 'Unknown'; // Use hostname
|
||||
|
||||
if (!allDevices[neighborId]) {
|
||||
allDevices[neighborId] = {
|
||||
name: neighbor.hostname || neighbor.mac || 'Unknown',
|
||||
type: neighbor.type || 'Router',
|
||||
brand: neighbor.brand || 'MikroTik'
|
||||
};
|
||||
}
|
||||
|
||||
if (neighbor.mac) {
|
||||
macToDevice[neighbor.mac] = neighborId; // Map to hostname
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Create nodes
|
||||
Object.entries(allDevices).forEach(([uniqueId, device]: [string, any]) => {
|
||||
const nodeId = `node_${nodeIdCounter++}`;
|
||||
deviceMap[uniqueId] = nodeId;
|
||||
|
||||
nodes.add({
|
||||
id: nodeId,
|
||||
label: device.name,
|
||||
shape: 'image',
|
||||
image: this.getDeviceIcon(device.type || 'Unknown', device.brand || 'Unknown'),
|
||||
size: 15,
|
||||
font: { size: 11, color: '#333', face: 'Arial, sans-serif' },
|
||||
...(hasSavedPositions && this.savedPositions[nodeId]
|
||||
? { x: this.savedPositions[nodeId].x, y: this.savedPositions[nodeId].y }
|
||||
: {})
|
||||
} as any);
|
||||
});
|
||||
|
||||
// Create edges - collect all connections first
|
||||
let connectionMap: { [key: string]: string[] } = {};
|
||||
|
||||
this.mikrotikData.forEach((device) => {
|
||||
const deviceName = device.name;
|
||||
Object.entries(device.interfaces).forEach(([ifaceName, iface]: [string, any]) => {
|
||||
const sourceDeviceId = iface.mac ? macToDevice[iface.mac] : deviceName;
|
||||
|
||||
if (iface.neighbors && Array.isArray(iface.neighbors)) {
|
||||
iface.neighbors.forEach((neighbor: any) => {
|
||||
const targetDeviceId = neighbor.mac ? macToDevice[neighbor.mac] : null;
|
||||
|
||||
if (deviceMap[sourceDeviceId] && targetDeviceId && deviceMap[targetDeviceId]) {
|
||||
const connectionKey = [sourceDeviceId, targetDeviceId].sort().join('_');
|
||||
const interfacePair = neighbor.interface ? `${ifaceName}↔${neighbor.interface}` : ifaceName;
|
||||
|
||||
if (!connectionMap[connectionKey]) {
|
||||
connectionMap[connectionKey] = [];
|
||||
}
|
||||
|
||||
if (!connectionMap[connectionKey].includes(interfacePair)) {
|
||||
connectionMap[connectionKey].push(interfacePair);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Create edges with combined labels
|
||||
Object.entries(connectionMap).forEach(([connectionKey, interfacePairs]) => {
|
||||
const [sourceDeviceId, targetDeviceId] = connectionKey.split('_');
|
||||
let edgeLabel = interfacePairs.join('\n');
|
||||
|
||||
// Limit to max 2 interface pairs to avoid overcrowding
|
||||
if (interfacePairs.length > 2) {
|
||||
edgeLabel = interfacePairs.slice(0, 2).join('\n') + '\n+' + (interfacePairs.length - 2);
|
||||
}
|
||||
|
||||
edges.add({
|
||||
id: connectionKey,
|
||||
from: deviceMap[sourceDeviceId],
|
||||
to: deviceMap[targetDeviceId],
|
||||
label: edgeLabel,
|
||||
color: { color: '#34495e', highlight: '#3498db' },
|
||||
width: 3,
|
||||
smooth: { type: 'continuous', roundness: 0.1 },
|
||||
font: {
|
||||
size: 9,
|
||||
color: '#2c3e50',
|
||||
face: 'Arial, sans-serif',
|
||||
strokeWidth: 2,
|
||||
strokeColor: '#ffffff',
|
||||
align: 'horizontal'
|
||||
}
|
||||
} as any);
|
||||
});
|
||||
const data = { nodes, edges };
|
||||
const options = { physics: { enabled: true, stabilization: { iterations: 100 }, barnesHut: { gravitationalConstant: -8000, centralGravity: 0.3, springLength: 200, springConstant: 0.04, damping: 0.09 } }, interaction: { hover: true, dragNodes: true, dragView: true, zoomView: true, hoverConnectedEdges: false, selectConnectedEdges: false, navigationButtons: false, keyboard: false }, nodes: { borderWidth: 3, shadow: true }, edges: { shadow: true, smooth: true, length: 150 }, manipulation: { enabled: false } };
|
||||
const network = new Network(container, data, options);
|
||||
|
||||
// Keep your existing events (dragEnd, click, stabilization, etc.)
|
||||
// No changes needed below
|
||||
network.on('dragEnd', () => {
|
||||
const positions = network.getPositions();
|
||||
this.savedPositions = positions;
|
||||
localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions));
|
||||
});
|
||||
|
||||
network.on('click', (event: any) => {
|
||||
if (event.nodes[0]) {
|
||||
const clickedNode = nodes.get(event.nodes[0]);
|
||||
const canvasPosition = network.canvasToDOM(event.pointer.canvas);
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const mainContainer = document.querySelector('.main-container') as HTMLElement;
|
||||
const mainRect = mainContainer?.getBoundingClientRect() || containerRect;
|
||||
|
||||
let adjustedX = canvasPosition.x + containerRect.left - mainRect.left + 20;
|
||||
let adjustedY = canvasPosition.y + containerRect.top - mainRect.top - 50;
|
||||
|
||||
const popupWidth = 280;
|
||||
const maxPopupHeight = window.innerHeight - 40;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
if (adjustedX + popupWidth > viewportWidth) adjustedX -= popupWidth + 40;
|
||||
if (adjustedY + maxPopupHeight > viewportHeight) adjustedY = viewportHeight - maxPopupHeight - 20;
|
||||
if (adjustedX < 20) adjustedX = 20;
|
||||
if (adjustedY < 20) adjustedY = 20;
|
||||
|
||||
this.handleNodeClick(clickedNode, { x: adjustedX, y: adjustedY });
|
||||
}
|
||||
});
|
||||
|
||||
network.on('stabilizationIterationsDone', () => {
|
||||
network.fit();
|
||||
if (!hasSavedPositions) {
|
||||
const positions = network.getPositions();
|
||||
this.savedPositions = positions;
|
||||
localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions));
|
||||
}
|
||||
});
|
||||
|
||||
if (hasSavedPositions) {
|
||||
setTimeout(() => network.fit(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
handleNodeClick(node: any, position?: { x: number, y: number }) {
|
||||
this.selectedDevice = node;
|
||||
if (position) {
|
||||
this.selectedDevice.popupPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
closeDeviceDetails() {
|
||||
this.selectedDevice = null;
|
||||
}
|
||||
|
||||
getDeviceInfo() {
|
||||
if (!this.selectedDevice) return null;
|
||||
|
||||
const device = this.mikrotikData.find(d => d.name === this.selectedDevice.label);
|
||||
|
||||
if (device) {
|
||||
// Main device found
|
||||
const interfaces = Object.entries(device.interfaces).map(([name, data]: [string, any]) => ({
|
||||
name,
|
||||
address: data.address || 'N/A',
|
||||
mac: data.mac || 'N/A',
|
||||
neighbors: data.neighbors?.length || 0
|
||||
}));
|
||||
|
||||
const interfaceWithNeighbors = Object.values(device.interfaces)
|
||||
.find((iface: any) => iface.neighbors?.length > 0) as any;
|
||||
const firstNeighbor = interfaceWithNeighbors?.neighbors?.[0];
|
||||
|
||||
return {
|
||||
name: device.name,
|
||||
deviceId: device.device_id,
|
||||
type: firstNeighbor?.type || 'Router',
|
||||
brand: firstNeighbor?.brand || 'MikroTik',
|
||||
board: firstNeighbor?.board || 'Unknown',
|
||||
version: firstNeighbor?.version || 'Unknown',
|
||||
systemDescription: firstNeighbor?.['system-description'] || null,
|
||||
softwareId: firstNeighbor?.['software-id'] || 'N/A',
|
||||
interfaces
|
||||
};
|
||||
} else {
|
||||
// Search in neighbor data
|
||||
for (const mainDevice of this.mikrotikData) {
|
||||
for (const iface of Object.values(mainDevice.interfaces)) {
|
||||
const neighbor = (iface as any).neighbors?.find((n: any) => n.hostname === this.selectedDevice.label);
|
||||
if (neighbor) {
|
||||
return {
|
||||
name: neighbor.hostname,
|
||||
deviceId: null,
|
||||
type: neighbor.type || 'Router',
|
||||
brand: neighbor.brand || 'MikroTik',
|
||||
board: neighbor.board || 'Unknown',
|
||||
version: neighbor.version || 'Unknown',
|
||||
systemDescription: neighbor['system-description'] || null,
|
||||
softwareId: neighbor['software-id'] || 'N/A',
|
||||
interfaces: [{ name: neighbor.interface || 'Unknown', address: neighbor.address || 'N/A', mac: neighbor.mac || 'N/A', neighbors: 0 }]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getNodeColor(deviceType: string): string {
|
||||
const colors = {
|
||||
'gateway': '#dc3545',
|
||||
'router': '#fd7e14',
|
||||
'switch': '#6f42c1',
|
||||
'ap': '#20c997',
|
||||
'cpe': '#0dcaf0'
|
||||
};
|
||||
return (colors as any)[deviceType] || '#6c757d';
|
||||
}
|
||||
|
||||
getNodeBorderColor(deviceType: string): string {
|
||||
const borderColors = {
|
||||
'gateway': '#b02a37',
|
||||
'router': '#e8681a',
|
||||
'switch': '#59359a',
|
||||
'ap': '#1aa179',
|
||||
'cpe': '#0baccc'
|
||||
};
|
||||
return (borderColors as any)[deviceType] || '#495057';
|
||||
}
|
||||
|
||||
getDeviceIcon(deviceType: string, brand: string): string {
|
||||
const basePath = './assets/Network-Icons-SVG/';
|
||||
const type = deviceType.toLowerCase();
|
||||
const brandName = brand.toLowerCase();
|
||||
|
||||
// MikroTik devices
|
||||
if (brandName === 'mikrotik') {
|
||||
if (type === 'switch') {
|
||||
return `${basePath}cumulus-switch-v2.svg`;
|
||||
}
|
||||
return `${basePath}cumulus-router-v2.svg`;
|
||||
}
|
||||
|
||||
// Cisco devices
|
||||
if (brandName === 'cisco') {
|
||||
if (type === 'switch') {
|
||||
return `${basePath}cisco-switch-l2.svg`;
|
||||
}
|
||||
return `${basePath}cisco-router.svg`;
|
||||
}
|
||||
|
||||
// Juniper devices
|
||||
if (brandName === 'juniper') {
|
||||
if (type === 'switch') {
|
||||
return `${basePath}juniper-switch-l2.svg`;
|
||||
}
|
||||
return `${basePath}juniper-router.svg`;
|
||||
}
|
||||
|
||||
// HPE/Aruba devices
|
||||
if (brandName === 'hpe/aruba' || brandName === 'aruba' || brandName === 'hpe') {
|
||||
if (type === 'server') {
|
||||
return `${basePath}generic-server-1.svg`;
|
||||
}
|
||||
return `${basePath}arista-switch.svg`;
|
||||
}
|
||||
|
||||
// Ubiquiti devices
|
||||
if (brandName === 'ubiquiti' || brandName === 'ubnt') {
|
||||
if (type === 'switch') {
|
||||
return `${basePath}generic-switch-l2-v1-colour.svg`;
|
||||
}
|
||||
return `${basePath}generic-router-colour.svg`;
|
||||
}
|
||||
|
||||
// Default icons by type
|
||||
const defaultIcons = {
|
||||
'switch': `${basePath}generic-switch-l2-v1-colour.svg`,
|
||||
'router': `${basePath}generic-router-colour.svg`,
|
||||
'router/switch': `${basePath}generic-router-colour.svg`,
|
||||
'server': `${basePath}generic-server-1.svg`,
|
||||
'unknown': `${basePath}generic-router-colour.svg`
|
||||
};
|
||||
|
||||
return (defaultIcons as any)[type] || `${basePath}generic-router-colour.svg`;
|
||||
}
|
||||
|
||||
getDefaultPosition(deviceName: string, index: number): { x: number, y: number } {
|
||||
const positions = {
|
||||
'Core Router': { x: 0, y: 0 },
|
||||
'Edge Router': { x: -200, y: -100 },
|
||||
'Distribution Switch': { x: 200, y: -100 },
|
||||
'Access Point 1': { x: 100, y: 100 },
|
||||
'Access Point 2': { x: 300, y: 100 },
|
||||
'Customer Router 1': { x: 0, y: 200 },
|
||||
'Customer Router 2': { x: 200, y: 200 }
|
||||
};
|
||||
return (positions as any)[deviceName] || { x: index * 100, y: index * 50 };
|
||||
}
|
||||
|
||||
webAccess() {
|
||||
if (!this.selectedDevice) return;
|
||||
this.currentDeviceInfo = this.getDeviceInfo();
|
||||
this.showWebAccessModal = true;
|
||||
this.closeDeviceDetails();
|
||||
}
|
||||
|
||||
openProxyAccess() {
|
||||
if (this.currentDeviceInfo?.deviceId) {
|
||||
window.open(`/api/proxy/init?devid=${this.currentDeviceInfo.deviceId}`, '_blank');
|
||||
} else {
|
||||
const ip = this.currentDeviceInfo?.interfaces.find((iface: any) => iface.address !== 'N/A')?.address?.split('/')[0];
|
||||
if (ip) {
|
||||
window.open(`/api/proxy/init?dev_ip=${ip}`, '_blank');
|
||||
}
|
||||
}
|
||||
this.showWebAccessModal = false;
|
||||
}
|
||||
|
||||
openDirectAccess() {
|
||||
const ip = this.currentDeviceInfo?.interfaces.find((iface: any) => iface.address !== 'N/A')?.address?.split('/')[0];
|
||||
if (ip) {
|
||||
window.open(`http://${ip}`, '_blank');
|
||||
}
|
||||
this.showWebAccessModal = false;
|
||||
}
|
||||
|
||||
closeWebAccessModal() {
|
||||
this.showWebAccessModal = false;
|
||||
}
|
||||
|
||||
getNeighborCount() {
|
||||
const deviceInfo = this.getDeviceInfo();
|
||||
return deviceInfo?.interfaces.reduce((total, iface) => total + iface.neighbors, 0) || 0;
|
||||
}
|
||||
|
||||
getPrimaryIP() {
|
||||
const deviceInfo = this.getDeviceInfo();
|
||||
const primaryInterface = deviceInfo?.interfaces.find(iface => iface.address !== 'N/A');
|
||||
return primaryInterface?.address?.split('/')[0] || 'N/A';
|
||||
}
|
||||
|
||||
getDeviceInterfaces() {
|
||||
const deviceInfo = this.getDeviceInfo();
|
||||
return deviceInfo?.interfaces || [];
|
||||
}
|
||||
|
||||
showMoreInfo() {
|
||||
const deviceInfo = this.getDeviceInfo();
|
||||
if (deviceInfo?.deviceId) {
|
||||
window.open(`/#/device-stats;id=${deviceInfo.deviceId}`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
pingDevice(devid: number) {
|
||||
console.log('Ping device:', this.selectedDevice);
|
||||
// Implement ping functionality
|
||||
}
|
||||
|
||||
configureDevice(devid: number) {
|
||||
console.log('Configure device:', this.selectedDevice);
|
||||
// Implement configuration interface
|
||||
}
|
||||
|
||||
}
|
||||
60
src/app/views/maps/maps.module.ts
Normal file
60
src/app/views/maps/maps.module.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { NgModule } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
import {
|
||||
ButtonGroupModule,
|
||||
ButtonModule,
|
||||
CardModule,
|
||||
GridModule,
|
||||
WidgetModule,
|
||||
ProgressModule,
|
||||
TemplateIdDirective,
|
||||
TooltipModule,
|
||||
BadgeModule,
|
||||
CarouselModule,
|
||||
ListGroupModule,
|
||||
ModalModule,
|
||||
TableModule,
|
||||
UtilitiesModule
|
||||
} from "@coreui/angular";
|
||||
import { IconModule } from "@coreui/icons-angular";
|
||||
|
||||
import { ChartjsModule } from "@coreui/angular-chartjs";
|
||||
import { NgScrollbarModule } from 'ngx-scrollbar';
|
||||
import { MapsRoutingModule } from "./maps-routing.module";
|
||||
import { MapsComponent } from "./maps.component";
|
||||
import { ClipboardModule } from "@angular/cdk/clipboard";
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
MapsRoutingModule,
|
||||
CardModule,
|
||||
WidgetModule,
|
||||
CommonModule,
|
||||
GridModule,
|
||||
ProgressModule,
|
||||
ReactiveFormsModule,
|
||||
ButtonModule,
|
||||
ModalModule,
|
||||
FormsModule,
|
||||
TemplateIdDirective,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
ChartjsModule,
|
||||
CarouselModule,
|
||||
BadgeModule,
|
||||
ClipboardModule,
|
||||
ListGroupModule,
|
||||
NgScrollbarModule,
|
||||
TableModule,
|
||||
TooltipModule,
|
||||
UtilitiesModule,
|
||||
InfiniteScrollModule,
|
||||
IconModule
|
||||
],
|
||||
declarations: [MapsComponent],
|
||||
})
|
||||
export class MapsModule {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue