mirror of
https://github.com/MetaCubeX/metacubexd.git
synced 2024-12-27 19:44:12 +08:00
feat(proxies): add proxy latency test history timeline
This commit is contained in:
parent
8252d04985
commit
1e7dcb13bc
@ -15,6 +15,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@commitlint/config-conventional": "^19.5.0",
|
||||
"@corvu/tooltip": "^0.2.1",
|
||||
"@eslint/js": "^9.12.0",
|
||||
"@felte/solid": "^1.2.13",
|
||||
"@felte/validator-zod": "^1.0.17",
|
||||
|
87
pnpm-lock.yaml
generated
87
pnpm-lock.yaml
generated
@ -10,6 +10,9 @@ importers:
|
||||
'@commitlint/config-conventional':
|
||||
specifier: ^19.5.0
|
||||
version: 19.5.0
|
||||
'@corvu/tooltip':
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(solid-js@1.9.2)
|
||||
'@eslint/js':
|
||||
specifier: ^9.12.0
|
||||
version: 9.12.0
|
||||
@ -1257,6 +1260,22 @@ packages:
|
||||
}
|
||||
engines: { node: '>=v18' }
|
||||
|
||||
'@corvu/tooltip@0.2.1':
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-y2CQ2/6DH/gJJZPo1fV3O7l4Jfgu5ZW58bpqPmKS+l8Pa6gIKV6zkrMoyBg8Hsn6z9RmdZFqkGFs/9C5fvwKpg==,
|
||||
}
|
||||
peerDependencies:
|
||||
solid-js: ^1.8
|
||||
|
||||
'@corvu/utils@0.4.2':
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA==,
|
||||
}
|
||||
peerDependencies:
|
||||
solid-js: ^1.8
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
resolution:
|
||||
{
|
||||
@ -1554,6 +1573,24 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.2.0
|
||||
|
||||
'@floating-ui/core@1.6.8':
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==,
|
||||
}
|
||||
|
||||
'@floating-ui/dom@1.6.11':
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==,
|
||||
}
|
||||
|
||||
'@floating-ui/utils@0.2.8':
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==,
|
||||
}
|
||||
|
||||
'@fontsource/fira-sans@5.1.0':
|
||||
resolution:
|
||||
{
|
||||
@ -5331,12 +5368,28 @@ packages:
|
||||
apexcharts: ^3.40.0
|
||||
solid-js: ^1.6.0
|
||||
|
||||
solid-dismissible@0.1.1:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-9kcKBJIMdS+586cA1g63HYWxKh3h89leeNHbPZ1csYjuni+NvPBtNr11l0iEX2AKKEt6FHk6qNhc/gjoYAW1pA==,
|
||||
}
|
||||
peerDependencies:
|
||||
solid-js: ^1.8
|
||||
|
||||
solid-js@1.9.2:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-fe/K03nV+kMFJYhAOE8AIQHcGxB4rMIEoEyrulbtmf217NffbbwBqJnJI4ovt16e+kaIt0czE2WA7mP/pYN9yg==,
|
||||
}
|
||||
|
||||
solid-presence@0.1.8:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA==,
|
||||
}
|
||||
peerDependencies:
|
||||
solid-js: ^1.8
|
||||
|
||||
solid-refresh@0.6.3:
|
||||
resolution:
|
||||
{
|
||||
@ -7228,6 +7281,19 @@ snapshots:
|
||||
'@types/conventional-commits-parser': 5.0.0
|
||||
chalk: 5.3.0
|
||||
|
||||
'@corvu/tooltip@0.2.1(solid-js@1.9.2)':
|
||||
dependencies:
|
||||
'@corvu/utils': 0.4.2(solid-js@1.9.2)
|
||||
'@floating-ui/dom': 1.6.11
|
||||
solid-dismissible: 0.1.1(solid-js@1.9.2)
|
||||
solid-js: 1.9.2
|
||||
solid-presence: 0.1.8(solid-js@1.9.2)
|
||||
|
||||
'@corvu/utils@0.4.2(solid-js@1.9.2)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.11
|
||||
solid-js: 1.9.2
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
optional: true
|
||||
|
||||
@ -7352,6 +7418,17 @@ snapshots:
|
||||
'@felte/common': 1.1.8
|
||||
zod: 3.23.8
|
||||
|
||||
'@floating-ui/core@1.6.8':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.8
|
||||
|
||||
'@floating-ui/dom@1.6.11':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.6.8
|
||||
'@floating-ui/utils': 0.2.8
|
||||
|
||||
'@floating-ui/utils@0.2.8': {}
|
||||
|
||||
'@fontsource/fira-sans@5.1.0': {}
|
||||
|
||||
'@humanfs/core@0.19.0': {}
|
||||
@ -9495,12 +9572,22 @@ snapshots:
|
||||
defu: 6.1.4
|
||||
solid-js: 1.9.2
|
||||
|
||||
solid-dismissible@0.1.1(solid-js@1.9.2):
|
||||
dependencies:
|
||||
'@corvu/utils': 0.4.2(solid-js@1.9.2)
|
||||
solid-js: 1.9.2
|
||||
|
||||
solid-js@1.9.2:
|
||||
dependencies:
|
||||
csstype: 3.1.3
|
||||
seroval: 1.1.0
|
||||
seroval-plugins: 1.1.0(seroval@1.1.0)
|
||||
|
||||
solid-presence@0.1.8(solid-js@1.9.2):
|
||||
dependencies:
|
||||
'@corvu/utils': 0.4.2(solid-js@1.9.2)
|
||||
solid-js: 1.9.2
|
||||
|
||||
solid-refresh@0.6.3(solid-js@1.9.2):
|
||||
dependencies:
|
||||
'@babel/generator': 7.24.7
|
||||
|
@ -1,36 +1,28 @@
|
||||
import { LATENCY_QUALITY_MAP_HTTP } from '~/constants'
|
||||
import { useI18n } from '~/i18n'
|
||||
import { latencyQualityMap, useProxies } from '~/signals'
|
||||
import { JSX, ParentComponent } from 'solid-js'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { getLatencyClassName } from '~/helpers'
|
||||
import { useProxies } from '~/signals'
|
||||
|
||||
export const Latency = (props: { name?: string; class?: string }) => {
|
||||
const [t] = useI18n()
|
||||
interface Props extends JSX.HTMLAttributes<HTMLSpanElement> {
|
||||
proxyName: string
|
||||
}
|
||||
|
||||
export const Latency: ParentComponent<Props> = (props) => {
|
||||
const [local, others] = splitProps(props, ['class'])
|
||||
const { getLatencyByName } = useProxies()
|
||||
const [textClassName, setTextClassName] = createSignal('')
|
||||
const latency = createMemo(() => getLatencyByName(props.name || ''))
|
||||
const latency = createMemo(() => getLatencyByName(others.proxyName || ''))
|
||||
|
||||
createEffect(() => {
|
||||
if (latency() > latencyQualityMap().HIGH) {
|
||||
setTextClassName('text-error')
|
||||
} else if (latency() > latencyQualityMap().MEDIUM) {
|
||||
setTextClassName('text-warning')
|
||||
} else {
|
||||
setTextClassName('text-success')
|
||||
}
|
||||
setTextClassName(getLatencyClassName(latency()))
|
||||
})
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={
|
||||
typeof latency() === 'number' &&
|
||||
latency() !== LATENCY_QUALITY_MAP_HTTP.NOT_CONNECTED
|
||||
}
|
||||
<span
|
||||
class={twMerge('badge whitespace-nowrap', textClassName(), local.class)}
|
||||
{...others}
|
||||
>
|
||||
<span
|
||||
class={`whitespace-nowrap text-xs ${textClassName()} ${props.class}`}
|
||||
>
|
||||
{latency()}
|
||||
{t('ms')}
|
||||
</span>
|
||||
</Show>
|
||||
{latency() || '-'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
@ -1,8 +1,14 @@
|
||||
import { IconBrandSpeedtest } from '@tabler/icons-solidjs'
|
||||
import Tooltip from '@corvu/tooltip'
|
||||
import { IconCircleCheckFilled } from '@tabler/icons-solidjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { Button, Latency } from '~/components'
|
||||
import { filterSpecialProxyType, formatProxyType } from '~/helpers'
|
||||
import { useProxies } from '~/signals'
|
||||
import { Latency } from '~/components'
|
||||
import {
|
||||
filterSpecialProxyType,
|
||||
formatProxyType,
|
||||
getLatencyClassName,
|
||||
} from '~/helpers'
|
||||
import { curTheme, useProxies } from '~/signals'
|
||||
|
||||
export const ProxyNodeCard = (props: {
|
||||
proxyName: string
|
||||
@ -32,56 +38,114 @@ export const ProxyNodeCard = (props: {
|
||||
: null
|
||||
|
||||
return (
|
||||
<div
|
||||
class={twMerge(
|
||||
'card tooltip card-compact tooltip-accent relative bg-neutral text-neutral-content',
|
||||
isSelected && 'bg-primary text-primary-content',
|
||||
onClick && 'cursor-pointer',
|
||||
)}
|
||||
data-tip={proxyName}
|
||||
onClick={onClick}
|
||||
<Tooltip
|
||||
placement="top"
|
||||
floatingOptions={{
|
||||
autoPlacement: true,
|
||||
shift: true,
|
||||
offset: 10,
|
||||
}}
|
||||
>
|
||||
<div class="badge badge-secondary badge-sm absolute bottom-0 left-1/2 -translate-x-1/2 font-bold uppercase">
|
||||
{formatProxyType(proxyNode()?.type)}
|
||||
</div>
|
||||
<Tooltip.Anchor
|
||||
class={twMerge(
|
||||
'card bg-neutral text-neutral-content',
|
||||
isSelected && 'bg-primary text-primary-content',
|
||||
onClick && 'cursor-pointer',
|
||||
)}
|
||||
title={proxyName}
|
||||
>
|
||||
<Tooltip.Trigger>
|
||||
<div class="card-body p-2.5" onClick={onClick}>
|
||||
<h2 class="card-title line-clamp-1 text-start text-sm">
|
||||
{proxyName}
|
||||
</h2>
|
||||
|
||||
<div class="card-body">
|
||||
<h2 class="card-title line-clamp-1 text-start text-sm">{proxyName}</h2>
|
||||
<span
|
||||
class={twMerge(
|
||||
'text-start text-xs',
|
||||
isSelected ? 'text-info-content' : 'text-neutral-content',
|
||||
)}
|
||||
>
|
||||
{[
|
||||
specialType(),
|
||||
supportIPv6() && 'IPv6',
|
||||
proxyNode().tfo && 'TFO',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' / ')}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class={twMerge(
|
||||
'text-start text-xs',
|
||||
isSelected ? 'text-info-content' : 'text-neutral-content',
|
||||
)}
|
||||
>
|
||||
{[specialType(), supportIPv6() && 'IPv6'].filter(Boolean).join(' / ')}
|
||||
</span>
|
||||
<div class="card-actions items-center justify-between">
|
||||
<div class="badge badge-secondary badge-sm font-bold uppercase">
|
||||
{formatProxyType(proxyNode()?.type)}
|
||||
</div>
|
||||
|
||||
<div class="card-actions items-center justify-end">
|
||||
<Latency
|
||||
name={props.proxyName}
|
||||
class={twMerge(isSelected && 'badge')}
|
||||
/>
|
||||
|
||||
<Button
|
||||
class="btn-square btn-sm"
|
||||
icon={
|
||||
<IconBrandSpeedtest
|
||||
<Latency
|
||||
proxyName={props.proxyName}
|
||||
class={twMerge(
|
||||
'size-6',
|
||||
proxyLatencyTestingMap()[proxyName] &&
|
||||
'animate-pulse text-success',
|
||||
proxyLatencyTestingMap()[proxyName] && 'animate-pulse',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
void proxyLatencyTest(proxyName, proxyNode().provider)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
void proxyLatencyTest(proxyName, proxyNode().provider)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content data-theme={curTheme()} class="z-50 bg-transparent">
|
||||
<Tooltip.Arrow class="text-neutral" />
|
||||
|
||||
<div class="flex flex-col items-center gap-2 rounded-box bg-neutral p-2.5 text-neutral-content">
|
||||
<h2 class="text-lg font-bold">{proxyName}</h2>
|
||||
|
||||
<ul class="timeline timeline-vertical timeline-compact timeline-snap-icon">
|
||||
<For each={proxyNode().latencyTestHistory}>
|
||||
{(latencyTestResult, index) => (
|
||||
<li>
|
||||
<Show when={index() > 0}>
|
||||
<hr />
|
||||
</Show>
|
||||
|
||||
<div class="timeline-start space-y-2">
|
||||
<time class="text-sm italic">
|
||||
{dayjs(latencyTestResult.time).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)}
|
||||
</time>
|
||||
|
||||
<div
|
||||
class={twMerge(
|
||||
'badge block',
|
||||
getLatencyClassName(latencyTestResult.delay),
|
||||
)}
|
||||
>
|
||||
{latencyTestResult.delay || '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-middle">
|
||||
<IconCircleCheckFilled class="size-4" />
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={
|
||||
index() !== proxyNode().latencyTestHistory.length - 1
|
||||
}
|
||||
>
|
||||
<hr />
|
||||
</Show>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Anchor>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ export const ProxyPreviewBar = (props: {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Latency name={props.now} />
|
||||
<Latency proxyName={props.now} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ export const ProxyPreviewDots = (props: {
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Latency name={props.now} />
|
||||
<Latency proxyName={props.now} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PROXIES_ORDERING_TYPE } from '~/constants'
|
||||
import { LATENCY_QUALITY_MAP_HTTP, PROXIES_ORDERING_TYPE } from '~/constants'
|
||||
import { latencyQualityMap, useProxies } from '~/signals'
|
||||
|
||||
export const formatProxyType = (type = '') => {
|
||||
@ -19,6 +19,18 @@ export const formatProxyType = (type = '') => {
|
||||
return t
|
||||
}
|
||||
|
||||
export const getLatencyClassName = (latency: LATENCY_QUALITY_MAP_HTTP) => {
|
||||
if (latency > latencyQualityMap().HIGH) {
|
||||
return 'text-error'
|
||||
} else if (latency > latencyQualityMap().MEDIUM) {
|
||||
return 'text-warning'
|
||||
} else if (latency === LATENCY_QUALITY_MAP_HTTP.NOT_CONNECTED) {
|
||||
return 'text-neutral-content'
|
||||
} else {
|
||||
return 'text-success'
|
||||
}
|
||||
}
|
||||
|
||||
export const filterSpecialProxyType = (type = '') => {
|
||||
const t = type.toLowerCase()
|
||||
const conditions = [
|
||||
|
@ -24,7 +24,12 @@ import type { Proxy, ProxyNode, ProxyProvider } from '~/types'
|
||||
type ProxyInfo = {
|
||||
name: string
|
||||
udp: boolean
|
||||
now: string
|
||||
tfo: boolean
|
||||
latencyTestHistory: {
|
||||
time: string
|
||||
delay: number
|
||||
}[]
|
||||
latency: string
|
||||
xudp: boolean
|
||||
type: string
|
||||
provider: string
|
||||
@ -99,9 +104,19 @@ const setProxiesInfo = (
|
||||
const newProxyIPv6SupportMap = { ...proxyIPv6SupportMap() }
|
||||
|
||||
proxies.forEach((proxy) => {
|
||||
const { udp, xudp, type, now, name, provider = '' } = proxy
|
||||
const { udp, xudp, type, now, history, name, tfo, provider = '' } = proxy
|
||||
|
||||
newProxyNodeMap[proxy.name] = {
|
||||
udp,
|
||||
xudp,
|
||||
type,
|
||||
latency: now,
|
||||
latencyTestHistory: history,
|
||||
name,
|
||||
tfo,
|
||||
provider,
|
||||
}
|
||||
|
||||
newProxyNodeMap[proxy.name] = { udp, xudp, type, now, name, provider }
|
||||
newLatencyMap[proxy.name] = getLatencyFromProxy(proxy, urlForLatencyTest())
|
||||
|
||||
// we don't set it when false because sing-box didn't have "extra" so it will always be false
|
||||
@ -326,8 +341,8 @@ export const useProxies = () => {
|
||||
return name
|
||||
}
|
||||
|
||||
while (node.now && node.now !== node.name) {
|
||||
const nextNode = proxyNodeMap()[node.now]
|
||||
while (node.latency && node.latency !== node.name) {
|
||||
const nextNode = proxyNodeMap()[node.latency]
|
||||
|
||||
if (!nextNode) {
|
||||
return node.name
|
||||
@ -353,7 +368,7 @@ export const useProxies = () => {
|
||||
return (
|
||||
['direct', 'reject', 'loadbalance'].includes(
|
||||
proxyNode.type.toLowerCase(),
|
||||
) || !!proxyNode.now
|
||||
) || !!proxyNode.latency
|
||||
)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user