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 { |
|||
0: "UNKNOWN_APP"; |
|||
1: "TEXT_MESSAGE_APP"; |
|||
2: "REMOTE_HARDWARE_APP"; |
|||
3: "POSITION_APP"; |
|||
4: "NODEINFO_APP"; |
|||
5: "ROUTING_APP"; |
|||
6: "ADMIN_APP"; |
|||
7: "TEXT_MESSAGE_COMPRESSED_APP"; |
|||
8: "WAYPOINT_APP"; |
|||
9: "AUDIO_APP"; |
|||
10: "DETECTION_SENSOR_APP"; |
|||
11: "ALERT_APP"; |
|||
32: "REPLY_APP"; |
|||
33: "IP_TUNNEL_APP"; |
|||
34: "PAXCOUNTER_APP"; |
|||
64: "SERIAL_APP"; |
|||
65: "STORE_FORWARD_APP"; |
|||
66: "RANGE_TEST_APP"; |
|||
67: "TELEMETRY_APP"; |
|||
68: "ZPS_APP"; |
|||
69: "SIMULATOR_APP"; |
|||
70: "TRACEROUTE_APP"; |
|||
71: "NEIGHBORINFO_APP"; |
|||
72: "ATAK_PLUGIN"; |
|||
73: "MAP_REPORT_APP"; |
|||
74: "POWERSTRESS_APP"; |
|||
76: "RETICULUM_TUNNEL_APP"; |
|||
256: "PRIVATE_APP"; |
|||
257: "ATAK_FORWARDER"; |
|||
511: "MAX"; |
|||
export let PortNums = { |
|||
0: "UNKNOWN_APP", |
|||
1: "TEXT_MESSAGE_APP", |
|||
2: "REMOTE_HARDWARE_APP", |
|||
3: "POSITION_APP", |
|||
4: "NODEINFO_APP", |
|||
5: "ROUTING_APP", |
|||
6: "ADMIN_APP", |
|||
7: "TEXT_MESSAGE_COMPRESSED_APP", |
|||
8: "WAYPOINT_APP", |
|||
9: "AUDIO_APP", |
|||
10: "DETECTION_SENSOR_APP", |
|||
11: "ALERT_APP", |
|||
32: "REPLY_APP", |
|||
33: "IP_TUNNEL_APP", |
|||
34: "PAXCOUNTER_APP", |
|||
64: "SERIAL_APP", |
|||
65: "STORE_FORWARD_APP", |
|||
66: "RANGE_TEST_APP", |
|||
67: "TELEMETRY_APP", |
|||
68: "ZPS_APP", |
|||
69: "SIMULATOR_APP", |
|||
70: "TRACEROUTE_APP", |
|||
71: "NEIGHBORINFO_APP", |
|||
72: "ATAK_PLUGIN", |
|||
73: "MAP_REPORT_APP", |
|||
74: "POWERSTRESS_APP", |
|||
76: "RETICULUM_TUNNEL_APP", |
|||
256: "PRIVATE_APP", |
|||
257: "ATAK_FORWARDER", |
|||
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