websocket update
parent
319e2cdd69
commit
4e1cd72f59
|
|
@ -4,13 +4,29 @@ import { MenuItem } from './menu.interface';
|
||||||
|
|
||||||
@Controller('menu')
|
@Controller('menu')
|
||||||
export class MenuController {
|
export class MenuController {
|
||||||
constructor(private readonly menuService: MenuService) {}
|
constructor(private readonly menuService: MenuService) { }
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async getMenu(): Promise<MenuItem> {
|
async getMenu(): Promise<MenuItem> {
|
||||||
return this.menuService.getFullMenu();
|
return this.menuService.getFullMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('save')
|
||||||
|
async saveMenu() {
|
||||||
|
await this.menuService.saveMenuToFile();
|
||||||
|
return { status: 'saved' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('full')
|
||||||
|
async getFullMenu(): Promise<MenuItem> {
|
||||||
|
return this.menuService.getFullMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('overrides')
|
||||||
|
async saveOverrides(@Body() data: { overrides: Partial<MenuItem>[] }) {
|
||||||
|
return this.menuService.saveOverrides(data.overrides);
|
||||||
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
async updateMenuItem(
|
async updateMenuItem(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,53 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrometheusService } from '../prometheus.service';
|
import { PrometheusService } from '../prometheus.service';
|
||||||
import { MenuItem } from './menu.interface';
|
import { MenuItem } from './menu.interface';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MenuService {
|
export class MenuService {
|
||||||
constructor(private readonly prometheusService: PrometheusService) {}
|
constructor(private readonly prometheusService: PrometheusService) { }
|
||||||
|
|
||||||
|
private readonly menuOverridesPath = path.join(process.cwd(), 'data', 'menu.json');
|
||||||
|
|
||||||
|
async saveMenuToFile(): Promise<void> {
|
||||||
|
const menu = await this.getFullMenu();
|
||||||
|
await fs.mkdir(path.dirname(this.menuOverridesPath), { recursive: true });
|
||||||
|
await fs.writeFile(this.menuOverridesPath, JSON.stringify(menu, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
async getFullMenu(): Promise<MenuItem> {
|
async getFullMenu(): Promise<MenuItem> {
|
||||||
const dynamicItems = await this.generateDynamicItems();
|
const dynamicItems = await this.generateDynamicItems();
|
||||||
return this.injectDynamicItems(this.getStaticStructure(), dynamicItems);
|
const baseMenu = this.injectDynamicItems(this.getStaticStructure(), dynamicItems);
|
||||||
|
const overrides = await this.loadOverrides();
|
||||||
|
return this.applyOverrides(baseMenu, overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyOverrides(menu: MenuItem, overrides: Partial<MenuItem>[]): MenuItem {
|
||||||
|
const overrideMap = new Map(overrides.map(o => [o.id, o]));
|
||||||
|
|
||||||
|
const apply = (item: MenuItem): MenuItem => {
|
||||||
|
const override = overrideMap.get(item.id);
|
||||||
|
const updated = override ? { ...item, ...override } : item;
|
||||||
|
|
||||||
|
if (updated.items) {
|
||||||
|
updated.items = updated.items.map(apply);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
};
|
||||||
|
|
||||||
|
return apply(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadOverrides(): Promise<Partial<MenuItem>[]> {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(this.menuOverridesPath, 'utf-8');
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
return parsed.overrides || [];
|
||||||
|
} catch (e) {
|
||||||
|
return []; // если файл не существует
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getStaticStructure(): MenuItem {
|
private getStaticStructure(): MenuItem {
|
||||||
|
|
@ -81,8 +120,17 @@ export class MenuService {
|
||||||
device.startsWith('lo') ||
|
device.startsWith('lo') ||
|
||||||
device.startsWith('/run');
|
device.startsWith('/run');
|
||||||
|
|
||||||
const devices = this.extractUniqueEntities(allSeries, 'device')
|
const isGarbageInstance = (instance: string) =>
|
||||||
.filter(device => !isGarbageDevice(device));
|
instance.includes('192.168.2.34:9049');
|
||||||
|
|
||||||
|
const filteredSeries = allSeries.filter(({ labels }) => {
|
||||||
|
const device = labels.device;
|
||||||
|
const instance = labels.instance;
|
||||||
|
|
||||||
|
return (!device || !isGarbageDevice(device)) &&
|
||||||
|
(!instance || !isGarbageInstance(instance));
|
||||||
|
});
|
||||||
|
const devices = this.extractUniqueEntities(filteredSeries, 'device');
|
||||||
|
|
||||||
return devices.map(device => ({
|
return devices.map(device => ({
|
||||||
id: `device_${device}`,
|
id: `device_${device}`,
|
||||||
|
|
@ -172,6 +220,10 @@ export class MenuService {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveOverrides(overrides: Partial<MenuItem>[]): Promise<void> {
|
||||||
|
await fs.writeFile(this.menuOverridesPath, JSON.stringify({ overrides }, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
private findMenuItem(menu: MenuItem, id: string): MenuItem | null {
|
private findMenuItem(menu: MenuItem, id: string): MenuItem | null {
|
||||||
if (menu.id === id) return menu;
|
if (menu.id === id) return menu;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,34 +116,54 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeMessage('subscribe-metric')
|
private getSubscriptionKey(metric: string, filters: Record<string, string>): string {
|
||||||
async handleSubscribeMetric(client: Socket, payload: { metric: string; interval?: number }) {
|
// Создаём уникальный ключ на основе метрики и фильтров
|
||||||
const { metric, interval = 5000 } = payload;
|
const filterKeys = Object.keys(filters).sort();
|
||||||
|
const filterString = filterKeys.map(k => `${k}=${filters[k]}`).join('&');
|
||||||
|
return `${metric}${filterString ? `?${filterString}` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.metricSubscriptions.has(metric)) {
|
@SubscribeMessage('subscribe-metric')
|
||||||
|
async handleSubscribeMetric(
|
||||||
|
client: Socket,
|
||||||
|
payload: {
|
||||||
|
metric: string;
|
||||||
|
interval?: number;
|
||||||
|
filters?: Record<string, string>;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { metric, interval = 5000, filters = {} } = payload;
|
||||||
|
const subscriptionKey = this.getSubscriptionKey(metric, filters);
|
||||||
|
|
||||||
|
if (!this.metricSubscriptions.has(subscriptionKey)) {
|
||||||
const stopUpdates = await this.sendPeriodicUpdates(
|
const stopUpdates = await this.sendPeriodicUpdates(
|
||||||
metric,
|
metric,
|
||||||
interval,
|
interval,
|
||||||
(data) => {
|
(data) => {
|
||||||
this.server.emit('metrics-data', { metric, data });
|
// Отправляем только подписчикам этой конкретной метрики с фильтрами
|
||||||
}
|
this.server.emit('metrics-data', {
|
||||||
|
metric: subscriptionKey,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
},
|
||||||
|
filters
|
||||||
);
|
);
|
||||||
|
|
||||||
this.metricSubscriptions.set(metric, {
|
this.metricSubscriptions.set(subscriptionKey, {
|
||||||
stopUpdates,
|
stopUpdates,
|
||||||
clients: new Set([client.id])
|
clients: new Set([client.id])
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.metricSubscriptions.get(metric)?.clients.add(client.id);
|
this.metricSubscriptions.get(subscriptionKey)?.clients.add(client.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribe = () => {
|
const unsubscribe = () => {
|
||||||
const subscription = this.metricSubscriptions.get(metric);
|
const subscription = this.metricSubscriptions.get(subscriptionKey);
|
||||||
if (subscription) {
|
if (subscription) {
|
||||||
subscription.clients.delete(client.id);
|
subscription.clients.delete(client.id);
|
||||||
if (subscription.clients.size === 0) {
|
if (subscription.clients.size === 0) {
|
||||||
subscription.stopUpdates();
|
subscription.stopUpdates();
|
||||||
this.metricSubscriptions.delete(metric);
|
this.metricSubscriptions.delete(subscriptionKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue