mirror of
https://github.com/itdoginfo/podkop.git
synced 2026-07-22 15:23:27 +03:00
feat: implement dashboard tab
This commit is contained in:
@@ -1,14 +1,43 @@
|
||||
import { Podkop } from './podkop/types';
|
||||
|
||||
function jsonStableStringify<T, V>(obj: T): string {
|
||||
return JSON.stringify(obj, (_, value) => {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return Object.keys(value)
|
||||
.sort()
|
||||
.reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = value[key];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, V>,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
function jsonEqual<A, B>(a: A, b: B): boolean {
|
||||
try {
|
||||
return jsonStableStringify(a) === jsonStableStringify(b);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
type Listener<T> = (next: T, prev: T, diff: Partial<T>) => void;
|
||||
|
||||
// eslint-disable-next-line
|
||||
class Store<T extends Record<string, any>> {
|
||||
private value: T;
|
||||
private readonly initial: T;
|
||||
private listeners = new Set<Listener<T>>();
|
||||
private lastHash = '';
|
||||
|
||||
constructor(initial: T) {
|
||||
this.value = initial;
|
||||
this.initial = structuredClone(initial);
|
||||
this.lastHash = jsonStableStringify(initial);
|
||||
}
|
||||
|
||||
get(): T {
|
||||
@@ -17,14 +46,33 @@ class Store<T extends Record<string, any>> {
|
||||
|
||||
set(next: Partial<T>): void {
|
||||
const prev = this.value;
|
||||
const merged = { ...this.value, ...next };
|
||||
if (Object.is(prev, merged)) return;
|
||||
const merged = { ...prev, ...next };
|
||||
|
||||
if (jsonEqual(prev, merged)) return;
|
||||
|
||||
this.value = merged;
|
||||
this.lastHash = jsonStableStringify(merged);
|
||||
|
||||
const diff: Partial<T> = {};
|
||||
for (const key in merged) {
|
||||
if (merged[key] !== prev[key]) diff[key] = merged[key];
|
||||
if (!jsonEqual(merged[key], prev[key])) diff[key] = merged[key];
|
||||
}
|
||||
|
||||
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
const prev = this.value;
|
||||
const next = structuredClone(this.initial);
|
||||
|
||||
if (jsonEqual(prev, next)) return;
|
||||
|
||||
this.value = next;
|
||||
this.lastHash = jsonStableStringify(next);
|
||||
|
||||
const diff: Partial<T> = {};
|
||||
for (const key in next) {
|
||||
if (!jsonEqual(next[key], prev[key])) diff[key] = next[key];
|
||||
}
|
||||
|
||||
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||
@@ -32,12 +80,16 @@ class Store<T extends Record<string, any>> {
|
||||
|
||||
subscribe(cb: Listener<T>): () => void {
|
||||
this.listeners.add(cb);
|
||||
cb(this.value, this.value, {}); // первый вызов без diff
|
||||
cb(this.value, this.value, {});
|
||||
return () => this.listeners.delete(cb);
|
||||
}
|
||||
|
||||
unsubscribe(cb: Listener<T>): void {
|
||||
this.listeners.delete(cb);
|
||||
}
|
||||
|
||||
patch<K extends keyof T>(key: K, value: T[K]): void {
|
||||
this.set({ ...this.value, [key]: value });
|
||||
this.set({ [key]: value } as unknown as Partial<T>);
|
||||
}
|
||||
|
||||
getKey<K extends keyof T>(key: K): T[K] {
|
||||
@@ -49,18 +101,27 @@ class Store<T extends Record<string, any>> {
|
||||
cb: (value: T[K]) => void,
|
||||
): () => void {
|
||||
let prev = this.value[key];
|
||||
const unsub = this.subscribe((val) => {
|
||||
if (val[key] !== prev) {
|
||||
const wrapper: Listener<T> = (val) => {
|
||||
if (!jsonEqual(val[key], prev)) {
|
||||
prev = val[key];
|
||||
cb(val[key]);
|
||||
}
|
||||
});
|
||||
return unsub;
|
||||
};
|
||||
this.listeners.add(wrapper);
|
||||
return () => this.listeners.delete(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
export const store = new Store<{
|
||||
sections: Podkop.OutboundGroup[];
|
||||
export interface StoreType {
|
||||
tabService: {
|
||||
current: string;
|
||||
all: string[];
|
||||
};
|
||||
dashboardSections: {
|
||||
loading: boolean;
|
||||
data: Podkop.OutboundGroup[];
|
||||
failed: boolean;
|
||||
};
|
||||
traffic: { up: number; down: number };
|
||||
memory: { inuse: number; oslimit: number };
|
||||
connections: {
|
||||
@@ -73,10 +134,26 @@ export const store = new Store<{
|
||||
singbox: number;
|
||||
podkop: number;
|
||||
};
|
||||
}>({
|
||||
sections: [],
|
||||
traffic: { up: 0, down: 0 },
|
||||
memory: { inuse: 0, oslimit: 0 },
|
||||
connections: { connections: [], memory: 0, downloadTotal: 0, uploadTotal: 0 },
|
||||
}
|
||||
|
||||
const initialStore: StoreType = {
|
||||
tabService: {
|
||||
current: '',
|
||||
all: [],
|
||||
},
|
||||
dashboardSections: {
|
||||
data: [],
|
||||
loading: true,
|
||||
},
|
||||
traffic: { up: -1, down: -1 },
|
||||
memory: { inuse: -1, oslimit: -1 },
|
||||
connections: {
|
||||
connections: [],
|
||||
memory: -1,
|
||||
downloadTotal: -1,
|
||||
uploadTotal: -1,
|
||||
},
|
||||
services: { singbox: -1, podkop: -1 },
|
||||
});
|
||||
};
|
||||
|
||||
export const store = new Store<StoreType>(initialStore);
|
||||
|
||||
Reference in New Issue
Block a user