mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-13 15:06:51 +03:00
refactor: make dashboard widgets reactive
This commit is contained in:
@@ -3,9 +3,7 @@ import {
|
|||||||
getPodkopStatus,
|
getPodkopStatus,
|
||||||
getSingboxStatus,
|
getSingboxStatus,
|
||||||
} from '../../methods';
|
} from '../../methods';
|
||||||
import { renderOutboundGroup } from './renderer/renderOutboundGroup';
|
|
||||||
import { getClashWsUrl, onMount } from '../../../helpers';
|
import { getClashWsUrl, onMount } from '../../../helpers';
|
||||||
import { renderDashboardWidget } from './renderer/renderWidget';
|
|
||||||
import {
|
import {
|
||||||
triggerLatencyGroupTest,
|
triggerLatencyGroupTest,
|
||||||
triggerLatencyProxyTest,
|
triggerLatencyProxyTest,
|
||||||
@@ -14,63 +12,114 @@ import {
|
|||||||
import { store, StoreType } from '../../../store';
|
import { store, StoreType } from '../../../store';
|
||||||
import { socket } from '../../../socket';
|
import { socket } from '../../../socket';
|
||||||
import { prettyBytes } from '../../../helpers/prettyBytes';
|
import { prettyBytes } from '../../../helpers/prettyBytes';
|
||||||
import { renderEmptyOutboundGroup } from './renderer/renderEmptyOutboundGroup';
|
import { renderSections } from './renderSections';
|
||||||
|
import { renderWidget } from './renderWidget';
|
||||||
|
|
||||||
// Fetchers
|
// Fetchers
|
||||||
|
|
||||||
async function fetchDashboardSections() {
|
async function fetchDashboardSections() {
|
||||||
|
const prev = store.get().sectionsWidget;
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
dashboardSections: {
|
sectionsWidget: {
|
||||||
...store.get().dashboardSections,
|
...prev,
|
||||||
failed: false,
|
failed: false,
|
||||||
loading: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, success } = await getDashboardSections();
|
const { data, success } = await getDashboardSections();
|
||||||
|
|
||||||
store.set({ dashboardSections: { loading: false, data, failed: !success } });
|
store.set({
|
||||||
|
sectionsWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: !success,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchServicesInfo() {
|
async function fetchServicesInfo() {
|
||||||
const podkop = await getPodkopStatus();
|
const [podkop, singbox] = await Promise.all([
|
||||||
const singbox = await getSingboxStatus();
|
getPodkopStatus(),
|
||||||
|
getSingboxStatus(),
|
||||||
|
]);
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
services: {
|
servicesInfoWidget: {
|
||||||
singbox: singbox.running,
|
loading: false,
|
||||||
podkop: podkop.enabled,
|
failed: false,
|
||||||
|
data: { singbox: singbox.running, podkop: podkop.enabled },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connectToClashSockets() {
|
async function connectToClashSockets() {
|
||||||
socket.subscribe(`${getClashWsUrl()}/traffic?token=`, (msg) => {
|
socket.subscribe(
|
||||||
const parsedMsg = JSON.parse(msg);
|
`${getClashWsUrl()}/traffic?token=`,
|
||||||
|
(msg) => {
|
||||||
|
const parsedMsg = JSON.parse(msg);
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
traffic: { up: parsedMsg.up, down: parsedMsg.down },
|
bandwidthWidget: {
|
||||||
});
|
loading: false,
|
||||||
});
|
failed: false,
|
||||||
|
data: { up: parsedMsg.up, down: parsedMsg.down },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(_err) => {
|
||||||
|
store.set({
|
||||||
|
bandwidthWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: { up: 0, down: 0 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
socket.subscribe(`${getClashWsUrl()}/connections?token=`, (msg) => {
|
socket.subscribe(
|
||||||
const parsedMsg = JSON.parse(msg);
|
`${getClashWsUrl()}/connections?token=`,
|
||||||
|
(msg) => {
|
||||||
|
const parsedMsg = JSON.parse(msg);
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
connections: {
|
trafficTotalWidget: {
|
||||||
connections: parsedMsg.connections,
|
loading: false,
|
||||||
downloadTotal: parsedMsg.downloadTotal,
|
failed: false,
|
||||||
uploadTotal: parsedMsg.uploadTotal,
|
data: {
|
||||||
memory: parsedMsg.memory,
|
downloadTotal: parsedMsg.downloadTotal,
|
||||||
},
|
uploadTotal: parsedMsg.uploadTotal,
|
||||||
});
|
},
|
||||||
});
|
},
|
||||||
|
systemInfoWidget: {
|
||||||
socket.subscribe(`${getClashWsUrl()}/memory?token=`, (msg) => {
|
loading: false,
|
||||||
store.set({
|
failed: false,
|
||||||
memory: { inuse: msg.inuse, oslimit: msg.oslimit },
|
data: {
|
||||||
});
|
connections: parsedMsg.connections?.length,
|
||||||
});
|
memory: parsedMsg.memory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(_err) => {
|
||||||
|
store.set({
|
||||||
|
trafficTotalWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: { downloadTotal: 0, uploadTotal: 0 },
|
||||||
|
},
|
||||||
|
systemInfoWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: {
|
||||||
|
connections: 0,
|
||||||
|
memory: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
@@ -104,18 +153,31 @@ function replaceTestLatencyButtonsWithSkeleton() {
|
|||||||
|
|
||||||
// Renderer
|
// Renderer
|
||||||
|
|
||||||
async function renderDashboardSections() {
|
async function renderSectionsWidget() {
|
||||||
const dashboardSections = store.get().dashboardSections;
|
console.log('renderSectionsWidget');
|
||||||
|
const sectionsWidget = store.get().sectionsWidget;
|
||||||
const container = document.getElementById('dashboard-sections-grid');
|
const container = document.getElementById('dashboard-sections-grid');
|
||||||
|
|
||||||
if (dashboardSections.failed) {
|
if (sectionsWidget.loading || sectionsWidget.failed) {
|
||||||
const rendered = renderEmptyOutboundGroup();
|
const renderedWidget = renderSections({
|
||||||
|
loading: sectionsWidget.loading,
|
||||||
return container!.replaceChildren(rendered);
|
failed: sectionsWidget.failed,
|
||||||
|
section: {
|
||||||
|
code: '',
|
||||||
|
displayName: '',
|
||||||
|
outbounds: [],
|
||||||
|
withTagSelect: false,
|
||||||
|
},
|
||||||
|
onTestLatency: () => {},
|
||||||
|
onChooseOutbound: () => {},
|
||||||
|
});
|
||||||
|
return container!.replaceChildren(renderedWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderedOutboundGroups = dashboardSections.data.map((section) =>
|
const renderedWidgets = sectionsWidget.data.map((section) =>
|
||||||
renderOutboundGroup({
|
renderSections({
|
||||||
|
loading: sectionsWidget.loading,
|
||||||
|
failed: sectionsWidget.failed,
|
||||||
section,
|
section,
|
||||||
onTestLatency: (tag) => {
|
onTestLatency: (tag) => {
|
||||||
replaceTestLatencyButtonsWithSkeleton();
|
replaceTestLatencyButtonsWithSkeleton();
|
||||||
@@ -132,18 +194,33 @@ async function renderDashboardSections() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
container!.replaceChildren(...renderedOutboundGroups);
|
return container!.replaceChildren(...renderedWidgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderTrafficWidget() {
|
async function renderBandwidthWidget() {
|
||||||
const traffic = store.get().traffic;
|
console.log('renderBandwidthWidget');
|
||||||
|
const traffic = store.get().bandwidthWidget;
|
||||||
|
|
||||||
const container = document.getElementById('dashboard-widget-traffic');
|
const container = document.getElementById('dashboard-widget-traffic');
|
||||||
const renderedWidget = renderDashboardWidget({
|
|
||||||
|
if (traffic.loading || traffic.failed) {
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: traffic.loading,
|
||||||
|
failed: traffic.failed,
|
||||||
|
title: '',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: traffic.loading,
|
||||||
|
failed: traffic.failed,
|
||||||
title: 'Traffic',
|
title: 'Traffic',
|
||||||
items: [
|
items: [
|
||||||
{ key: 'Uplink', value: `${prettyBytes(traffic.up)}/s` },
|
{ key: 'Uplink', value: `${prettyBytes(traffic.data.up)}/s` },
|
||||||
{ key: 'Downlink', value: `${prettyBytes(traffic.down)}/s` },
|
{ key: 'Downlink', value: `${prettyBytes(traffic.data.down)}/s` },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,16 +228,34 @@ async function renderTrafficWidget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderTrafficTotalWidget() {
|
async function renderTrafficTotalWidget() {
|
||||||
const connections = store.get().connections;
|
console.log('renderTrafficTotalWidget');
|
||||||
|
const trafficTotalWidget = store.get().trafficTotalWidget;
|
||||||
|
|
||||||
const container = document.getElementById('dashboard-widget-traffic-total');
|
const container = document.getElementById('dashboard-widget-traffic-total');
|
||||||
const renderedWidget = renderDashboardWidget({
|
|
||||||
|
if (trafficTotalWidget.loading || trafficTotalWidget.failed) {
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: trafficTotalWidget.loading,
|
||||||
|
failed: trafficTotalWidget.failed,
|
||||||
|
title: '',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: trafficTotalWidget.loading,
|
||||||
|
failed: trafficTotalWidget.failed,
|
||||||
title: 'Traffic Total',
|
title: 'Traffic Total',
|
||||||
items: [
|
items: [
|
||||||
{ key: 'Uplink', value: String(prettyBytes(connections.uploadTotal)) },
|
{
|
||||||
|
key: 'Uplink',
|
||||||
|
value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'Downlink',
|
key: 'Downlink',
|
||||||
value: String(prettyBytes(connections.downloadTotal)),
|
value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -169,44 +264,77 @@ async function renderTrafficTotalWidget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderSystemInfoWidget() {
|
async function renderSystemInfoWidget() {
|
||||||
const connections = store.get().connections;
|
console.log('renderSystemInfoWidget');
|
||||||
|
const systemInfoWidget = store.get().systemInfoWidget;
|
||||||
|
|
||||||
const container = document.getElementById('dashboard-widget-system-info');
|
const container = document.getElementById('dashboard-widget-system-info');
|
||||||
const renderedWidget = renderDashboardWidget({
|
|
||||||
|
if (systemInfoWidget.loading || systemInfoWidget.failed) {
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: systemInfoWidget.loading,
|
||||||
|
failed: systemInfoWidget.failed,
|
||||||
|
title: '',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: systemInfoWidget.loading,
|
||||||
|
failed: systemInfoWidget.failed,
|
||||||
title: 'System info',
|
title: 'System info',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'Active Connections',
|
key: 'Active Connections',
|
||||||
value: String(connections.connections.length),
|
value: String(systemInfoWidget.data.connections),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Memory Usage',
|
||||||
|
value: String(prettyBytes(systemInfoWidget.data.memory)),
|
||||||
},
|
},
|
||||||
{ key: 'Memory Usage', value: String(prettyBytes(connections.memory)) },
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
container!.replaceChildren(renderedWidget);
|
container!.replaceChildren(renderedWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderServiceInfoWidget() {
|
async function renderServicesInfoWidget() {
|
||||||
const services = store.get().services;
|
console.log('renderServicesInfoWidget');
|
||||||
|
const servicesInfoWidget = store.get().servicesInfoWidget;
|
||||||
|
|
||||||
const container = document.getElementById('dashboard-widget-service-info');
|
const container = document.getElementById('dashboard-widget-service-info');
|
||||||
const renderedWidget = renderDashboardWidget({
|
|
||||||
|
if (servicesInfoWidget.loading || servicesInfoWidget.failed) {
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: servicesInfoWidget.loading,
|
||||||
|
failed: servicesInfoWidget.failed,
|
||||||
|
title: '',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: servicesInfoWidget.loading,
|
||||||
|
failed: servicesInfoWidget.failed,
|
||||||
title: 'Services info',
|
title: 'Services info',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'Podkop',
|
key: 'Podkop',
|
||||||
value: services.podkop ? '✔ Enabled' : '✘ Disabled',
|
value: servicesInfoWidget.data.podkop ? '✔ Enabled' : '✘ Disabled',
|
||||||
attributes: {
|
attributes: {
|
||||||
class: services.podkop
|
class: servicesInfoWidget.data.podkop
|
||||||
? 'pdk_dashboard-page__widgets-section__item__row--success'
|
? 'pdk_dashboard-page__widgets-section__item__row--success'
|
||||||
: 'pdk_dashboard-page__widgets-section__item__row--error',
|
: 'pdk_dashboard-page__widgets-section__item__row--error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Sing-box',
|
key: 'Sing-box',
|
||||||
value: services.singbox ? '✔ Running' : '✘ Stopped',
|
value: servicesInfoWidget.data.singbox ? '✔ Running' : '✘ Stopped',
|
||||||
attributes: {
|
attributes: {
|
||||||
class: services.singbox
|
class: servicesInfoWidget.data.singbox
|
||||||
? 'pdk_dashboard-page__widgets-section__item__row--success'
|
? 'pdk_dashboard-page__widgets-section__item__row--success'
|
||||||
: 'pdk_dashboard-page__widgets-section__item__row--error',
|
: 'pdk_dashboard-page__widgets-section__item__row--error',
|
||||||
},
|
},
|
||||||
@@ -222,21 +350,24 @@ async function onStoreUpdate(
|
|||||||
prev: StoreType,
|
prev: StoreType,
|
||||||
diff: Partial<StoreType>,
|
diff: Partial<StoreType>,
|
||||||
) {
|
) {
|
||||||
if (diff?.dashboardSections) {
|
if (diff.sectionsWidget) {
|
||||||
renderDashboardSections();
|
renderSectionsWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diff?.traffic) {
|
if (diff.bandwidthWidget) {
|
||||||
renderTrafficWidget();
|
renderBandwidthWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diff?.connections) {
|
if (diff.trafficTotalWidget) {
|
||||||
renderTrafficTotalWidget();
|
renderTrafficTotalWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff.systemInfoWidget) {
|
||||||
renderSystemInfoWidget();
|
renderSystemInfoWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diff?.services) {
|
if (diff.servicesInfoWidget) {
|
||||||
renderServiceInfoWidget();
|
renderServicesInfoWidget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { renderSections } from './renderSections';
|
||||||
|
import { renderWidget } from './renderWidget';
|
||||||
|
|
||||||
export function renderDashboard() {
|
export function renderDashboard() {
|
||||||
return E(
|
return E(
|
||||||
'div',
|
'div',
|
||||||
@@ -8,59 +11,44 @@ export function renderDashboard() {
|
|||||||
[
|
[
|
||||||
// Widgets section
|
// Widgets section
|
||||||
E('div', { class: 'pdk_dashboard-page__widgets-section' }, [
|
E('div', { class: 'pdk_dashboard-page__widgets-section' }, [
|
||||||
E('div', { id: 'dashboard-widget-traffic' }, [
|
E(
|
||||||
E(
|
'div',
|
||||||
'div',
|
{ id: 'dashboard-widget-traffic' },
|
||||||
{
|
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||||
id: '',
|
),
|
||||||
style: 'height: 78px',
|
E(
|
||||||
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
'div',
|
||||||
},
|
{ id: 'dashboard-widget-traffic-total' },
|
||||||
'',
|
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||||
),
|
),
|
||||||
]),
|
E(
|
||||||
E('div', { id: 'dashboard-widget-traffic-total' }, [
|
'div',
|
||||||
E(
|
{ id: 'dashboard-widget-system-info' },
|
||||||
'div',
|
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||||
{
|
),
|
||||||
id: '',
|
E(
|
||||||
style: 'height: 78px',
|
'div',
|
||||||
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
{ id: 'dashboard-widget-service-info' },
|
||||||
},
|
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||||
'',
|
),
|
||||||
),
|
|
||||||
]),
|
|
||||||
E('div', { id: 'dashboard-widget-system-info' }, [
|
|
||||||
E(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
id: '',
|
|
||||||
style: 'height: 78px',
|
|
||||||
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
|
||||||
},
|
|
||||||
'',
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
E('div', { id: 'dashboard-widget-service-info' }, [
|
|
||||||
E(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
id: '',
|
|
||||||
style: 'height: 78px',
|
|
||||||
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
|
||||||
},
|
|
||||||
'',
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
]),
|
]),
|
||||||
// All outbounds
|
// All outbounds
|
||||||
E('div', { id: 'dashboard-sections-grid' }, [
|
E(
|
||||||
E('div', {
|
'div',
|
||||||
id: 'dashboard-sections-grid-skeleton',
|
{ id: 'dashboard-sections-grid' },
|
||||||
class: 'pdk_dashboard-page__outbound-section skeleton',
|
renderSections({
|
||||||
style: 'height: 127px',
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
section: {
|
||||||
|
code: '',
|
||||||
|
displayName: '',
|
||||||
|
outbounds: [],
|
||||||
|
withTagSelect: false,
|
||||||
|
},
|
||||||
|
onTestLatency: () => {},
|
||||||
|
onChooseOutbound: () => {},
|
||||||
}),
|
}),
|
||||||
]),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,37 @@
|
|||||||
import { Podkop } from '../../../types';
|
import { Podkop } from '../../types';
|
||||||
|
|
||||||
interface IRenderOutboundGroupProps {
|
interface IRenderSectionsProps {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
section: Podkop.OutboundGroup;
|
section: Podkop.OutboundGroup;
|
||||||
onTestLatency: (tag: string) => void;
|
onTestLatency: (tag: string) => void;
|
||||||
onChooseOutbound: (selector: string, tag: string) => void;
|
onChooseOutbound: (selector: string, tag: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderOutboundGroup({
|
function renderFailedState() {
|
||||||
|
return E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: 'pdk_dashboard-page__outbound-section centered',
|
||||||
|
style: 'height: 127px',
|
||||||
|
},
|
||||||
|
E('span', {}, 'Dashboard currently unavailable'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLoadingState() {
|
||||||
|
return E('div', {
|
||||||
|
id: 'dashboard-sections-grid-skeleton',
|
||||||
|
class: 'pdk_dashboard-page__outbound-section skeleton',
|
||||||
|
style: 'height: 127px',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderDefaultState({
|
||||||
section,
|
section,
|
||||||
onTestLatency,
|
|
||||||
onChooseOutbound,
|
onChooseOutbound,
|
||||||
}: IRenderOutboundGroupProps) {
|
onTestLatency,
|
||||||
|
}: IRenderSectionsProps) {
|
||||||
function testLatency() {
|
function testLatency() {
|
||||||
if (section.withTagSelect) {
|
if (section.withTagSelect) {
|
||||||
return onTestLatency(section.code);
|
return onTestLatency(section.code);
|
||||||
@@ -90,3 +111,15 @@ export function renderOutboundGroup({
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renderSections(props: IRenderSectionsProps) {
|
||||||
|
if (props.failed) {
|
||||||
|
return renderFailedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.loading) {
|
||||||
|
return renderLoadingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderDefaultState(props);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
interface IRenderWidgetParams {
|
interface IRenderWidgetProps {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
items: Array<{
|
items: Array<{
|
||||||
key: string;
|
key: string;
|
||||||
@@ -9,7 +11,31 @@ interface IRenderWidgetParams {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderDashboardWidget({ title, items }: IRenderWidgetParams) {
|
function renderFailedState() {
|
||||||
|
return E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
style: 'height: 78px',
|
||||||
|
class: 'pdk_dashboard-page__widgets-section__item centered',
|
||||||
|
},
|
||||||
|
'Currently unavailable',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLoadingState() {
|
||||||
|
return E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
style: 'height: 78px',
|
||||||
|
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDefaultState({ title, items }: IRenderWidgetProps) {
|
||||||
return E('div', { class: 'pdk_dashboard-page__widgets-section__item' }, [
|
return E('div', { class: 'pdk_dashboard-page__widgets-section__item' }, [
|
||||||
E(
|
E(
|
||||||
'b',
|
'b',
|
||||||
@@ -38,3 +64,15 @@ export function renderDashboardWidget({ title, items }: IRenderWidgetParams) {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renderWidget(props: IRenderWidgetProps) {
|
||||||
|
if (props.loading) {
|
||||||
|
return renderLoadingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.failed) {
|
||||||
|
return renderFailedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderDefaultState(props);
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export function renderEmptyOutboundGroup() {
|
|
||||||
return E(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
class: 'pdk_dashboard-page__outbound-section centered',
|
|
||||||
style: 'height: 127px',
|
|
||||||
},
|
|
||||||
E('span', {}, 'Dashboard currently unavailable'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
type Listener = (data: any) => void;
|
type Listener = (data: any) => void;
|
||||||
|
type ErrorListener = (error: Event | string) => void;
|
||||||
|
|
||||||
class SocketManager {
|
class SocketManager {
|
||||||
private static instance: SocketManager;
|
private static instance: SocketManager;
|
||||||
private sockets = new Map<string, WebSocket>();
|
private sockets = new Map<string, WebSocket>();
|
||||||
private listeners = new Map<string, Set<Listener>>();
|
private listeners = new Map<string, Set<Listener>>();
|
||||||
private connected = new Map<string, boolean>();
|
private connected = new Map<string, boolean>();
|
||||||
|
private errorListeners = new Map<string, Set<ErrorListener>>();
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
@@ -23,9 +25,11 @@ class SocketManager {
|
|||||||
this.sockets.set(url, ws);
|
this.sockets.set(url, ws);
|
||||||
this.connected.set(url, false);
|
this.connected.set(url, false);
|
||||||
this.listeners.set(url, new Set());
|
this.listeners.set(url, new Set());
|
||||||
|
this.errorListeners.set(url, new Set());
|
||||||
|
|
||||||
ws.addEventListener('open', () => {
|
ws.addEventListener('open', () => {
|
||||||
this.connected.set(url, true);
|
this.connected.set(url, true);
|
||||||
|
console.info(`Connected: ${url}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener('message', (event) => {
|
ws.addEventListener('message', (event) => {
|
||||||
@@ -43,23 +47,33 @@ class SocketManager {
|
|||||||
|
|
||||||
ws.addEventListener('close', () => {
|
ws.addEventListener('close', () => {
|
||||||
this.connected.set(url, false);
|
this.connected.set(url, false);
|
||||||
console.warn(`⚠️ Disconnected: ${url}`);
|
console.warn(`Disconnected: ${url}`);
|
||||||
|
this.triggerError(url, 'Connection closed');
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener('error', (err) => {
|
ws.addEventListener('error', (err) => {
|
||||||
console.error(`❌ Socket error for ${url}:`, err);
|
console.error(`Socket error for ${url}:`, err);
|
||||||
|
this.triggerError(url, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(url: string, listener: Listener): void {
|
subscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
||||||
if (!this.sockets.has(url)) {
|
if (!this.sockets.has(url)) {
|
||||||
this.connect(url);
|
this.connect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listeners.get(url)?.add(listener);
|
this.listeners.get(url)?.add(listener);
|
||||||
|
|
||||||
|
if (onError) {
|
||||||
|
this.errorListeners.get(url)?.add(onError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe(url: string, listener: Listener): void {
|
unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
||||||
this.listeners.get(url)?.delete(listener);
|
this.listeners.get(url)?.delete(listener);
|
||||||
|
if (onError) {
|
||||||
|
this.errorListeners.get(url)?.delete(onError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -68,7 +82,8 @@ class SocketManager {
|
|||||||
if (ws && this.connected.get(url)) {
|
if (ws && this.connected.get(url)) {
|
||||||
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
||||||
} else {
|
} else {
|
||||||
console.warn(`⚠️ Cannot send: not connected to ${url}`);
|
console.warn(`Cannot send: not connected to ${url}`);
|
||||||
|
this.triggerError(url, 'Not connected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +93,7 @@ class SocketManager {
|
|||||||
ws.close();
|
ws.close();
|
||||||
this.sockets.delete(url);
|
this.sockets.delete(url);
|
||||||
this.listeners.delete(url);
|
this.listeners.delete(url);
|
||||||
|
this.errorListeners.delete(url);
|
||||||
this.connected.delete(url);
|
this.connected.delete(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,6 +103,19 @@ class SocketManager {
|
|||||||
this.disconnect(url);
|
this.disconnect(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private triggerError(url: string, err: Event | string): void {
|
||||||
|
const handlers = this.errorListeners.get(url);
|
||||||
|
if (handlers) {
|
||||||
|
for (const cb of handlers) {
|
||||||
|
try {
|
||||||
|
cb(err);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error handler threw for ${url}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const socket = SocketManager.getInstance();
|
export const socket = SocketManager.getInstance();
|
||||||
|
|||||||
@@ -117,22 +117,30 @@ export interface StoreType {
|
|||||||
current: string;
|
current: string;
|
||||||
all: string[];
|
all: string[];
|
||||||
};
|
};
|
||||||
dashboardSections: {
|
bandwidthWidget: {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
data: Podkop.OutboundGroup[];
|
|
||||||
failed: boolean;
|
failed: boolean;
|
||||||
|
data: { up: number; down: number };
|
||||||
};
|
};
|
||||||
traffic: { up: number; down: number };
|
trafficTotalWidget: {
|
||||||
memory: { inuse: number; oslimit: number };
|
loading: boolean;
|
||||||
connections: {
|
failed: boolean;
|
||||||
connections: unknown[];
|
data: { downloadTotal: number; uploadTotal: number };
|
||||||
downloadTotal: number;
|
|
||||||
memory: number;
|
|
||||||
uploadTotal: number;
|
|
||||||
};
|
};
|
||||||
services: {
|
systemInfoWidget: {
|
||||||
singbox: number;
|
loading: boolean;
|
||||||
podkop: number;
|
failed: boolean;
|
||||||
|
data: { connections: number; memory: number };
|
||||||
|
};
|
||||||
|
servicesInfoWidget: {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
data: { singbox: number; podkop: number };
|
||||||
|
};
|
||||||
|
sectionsWidget: {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
data: Podkop.OutboundGroup[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,19 +149,31 @@ const initialStore: StoreType = {
|
|||||||
current: '',
|
current: '',
|
||||||
all: [],
|
all: [],
|
||||||
},
|
},
|
||||||
dashboardSections: {
|
bandwidthWidget: {
|
||||||
data: [],
|
|
||||||
loading: true,
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: { up: 0, down: 0 },
|
||||||
},
|
},
|
||||||
traffic: { up: -1, down: -1 },
|
trafficTotalWidget: {
|
||||||
memory: { inuse: -1, oslimit: -1 },
|
loading: true,
|
||||||
connections: {
|
failed: false,
|
||||||
connections: [],
|
data: { downloadTotal: 0, uploadTotal: 0 },
|
||||||
memory: -1,
|
},
|
||||||
downloadTotal: -1,
|
systemInfoWidget: {
|
||||||
uploadTotal: -1,
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: { connections: 0, memory: 0 },
|
||||||
|
},
|
||||||
|
servicesInfoWidget: {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: { singbox: 0, podkop: 0 },
|
||||||
|
},
|
||||||
|
sectionsWidget: {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: [],
|
||||||
},
|
},
|
||||||
services: { singbox: -1, podkop: -1 },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const store = new Store<StoreType>(initialStore);
|
export const store = new Store<StoreType>(initialStore);
|
||||||
|
|||||||
@@ -1166,19 +1166,31 @@ var initialStore = {
|
|||||||
current: "",
|
current: "",
|
||||||
all: []
|
all: []
|
||||||
},
|
},
|
||||||
dashboardSections: {
|
bandwidthWidget: {
|
||||||
data: [],
|
loading: true,
|
||||||
loading: true
|
failed: false,
|
||||||
|
data: { up: 0, down: 0 }
|
||||||
},
|
},
|
||||||
traffic: { up: -1, down: -1 },
|
trafficTotalWidget: {
|
||||||
memory: { inuse: -1, oslimit: -1 },
|
loading: true,
|
||||||
connections: {
|
failed: false,
|
||||||
connections: [],
|
data: { downloadTotal: 0, uploadTotal: 0 }
|
||||||
memory: -1,
|
|
||||||
downloadTotal: -1,
|
|
||||||
uploadTotal: -1
|
|
||||||
},
|
},
|
||||||
services: { singbox: -1, podkop: -1 }
|
systemInfoWidget: {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: { connections: 0, memory: 0 }
|
||||||
|
},
|
||||||
|
servicesInfoWidget: {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: { singbox: 0, podkop: 0 }
|
||||||
|
},
|
||||||
|
sectionsWidget: {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: []
|
||||||
|
}
|
||||||
};
|
};
|
||||||
var store = new Store(initialStore);
|
var store = new Store(initialStore);
|
||||||
|
|
||||||
@@ -1194,79 +1206,28 @@ function coreService() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/podkop/tabs/dashboard/renderDashboard.ts
|
// src/podkop/tabs/dashboard/renderSections.ts
|
||||||
function renderDashboard() {
|
function renderFailedState() {
|
||||||
return E(
|
return E(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
id: "dashboard-status",
|
class: "pdk_dashboard-page__outbound-section centered",
|
||||||
class: "pdk_dashboard-page"
|
style: "height: 127px"
|
||||||
},
|
},
|
||||||
[
|
E("span", {}, "Dashboard currently unavailable")
|
||||||
// Widgets section
|
|
||||||
E("div", { class: "pdk_dashboard-page__widgets-section" }, [
|
|
||||||
E("div", { id: "dashboard-widget-traffic" }, [
|
|
||||||
E(
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
style: "height: 78px",
|
|
||||||
class: "pdk_dashboard-page__widgets-section__item skeleton"
|
|
||||||
},
|
|
||||||
""
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
E("div", { id: "dashboard-widget-traffic-total" }, [
|
|
||||||
E(
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
style: "height: 78px",
|
|
||||||
class: "pdk_dashboard-page__widgets-section__item skeleton"
|
|
||||||
},
|
|
||||||
""
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
E("div", { id: "dashboard-widget-system-info" }, [
|
|
||||||
E(
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
style: "height: 78px",
|
|
||||||
class: "pdk_dashboard-page__widgets-section__item skeleton"
|
|
||||||
},
|
|
||||||
""
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
E("div", { id: "dashboard-widget-service-info" }, [
|
|
||||||
E(
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
style: "height: 78px",
|
|
||||||
class: "pdk_dashboard-page__widgets-section__item skeleton"
|
|
||||||
},
|
|
||||||
""
|
|
||||||
)
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
// All outbounds
|
|
||||||
E("div", { id: "dashboard-sections-grid" }, [
|
|
||||||
E("div", {
|
|
||||||
id: "dashboard-sections-grid-skeleton",
|
|
||||||
class: "pdk_dashboard-page__outbound-section skeleton",
|
|
||||||
style: "height: 127px"
|
|
||||||
})
|
|
||||||
])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function renderLoadingState() {
|
||||||
// src/podkop/tabs/dashboard/renderer/renderOutboundGroup.ts
|
return E("div", {
|
||||||
function renderOutboundGroup({
|
id: "dashboard-sections-grid-skeleton",
|
||||||
|
class: "pdk_dashboard-page__outbound-section skeleton",
|
||||||
|
style: "height: 127px"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderDefaultState({
|
||||||
section,
|
section,
|
||||||
onTestLatency,
|
onChooseOutbound,
|
||||||
onChooseOutbound
|
onTestLatency
|
||||||
}) {
|
}) {
|
||||||
function testLatency() {
|
function testLatency() {
|
||||||
if (section.withTagSelect) {
|
if (section.withTagSelect) {
|
||||||
@@ -1338,9 +1299,40 @@ function renderOutboundGroup({
|
|||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
function renderSections(props) {
|
||||||
|
if (props.failed) {
|
||||||
|
return renderFailedState();
|
||||||
|
}
|
||||||
|
if (props.loading) {
|
||||||
|
return renderLoadingState();
|
||||||
|
}
|
||||||
|
return renderDefaultState(props);
|
||||||
|
}
|
||||||
|
|
||||||
// src/podkop/tabs/dashboard/renderer/renderWidget.ts
|
// src/podkop/tabs/dashboard/renderWidget.ts
|
||||||
function renderDashboardWidget({ title, items }) {
|
function renderFailedState2() {
|
||||||
|
return E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
style: "height: 78px",
|
||||||
|
class: "pdk_dashboard-page__widgets-section__item centered"
|
||||||
|
},
|
||||||
|
"Currently unavailable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function renderLoadingState2() {
|
||||||
|
return E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
style: "height: 78px",
|
||||||
|
class: "pdk_dashboard-page__widgets-section__item skeleton"
|
||||||
|
},
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function renderDefaultState2({ title, items }) {
|
||||||
return E("div", { class: "pdk_dashboard-page__widgets-section__item" }, [
|
return E("div", { class: "pdk_dashboard-page__widgets-section__item" }, [
|
||||||
E(
|
E(
|
||||||
"b",
|
"b",
|
||||||
@@ -1369,6 +1361,70 @@ function renderDashboardWidget({ title, items }) {
|
|||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
function renderWidget(props) {
|
||||||
|
if (props.loading) {
|
||||||
|
return renderLoadingState2();
|
||||||
|
}
|
||||||
|
if (props.failed) {
|
||||||
|
return renderFailedState2();
|
||||||
|
}
|
||||||
|
return renderDefaultState2(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/podkop/tabs/dashboard/renderDashboard.ts
|
||||||
|
function renderDashboard() {
|
||||||
|
return E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
id: "dashboard-status",
|
||||||
|
class: "pdk_dashboard-page"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Widgets section
|
||||||
|
E("div", { class: "pdk_dashboard-page__widgets-section" }, [
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{ id: "dashboard-widget-traffic" },
|
||||||
|
renderWidget({ loading: true, failed: false, title: "", items: [] })
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{ id: "dashboard-widget-traffic-total" },
|
||||||
|
renderWidget({ loading: true, failed: false, title: "", items: [] })
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{ id: "dashboard-widget-system-info" },
|
||||||
|
renderWidget({ loading: true, failed: false, title: "", items: [] })
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{ id: "dashboard-widget-service-info" },
|
||||||
|
renderWidget({ loading: true, failed: false, title: "", items: [] })
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
// All outbounds
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{ id: "dashboard-sections-grid" },
|
||||||
|
renderSections({
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
section: {
|
||||||
|
code: "",
|
||||||
|
displayName: "",
|
||||||
|
outbounds: [],
|
||||||
|
withTagSelect: false
|
||||||
|
},
|
||||||
|
onTestLatency: () => {
|
||||||
|
},
|
||||||
|
onChooseOutbound: () => {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// src/socket.ts
|
// src/socket.ts
|
||||||
var SocketManager = class _SocketManager {
|
var SocketManager = class _SocketManager {
|
||||||
@@ -1376,6 +1432,7 @@ var SocketManager = class _SocketManager {
|
|||||||
this.sockets = /* @__PURE__ */ new Map();
|
this.sockets = /* @__PURE__ */ new Map();
|
||||||
this.listeners = /* @__PURE__ */ new Map();
|
this.listeners = /* @__PURE__ */ new Map();
|
||||||
this.connected = /* @__PURE__ */ new Map();
|
this.connected = /* @__PURE__ */ new Map();
|
||||||
|
this.errorListeners = /* @__PURE__ */ new Map();
|
||||||
}
|
}
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
if (!_SocketManager.instance) {
|
if (!_SocketManager.instance) {
|
||||||
@@ -1389,8 +1446,10 @@ var SocketManager = class _SocketManager {
|
|||||||
this.sockets.set(url, ws);
|
this.sockets.set(url, ws);
|
||||||
this.connected.set(url, false);
|
this.connected.set(url, false);
|
||||||
this.listeners.set(url, /* @__PURE__ */ new Set());
|
this.listeners.set(url, /* @__PURE__ */ new Set());
|
||||||
|
this.errorListeners.set(url, /* @__PURE__ */ new Set());
|
||||||
ws.addEventListener("open", () => {
|
ws.addEventListener("open", () => {
|
||||||
this.connected.set(url, true);
|
this.connected.set(url, true);
|
||||||
|
console.info(`Connected: ${url}`);
|
||||||
});
|
});
|
||||||
ws.addEventListener("message", (event) => {
|
ws.addEventListener("message", (event) => {
|
||||||
const handlers = this.listeners.get(url);
|
const handlers = this.listeners.get(url);
|
||||||
@@ -1406,20 +1465,28 @@ var SocketManager = class _SocketManager {
|
|||||||
});
|
});
|
||||||
ws.addEventListener("close", () => {
|
ws.addEventListener("close", () => {
|
||||||
this.connected.set(url, false);
|
this.connected.set(url, false);
|
||||||
console.warn(`\u26A0\uFE0F Disconnected: ${url}`);
|
console.warn(`Disconnected: ${url}`);
|
||||||
|
this.triggerError(url, "Connection closed");
|
||||||
});
|
});
|
||||||
ws.addEventListener("error", (err) => {
|
ws.addEventListener("error", (err) => {
|
||||||
console.error(`\u274C Socket error for ${url}:`, err);
|
console.error(`Socket error for ${url}:`, err);
|
||||||
|
this.triggerError(url, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
subscribe(url, listener) {
|
subscribe(url, listener, onError) {
|
||||||
if (!this.sockets.has(url)) {
|
if (!this.sockets.has(url)) {
|
||||||
this.connect(url);
|
this.connect(url);
|
||||||
}
|
}
|
||||||
this.listeners.get(url)?.add(listener);
|
this.listeners.get(url)?.add(listener);
|
||||||
|
if (onError) {
|
||||||
|
this.errorListeners.get(url)?.add(onError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
unsubscribe(url, listener) {
|
unsubscribe(url, listener, onError) {
|
||||||
this.listeners.get(url)?.delete(listener);
|
this.listeners.get(url)?.delete(listener);
|
||||||
|
if (onError) {
|
||||||
|
this.errorListeners.get(url)?.delete(onError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
send(url, data) {
|
send(url, data) {
|
||||||
@@ -1427,7 +1494,8 @@ var SocketManager = class _SocketManager {
|
|||||||
if (ws && this.connected.get(url)) {
|
if (ws && this.connected.get(url)) {
|
||||||
ws.send(typeof data === "string" ? data : JSON.stringify(data));
|
ws.send(typeof data === "string" ? data : JSON.stringify(data));
|
||||||
} else {
|
} else {
|
||||||
console.warn(`\u26A0\uFE0F Cannot send: not connected to ${url}`);
|
console.warn(`Cannot send: not connected to ${url}`);
|
||||||
|
this.triggerError(url, "Not connected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disconnect(url) {
|
disconnect(url) {
|
||||||
@@ -1436,6 +1504,7 @@ var SocketManager = class _SocketManager {
|
|||||||
ws.close();
|
ws.close();
|
||||||
this.sockets.delete(url);
|
this.sockets.delete(url);
|
||||||
this.listeners.delete(url);
|
this.listeners.delete(url);
|
||||||
|
this.errorListeners.delete(url);
|
||||||
this.connected.delete(url);
|
this.connected.delete(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1444,6 +1513,18 @@ var SocketManager = class _SocketManager {
|
|||||||
this.disconnect(url);
|
this.disconnect(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
triggerError(url, err) {
|
||||||
|
const handlers = this.errorListeners.get(url);
|
||||||
|
if (handlers) {
|
||||||
|
for (const cb of handlers) {
|
||||||
|
try {
|
||||||
|
cb(err);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error handler threw for ${url}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
var socket = SocketManager.getInstance();
|
var socket = SocketManager.getInstance();
|
||||||
|
|
||||||
@@ -1459,63 +1540,101 @@ function prettyBytes(n) {
|
|||||||
return n + " " + unit;
|
return n + " " + unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/podkop/tabs/dashboard/renderer/renderEmptyOutboundGroup.ts
|
|
||||||
function renderEmptyOutboundGroup() {
|
|
||||||
return E(
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
class: "pdk_dashboard-page__outbound-section centered",
|
|
||||||
style: "height: 127px"
|
|
||||||
},
|
|
||||||
E("span", {}, "Dashboard currently unavailable")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/podkop/tabs/dashboard/initDashboardController.ts
|
// src/podkop/tabs/dashboard/initDashboardController.ts
|
||||||
async function fetchDashboardSections() {
|
async function fetchDashboardSections() {
|
||||||
|
const prev = store.get().sectionsWidget;
|
||||||
store.set({
|
store.set({
|
||||||
dashboardSections: {
|
sectionsWidget: {
|
||||||
...store.get().dashboardSections,
|
...prev,
|
||||||
failed: false,
|
failed: false
|
||||||
loading: true
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const { data, success } = await getDashboardSections();
|
const { data, success } = await getDashboardSections();
|
||||||
store.set({ dashboardSections: { loading: false, data, failed: !success } });
|
store.set({
|
||||||
|
sectionsWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: !success,
|
||||||
|
data
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
async function fetchServicesInfo() {
|
async function fetchServicesInfo() {
|
||||||
const podkop = await getPodkopStatus();
|
const [podkop, singbox] = await Promise.all([
|
||||||
const singbox = await getSingboxStatus();
|
getPodkopStatus(),
|
||||||
|
getSingboxStatus()
|
||||||
|
]);
|
||||||
store.set({
|
store.set({
|
||||||
services: {
|
servicesInfoWidget: {
|
||||||
singbox: singbox.running,
|
loading: false,
|
||||||
podkop: podkop.enabled
|
failed: false,
|
||||||
|
data: { singbox: singbox.running, podkop: podkop.enabled }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function connectToClashSockets() {
|
async function connectToClashSockets() {
|
||||||
socket.subscribe(`${getClashWsUrl()}/traffic?token=`, (msg) => {
|
socket.subscribe(
|
||||||
const parsedMsg = JSON.parse(msg);
|
`${getClashWsUrl()}/traffic?token=`,
|
||||||
store.set({
|
(msg) => {
|
||||||
traffic: { up: parsedMsg.up, down: parsedMsg.down }
|
const parsedMsg = JSON.parse(msg);
|
||||||
});
|
store.set({
|
||||||
});
|
bandwidthWidget: {
|
||||||
socket.subscribe(`${getClashWsUrl()}/connections?token=`, (msg) => {
|
loading: false,
|
||||||
const parsedMsg = JSON.parse(msg);
|
failed: false,
|
||||||
store.set({
|
data: { up: parsedMsg.up, down: parsedMsg.down }
|
||||||
connections: {
|
}
|
||||||
connections: parsedMsg.connections,
|
});
|
||||||
downloadTotal: parsedMsg.downloadTotal,
|
},
|
||||||
uploadTotal: parsedMsg.uploadTotal,
|
(_err) => {
|
||||||
memory: parsedMsg.memory
|
store.set({
|
||||||
}
|
bandwidthWidget: {
|
||||||
});
|
loading: false,
|
||||||
});
|
failed: true,
|
||||||
socket.subscribe(`${getClashWsUrl()}/memory?token=`, (msg) => {
|
data: { up: 0, down: 0 }
|
||||||
store.set({
|
}
|
||||||
memory: { inuse: msg.inuse, oslimit: msg.oslimit }
|
});
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
|
socket.subscribe(
|
||||||
|
`${getClashWsUrl()}/connections?token=`,
|
||||||
|
(msg) => {
|
||||||
|
const parsedMsg = JSON.parse(msg);
|
||||||
|
store.set({
|
||||||
|
trafficTotalWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
data: {
|
||||||
|
downloadTotal: parsedMsg.downloadTotal,
|
||||||
|
uploadTotal: parsedMsg.uploadTotal
|
||||||
|
}
|
||||||
|
},
|
||||||
|
systemInfoWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
data: {
|
||||||
|
connections: parsedMsg.connections?.length,
|
||||||
|
memory: parsedMsg.memory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(_err) => {
|
||||||
|
store.set({
|
||||||
|
trafficTotalWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: { downloadTotal: 0, uploadTotal: 0 }
|
||||||
|
},
|
||||||
|
systemInfoWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: {
|
||||||
|
connections: 0,
|
||||||
|
memory: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
async function handleChooseOutbound(selector, tag) {
|
async function handleChooseOutbound(selector, tag) {
|
||||||
await triggerProxySelector(selector, tag);
|
await triggerProxySelector(selector, tag);
|
||||||
@@ -1538,15 +1657,31 @@ function replaceTestLatencyButtonsWithSkeleton() {
|
|||||||
el.replaceWith(newDiv);
|
el.replaceWith(newDiv);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function renderDashboardSections() {
|
async function renderSectionsWidget() {
|
||||||
const dashboardSections = store.get().dashboardSections;
|
console.log("renderSectionsWidget");
|
||||||
|
const sectionsWidget = store.get().sectionsWidget;
|
||||||
const container = document.getElementById("dashboard-sections-grid");
|
const container = document.getElementById("dashboard-sections-grid");
|
||||||
if (dashboardSections.failed) {
|
if (sectionsWidget.loading || sectionsWidget.failed) {
|
||||||
const rendered = renderEmptyOutboundGroup();
|
const renderedWidget = renderSections({
|
||||||
return container.replaceChildren(rendered);
|
loading: sectionsWidget.loading,
|
||||||
|
failed: sectionsWidget.failed,
|
||||||
|
section: {
|
||||||
|
code: "",
|
||||||
|
displayName: "",
|
||||||
|
outbounds: [],
|
||||||
|
withTagSelect: false
|
||||||
|
},
|
||||||
|
onTestLatency: () => {
|
||||||
|
},
|
||||||
|
onChooseOutbound: () => {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return container.replaceChildren(renderedWidget);
|
||||||
}
|
}
|
||||||
const renderedOutboundGroups = dashboardSections.data.map(
|
const renderedWidgets = sectionsWidget.data.map(
|
||||||
(section) => renderOutboundGroup({
|
(section) => renderSections({
|
||||||
|
loading: sectionsWidget.loading,
|
||||||
|
failed: sectionsWidget.failed,
|
||||||
section,
|
section,
|
||||||
onTestLatency: (tag) => {
|
onTestLatency: (tag) => {
|
||||||
replaceTestLatencyButtonsWithSkeleton();
|
replaceTestLatencyButtonsWithSkeleton();
|
||||||
@@ -1560,68 +1695,122 @@ async function renderDashboardSections() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
container.replaceChildren(...renderedOutboundGroups);
|
return container.replaceChildren(...renderedWidgets);
|
||||||
}
|
}
|
||||||
async function renderTrafficWidget() {
|
async function renderBandwidthWidget() {
|
||||||
const traffic = store.get().traffic;
|
console.log("renderBandwidthWidget");
|
||||||
|
const traffic = store.get().bandwidthWidget;
|
||||||
const container = document.getElementById("dashboard-widget-traffic");
|
const container = document.getElementById("dashboard-widget-traffic");
|
||||||
const renderedWidget = renderDashboardWidget({
|
if (traffic.loading || traffic.failed) {
|
||||||
|
const renderedWidget2 = renderWidget({
|
||||||
|
loading: traffic.loading,
|
||||||
|
failed: traffic.failed,
|
||||||
|
title: "",
|
||||||
|
items: []
|
||||||
|
});
|
||||||
|
return container.replaceChildren(renderedWidget2);
|
||||||
|
}
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: traffic.loading,
|
||||||
|
failed: traffic.failed,
|
||||||
title: "Traffic",
|
title: "Traffic",
|
||||||
items: [
|
items: [
|
||||||
{ key: "Uplink", value: `${prettyBytes(traffic.up)}/s` },
|
{ key: "Uplink", value: `${prettyBytes(traffic.data.up)}/s` },
|
||||||
{ key: "Downlink", value: `${prettyBytes(traffic.down)}/s` }
|
{ key: "Downlink", value: `${prettyBytes(traffic.data.down)}/s` }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
container.replaceChildren(renderedWidget);
|
container.replaceChildren(renderedWidget);
|
||||||
}
|
}
|
||||||
async function renderTrafficTotalWidget() {
|
async function renderTrafficTotalWidget() {
|
||||||
const connections = store.get().connections;
|
console.log("renderTrafficTotalWidget");
|
||||||
|
const trafficTotalWidget = store.get().trafficTotalWidget;
|
||||||
const container = document.getElementById("dashboard-widget-traffic-total");
|
const container = document.getElementById("dashboard-widget-traffic-total");
|
||||||
const renderedWidget = renderDashboardWidget({
|
if (trafficTotalWidget.loading || trafficTotalWidget.failed) {
|
||||||
|
const renderedWidget2 = renderWidget({
|
||||||
|
loading: trafficTotalWidget.loading,
|
||||||
|
failed: trafficTotalWidget.failed,
|
||||||
|
title: "",
|
||||||
|
items: []
|
||||||
|
});
|
||||||
|
return container.replaceChildren(renderedWidget2);
|
||||||
|
}
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: trafficTotalWidget.loading,
|
||||||
|
failed: trafficTotalWidget.failed,
|
||||||
title: "Traffic Total",
|
title: "Traffic Total",
|
||||||
items: [
|
items: [
|
||||||
{ key: "Uplink", value: String(prettyBytes(connections.uploadTotal)) },
|
{
|
||||||
|
key: "Uplink",
|
||||||
|
value: String(prettyBytes(trafficTotalWidget.data.uploadTotal))
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "Downlink",
|
key: "Downlink",
|
||||||
value: String(prettyBytes(connections.downloadTotal))
|
value: String(prettyBytes(trafficTotalWidget.data.downloadTotal))
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
container.replaceChildren(renderedWidget);
|
container.replaceChildren(renderedWidget);
|
||||||
}
|
}
|
||||||
async function renderSystemInfoWidget() {
|
async function renderSystemInfoWidget() {
|
||||||
const connections = store.get().connections;
|
console.log("renderSystemInfoWidget");
|
||||||
|
const systemInfoWidget = store.get().systemInfoWidget;
|
||||||
const container = document.getElementById("dashboard-widget-system-info");
|
const container = document.getElementById("dashboard-widget-system-info");
|
||||||
const renderedWidget = renderDashboardWidget({
|
if (systemInfoWidget.loading || systemInfoWidget.failed) {
|
||||||
|
const renderedWidget2 = renderWidget({
|
||||||
|
loading: systemInfoWidget.loading,
|
||||||
|
failed: systemInfoWidget.failed,
|
||||||
|
title: "",
|
||||||
|
items: []
|
||||||
|
});
|
||||||
|
return container.replaceChildren(renderedWidget2);
|
||||||
|
}
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: systemInfoWidget.loading,
|
||||||
|
failed: systemInfoWidget.failed,
|
||||||
title: "System info",
|
title: "System info",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: "Active Connections",
|
key: "Active Connections",
|
||||||
value: String(connections.connections.length)
|
value: String(systemInfoWidget.data.connections)
|
||||||
},
|
},
|
||||||
{ key: "Memory Usage", value: String(prettyBytes(connections.memory)) }
|
{
|
||||||
|
key: "Memory Usage",
|
||||||
|
value: String(prettyBytes(systemInfoWidget.data.memory))
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
container.replaceChildren(renderedWidget);
|
container.replaceChildren(renderedWidget);
|
||||||
}
|
}
|
||||||
async function renderServiceInfoWidget() {
|
async function renderServicesInfoWidget() {
|
||||||
const services = store.get().services;
|
console.log("renderServicesInfoWidget");
|
||||||
|
const servicesInfoWidget = store.get().servicesInfoWidget;
|
||||||
const container = document.getElementById("dashboard-widget-service-info");
|
const container = document.getElementById("dashboard-widget-service-info");
|
||||||
const renderedWidget = renderDashboardWidget({
|
if (servicesInfoWidget.loading || servicesInfoWidget.failed) {
|
||||||
|
const renderedWidget2 = renderWidget({
|
||||||
|
loading: servicesInfoWidget.loading,
|
||||||
|
failed: servicesInfoWidget.failed,
|
||||||
|
title: "",
|
||||||
|
items: []
|
||||||
|
});
|
||||||
|
return container.replaceChildren(renderedWidget2);
|
||||||
|
}
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: servicesInfoWidget.loading,
|
||||||
|
failed: servicesInfoWidget.failed,
|
||||||
title: "Services info",
|
title: "Services info",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: "Podkop",
|
key: "Podkop",
|
||||||
value: services.podkop ? "\u2714 Enabled" : "\u2718 Disabled",
|
value: servicesInfoWidget.data.podkop ? "\u2714 Enabled" : "\u2718 Disabled",
|
||||||
attributes: {
|
attributes: {
|
||||||
class: services.podkop ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error"
|
class: servicesInfoWidget.data.podkop ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Sing-box",
|
key: "Sing-box",
|
||||||
value: services.singbox ? "\u2714 Running" : "\u2718 Stopped",
|
value: servicesInfoWidget.data.singbox ? "\u2714 Running" : "\u2718 Stopped",
|
||||||
attributes: {
|
attributes: {
|
||||||
class: services.singbox ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error"
|
class: servicesInfoWidget.data.singbox ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1629,18 +1818,20 @@ async function renderServiceInfoWidget() {
|
|||||||
container.replaceChildren(renderedWidget);
|
container.replaceChildren(renderedWidget);
|
||||||
}
|
}
|
||||||
async function onStoreUpdate(next, prev, diff) {
|
async function onStoreUpdate(next, prev, diff) {
|
||||||
if (diff?.dashboardSections) {
|
if (diff.sectionsWidget) {
|
||||||
renderDashboardSections();
|
renderSectionsWidget();
|
||||||
}
|
}
|
||||||
if (diff?.traffic) {
|
if (diff.bandwidthWidget) {
|
||||||
renderTrafficWidget();
|
renderBandwidthWidget();
|
||||||
}
|
}
|
||||||
if (diff?.connections) {
|
if (diff.trafficTotalWidget) {
|
||||||
renderTrafficTotalWidget();
|
renderTrafficTotalWidget();
|
||||||
|
}
|
||||||
|
if (diff.systemInfoWidget) {
|
||||||
renderSystemInfoWidget();
|
renderSystemInfoWidget();
|
||||||
}
|
}
|
||||||
if (diff?.services) {
|
if (diff.servicesInfoWidget) {
|
||||||
renderServiceInfoWidget();
|
renderServicesInfoWidget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function initDashboardController() {
|
async function initDashboardController() {
|
||||||
|
|||||||
Reference in New Issue
Block a user