mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-06 11:36:50 +03:00
feat: implement some diagnostics widget
This commit is contained in:
@@ -431,7 +431,13 @@ export async function initDashboardController(): Promise<void> {
|
||||
// Remove old listener
|
||||
store.unsubscribe(onStoreUpdate);
|
||||
// Clear store
|
||||
store.reset();
|
||||
store.reset([
|
||||
'bandwidthWidget',
|
||||
'trafficTotalWidget',
|
||||
'systemInfoWidget',
|
||||
'servicesInfoWidget',
|
||||
'sectionsWidget',
|
||||
]);
|
||||
|
||||
// Add new listener
|
||||
store.subscribe(onStoreUpdate);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export enum DIAGNOSTICS_CHECKS {
|
||||
DNS = 'DNS',
|
||||
SINGBOX = 'SINGBOX',
|
||||
NFT = 'NFT',
|
||||
FAKEIP = 'FAKEIP',
|
||||
}
|
||||
|
||||
export const DIAGNOSTICS_CHECKS_MAP: Record<
|
||||
DIAGNOSTICS_CHECKS,
|
||||
{ order: number; title: string; code: DIAGNOSTICS_CHECKS }
|
||||
> = {
|
||||
[DIAGNOSTICS_CHECKS.DNS]: {
|
||||
order: 1,
|
||||
title: _('DNS checks'),
|
||||
code: DIAGNOSTICS_CHECKS.DNS,
|
||||
},
|
||||
[DIAGNOSTICS_CHECKS.SINGBOX]: {
|
||||
order: 2,
|
||||
title: _('Sing-box checks'),
|
||||
code: DIAGNOSTICS_CHECKS.SINGBOX,
|
||||
},
|
||||
[DIAGNOSTICS_CHECKS.NFT]: {
|
||||
order: 3,
|
||||
title: _('Nftables checks'),
|
||||
code: DIAGNOSTICS_CHECKS.NFT,
|
||||
},
|
||||
[DIAGNOSTICS_CHECKS.FAKEIP]: {
|
||||
order: 4,
|
||||
title: _('FakeIP checks'),
|
||||
code: DIAGNOSTICS_CHECKS.FAKEIP,
|
||||
},
|
||||
};
|
||||
@@ -2,13 +2,15 @@ import { getDNSCheck } from '../../../methods';
|
||||
import { updateDiagnosticsCheck } from '../updateDiagnosticsCheck';
|
||||
import { insertIf } from '../../../../helpers';
|
||||
import { IDiagnosticsChecksItem } from '../../../../store';
|
||||
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||
|
||||
export async function runDnsCheck() {
|
||||
const code = 'dns_check';
|
||||
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS;
|
||||
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('DNS checks'),
|
||||
title,
|
||||
description: _('Checking dns, please wait'),
|
||||
state: 'loading',
|
||||
items: [],
|
||||
@@ -18,8 +20,9 @@ export async function runDnsCheck() {
|
||||
|
||||
if (!dnsChecks.success) {
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('DNS checks'),
|
||||
title,
|
||||
description: _('Cannot receive DNS checks result'),
|
||||
state: 'error',
|
||||
items: [],
|
||||
@@ -55,8 +58,9 @@ export async function runDnsCheck() {
|
||||
}
|
||||
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('DNS checks'),
|
||||
title,
|
||||
description: _('DNS checks passed'),
|
||||
state: getStatus(),
|
||||
items: [
|
||||
|
||||
@@ -3,13 +3,15 @@ import * as fakeIPMethods from '../../../../fakeip/methods';
|
||||
import { updateDiagnosticsCheck } from '../updateDiagnosticsCheck';
|
||||
import { insertIf } from '../../../../helpers';
|
||||
import { IDiagnosticsChecksItem } from '../../../../store';
|
||||
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||
|
||||
export async function runFakeIPCheck() {
|
||||
const code = 'fake_ip_check';
|
||||
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP;
|
||||
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('FakeIP checks'),
|
||||
title,
|
||||
description: _('Checking FakeIP, please wait'),
|
||||
state: 'loading',
|
||||
items: [],
|
||||
@@ -68,8 +70,9 @@ export async function runFakeIPCheck() {
|
||||
const { state, description } = getMeta();
|
||||
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('FakeIP checks'),
|
||||
title,
|
||||
description,
|
||||
state,
|
||||
items: [
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { getNftRulesCheck } from '../../../methods';
|
||||
import { updateDiagnosticsCheck } from '../updateDiagnosticsCheck';
|
||||
import { getFakeIpCheck, getIpCheck } from '../../../../fakeip';
|
||||
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||
|
||||
export async function runNftCheck() {
|
||||
const code = 'nft_check';
|
||||
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.NFT;
|
||||
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('Nftables checks'),
|
||||
title,
|
||||
description: _('Checking nftables, please wait'),
|
||||
state: 'loading',
|
||||
items: [],
|
||||
@@ -20,8 +22,9 @@ export async function runNftCheck() {
|
||||
|
||||
if (!nftablesChecks.success) {
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('Nftables checks'),
|
||||
title,
|
||||
description: _('Cannot receive nftables checks result'),
|
||||
state: 'error',
|
||||
items: [],
|
||||
@@ -67,8 +70,9 @@ export async function runNftCheck() {
|
||||
}
|
||||
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('Nftables checks'),
|
||||
title,
|
||||
description: allGood
|
||||
? _('Nftables checks passed')
|
||||
: _('Nftables checks partially passed'),
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { getSingBoxCheck } from '../../../methods';
|
||||
import { updateDiagnosticsCheck } from '../updateDiagnosticsCheck';
|
||||
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||
|
||||
export async function runSingBoxCheck() {
|
||||
const code = 'sing_box_check';
|
||||
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.SINGBOX;
|
||||
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('Sing-box checks'),
|
||||
title,
|
||||
description: _('Checking sing-box, please wait'),
|
||||
state: 'loading',
|
||||
items: [],
|
||||
@@ -16,8 +18,9 @@ export async function runSingBoxCheck() {
|
||||
|
||||
if (!singBoxChecks.success) {
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('Sing-box checks'),
|
||||
title,
|
||||
description: _('Cannot receive Sing-box checks result'),
|
||||
state: 'error',
|
||||
items: [],
|
||||
@@ -59,8 +62,9 @@ export async function runSingBoxCheck() {
|
||||
}
|
||||
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _('Sing-box checks'),
|
||||
title,
|
||||
description: _('Sing-box checks passed'),
|
||||
state: getStatus(),
|
||||
items: [
|
||||
|
||||
86
fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts
Normal file
86
fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
DIAGNOSTICS_CHECKS,
|
||||
DIAGNOSTICS_CHECKS_MAP,
|
||||
} from './checks/contstants';
|
||||
import { StoreType } from '../../../store';
|
||||
|
||||
export const initialDiagnosticStore: Pick<
|
||||
StoreType,
|
||||
'diagnosticsChecks' | 'diagnosticsRunAction'
|
||||
> = {
|
||||
diagnosticsRunAction: { loading: false },
|
||||
diagnosticsChecks: [
|
||||
{
|
||||
code: DIAGNOSTICS_CHECKS.DNS,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.DNS.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.DNS.order,
|
||||
description: _('Not running'),
|
||||
items: [],
|
||||
state: 'skipped',
|
||||
},
|
||||
{
|
||||
code: DIAGNOSTICS_CHECKS.SINGBOX,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order,
|
||||
description: _('Not running'),
|
||||
items: [],
|
||||
state: 'skipped',
|
||||
},
|
||||
{
|
||||
code: DIAGNOSTICS_CHECKS.NFT,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.NFT.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.NFT.order,
|
||||
description: _('Not running'),
|
||||
items: [],
|
||||
state: 'skipped',
|
||||
},
|
||||
{
|
||||
code: DIAGNOSTICS_CHECKS.FAKEIP,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order,
|
||||
description: _('Not running'),
|
||||
items: [],
|
||||
state: 'skipped',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const loadingDiagnosticsChecksStore: Pick<
|
||||
StoreType,
|
||||
'diagnosticsChecks'
|
||||
> = {
|
||||
diagnosticsChecks: [
|
||||
{
|
||||
code: DIAGNOSTICS_CHECKS.DNS,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.DNS.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.DNS.order,
|
||||
description: _('Queued'),
|
||||
items: [],
|
||||
state: 'skipped',
|
||||
},
|
||||
{
|
||||
code: DIAGNOSTICS_CHECKS.SINGBOX,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order,
|
||||
description: _('Queued'),
|
||||
items: [],
|
||||
state: 'skipped',
|
||||
},
|
||||
{
|
||||
code: DIAGNOSTICS_CHECKS.NFT,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.NFT.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.NFT.order,
|
||||
description: _('Queued'),
|
||||
items: [],
|
||||
state: 'skipped',
|
||||
},
|
||||
{
|
||||
code: DIAGNOSTICS_CHECKS.FAKEIP,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order,
|
||||
description: _('Queued'),
|
||||
items: [],
|
||||
state: 'skipped',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -5,10 +5,16 @@ import { runDnsCheck } from './checks/runDnsCheck';
|
||||
import { runSingBoxCheck } from './checks/runSingBoxCheck';
|
||||
import { runNftCheck } from './checks/runNftCheck';
|
||||
import { runFakeIPCheck } from './checks/runFakeIPCheck';
|
||||
import { renderDiagnosticRunAction } from './renderDiagnosticRunAction';
|
||||
import { renderAvailableActions } from './renderAvailableActions';
|
||||
import { renderSystemInfo } from './renderSystemInfo';
|
||||
import { loadingDiagnosticsChecksStore } from './diagnostic.store';
|
||||
|
||||
async function renderDiagnosticsChecks() {
|
||||
function renderDiagnosticsChecks() {
|
||||
console.log('renderDiagnosticsChecks');
|
||||
const diagnosticsChecks = store.get().diagnosticsChecks;
|
||||
const diagnosticsChecks = store
|
||||
.get()
|
||||
.diagnosticsChecks.sort((a, b) => a.order - b.order);
|
||||
const container = document.getElementById('pdk_diagnostic-page-checks');
|
||||
|
||||
const renderedDiagnosticsChecks = diagnosticsChecks.map((check) =>
|
||||
@@ -20,6 +26,69 @@ async function renderDiagnosticsChecks() {
|
||||
});
|
||||
}
|
||||
|
||||
function renderDiagnosticRunActionWidget() {
|
||||
console.log('renderDiagnosticRunActionWidget');
|
||||
|
||||
const { loading } = store.get().diagnosticsRunAction;
|
||||
const container = document.getElementById('pdk_diagnostic-page-run-check');
|
||||
|
||||
const renderedAction = renderDiagnosticRunAction({
|
||||
loading,
|
||||
click: () => runChecks(),
|
||||
});
|
||||
|
||||
return preserveScrollForPage(() => {
|
||||
container!.replaceChildren(renderedAction);
|
||||
});
|
||||
}
|
||||
|
||||
function renderDiagnosticAvailableActionsWidget() {
|
||||
console.log('renderDiagnosticActionsWidget');
|
||||
|
||||
const container = document.getElementById('pdk_diagnostic-page-actions');
|
||||
|
||||
const renderedActions = renderAvailableActions();
|
||||
|
||||
return preserveScrollForPage(() => {
|
||||
container!.replaceChildren(renderedActions);
|
||||
});
|
||||
}
|
||||
|
||||
function renderDiagnosticSystemInfoWidget() {
|
||||
console.log('renderDiagnosticSystemInfoWidget');
|
||||
|
||||
const container = document.getElementById('pdk_diagnostic-page-system-info');
|
||||
|
||||
const renderedSystemInfo = renderSystemInfo({
|
||||
items: [
|
||||
{
|
||||
key: 'Podkop',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
key: 'Luci App',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
key: 'Sing-box',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
key: 'OS',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
key: 'Device',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return preserveScrollForPage(() => {
|
||||
container!.replaceChildren(renderedSystemInfo);
|
||||
});
|
||||
}
|
||||
|
||||
async function onStoreUpdate(
|
||||
next: StoreType,
|
||||
prev: StoreType,
|
||||
@@ -28,16 +97,31 @@ async function onStoreUpdate(
|
||||
if (diff.diagnosticsChecks) {
|
||||
renderDiagnosticsChecks();
|
||||
}
|
||||
|
||||
if (diff.diagnosticsRunAction) {
|
||||
renderDiagnosticRunActionWidget();
|
||||
}
|
||||
}
|
||||
|
||||
async function runChecks() {
|
||||
await runDnsCheck();
|
||||
try {
|
||||
store.set({
|
||||
diagnosticsRunAction: { loading: true },
|
||||
diagnosticsChecks: loadingDiagnosticsChecksStore.diagnosticsChecks,
|
||||
});
|
||||
|
||||
await runSingBoxCheck();
|
||||
await runDnsCheck();
|
||||
|
||||
await runNftCheck();
|
||||
await runSingBoxCheck();
|
||||
|
||||
await runFakeIPCheck();
|
||||
await runNftCheck();
|
||||
|
||||
await runFakeIPCheck();
|
||||
} catch (e) {
|
||||
console.log('runChecks - e', e);
|
||||
} finally {
|
||||
store.set({ diagnosticsRunAction: { loading: false } });
|
||||
}
|
||||
}
|
||||
|
||||
export async function initDiagnosticController(): Promise<void> {
|
||||
@@ -46,13 +130,19 @@ export async function initDiagnosticController(): Promise<void> {
|
||||
// Remove old listener
|
||||
store.unsubscribe(onStoreUpdate);
|
||||
|
||||
// Clear store
|
||||
store.reset();
|
||||
|
||||
// Add new listener
|
||||
store.subscribe(onStoreUpdate);
|
||||
|
||||
// TMP run checks on mount
|
||||
runChecks();
|
||||
// Initial checks render
|
||||
renderDiagnosticsChecks();
|
||||
|
||||
// Initial run checks action render
|
||||
renderDiagnosticRunActionWidget();
|
||||
|
||||
// Initial available actions render
|
||||
renderDiagnosticAvailableActionsWidget();
|
||||
|
||||
// Initial system info render
|
||||
renderDiagnosticSystemInfoWidget();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export function renderAvailableActions() {
|
||||
return E('div', { class: 'pdk_diagnostic-page__right-bar__actions' }, [
|
||||
E('b', {}, 'Available actions'),
|
||||
E('button', { class: 'btn' }, 'Restart podkop'),
|
||||
E('button', { class: 'btn' }, 'Stop podkop'),
|
||||
E('button', { class: 'btn' }, 'Disable podkop'),
|
||||
E('button', { class: 'btn' }, 'Get global check'),
|
||||
E('button', { class: 'btn' }, 'View logs'),
|
||||
E('button', { class: 'btn' }, 'Show sing-box config'),
|
||||
]);
|
||||
}
|
||||
@@ -1,45 +1,15 @@
|
||||
export function renderDiagnostic() {
|
||||
return E(
|
||||
'div',
|
||||
{ id: 'diagnostic-status', class: 'pdk_diagnostic-page' },
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
return E('div', { id: 'diagnostic-status', class: 'pdk_diagnostic-page' }, [
|
||||
E('div', { class: 'pdk_diagnostic-page__left-bar' }, [
|
||||
E('div', { id: 'pdk_diagnostic-page-run-check' }),
|
||||
E('div', {
|
||||
class: 'pdk_diagnostic-page__checks',
|
||||
id: 'pdk_diagnostic-page-checks',
|
||||
},
|
||||
// [
|
||||
// renderCheckSection({
|
||||
// state: 'loading',
|
||||
// title: _('DNS Checks'),
|
||||
// description: _('Checking, please wait'),
|
||||
// items: [],
|
||||
// }),
|
||||
// renderCheckSection({
|
||||
// state: 'warning',
|
||||
// title: _('DNS Checks'),
|
||||
// description: _('Some checks was failed'),
|
||||
// items: [],
|
||||
// }),
|
||||
// renderCheckSection({
|
||||
// state: 'error',
|
||||
// title: _('DNS Checks'),
|
||||
// description: _('Checks was failed'),
|
||||
// items: [],
|
||||
// }),
|
||||
// renderCheckSection({
|
||||
// state: 'success',
|
||||
// title: _('DNS Checks'),
|
||||
// description: _('Checks was passed'),
|
||||
// items: [],
|
||||
// }),
|
||||
// renderCheckSection({
|
||||
// state: 'skipped',
|
||||
// title: _('DNS Checks'),
|
||||
// description: _('Checks was skipped'),
|
||||
// items: [],
|
||||
// }),
|
||||
// ],
|
||||
),
|
||||
);
|
||||
}),
|
||||
]),
|
||||
E('div', { class: 'pdk_diagnostic-page__right-bar' }, [
|
||||
E('div', { id: 'pdk_diagnostic-page-actions' }),
|
||||
E('div', { id: 'pdk_diagnostic-page-system-info' }),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
interface IRenderDiagnosticRunActionProps {
|
||||
loading: boolean;
|
||||
click: () => void;
|
||||
}
|
||||
|
||||
export function renderDiagnosticRunAction({
|
||||
loading,
|
||||
click,
|
||||
}: IRenderDiagnosticRunActionProps) {
|
||||
return E('div', { class: 'pdk_diagnostic-page__run_check_wrapper' }, [
|
||||
E(
|
||||
'button',
|
||||
{ class: 'btn', disabled: loading ? true : undefined, click },
|
||||
loading ? _('Running... please wait') : _('Run Diagnostic'),
|
||||
),
|
||||
]);
|
||||
}
|
||||
19
fe-app-podkop/src/podkop/tabs/diagnostic/renderSystemInfo.ts
Normal file
19
fe-app-podkop/src/podkop/tabs/diagnostic/renderSystemInfo.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
interface IRenderSystemInfoProps {
|
||||
items: Array<{ key: string; value: string }>;
|
||||
}
|
||||
|
||||
export function renderSystemInfo({ items }: IRenderSystemInfoProps) {
|
||||
return E('div', { class: 'pdk_diagnostic-page__right-bar__system-info' }, [
|
||||
E(
|
||||
'b',
|
||||
{ class: 'pdk_diagnostic-page__right-bar__system-info__title' },
|
||||
'System information',
|
||||
),
|
||||
...items.map((item) =>
|
||||
E('div', { class: 'pdk_diagnostic-page__right-bar__system-info__row' }, [
|
||||
E('b', {}, item.key),
|
||||
E('div', {}, item.value),
|
||||
]),
|
||||
),
|
||||
]);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Podkop } from './podkop/types';
|
||||
import { initialDiagnosticStore } from './podkop/tabs/diagnostic/diagnostic.store';
|
||||
|
||||
function jsonStableStringify<T, V>(obj: T): string {
|
||||
return JSON.stringify(obj, (_, value) => {
|
||||
@@ -61,9 +62,17 @@ class Store<T extends Record<string, any>> {
|
||||
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
reset<K extends keyof T>(keys?: K[]): void {
|
||||
const prev = this.value;
|
||||
const next = structuredClone(this.initial);
|
||||
const next = structuredClone(this.value);
|
||||
|
||||
if (keys && keys.length > 0) {
|
||||
keys.forEach((key) => {
|
||||
next[key] = structuredClone(this.initial[key]);
|
||||
});
|
||||
} else {
|
||||
Object.assign(next, structuredClone(this.initial));
|
||||
}
|
||||
|
||||
if (jsonEqual(prev, next)) return;
|
||||
|
||||
@@ -119,6 +128,7 @@ export interface IDiagnosticsChecksItem {
|
||||
}
|
||||
|
||||
export interface IDiagnosticsChecksStoreItem {
|
||||
order: number;
|
||||
code: string;
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -157,6 +167,9 @@ export interface StoreType {
|
||||
data: Podkop.OutboundGroup[];
|
||||
latencyFetching: boolean;
|
||||
};
|
||||
diagnosticsRunAction: {
|
||||
loading: boolean;
|
||||
};
|
||||
diagnosticsChecks: Array<IDiagnosticsChecksStoreItem>;
|
||||
}
|
||||
|
||||
@@ -191,7 +204,7 @@ const initialStore: StoreType = {
|
||||
latencyFetching: false,
|
||||
data: [],
|
||||
},
|
||||
diagnosticsChecks: [],
|
||||
...initialDiagnosticStore,
|
||||
};
|
||||
|
||||
export const store = new Store<StoreType>(initialStore);
|
||||
|
||||
@@ -213,6 +213,62 @@ export const GlobalStyles = `
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
grid-column-gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__right-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__right-bar__actions {
|
||||
border: 2px var(--background-color-low, lightgray) solid;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-row-gap: 10px;
|
||||
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__right-bar__system-info {
|
||||
border: 2px var(--background-color-low, lightgray) solid;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__right-bar__system-info__title {
|
||||
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__right-bar__system-info__row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-column-gap: 5px;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__left-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__run_check_wrapper {}
|
||||
|
||||
.pdk_diagnostic-page__run_check_wrapper button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__checks {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -436,6 +436,62 @@ var GlobalStyles = `
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
grid-column-gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__right-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__right-bar__actions {
|
||||
border: 2px var(--background-color-low, lightgray) solid;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-row-gap: 10px;
|
||||
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__right-bar__system-info {
|
||||
border: 2px var(--background-color-low, lightgray) solid;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__right-bar__system-info__title {
|
||||
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__right-bar__system-info__row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-column-gap: 5px;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__left-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__run_check_wrapper {}
|
||||
|
||||
.pdk_diagnostic-page__run_check_wrapper button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdk_diagnostic-page__checks {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@@ -1354,6 +1410,105 @@ var TabService = class _TabService {
|
||||
};
|
||||
var TabServiceInstance = TabService.getInstance();
|
||||
|
||||
// src/podkop/tabs/diagnostic/checks/contstants.ts
|
||||
var DIAGNOSTICS_CHECKS_MAP = {
|
||||
["DNS" /* DNS */]: {
|
||||
order: 1,
|
||||
title: _("DNS checks"),
|
||||
code: "DNS" /* DNS */
|
||||
},
|
||||
["SINGBOX" /* SINGBOX */]: {
|
||||
order: 2,
|
||||
title: _("Sing-box checks"),
|
||||
code: "SINGBOX" /* SINGBOX */
|
||||
},
|
||||
["NFT" /* NFT */]: {
|
||||
order: 3,
|
||||
title: _("Nftables checks"),
|
||||
code: "NFT" /* NFT */
|
||||
},
|
||||
["FAKEIP" /* FAKEIP */]: {
|
||||
order: 4,
|
||||
title: _("FakeIP checks"),
|
||||
code: "FAKEIP" /* FAKEIP */
|
||||
}
|
||||
};
|
||||
|
||||
// src/podkop/tabs/diagnostic/diagnostic.store.ts
|
||||
var initialDiagnosticStore = {
|
||||
diagnosticsRunAction: { loading: false },
|
||||
diagnosticsChecks: [
|
||||
{
|
||||
code: "DNS" /* DNS */,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.DNS.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.DNS.order,
|
||||
description: _("Not running"),
|
||||
items: [],
|
||||
state: "skipped"
|
||||
},
|
||||
{
|
||||
code: "SINGBOX" /* SINGBOX */,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order,
|
||||
description: _("Not running"),
|
||||
items: [],
|
||||
state: "skipped"
|
||||
},
|
||||
{
|
||||
code: "NFT" /* NFT */,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.NFT.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.NFT.order,
|
||||
description: _("Not running"),
|
||||
items: [],
|
||||
state: "skipped"
|
||||
},
|
||||
{
|
||||
code: "FAKEIP" /* FAKEIP */,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order,
|
||||
description: _("Not running"),
|
||||
items: [],
|
||||
state: "skipped"
|
||||
}
|
||||
]
|
||||
};
|
||||
var loadingDiagnosticsChecksStore = {
|
||||
diagnosticsChecks: [
|
||||
{
|
||||
code: "DNS" /* DNS */,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.DNS.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.DNS.order,
|
||||
description: _("Queued"),
|
||||
items: [],
|
||||
state: "skipped"
|
||||
},
|
||||
{
|
||||
code: "SINGBOX" /* SINGBOX */,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order,
|
||||
description: _("Queued"),
|
||||
items: [],
|
||||
state: "skipped"
|
||||
},
|
||||
{
|
||||
code: "NFT" /* NFT */,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.NFT.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.NFT.order,
|
||||
description: _("Queued"),
|
||||
items: [],
|
||||
state: "skipped"
|
||||
},
|
||||
{
|
||||
code: "FAKEIP" /* FAKEIP */,
|
||||
title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title,
|
||||
order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order,
|
||||
description: _("Queued"),
|
||||
items: [],
|
||||
state: "skipped"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// src/store.ts
|
||||
function jsonStableStringify(obj) {
|
||||
return JSON.stringify(obj, (_2, value) => {
|
||||
@@ -1399,9 +1554,16 @@ var Store = class {
|
||||
}
|
||||
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||
}
|
||||
reset() {
|
||||
reset(keys) {
|
||||
const prev = this.value;
|
||||
const next = structuredClone(this.initial);
|
||||
const next = structuredClone(this.value);
|
||||
if (keys && keys.length > 0) {
|
||||
keys.forEach((key) => {
|
||||
next[key] = structuredClone(this.initial[key]);
|
||||
});
|
||||
} else {
|
||||
Object.assign(next, structuredClone(this.initial));
|
||||
}
|
||||
if (jsonEqual(prev, next)) return;
|
||||
this.value = next;
|
||||
this.lastHash = jsonStableStringify(next);
|
||||
@@ -1468,7 +1630,7 @@ var initialStore = {
|
||||
latencyFetching: false,
|
||||
data: []
|
||||
},
|
||||
diagnosticsChecks: []
|
||||
...initialDiagnosticStore
|
||||
};
|
||||
var store = new Store(initialStore);
|
||||
|
||||
@@ -2163,7 +2325,13 @@ async function onStoreUpdate(next, prev, diff) {
|
||||
async function initDashboardController() {
|
||||
onMount("dashboard-status").then(() => {
|
||||
store.unsubscribe(onStoreUpdate);
|
||||
store.reset();
|
||||
store.reset([
|
||||
"bandwidthWidget",
|
||||
"trafficTotalWidget",
|
||||
"systemInfoWidget",
|
||||
"servicesInfoWidget",
|
||||
"sectionsWidget"
|
||||
]);
|
||||
store.subscribe(onStoreUpdate);
|
||||
fetchDashboardSections();
|
||||
fetchServicesInfo();
|
||||
@@ -2173,49 +2341,19 @@ async function initDashboardController() {
|
||||
|
||||
// src/podkop/tabs/diagnostic/renderDiagnostic.ts
|
||||
function renderDiagnostic() {
|
||||
return E(
|
||||
"div",
|
||||
{ id: "diagnostic-status", class: "pdk_diagnostic-page" },
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
return E("div", { id: "diagnostic-status", class: "pdk_diagnostic-page" }, [
|
||||
E("div", { class: "pdk_diagnostic-page__left-bar" }, [
|
||||
E("div", { id: "pdk_diagnostic-page-run-check" }),
|
||||
E("div", {
|
||||
class: "pdk_diagnostic-page__checks",
|
||||
id: "pdk_diagnostic-page-checks"
|
||||
}
|
||||
// [
|
||||
// renderCheckSection({
|
||||
// state: 'loading',
|
||||
// title: _('DNS Checks'),
|
||||
// description: _('Checking, please wait'),
|
||||
// items: [],
|
||||
// }),
|
||||
// renderCheckSection({
|
||||
// state: 'warning',
|
||||
// title: _('DNS Checks'),
|
||||
// description: _('Some checks was failed'),
|
||||
// items: [],
|
||||
// }),
|
||||
// renderCheckSection({
|
||||
// state: 'error',
|
||||
// title: _('DNS Checks'),
|
||||
// description: _('Checks was failed'),
|
||||
// items: [],
|
||||
// }),
|
||||
// renderCheckSection({
|
||||
// state: 'success',
|
||||
// title: _('DNS Checks'),
|
||||
// description: _('Checks was passed'),
|
||||
// items: [],
|
||||
// }),
|
||||
// renderCheckSection({
|
||||
// state: 'skipped',
|
||||
// title: _('DNS Checks'),
|
||||
// description: _('Checks was skipped'),
|
||||
// items: [],
|
||||
// }),
|
||||
// ],
|
||||
)
|
||||
);
|
||||
})
|
||||
]),
|
||||
E("div", { class: "pdk_diagnostic-page__right-bar" }, [
|
||||
E("div", { id: "pdk_diagnostic-page-actions" }),
|
||||
E("div", { id: "pdk_diagnostic-page-system-info" })
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// src/icons/renderLoaderCircleIcon24.ts
|
||||
@@ -2625,10 +2763,11 @@ function updateDiagnosticsCheck(check, minified) {
|
||||
|
||||
// src/podkop/tabs/diagnostic/checks/runDnsCheck.ts
|
||||
async function runDnsCheck() {
|
||||
const code = "dns_check";
|
||||
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS;
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("DNS checks"),
|
||||
title,
|
||||
description: _("Checking dns, please wait"),
|
||||
state: "loading",
|
||||
items: []
|
||||
@@ -2636,8 +2775,9 @@ async function runDnsCheck() {
|
||||
const dnsChecks = await getDNSCheck();
|
||||
if (!dnsChecks.success) {
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("DNS checks"),
|
||||
title,
|
||||
description: _("Cannot receive DNS checks result"),
|
||||
state: "error",
|
||||
items: []
|
||||
@@ -2658,8 +2798,9 @@ async function runDnsCheck() {
|
||||
return "error";
|
||||
}
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("DNS checks"),
|
||||
title,
|
||||
description: _("DNS checks passed"),
|
||||
state: getStatus(),
|
||||
items: [
|
||||
@@ -2692,10 +2833,11 @@ async function runDnsCheck() {
|
||||
|
||||
// src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts
|
||||
async function runSingBoxCheck() {
|
||||
const code = "sing_box_check";
|
||||
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.SINGBOX;
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("Sing-box checks"),
|
||||
title,
|
||||
description: _("Checking sing-box, please wait"),
|
||||
state: "loading",
|
||||
items: []
|
||||
@@ -2703,8 +2845,9 @@ async function runSingBoxCheck() {
|
||||
const singBoxChecks = await getSingBoxCheck();
|
||||
if (!singBoxChecks.success) {
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("Sing-box checks"),
|
||||
title,
|
||||
description: _("Cannot receive Sing-box checks result"),
|
||||
state: "error",
|
||||
items: []
|
||||
@@ -2725,8 +2868,9 @@ async function runSingBoxCheck() {
|
||||
return "error";
|
||||
}
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("Sing-box checks"),
|
||||
title,
|
||||
description: _("Sing-box checks passed"),
|
||||
state: getStatus(),
|
||||
items: [
|
||||
@@ -2797,10 +2941,11 @@ async function getFakeIpCheck() {
|
||||
|
||||
// src/podkop/tabs/diagnostic/checks/runNftCheck.ts
|
||||
async function runNftCheck() {
|
||||
const code = "nft_check";
|
||||
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.NFT;
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("Nftables checks"),
|
||||
title,
|
||||
description: _("Checking nftables, please wait"),
|
||||
state: "loading",
|
||||
items: []
|
||||
@@ -2810,8 +2955,9 @@ async function runNftCheck() {
|
||||
const nftablesChecks = await getNftRulesCheck();
|
||||
if (!nftablesChecks.success) {
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("Nftables checks"),
|
||||
title,
|
||||
description: _("Cannot receive nftables checks result"),
|
||||
state: "error",
|
||||
items: []
|
||||
@@ -2832,8 +2978,9 @@ async function runNftCheck() {
|
||||
return "error";
|
||||
}
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("Nftables checks"),
|
||||
title,
|
||||
description: allGood ? _("Nftables checks passed") : _("Nftables checks partially passed"),
|
||||
state: getStatus(),
|
||||
items: [
|
||||
@@ -2886,10 +3033,11 @@ async function runNftCheck() {
|
||||
|
||||
// src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts
|
||||
async function runFakeIPCheck() {
|
||||
const code = "fake_ip_check";
|
||||
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP;
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("FakeIP checks"),
|
||||
title,
|
||||
description: _("Checking FakeIP, please wait"),
|
||||
state: "loading",
|
||||
items: []
|
||||
@@ -2930,8 +3078,9 @@ async function runFakeIPCheck() {
|
||||
}
|
||||
const { state, description } = getMeta();
|
||||
updateDiagnosticsCheck({
|
||||
order,
|
||||
code,
|
||||
title: _("FakeIP checks"),
|
||||
title,
|
||||
description,
|
||||
state,
|
||||
items: [
|
||||
@@ -2956,10 +3105,54 @@ async function runFakeIPCheck() {
|
||||
});
|
||||
}
|
||||
|
||||
// src/podkop/tabs/diagnostic/renderDiagnosticRunAction.ts
|
||||
function renderDiagnosticRunAction({
|
||||
loading,
|
||||
click
|
||||
}) {
|
||||
return E("div", { class: "pdk_diagnostic-page__run_check_wrapper" }, [
|
||||
E(
|
||||
"button",
|
||||
{ class: "btn", disabled: loading ? true : void 0, click },
|
||||
loading ? _("Running... please wait") : _("Run Diagnostic")
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
// src/podkop/tabs/diagnostic/renderAvailableActions.ts
|
||||
function renderAvailableActions() {
|
||||
return E("div", { class: "pdk_diagnostic-page__right-bar__actions" }, [
|
||||
E("b", {}, "Available actions"),
|
||||
E("button", { class: "btn" }, "Restart podkop"),
|
||||
E("button", { class: "btn" }, "Stop podkop"),
|
||||
E("button", { class: "btn" }, "Disable podkop"),
|
||||
E("button", { class: "btn" }, "Get global check"),
|
||||
E("button", { class: "btn" }, "View logs"),
|
||||
E("button", { class: "btn" }, "Show sing-box config")
|
||||
]);
|
||||
}
|
||||
|
||||
// src/podkop/tabs/diagnostic/renderSystemInfo.ts
|
||||
function renderSystemInfo({ items }) {
|
||||
return E("div", { class: "pdk_diagnostic-page__right-bar__system-info" }, [
|
||||
E(
|
||||
"b",
|
||||
{ class: "pdk_diagnostic-page__right-bar__system-info__title" },
|
||||
"System information"
|
||||
),
|
||||
...items.map(
|
||||
(item) => E("div", { class: "pdk_diagnostic-page__right-bar__system-info__row" }, [
|
||||
E("b", {}, item.key),
|
||||
E("div", {}, item.value)
|
||||
])
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
// src/podkop/tabs/diagnostic/initDiagnosticController.ts
|
||||
async function renderDiagnosticsChecks() {
|
||||
function renderDiagnosticsChecks() {
|
||||
console.log("renderDiagnosticsChecks");
|
||||
const diagnosticsChecks = store.get().diagnosticsChecks;
|
||||
const diagnosticsChecks = store.get().diagnosticsChecks.sort((a, b) => a.order - b.order);
|
||||
const container = document.getElementById("pdk_diagnostic-page-checks");
|
||||
const renderedDiagnosticsChecks = diagnosticsChecks.map(
|
||||
(check) => renderCheckSection(check)
|
||||
@@ -2968,24 +3161,90 @@ async function renderDiagnosticsChecks() {
|
||||
container.replaceChildren(...renderedDiagnosticsChecks);
|
||||
});
|
||||
}
|
||||
function renderDiagnosticRunActionWidget() {
|
||||
console.log("renderDiagnosticRunActionWidget");
|
||||
const { loading } = store.get().diagnosticsRunAction;
|
||||
const container = document.getElementById("pdk_diagnostic-page-run-check");
|
||||
const renderedAction = renderDiagnosticRunAction({
|
||||
loading,
|
||||
click: () => runChecks()
|
||||
});
|
||||
return preserveScrollForPage(() => {
|
||||
container.replaceChildren(renderedAction);
|
||||
});
|
||||
}
|
||||
function renderDiagnosticAvailableActionsWidget() {
|
||||
console.log("renderDiagnosticActionsWidget");
|
||||
const container = document.getElementById("pdk_diagnostic-page-actions");
|
||||
const renderedActions = renderAvailableActions();
|
||||
return preserveScrollForPage(() => {
|
||||
container.replaceChildren(renderedActions);
|
||||
});
|
||||
}
|
||||
function renderDiagnosticSystemInfoWidget() {
|
||||
console.log("renderDiagnosticSystemInfoWidget");
|
||||
const container = document.getElementById("pdk_diagnostic-page-system-info");
|
||||
const renderedSystemInfo = renderSystemInfo({
|
||||
items: [
|
||||
{
|
||||
key: "Podkop",
|
||||
value: "1"
|
||||
},
|
||||
{
|
||||
key: "Luci App",
|
||||
value: "1"
|
||||
},
|
||||
{
|
||||
key: "Sing-box",
|
||||
value: "1"
|
||||
},
|
||||
{
|
||||
key: "OS",
|
||||
value: "1"
|
||||
},
|
||||
{
|
||||
key: "Device",
|
||||
value: "1"
|
||||
}
|
||||
]
|
||||
});
|
||||
return preserveScrollForPage(() => {
|
||||
container.replaceChildren(renderedSystemInfo);
|
||||
});
|
||||
}
|
||||
async function onStoreUpdate2(next, prev, diff) {
|
||||
if (diff.diagnosticsChecks) {
|
||||
renderDiagnosticsChecks();
|
||||
}
|
||||
if (diff.diagnosticsRunAction) {
|
||||
renderDiagnosticRunActionWidget();
|
||||
}
|
||||
}
|
||||
async function runChecks() {
|
||||
await runDnsCheck();
|
||||
await runSingBoxCheck();
|
||||
await runNftCheck();
|
||||
await runFakeIPCheck();
|
||||
try {
|
||||
store.set({
|
||||
diagnosticsRunAction: { loading: true },
|
||||
diagnosticsChecks: loadingDiagnosticsChecksStore.diagnosticsChecks
|
||||
});
|
||||
await runDnsCheck();
|
||||
await runSingBoxCheck();
|
||||
await runNftCheck();
|
||||
await runFakeIPCheck();
|
||||
} catch (e) {
|
||||
console.log("runChecks - e", e);
|
||||
} finally {
|
||||
store.set({ diagnosticsRunAction: { loading: false } });
|
||||
}
|
||||
}
|
||||
async function initDiagnosticController() {
|
||||
onMount("diagnostic-status").then(() => {
|
||||
console.log("diagnostic controller initialized.");
|
||||
store.unsubscribe(onStoreUpdate2);
|
||||
store.reset();
|
||||
store.subscribe(onStoreUpdate2);
|
||||
runChecks();
|
||||
renderDiagnosticsChecks();
|
||||
renderDiagnosticRunActionWidget();
|
||||
renderDiagnosticAvailableActionsWidget();
|
||||
renderDiagnosticSystemInfoWidget();
|
||||
});
|
||||
}
|
||||
return baseclass.extend({
|
||||
|
||||
Reference in New Issue
Block a user