websocket update
parent
319e2cdd69
commit
4e1cd72f59
|
|
@ -4,13 +4,29 @@ import { MenuItem } from './menu.interface';
|
|||
|
||||
@Controller('menu')
|
||||
export class MenuController {
|
||||
constructor(private readonly menuService: MenuService) {}
|
||||
constructor(private readonly menuService: MenuService) { }
|
||||
|
||||
@Get()
|
||||
async getMenu(): Promise<MenuItem> {
|
||||
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')
|
||||
async updateMenuItem(
|
||||
@Param('id') id: string,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,53 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PrometheusService } from '../prometheus.service';
|
||||
import { MenuItem } from './menu.interface';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
@Injectable()
|
||||
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> {
|
||||
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 {
|
||||
|
|
@ -81,8 +120,17 @@ export class MenuService {
|
|||
device.startsWith('lo') ||
|
||||
device.startsWith('/run');
|
||||
|
||||
const devices = this.extractUniqueEntities(allSeries, 'device')
|
||||
.filter(device => !isGarbageDevice(device));
|
||||
const isGarbageInstance = (instance: string) =>
|
||||
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 => ({
|
||||
id: `device_${device}`,
|
||||
|
|
@ -172,6 +220,10 @@ export class MenuService {
|
|||
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 {
|
||||
if (menu.id === id) return menu;
|
||||
|
||||
|
|
|
|||
|
|
@ -116,34 +116,54 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
|||
}
|
||||
}
|
||||
|
||||
@SubscribeMessage('subscribe-metric')
|
||||
async handleSubscribeMetric(client: Socket, payload: { metric: string; interval?: number }) {
|
||||
const { metric, interval = 5000 } = payload;
|
||||
private getSubscriptionKey(metric: string, filters: Record<string, string>): string {
|
||||
// Создаём уникальный ключ на основе метрики и фильтров
|
||||
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(
|
||||
metric,
|
||||
interval,
|
||||
(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,
|
||||
clients: new Set([client.id])
|
||||
});
|
||||
} else {
|
||||
this.metricSubscriptions.get(metric)?.clients.add(client.id);
|
||||
this.metricSubscriptions.get(subscriptionKey)?.clients.add(client.id);
|
||||
}
|
||||
|
||||
const unsubscribe = () => {
|
||||
const subscription = this.metricSubscriptions.get(metric);
|
||||
const subscription = this.metricSubscriptions.get(subscriptionKey);
|
||||
if (subscription) {
|
||||
subscription.clients.delete(client.id);
|
||||
if (subscription.clients.size === 0) {
|
||||
subscription.stopUpdates();
|
||||
this.metricSubscriptions.delete(metric);
|
||||
this.metricSubscriptions.delete(subscriptionKey);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue