feat(proxies): add proxy latency test history timeline

This commit is contained in:
kunish 2024-10-12 22:20:51 +08:00
parent 8252d04985
commit 1e7dcb13bc
No known key found for this signature in database
GPG Key ID: 67D3ACD788F3A7CD
8 changed files with 253 additions and 82 deletions

View File

@ -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",

View File

@ -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

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -68,7 +68,7 @@ export const ProxyPreviewBar = (props: {
/>
</div>
<Latency name={props.now} />
<Latency proxyName={props.now} />
</div>
)
}

View File

@ -65,7 +65,7 @@ export const ProxyPreviewDots = (props: {
</For>
</div>
<Latency name={props.now} />
<Latency proxyName={props.now} />
</div>
)
}

View File

@ -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 = [

View File

@ -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
)
}