summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorOlivi <[email protected]>2025-12-13 11:40:54 +0000
committerOlivi <[email protected]>2025-12-13 11:40:54 +0000
commit4ac43e692cea4682179612e7fecb4c159e96c039 (patch)
tree504322c2fbc1893ff8cab233075d1d629dd54962 /src/components
parent3959e45747aabe9ec3dea011f66851d8c219a5fb (diff)
feat: add healthcheck functionality for specified proxy
Diffstat (limited to 'src/components')
-rw-r--r--src/components/proxies/Proxy.tsx44
-rw-r--r--src/components/proxies/ProxyLatency.module.scss48
-rw-r--r--src/components/proxies/ProxyLatency.tsx51
3 files changed, 125 insertions, 18 deletions
diff --git a/src/components/proxies/Proxy.tsx b/src/components/proxies/Proxy.tsx
index 833d94b..1ba966e 100644
--- a/src/components/proxies/Proxy.tsx
+++ b/src/components/proxies/Proxy.tsx
@@ -3,10 +3,11 @@ import cx from 'clsx';
import * as React from 'react';
import { keyCodes } from '~/misc/keycode';
-import { getLatencyTestUrl } from '~/store/app';
-import { ProxyItem } from '~/store/types';
+import { getClashAPIConfig, getLatencyTestUrl } from '~/store/app';
+import { DispatchFn, ProxyItem } from '~/store/types';
+import { ClashAPIConfig } from '~/types';
-import { getDelay, getProxies } from '../../store/proxies';
+import { getDelay, getProxies, healthcheckProxy } from '../../store/proxies';
import { connect } from '../StateProvider';
import s0 from './Proxy.module.scss';
import { ProxyLatency } from './ProxyLatency';
@@ -65,12 +66,12 @@ type ProxyProps = {
name: string;
now?: boolean;
proxy: ProxyItem;
- latency: any;
+ latency: { number?: number; error?: string; testing?: boolean };
httpsLatencyTest: boolean;
isSelectable?: boolean;
- udp: boolean;
- tfo: boolean;
onClick?: (proxyName: string) => unknown;
+ apiConfig: ClashAPIConfig;
+ dispatch: DispatchFn;
};
function ProxySmallImpl({
@@ -86,7 +87,7 @@ function ProxySmallImpl({
const latencyNumber = latency?.number ?? delay;
const color = useMemo(
() => getProxyDotBackgroundColor({ number: latencyNumber }, httpsLatencyTest),
- [latencyNumber]
+ [latencyNumber, httpsLatencyTest]
);
const title = useMemo(() => {
@@ -161,13 +162,22 @@ function ProxyImpl({
httpsLatencyTest,
isSelectable,
onClick,
+ apiConfig,
+ dispatch,
}: ProxyProps) {
const delay = proxy.history[proxy.history.length - 1]?.delay;
- const latencyNumber = latency?.number ?? delay;
+ const latencyNumber =
+ typeof latency?.number === 'number'
+ ? latency.number
+ : typeof delay === 'number'
+ ? delay
+ : undefined;
+ const hasLatencyNumber = typeof latencyNumber === 'number' && latencyNumber > 0;
const color = useMemo(
- () => getLabelColor({ number: latencyNumber }, httpsLatencyTest),
- [latencyNumber]
+ () => getLabelColor({ number: hasLatencyNumber ? latencyNumber : undefined }, httpsLatencyTest),
+ [hasLatencyNumber, latencyNumber, httpsLatencyTest]
);
+ const isTestingLatency = Boolean(latency?.testing);
const doSelect = React.useCallback(() => {
isSelectable && onClick && onClick(name);
@@ -210,7 +220,10 @@ function ProxyImpl({
});
}, [isSelectable, now, latency]);
- // const latencyNumber = latency?.number ?? proxy.history[proxy.history.length - 1]?.delay;
+ const runLatencyTest = React.useCallback(() => {
+ if (isTestingLatency) return;
+ dispatch(healthcheckProxy(apiConfig, name));
+ }, [apiConfig, dispatch, isTestingLatency, name]);
return (
<div
@@ -241,7 +254,13 @@ function ProxyImpl({
{formatTfo(proxy.tfo)}
</div>
- {latencyNumber ? <ProxyLatency number={latencyNumber} color={color} /> : null}
+ <ProxyLatency
+ number={hasLatencyNumber ? latencyNumber : undefined}
+ color={color}
+ isTesting={isTestingLatency}
+ error={latency?.error}
+ onClick={runLatencyTest}
+ />
</div>
</div>
);
@@ -256,6 +275,7 @@ const mapState = (s: any, { name }) => {
proxy: proxy,
latency: delay[name],
httpsLatencyTest: latencyTestUrl.startsWith('https://'),
+ apiConfig: getClashAPIConfig(s),
};
};
diff --git a/src/components/proxies/ProxyLatency.module.scss b/src/components/proxies/ProxyLatency.module.scss
index 37502a8..f1731f6 100644
--- a/src/components/proxies/ProxyLatency.module.scss
+++ b/src/components/proxies/ProxyLatency.module.scss
@@ -1,10 +1,54 @@
@import '~/styles/utils/custom-media';
.proxyLatency {
- border-radius: 20px;
- color: #eee;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 50px;
+ padding: 4px 10px;
+ gap: 4px;
+ border-radius: 9999px;
+ border: 1px solid var(--color-proxy-border);
+ background: var(--bg-near-transparent);
+ color: var(--color-text);
font-size: 0.75em;
+ transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease,
+ color 0.15s ease, transform 0.15s ease;
+ user-select: none;
+ outline: none;
@media (--breakpoint-not-small) {
+ padding: 5px 12px;
font-size: 0.8em;
}
}
+
+.clickable {
+ cursor: pointer;
+}
+
+.clickable:hover,
+.clickable:focus-visible {
+ background: var(--color-bg-proxy);
+ border-color: var(--card-hover-border-lightness);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
+}
+
+.placeholder {
+ color: var(--color-text-secondary);
+}
+
+.testing {
+ animation: proxyLatencyPulse 1s ease-in-out infinite;
+}
+
+@keyframes proxyLatencyPulse {
+ 0% {
+ opacity: 0.8;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0.8;
+ }
+}
diff --git a/src/components/proxies/ProxyLatency.tsx b/src/components/proxies/ProxyLatency.tsx
index 48e55af..3f91eac 100644
--- a/src/components/proxies/ProxyLatency.tsx
+++ b/src/components/proxies/ProxyLatency.tsx
@@ -1,16 +1,59 @@
+import cx from 'clsx';
import * as React from 'react';
import s0 from './ProxyLatency.module.scss';
type ProxyLatencyProps = {
- number: number;
+ number?: number;
color: string;
+ isTesting?: boolean;
+ error?: string;
+ onClick?: () => void;
};
-export function ProxyLatency({ number, color }: ProxyLatencyProps) {
+export function ProxyLatency({ number, color, isTesting, error, onClick }: ProxyLatencyProps) {
+ const hasNumber = typeof number === 'number';
+ const label = isTesting ? 'Testing...' : hasNumber ? `${number} ms` : error || '--';
+
+ const className = cx(s0.proxyLatency, {
+ [s0.clickable]: Boolean(onClick),
+ [s0.placeholder]: !hasNumber || Boolean(error),
+ [s0.testing]: isTesting,
+ });
+
+ const handleClick = React.useCallback(
+ (e: React.MouseEvent) => {
+ if (!onClick || isTesting) return;
+ e.preventDefault();
+ e.stopPropagation();
+ onClick();
+ },
+ [isTesting, onClick]
+ );
+
+ const handleKeyDown = React.useCallback(
+ (e: React.KeyboardEvent) => {
+ if (!onClick || isTesting) return;
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ e.stopPropagation();
+ onClick();
+ }
+ },
+ [isTesting, onClick]
+ );
+
return (
- <span className={s0.proxyLatency} style={{ color }}>
- <span>{number} ms</span>
+ <span
+ className={className}
+ style={{ color: hasNumber ? color : undefined }}
+ role={onClick ? 'button' : undefined}
+ tabIndex={onClick ? 0 : undefined}
+ onClick={handleClick}
+ onKeyDown={handleKeyDown}
+ title={label}
+ >
+ <span>{label}</span>
</span>
);
}