mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 00:21:56 +08:00
新增当前会话请求数统计,优化请求窗口逻辑,更新仪表板以展示缓存管理和智能缓存统计,添加工具提示功能以增强用户体验。
This commit is contained in:
parent
19c25b8aca
commit
775814eb24
@ -25,8 +25,9 @@ type Metrics struct {
|
||||
MemoryUsage string `json:"memory_usage"`
|
||||
|
||||
// 性能指标
|
||||
AverageResponseTime string `json:"avg_response_time"`
|
||||
RequestsPerSecond float64 `json:"requests_per_second"`
|
||||
AverageResponseTime string `json:"avg_response_time"`
|
||||
RequestsPerSecond float64 `json:"requests_per_second"`
|
||||
CurrentSessionRequests int64 `json:"current_session_requests"`
|
||||
|
||||
// 传输指标
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
@ -61,20 +62,21 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if stats == nil {
|
||||
stats = map[string]interface{}{
|
||||
"uptime": metrics.FormatUptime(uptime),
|
||||
"active_requests": int64(0),
|
||||
"total_requests": int64(0),
|
||||
"total_errors": int64(0),
|
||||
"error_rate": float64(0),
|
||||
"num_goroutine": runtime.NumGoroutine(),
|
||||
"memory_usage": "0 B",
|
||||
"avg_response_time": "0 ms",
|
||||
"total_bytes": int64(0),
|
||||
"bytes_per_second": float64(0),
|
||||
"requests_per_second": float64(0),
|
||||
"status_code_stats": make(map[string]int64),
|
||||
"recent_requests": make([]models.RequestLog, 0),
|
||||
"top_referers": make([]models.PathMetrics, 0),
|
||||
"uptime": metrics.FormatUptime(uptime),
|
||||
"active_requests": int64(0),
|
||||
"total_requests": int64(0),
|
||||
"total_errors": int64(0),
|
||||
"error_rate": float64(0),
|
||||
"num_goroutine": runtime.NumGoroutine(),
|
||||
"memory_usage": "0 B",
|
||||
"avg_response_time": "0 ms",
|
||||
"total_bytes": int64(0),
|
||||
"bytes_per_second": float64(0),
|
||||
"requests_per_second": float64(0),
|
||||
"current_session_requests": int64(0),
|
||||
"status_code_stats": make(map[string]int64),
|
||||
"recent_requests": make([]models.RequestLog, 0),
|
||||
"top_referers": make([]models.PathMetrics, 0),
|
||||
"latency_stats": map[string]interface{}{
|
||||
"min": "0ms",
|
||||
"max": "0ms",
|
||||
@ -108,22 +110,23 @@ func (h *ProxyHandler) MetricsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
statusCodeStats := models.SafeStatusCodeStats(stats["status_code_stats"])
|
||||
|
||||
metrics := Metrics{
|
||||
Uptime: metrics.FormatUptime(uptime),
|
||||
ActiveRequests: utils.SafeInt64(stats["active_requests"]),
|
||||
TotalRequests: totalRequests,
|
||||
TotalErrors: totalErrors,
|
||||
ErrorRate: float64(totalErrors) / float64(utils.Max(totalRequests, 1)),
|
||||
NumGoroutine: utils.SafeInt(stats["num_goroutine"]),
|
||||
MemoryUsage: utils.SafeString(stats["memory_usage"], "0 B"),
|
||||
AverageResponseTime: utils.SafeString(stats["avg_response_time"], "0 ms"),
|
||||
TotalBytes: totalBytes,
|
||||
BytesPerSecond: float64(totalBytes) / utils.MaxFloat64(uptimeSeconds, 1),
|
||||
RequestsPerSecond: float64(totalRequests) / utils.MaxFloat64(uptimeSeconds, 1),
|
||||
StatusCodeStats: statusCodeStats,
|
||||
RecentRequests: models.SafeRequestLogs(stats["recent_requests"]),
|
||||
TopReferers: models.SafePathMetrics(stats["top_referers"]),
|
||||
BandwidthHistory: bandwidthHistory,
|
||||
CurrentBandwidth: utils.SafeString(stats["current_bandwidth"], "0 B/s"),
|
||||
Uptime: metrics.FormatUptime(uptime),
|
||||
ActiveRequests: utils.SafeInt64(stats["active_requests"]),
|
||||
TotalRequests: totalRequests,
|
||||
TotalErrors: totalErrors,
|
||||
ErrorRate: float64(totalErrors) / float64(utils.Max(totalRequests, 1)),
|
||||
NumGoroutine: utils.SafeInt(stats["num_goroutine"]),
|
||||
MemoryUsage: utils.SafeString(stats["memory_usage"], "0 B"),
|
||||
AverageResponseTime: utils.SafeString(stats["avg_response_time"], "0 ms"),
|
||||
TotalBytes: totalBytes,
|
||||
BytesPerSecond: float64(totalBytes) / utils.MaxFloat64(uptimeSeconds, 1),
|
||||
RequestsPerSecond: utils.SafeFloat64(stats["requests_per_second"]),
|
||||
CurrentSessionRequests: utils.SafeInt64(stats["current_session_requests"]),
|
||||
StatusCodeStats: statusCodeStats,
|
||||
RecentRequests: models.SafeRequestLogs(stats["recent_requests"]),
|
||||
TopReferers: models.SafePathMetrics(stats["top_referers"]),
|
||||
BandwidthHistory: bandwidthHistory,
|
||||
CurrentBandwidth: utils.SafeString(stats["current_bandwidth"], "0 B/s"),
|
||||
}
|
||||
|
||||
// 填充延迟统计数据
|
||||
|
@ -583,44 +583,39 @@ func (c *Collector) updateRequestsWindow(count int64) {
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 计算当前应该在哪个桶
|
||||
// 如果是第一次调用,初始化时间
|
||||
if c.requestsWindow.lastUpdate.IsZero() {
|
||||
c.requestsWindow.lastUpdate = now
|
||||
}
|
||||
|
||||
// 计算当前时间桶的索引
|
||||
timeSinceLastUpdate := now.Sub(c.requestsWindow.lastUpdate)
|
||||
|
||||
// 如果时间跨度超过桶大小,需要移动到新桶
|
||||
if timeSinceLastUpdate >= c.requestsWindow.bucketSize {
|
||||
// 保存当前桶的数据
|
||||
if len(c.requestsWindow.buckets) > 0 {
|
||||
// 向前移动桶(最新的在前面)
|
||||
bucketsToMove := int(timeSinceLastUpdate / c.requestsWindow.bucketSize)
|
||||
if bucketsToMove >= len(c.requestsWindow.buckets) {
|
||||
// 如果移动的桶数超过总桶数,清空所有桶
|
||||
for i := range c.requestsWindow.buckets {
|
||||
c.requestsWindow.buckets[i] = 0
|
||||
}
|
||||
} else {
|
||||
// 移动桶数据
|
||||
copy(c.requestsWindow.buckets[bucketsToMove:], c.requestsWindow.buckets[:len(c.requestsWindow.buckets)-bucketsToMove])
|
||||
// 清空前面的桶
|
||||
for i := 0; i < bucketsToMove; i++ {
|
||||
c.requestsWindow.buckets[i] = 0
|
||||
}
|
||||
bucketsToMove := int(timeSinceLastUpdate / c.requestsWindow.bucketSize)
|
||||
|
||||
if bucketsToMove >= len(c.requestsWindow.buckets) {
|
||||
// 如果移动的桶数超过总桶数,清空所有桶
|
||||
for i := range c.requestsWindow.buckets {
|
||||
c.requestsWindow.buckets[i] = 0
|
||||
}
|
||||
} else {
|
||||
// 向右移动桶数据(新数据在索引0)
|
||||
copy(c.requestsWindow.buckets[bucketsToMove:], c.requestsWindow.buckets[:len(c.requestsWindow.buckets)-bucketsToMove])
|
||||
// 清空前面的桶
|
||||
for i := 0; i < bucketsToMove; i++ {
|
||||
c.requestsWindow.buckets[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前桶为新请求数
|
||||
c.requestsWindow.current = count
|
||||
c.requestsWindow.lastUpdate = now
|
||||
// 更新时间为当前桶的开始时间
|
||||
c.requestsWindow.lastUpdate = now.Truncate(c.requestsWindow.bucketSize)
|
||||
}
|
||||
|
||||
// 将当前请求数加到第一个桶
|
||||
if len(c.requestsWindow.buckets) > 0 {
|
||||
c.requestsWindow.buckets[0] = count
|
||||
}
|
||||
} else {
|
||||
// 在同一个桶内,累加请求数
|
||||
c.requestsWindow.current += count
|
||||
if len(c.requestsWindow.buckets) > 0 {
|
||||
c.requestsWindow.buckets[0] += count
|
||||
}
|
||||
// 将请求数加到第一个桶(当前时间桶)
|
||||
if len(c.requestsWindow.buckets) > 0 {
|
||||
c.requestsWindow.buckets[0] += count
|
||||
}
|
||||
}
|
||||
|
||||
@ -640,7 +635,7 @@ func (c *Collector) getRecentRequestsPerSecond() float64 {
|
||||
actualWindow := c.requestsWindow.window
|
||||
|
||||
// 如果程序运行时间不足5分钟,使用实际运行时间
|
||||
if runTime := now.Sub(c.requestsWindow.lastUpdate); runTime < c.requestsWindow.window {
|
||||
if runTime := now.Sub(c.startTime); runTime < c.requestsWindow.window {
|
||||
actualWindow = runTime
|
||||
}
|
||||
|
||||
|
@ -459,6 +459,26 @@ func SafeString(v interface{}, defaultValue string) string {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func SafeFloat64(v interface{}) float64 {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return val
|
||||
case float32:
|
||||
return float64(val)
|
||||
case int64:
|
||||
return float64(val)
|
||||
case int:
|
||||
return float64(val)
|
||||
case int32:
|
||||
return float64(val)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Max 返回两个 int64 中的较大值
|
||||
func Max(a, b int64) int64 {
|
||||
if a > b {
|
||||
|
533
web/app/dashboard/cache/page.tsx
vendored
533
web/app/dashboard/cache/page.tsx
vendored
@ -7,7 +7,24 @@ import { useToast } from "@/components/ui/use-toast"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { useRouter } from "next/navigation"
|
||||
import {
|
||||
HardDrive,
|
||||
Database,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Activity,
|
||||
Image as ImageIcon,
|
||||
FileText,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
Settings,
|
||||
Info,
|
||||
Zap,
|
||||
Target,
|
||||
RotateCcw
|
||||
} from "lucide-react"
|
||||
|
||||
interface CacheStats {
|
||||
total_items: number
|
||||
@ -259,15 +276,19 @@ export default function CachePage() {
|
||||
|
||||
const config = configs[type]
|
||||
return (
|
||||
<div className="space-y-4 mt-4">
|
||||
<h3 className="text-sm font-medium">缓存配置</h3>
|
||||
<div className="space-y-4 mt-4 p-4 bg-gray-50 rounded-lg border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-gray-600" />
|
||||
<h3 className="text-sm font-medium text-gray-800">缓存配置</h3>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-2 items-center gap-4">
|
||||
<Label htmlFor={`${type}-max-age`}>最大缓存时间(分钟)</Label>
|
||||
<Label htmlFor={`${type}-max-age`} className="text-sm">最大缓存时间(分钟)</Label>
|
||||
<Input
|
||||
id={`${type}-max-age`}
|
||||
type="number"
|
||||
value={config.max_age}
|
||||
className="h-8"
|
||||
onChange={(e) => {
|
||||
const newConfigs = { ...configs }
|
||||
newConfigs[type].max_age = parseInt(e.target.value)
|
||||
@ -277,11 +298,12 @@ export default function CachePage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center gap-4">
|
||||
<Label htmlFor={`${type}-cleanup-tick`}>清理间隔(分钟)</Label>
|
||||
<Label htmlFor={`${type}-cleanup-tick`} className="text-sm">清理间隔(分钟)</Label>
|
||||
<Input
|
||||
id={`${type}-cleanup-tick`}
|
||||
type="number"
|
||||
value={config.cleanup_tick}
|
||||
className="h-8"
|
||||
onChange={(e) => {
|
||||
const newConfigs = { ...configs }
|
||||
newConfigs[type].cleanup_tick = parseInt(e.target.value)
|
||||
@ -291,11 +313,12 @@ export default function CachePage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center gap-4">
|
||||
<Label htmlFor={`${type}-max-cache-size`}>最大缓存大小(GB)</Label>
|
||||
<Label htmlFor={`${type}-max-cache-size`} className="text-sm">最大缓存大小(GB)</Label>
|
||||
<Input
|
||||
id={`${type}-max-cache-size`}
|
||||
type="number"
|
||||
value={config.max_cache_size}
|
||||
className="h-8"
|
||||
onChange={(e) => {
|
||||
const newConfigs = { ...configs }
|
||||
newConfigs[type].max_cache_size = parseInt(e.target.value)
|
||||
@ -313,6 +336,7 @@ export default function CachePage() {
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] items-center justify-center">
|
||||
<div className="text-center">
|
||||
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-4 text-blue-500" />
|
||||
<div className="text-lg font-medium">加载中...</div>
|
||||
<div className="text-sm text-gray-500 mt-1">正在获取缓存统计信息</div>
|
||||
</div>
|
||||
@ -321,185 +345,352 @@ export default function CachePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => handleClearCache("all")}>
|
||||
清理所有缓存
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 智能缓存汇总 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>智能缓存汇总</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center" title="所有常规文件的精确缓存命中总数">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{(stats?.proxy.regular_cache_hit ?? 0) + (stats?.mirror.regular_cache_hit ?? 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">常规缓存命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="所有图片文件的精确格式缓存命中总数">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{(stats?.proxy.image_cache_hit ?? 0) + (stats?.mirror.image_cache_hit ?? 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">图片精确命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="图片格式回退命中总数,提高了缓存效率">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{(stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">格式回退命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="格式回退在所有图片请求中的占比,显示智能缓存的效果">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{(() => {
|
||||
const totalImageRequests = (stats?.proxy.image_cache_hit ?? 0) + (stats?.mirror.image_cache_hit ?? 0) + (stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)
|
||||
const fallbackHits = (stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)
|
||||
return totalImageRequests > 0 ? ((fallbackHits / totalImageRequests) * 100).toFixed(1) : '0.0'
|
||||
})()}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">格式回退率</div>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-6 w-6 text-blue-600" />
|
||||
<h1 className="text-2xl font-bold">缓存管理</h1>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleClearCache("all")}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
清理所有缓存
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* 代理缓存 */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle>代理缓存</CardTitle>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={stats?.proxy.enabled ?? false}
|
||||
onCheckedChange={(checked) => handleToggleCache("proxy", checked)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleClearCache("proxy")}
|
||||
>
|
||||
清理
|
||||
</Button>
|
||||
</div>
|
||||
{/* 智能缓存汇总 */}
|
||||
<Card className="border-2 border-blue-100 bg-gradient-to-r from-blue-50 to-purple-50">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-blue-800">
|
||||
<Zap className="h-5 w-5" />
|
||||
智能缓存汇总
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">缓存项数量</dt>
|
||||
<dd className="text-sm text-gray-900">{stats?.proxy.total_items ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">总大小</dt>
|
||||
<dd className="text-sm text-gray-900">{formatBytes(stats?.proxy.total_size ?? 0)}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">命中次数</dt>
|
||||
<dd className="text-sm text-gray-900">{stats?.proxy.hit_count ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">未命中次数</dt>
|
||||
<dd className="text-sm text-gray-900">{stats?.proxy.miss_count ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">命中率</dt>
|
||||
<dd className="text-sm text-gray-900">{(stats?.proxy.hit_rate ?? 0).toFixed(2)}%</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">节省带宽</dt>
|
||||
<dd className="text-sm text-gray-900">{formatBytes(stats?.proxy.bytes_saved ?? 0)}</dd>
|
||||
</div>
|
||||
<div className="border-t pt-2 mt-2">
|
||||
<div className="text-xs font-medium text-gray-600 mb-1">智能缓存统计</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="text-center" title="常规文件的精确缓存命中">
|
||||
<div className="text-blue-600 font-medium">{stats?.proxy.regular_cache_hit ?? 0}</div>
|
||||
<div className="text-gray-500">常规命中</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-center p-4 bg-white rounded-lg shadow-sm border cursor-help hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<FileText className="h-5 w-5 text-blue-600" />
|
||||
<Info className="h-3 w-3 text-gray-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{(stats?.proxy.regular_cache_hit ?? 0) + (stats?.mirror.regular_cache_hit ?? 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 font-medium">常规缓存命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="图片文件的精确格式缓存命中">
|
||||
<div className="text-green-600 font-medium">{stats?.proxy.image_cache_hit ?? 0}</div>
|
||||
<div className="text-gray-500">图片命中</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>所有常规文件的精确缓存命中总数</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-center p-4 bg-white rounded-lg shadow-sm border cursor-help hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<ImageIcon className="h-5 w-5 text-green-600" aria-hidden="true" />
|
||||
<Info className="h-3 w-3 text-gray-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{(stats?.proxy.image_cache_hit ?? 0) + (stats?.mirror.image_cache_hit ?? 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 font-medium">图片精确命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="图片格式回退命中(如请求WebP但提供JPEG)">
|
||||
<div className="text-orange-600 font-medium">{stats?.proxy.format_fallback_hit ?? 0}</div>
|
||||
<div className="text-gray-500">格式回退</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>所有图片文件的精确格式缓存命中总数</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-center p-4 bg-white rounded-lg shadow-sm border cursor-help hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<RotateCcw className="h-5 w-5 text-orange-600" />
|
||||
<Info className="h-3 w-3 text-gray-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{(stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 font-medium">格式回退命中</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
{renderCacheConfig("proxy")}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>图片格式回退命中总数,提高了缓存效率</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-center p-4 bg-white rounded-lg shadow-sm border cursor-help hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<Target className="h-5 w-5 text-purple-600" />
|
||||
<Info className="h-3 w-3 text-gray-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{(() => {
|
||||
const totalImageRequests = (stats?.proxy.image_cache_hit ?? 0) + (stats?.mirror.image_cache_hit ?? 0) + (stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)
|
||||
const fallbackHits = (stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)
|
||||
return totalImageRequests > 0 ? ((fallbackHits / totalImageRequests) * 100).toFixed(1) : '0.0'
|
||||
})()}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 font-medium">格式回退率</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>格式回退在所有图片请求中的占比,显示智能缓存的效果</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 镜像缓存 */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle>镜像缓存</CardTitle>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={stats?.mirror.enabled ?? false}
|
||||
onCheckedChange={(checked) => handleToggleCache("mirror", checked)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleClearCache("mirror")}
|
||||
>
|
||||
清理
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">缓存项数量</dt>
|
||||
<dd className="text-sm text-gray-900">{stats?.mirror.total_items ?? 0}</dd>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* 代理缓存 */}
|
||||
<Card className="border-l-4 border-l-blue-500">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<HardDrive className="h-5 w-5 text-blue-600" />
|
||||
代理缓存
|
||||
</CardTitle>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={stats?.proxy.enabled ?? false}
|
||||
onCheckedChange={(checked) => handleToggleCache("proxy", checked)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleClearCache("proxy")}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
清理
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">总大小</dt>
|
||||
<dd className="text-sm text-gray-900">{formatBytes(stats?.mirror.total_size ?? 0)}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">命中次数</dt>
|
||||
<dd className="text-sm text-gray-900">{stats?.mirror.hit_count ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">未命中次数</dt>
|
||||
<dd className="text-sm text-gray-900">{stats?.mirror.miss_count ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">命中率</dt>
|
||||
<dd className="text-sm text-gray-900">{(stats?.mirror.hit_rate ?? 0).toFixed(2)}%</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm font-medium text-gray-500">节省带宽</dt>
|
||||
<dd className="text-sm text-gray-900">{formatBytes(stats?.mirror.bytes_saved ?? 0)}</dd>
|
||||
</div>
|
||||
<div className="border-t pt-2 mt-2">
|
||||
<div className="text-xs font-medium text-gray-600 mb-1">智能缓存统计</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="text-center" title="常规文件的精确缓存命中">
|
||||
<div className="text-blue-600 font-medium">{stats?.mirror.regular_cache_hit ?? 0}</div>
|
||||
<div className="text-gray-500">常规命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="图片文件的精确格式缓存命中">
|
||||
<div className="text-green-600 font-medium">{stats?.mirror.image_cache_hit ?? 0}</div>
|
||||
<div className="text-gray-500">图片命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="图片格式回退命中(如请求WebP但提供JPEG)">
|
||||
<div className="text-orange-600 font-medium">{stats?.mirror.format_fallback_hit ?? 0}</div>
|
||||
<div className="text-gray-500">格式回退</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl className="space-y-3">
|
||||
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
<dt className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||
<Database className="h-4 w-4" />
|
||||
缓存项数量
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-gray-900">{stats?.proxy.total_items ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
<dt className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
总大小
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-gray-900">{formatBytes(stats?.proxy.total_size ?? 0)}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-green-50 rounded">
|
||||
<dt className="text-sm font-medium text-green-700 flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
命中次数
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-green-800">{stats?.proxy.hit_count ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-red-50 rounded">
|
||||
<dt className="text-sm font-medium text-red-700 flex items-center gap-2">
|
||||
<TrendingDown className="h-4 w-4" />
|
||||
未命中次数
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-red-800">{stats?.proxy.miss_count ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-blue-50 rounded">
|
||||
<dt className="text-sm font-medium text-blue-700 flex items-center gap-2">
|
||||
<Activity className="h-4 w-4" />
|
||||
命中率
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-blue-800">{(stats?.proxy.hit_rate ?? 0).toFixed(2)}%</dd>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-purple-50 rounded">
|
||||
<dt className="text-sm font-medium text-purple-700 flex items-center gap-2">
|
||||
<Zap className="h-4 w-4" />
|
||||
节省带宽
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-purple-800">{formatBytes(stats?.proxy.bytes_saved ?? 0)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="border-t pt-4 mt-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Zap className="h-4 w-4 text-gray-600" />
|
||||
<div className="text-sm font-medium text-gray-800">智能缓存统计</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-center p-3 bg-blue-50 rounded-lg border cursor-help hover:bg-blue-100 transition-colors">
|
||||
<FileText className="h-4 w-4 mx-auto mb-1 text-blue-600" />
|
||||
<div className="text-lg font-bold text-blue-600">{stats?.proxy.regular_cache_hit ?? 0}</div>
|
||||
<div className="text-xs text-blue-700">常规命中</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>常规文件的精确缓存命中</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-center p-3 bg-green-50 rounded-lg border cursor-help hover:bg-green-100 transition-colors">
|
||||
<ImageIcon className="h-4 w-4 mx-auto mb-1 text-green-600" aria-hidden="true" />
|
||||
<div className="text-lg font-bold text-green-600">{stats?.proxy.image_cache_hit ?? 0}</div>
|
||||
<div className="text-xs text-green-700">图片命中</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>图片文件的精确格式缓存命中</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-center p-3 bg-orange-50 rounded-lg border cursor-help hover:bg-orange-100 transition-colors">
|
||||
<RotateCcw className="h-4 w-4 mx-auto mb-1 text-orange-600" />
|
||||
<div className="text-lg font-bold text-orange-600">{stats?.proxy.format_fallback_hit ?? 0}</div>
|
||||
<div className="text-xs text-orange-700">格式回退</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>图片格式回退命中(如请求WebP但提供JPEG)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
{renderCacheConfig("mirror")}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{renderCacheConfig("proxy")}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 镜像缓存 */}
|
||||
<Card className="border-l-4 border-l-green-500">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5 text-green-600" />
|
||||
镜像缓存
|
||||
</CardTitle>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={stats?.mirror.enabled ?? false}
|
||||
onCheckedChange={(checked) => handleToggleCache("mirror", checked)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleClearCache("mirror")}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
清理
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl className="space-y-3">
|
||||
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
<dt className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||
<Database className="h-4 w-4" />
|
||||
缓存项数量
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-gray-900">{stats?.mirror.total_items ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
<dt className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
总大小
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-gray-900">{formatBytes(stats?.mirror.total_size ?? 0)}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-green-50 rounded">
|
||||
<dt className="text-sm font-medium text-green-700 flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
命中次数
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-green-800">{stats?.mirror.hit_count ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-red-50 rounded">
|
||||
<dt className="text-sm font-medium text-red-700 flex items-center gap-2">
|
||||
<TrendingDown className="h-4 w-4" />
|
||||
未命中次数
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-red-800">{stats?.mirror.miss_count ?? 0}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-blue-50 rounded">
|
||||
<dt className="text-sm font-medium text-blue-700 flex items-center gap-2">
|
||||
<Activity className="h-4 w-4" />
|
||||
命中率
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-blue-800">{(stats?.mirror.hit_rate ?? 0).toFixed(2)}%</dd>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-purple-50 rounded">
|
||||
<dt className="text-sm font-medium text-purple-700 flex items-center gap-2">
|
||||
<Zap className="h-4 w-4" />
|
||||
节省带宽
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-purple-800">{formatBytes(stats?.mirror.bytes_saved ?? 0)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="border-t pt-4 mt-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Zap className="h-4 w-4 text-gray-600" />
|
||||
<div className="text-sm font-medium text-gray-800">智能缓存统计</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-center p-3 bg-blue-50 rounded-lg border cursor-help hover:bg-blue-100 transition-colors">
|
||||
<FileText className="h-4 w-4 mx-auto mb-1 text-blue-600" />
|
||||
<div className="text-lg font-bold text-blue-600">{stats?.mirror.regular_cache_hit ?? 0}</div>
|
||||
<div className="text-xs text-blue-700">常规命中</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>常规文件的精确缓存命中</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-center p-3 bg-green-50 rounded-lg border cursor-help hover:bg-green-100 transition-colors">
|
||||
<ImageIcon className="h-4 w-4 mx-auto mb-1 text-green-600" aria-hidden="true" />
|
||||
<div className="text-lg font-bold text-green-600">{stats?.mirror.image_cache_hit ?? 0}</div>
|
||||
<div className="text-xs text-green-700">图片命中</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>图片文件的精确格式缓存命中</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-center p-3 bg-orange-50 rounded-lg border cursor-help hover:bg-orange-100 transition-colors">
|
||||
<RotateCcw className="h-4 w-4 mx-auto mb-1 text-orange-600" />
|
||||
<div className="text-lg font-bold text-orange-600">{stats?.mirror.format_fallback_hit ?? 0}</div>
|
||||
<div className="text-xs text-orange-700">格式回退</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>图片格式回退命中(如请求WebP但提供JPEG)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{renderCacheConfig("mirror")}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
32
web/components/ui/tooltip.tsx
Normal file
32
web/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
425
web/package-lock.json
generated
425
web/package-lock.json
generated
@ -17,6 +17,7 @@
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.475.0",
|
||||
@ -1542,6 +1543,397 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
|
||||
"integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.7",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-visually-hidden": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
|
||||
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
|
||||
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-rect": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1",
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
|
||||
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
@ -1575,6 +1967,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||
|
@ -18,6 +18,7 @@
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.475.0",
|
||||
|
Loading…
x
Reference in New Issue
Block a user