15 changed files with 419 additions and 139 deletions
@ -1,75 +0,0 @@ |
|||||
import {Component, OnInit} from "@angular/core"; |
|
||||
import {HttpClient} from "@angular/common/http"; |
|
||||
import {NodeDTO} from "../../entities/NodeDTO"; |
|
||||
|
|
||||
//todo abs this
|
|
||||
@Component({ |
|
||||
selector: 'app-direct-nodes', |
|
||||
styleUrls: ['nodes.styles.scss'], |
|
||||
template: ` |
|
||||
<!--<p *ngFor="let node of nodes">{{node.long_name}}</p>--> |
|
||||
<div style="width: 80%; padding: 0 10%; padding-top: 8px"> |
|
||||
<div style="display: flex; justify-content: start"> |
|
||||
<mat-form-field appearance="fill" style="width: 250px"> |
|
||||
<mat-label>Сортировать по ...</mat-label> |
|
||||
<mat-select [(ngModel)]="sort" > |
|
||||
<mat-option *ngFor="let s of sortVars" [value]="s"> |
|
||||
{{s.name}} |
|
||||
</mat-option> |
|
||||
</mat-select> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
<div class="card-wrapper"> |
|
||||
<mat-card *ngFor="let node of nodes | NodeDtoSort: sort.type"> |
|
||||
<mat-card-header> |
|
||||
<mat-card-title>{{node.long_name}}</mat-card-title> |
|
||||
<mat-card-subtitle>{{node.short_name}} ({{node.num}})</mat-card-subtitle> |
|
||||
</mat-card-header> |
|
||||
<!--<mat-card-content> |
|
||||
|
|
||||
</mat-card-content>--> |
|
||||
<mat-card-actions> |
|
||||
<button mat-button *ngIf="node.hops_away > 0">Прыжков: {{node.hops_away}}</button> |
|
||||
<button mat-button>SNR: {{node.snr}}</button> |
|
||||
<button mat-button>{{node.ts * 1000 | date:"HH:mm dd.MM.yyyy"}}</button> |
|
||||
</mat-card-actions> |
|
||||
</mat-card> |
|
||||
</div> |
|
||||
</div> |
|
||||
` |
|
||||
}) |
|
||||
export class DirectNodesComponent implements OnInit { |
|
||||
constructor(private http: HttpClient) { |
|
||||
} |
|
||||
|
|
||||
nodes: NodeDTO[] = []; |
|
||||
sortVars:{name: string, type: string}[] = [ |
|
||||
{name: "Последнему пингу", type: "ts"}, |
|
||||
{name: "SNR", type: "snr"}, |
|
||||
//{name: "Имени", type: "long_name"}
|
|
||||
] |
|
||||
sort: {name: string, type: string} = this.sortVars[0] |
|
||||
|
|
||||
ngOnInit(): void { |
|
||||
this.http.get(`api/nodes/direct`).subscribe( |
|
||||
(res) => this.nodes = res as NodeDTO[] |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
import { Pipe, PipeTransform } from '@angular/core'; |
|
||||
@Pipe({ |
|
||||
name: 'NodeDtoSort', |
|
||||
standalone: true |
|
||||
}) |
|
||||
export class NodeDtoSortPipe implements PipeTransform { |
|
||||
|
|
||||
transform(values: NodeDTO[], field: string = ""): NodeDTO[] { |
|
||||
if (field == "") return values; |
|
||||
return values.sort((n1,n2) => |
|
||||
{ |
|
||||
// @ts-ignore
|
|
||||
return n2[field] - n1[field] |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,127 @@ |
|||||
|
import {Component, OnInit} from "@angular/core"; |
||||
|
import {HttpClient} from "@angular/common/http"; |
||||
|
import {NodeDTO} from "../../entities/NodeDTO"; |
||||
|
import {numToColor} from "../../utils/Utils"; |
||||
|
|
||||
|
//todo abs this
|
||||
|
@Component({ |
||||
|
selector: 'app-direct-nodes', |
||||
|
styleUrls: ['nodes.styles.scss'], |
||||
|
template: ` |
||||
|
<!--<p *ngFor="let node of nodes">{{node.long_name}}</p>--> |
||||
|
<div style="width: 80%; padding: 0 10%; padding-top: 8px"> |
||||
|
<div style="display: flex; justify-content: start"> |
||||
|
<mat-form-field appearance="fill" style="width: 250px"> |
||||
|
<mat-label>Сортировать по ...</mat-label> |
||||
|
<mat-select [(ngModel)]="sort" > |
||||
|
<mat-option *ngFor="let s of sortVars" [value]="s"> |
||||
|
{{s.name}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field appearance="fill" style="width: 250px; padding-left: 8px"> |
||||
|
<mat-label>Искать по ...</mat-label> |
||||
|
<mat-select [(ngModel)]="search" > |
||||
|
<mat-option *ngFor="let s of searchVars" [value]="s"> |
||||
|
{{s.name}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field style="width: 250px; padding-left: 8px; padding-top: 14px"> |
||||
|
<mat-label>Искать по ...</mat-label> |
||||
|
<input matInput [(ngModel)]="searchContent"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="card-wrapper-350"> |
||||
|
<mat-card |
||||
|
*ngFor="let node of nodes | NodeDtoSort: sort.type | NodeDtoSearch: search.type: searchContent" |
||||
|
[style]="{'background-color':numToColor(node.num)}"> |
||||
|
<mat-card-header> |
||||
|
<mat-card-title>{{node.long_name}}</mat-card-title> |
||||
|
<mat-card-subtitle>{{node.short_name}} ({{node.num}})</mat-card-subtitle> |
||||
|
</mat-card-header> |
||||
|
<!--<mat-card-content> |
||||
|
|
||||
|
</mat-card-content>--> |
||||
|
<mat-card-actions> |
||||
|
<button mat-button *ngIf="node.hops_away > 0">Прыжков: {{node.hops_away}}</button> |
||||
|
<button mat-button>SNR: {{node.snr}}</button> |
||||
|
<button mat-button>{{node.ts * 1000 | date:"HH:mm dd.MM.yyyy"}}</button> |
||||
|
</mat-card-actions> |
||||
|
</mat-card> |
||||
|
</div> |
||||
|
</div> |
||||
|
` |
||||
|
}) |
||||
|
export class NodesListComponent implements OnInit { |
||||
|
numToColor = numToColor |
||||
|
constructor(private http: HttpClient, |
||||
|
private route: ActivatedRoute) { |
||||
|
} |
||||
|
|
||||
|
nodes: NodeDTO[] = []; |
||||
|
sortVars:{name: string, type: string}[] = [ |
||||
|
{name: "Последнему пингу", type: "ts"}, |
||||
|
{name: "SNR", type: "snr"}, |
||||
|
//{name: "Имени", type: "long_name"}
|
||||
|
] |
||||
|
sort: {name: string, type: string} = this.sortVars[0] |
||||
|
|
||||
|
searchVars: {name: string, type: string}[] = [ |
||||
|
{name: "Длинному имени", type: "long_name"}, |
||||
|
{name: "Короткому имени", type: "short_name"}, |
||||
|
{name: "Хопов больше чем", type: "hops_away"} |
||||
|
] |
||||
|
search: {name: string, type: string} = this.searchVars[0] |
||||
|
searchContent: string = ""; |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.route.params.subscribe( |
||||
|
(params) => { |
||||
|
this.http.get(`api/nodes/${params['type']}`).subscribe( |
||||
|
(res) => this.nodes = res as NodeDTO[] |
||||
|
) |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
import { Pipe, PipeTransform } from '@angular/core'; |
||||
|
import {ActivatedRoute} from "@angular/router"; |
||||
|
@Pipe({ |
||||
|
name: 'NodeDtoSort', |
||||
|
standalone: true |
||||
|
}) |
||||
|
export class NodeDtoSortPipe implements PipeTransform { |
||||
|
|
||||
|
transform(values: NodeDTO[], field: string = ""): NodeDTO[] { |
||||
|
if (field == "") return values; |
||||
|
return values.sort((n1,n2) => |
||||
|
{ |
||||
|
// @ts-ignore
|
||||
|
return n2[field] - n1[field] |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Pipe({ |
||||
|
name: 'NodeDtoSearch', |
||||
|
standalone: true |
||||
|
}) |
||||
|
export class NodeDtoSearchPipe implements PipeTransform { |
||||
|
|
||||
|
transform(values: NodeDTO[], field: string = "", search: any = ""): NodeDTO[] { |
||||
|
if (field == "" || search == "") return values; |
||||
|
return values.filter((node) => { |
||||
|
switch (field) { |
||||
|
case "long_name": |
||||
|
return node.long_name && node.long_name.toLowerCase().indexOf(search.toLowerCase()) != -1; |
||||
|
case "short_name": |
||||
|
return node.short_name && node.short_name.toLowerCase().indexOf(search.toLowerCase()) != -1; |
||||
|
case "hops_away": |
||||
|
return node.hops_away != null && node.hops_away > Number.parseInt(search); |
||||
|
default: return true; |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
@ -1,5 +0,0 @@ |
|||||
.card-wrapper { |
|
||||
display: grid; |
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); |
|
||||
gap: 24px; |
|
||||
} |
|
||||
@ -0,0 +1,164 @@ |
|||||
|
import {Component, OnInit} from "@angular/core"; |
||||
|
import {HttpClient} from "@angular/common/http"; |
||||
|
import {PacketGroup} from "../../entities/PacketGroup"; |
||||
|
import {Chart} from "chart.js/auto"; |
||||
|
import {numToColor} from "../../utils/Utils"; |
||||
|
import {KeyValueMap} from "../../entities/KeyValueMap"; |
||||
|
import {NodeDTO} from "../../entities/NodeDTO"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: "app-network-status", |
||||
|
template: ` |
||||
|
<div style="width: 80%; padding: 0 10%;"> |
||||
|
<div *ngFor="let type of graphs" class="card-wrapper-450" style="padding-top: 12px"> |
||||
|
<mat-card *ngFor="let p1 of type.cards"> |
||||
|
<mat-card-content> |
||||
|
<canvas [id]="p1.canvasId">{{p1.chart}}</canvas> |
||||
|
</mat-card-content> |
||||
|
</mat-card> |
||||
|
</div> |
||||
|
</div> |
||||
|
` |
||||
|
}) |
||||
|
export class NetworkStatusComponent implements OnInit { |
||||
|
DAY = 86400; |
||||
|
WEEK = this.DAY * 7; |
||||
|
MONTH = this.DAY * 30; |
||||
|
|
||||
|
nodesNames: KeyValueMap<NodeDTO> = {} |
||||
|
|
||||
|
//общая статичтика по perPortNum за день\неделю\месяц / ?=
|
||||
|
//top кто насрал пакетами всего за день\неделю\месяц / ?=packetsSumNode=true
|
||||
|
graphs:any[] = [ |
||||
|
{ |
||||
|
header: "Cтатистика пакетов в сети", |
||||
|
type: 'perPortNum', |
||||
|
cards:[{ |
||||
|
chart: Chart, |
||||
|
canvasId: "DayPerPB", |
||||
|
params: "?=", |
||||
|
before: new Date().getTime()/1000, |
||||
|
after: (new Date().getTime()/1000) - this.DAY, |
||||
|
config: this.generateConfigTChart("Cтатистика по пакетам в сети за день") |
||||
|
},{ |
||||
|
chart: Chart, |
||||
|
canvasId: "WeekPerPB", |
||||
|
params: "?=", |
||||
|
before: new Date().getTime()/1000, |
||||
|
after: (new Date().getTime()/1000) - this.WEEK, |
||||
|
config: this.generateConfigTChart("Cтатистика по пакетам в сети за неделю") |
||||
|
},{ |
||||
|
chart: Chart, |
||||
|
canvasId: "MonthPerPB", |
||||
|
params: "?=", |
||||
|
before: new Date().getTime()/1000, |
||||
|
after: (new Date().getTime()/1000) - this.MONTH, |
||||
|
config: this.generateConfigTChart("Cтатистика по пакетам в сети за месяц") |
||||
|
}] |
||||
|
}, |
||||
|
{ |
||||
|
header: "Общая статистика пакетов в сети", |
||||
|
type: 'perSumNode', |
||||
|
cards:[{ |
||||
|
chart: Chart, |
||||
|
canvasId: "DaySumPB", |
||||
|
params: "?packetsSumNode=true", |
||||
|
before: new Date().getTime()/1000, |
||||
|
after: (new Date().getTime()/1000) - this.DAY, |
||||
|
config: this.generateConfigTChart("Количество пакетов от пользоватей за день", false) |
||||
|
},{ |
||||
|
chart: Chart, |
||||
|
canvasId: "WeekSumPB", |
||||
|
params: "?packetsSumNode=true", |
||||
|
before: new Date().getTime()/1000, |
||||
|
after: (new Date().getTime()/1000) - this.WEEK, |
||||
|
config: this.generateConfigTChart("Количество пакетов от пользоватей за неделю", false) |
||||
|
},{ |
||||
|
chart: Chart, |
||||
|
canvasId: "MonthSumPB", |
||||
|
params: "?packetsSumNode=true", |
||||
|
before: new Date().getTime()/1000, |
||||
|
after: (new Date().getTime()/1000) - this.MONTH, |
||||
|
config: this.generateConfigTChart("Количество пакетов от пользоватей за месяц", false) |
||||
|
}] |
||||
|
} |
||||
|
]; |
||||
|
constructor(private http: HttpClient) {} |
||||
|
|
||||
|
generateConfigTChart(name: string, legend: boolean = true) { |
||||
|
return { |
||||
|
type: 'pie', |
||||
|
data: {}, |
||||
|
options: { |
||||
|
responsive: true, |
||||
|
plugins: { |
||||
|
legend: { |
||||
|
position: 'bottom', |
||||
|
display: legend |
||||
|
}, |
||||
|
title: { |
||||
|
display: true, |
||||
|
text: name |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.http.get(`api/nodes/list`).subscribe( |
||||
|
(obj) => { |
||||
|
(obj as NodeDTO[]).forEach( |
||||
|
(node) => { |
||||
|
this.nodesNames[`${node.num}`] = node; |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
).add( |
||||
|
() => { |
||||
|
this.graphs.forEach( |
||||
|
(graph) => { |
||||
|
graph.cards.forEach((settings:any) => { |
||||
|
this.http.get(`api/packet/stats${settings.params}&before=${settings.before}&after=${settings.after}`) |
||||
|
.subscribe((data) => { |
||||
|
settings.config.data = { |
||||
|
labels: [], |
||||
|
datasets: [ |
||||
|
{ |
||||
|
label: settings.canvasId, |
||||
|
data: [], |
||||
|
backgroundColor: [] |
||||
|
} |
||||
|
] |
||||
|
}; |
||||
|
(data as Object[]).map((obj) => PacketGroup.fromDto(obj)).sort((p1, p2) => p2.count - p1.count).forEach( |
||||
|
(d) => { |
||||
|
switch (graph.type) { |
||||
|
case "perPortNum": { |
||||
|
settings.config.data.labels.push(`${d.portnumName} - ${d.count}`) |
||||
|
settings.config.data.datasets[0].data.push(d.count) |
||||
|
settings.config.data.datasets[0].backgroundColor.push(numToColor(d.portnum, 0)) |
||||
|
break; |
||||
|
} |
||||
|
case "perSumNode": { |
||||
|
const name = `${d.from}` in this.nodesNames ? this.nodesNames[`${d.from}`].long_name : `${d.from}` |
||||
|
settings.config.data.labels.push(`${name} - ${d.count}`) |
||||
|
settings.config.data.datasets[0].data.push(d.count) |
||||
|
settings.config.data.datasets[0].backgroundColor.push(numToColor(d.from, 0)) |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
//if (settings.chart)
|
||||
|
// settings.chart.destroy()
|
||||
|
|
||||
|
settings.chart = new Chart(settings.canvasId, settings.config) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
import {PortNums} from "../utils/PortNums"; |
||||
|
|
||||
|
export class PacketGroup { |
||||
|
count!: number; |
||||
|
portnum!: number; |
||||
|
portnumName!:string; |
||||
|
from!: number; |
||||
|
type!: string; |
||||
|
|
||||
|
static fromDto(data:any):PacketGroup { |
||||
|
const p = new PacketGroup(); |
||||
|
Object.assign(p, data); |
||||
|
// @ts-ignore
|
||||
|
p.portnumName = p.portnum in PortNums ? PortNums[p.portnum] : "UNKNOWN OR MISSED" |
||||
|
return p; |
||||
|
} |
||||
|
} |
||||
@ -1,32 +1,32 @@ |
|||||
export class PortNums { |
export let PortNums = { |
||||
0: "UNKNOWN_APP"; |
0: "UNKNOWN_APP", |
||||
1: "TEXT_MESSAGE_APP"; |
1: "TEXT_MESSAGE_APP", |
||||
2: "REMOTE_HARDWARE_APP"; |
2: "REMOTE_HARDWARE_APP", |
||||
3: "POSITION_APP"; |
3: "POSITION_APP", |
||||
4: "NODEINFO_APP"; |
4: "NODEINFO_APP", |
||||
5: "ROUTING_APP"; |
5: "ROUTING_APP", |
||||
6: "ADMIN_APP"; |
6: "ADMIN_APP", |
||||
7: "TEXT_MESSAGE_COMPRESSED_APP"; |
7: "TEXT_MESSAGE_COMPRESSED_APP", |
||||
8: "WAYPOINT_APP"; |
8: "WAYPOINT_APP", |
||||
9: "AUDIO_APP"; |
9: "AUDIO_APP", |
||||
10: "DETECTION_SENSOR_APP"; |
10: "DETECTION_SENSOR_APP", |
||||
11: "ALERT_APP"; |
11: "ALERT_APP", |
||||
32: "REPLY_APP"; |
32: "REPLY_APP", |
||||
33: "IP_TUNNEL_APP"; |
33: "IP_TUNNEL_APP", |
||||
34: "PAXCOUNTER_APP"; |
34: "PAXCOUNTER_APP", |
||||
64: "SERIAL_APP"; |
64: "SERIAL_APP", |
||||
65: "STORE_FORWARD_APP"; |
65: "STORE_FORWARD_APP", |
||||
66: "RANGE_TEST_APP"; |
66: "RANGE_TEST_APP", |
||||
67: "TELEMETRY_APP"; |
67: "TELEMETRY_APP", |
||||
68: "ZPS_APP"; |
68: "ZPS_APP", |
||||
69: "SIMULATOR_APP"; |
69: "SIMULATOR_APP", |
||||
70: "TRACEROUTE_APP"; |
70: "TRACEROUTE_APP", |
||||
71: "NEIGHBORINFO_APP"; |
71: "NEIGHBORINFO_APP", |
||||
72: "ATAK_PLUGIN"; |
72: "ATAK_PLUGIN", |
||||
73: "MAP_REPORT_APP"; |
73: "MAP_REPORT_APP", |
||||
74: "POWERSTRESS_APP"; |
74: "POWERSTRESS_APP", |
||||
76: "RETICULUM_TUNNEL_APP"; |
76: "RETICULUM_TUNNEL_APP", |
||||
256: "PRIVATE_APP"; |
256: "PRIVATE_APP", |
||||
257: "ATAK_FORWARDER"; |
257: "ATAK_FORWARDER", |
||||
511: "MAX"; |
511: "MAX", |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,20 @@ |
|||||
|
export function numToColor(num:number, alpha = 0.2) { |
||||
|
// Приводим к беззнаковому 32-битному и перемешиваем биты
|
||||
|
let n = num >>> 0; |
||||
|
// Мультипликативная хеш-функция с простыми числами
|
||||
|
n = (n * 2654435761) >>> 0; // константа из Knuth
|
||||
|
n ^= (n >> 16); |
||||
|
n = (n * 0x85EBCA6B) >>> 0; |
||||
|
n ^= (n >> 13); |
||||
|
n = (n * 0xC2B2AE35) >>> 0; |
||||
|
n ^= (n >> 16); |
||||
|
|
||||
|
const r = (n >> 16) & 0xFF; |
||||
|
const g = (n >> 8) & 0xFF; |
||||
|
const b = n & 0xFF; |
||||
|
|
||||
|
if (alpha == 0) |
||||
|
return `rgb(${r}, ${g}, ${b})` |
||||
|
else |
||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})` |
||||
|
} |
||||
Loading…
Reference in new issue