feat(fwd-proxy): Squid cache stats card — GET /forward-proxy/stats + squidclient mgr:counters
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import {
|
||||
Alert, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, Tooltip, Typography, message,
|
||||
Alert, Button, Card, Col, Form, Input, InputNumber, Modal, Progress, Row, Select, Space, Statistic, Switch, Tag, Tooltip, Typography, message,
|
||||
} from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { ArrowDownOutlined, ArrowUpOutlined, CheckCircleOutlined, CloseCircleOutlined, CloudServerOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { ArrowDownOutlined, ArrowUpOutlined, BarChartOutlined, CheckCircleOutlined, CloseCircleOutlined, CloudServerOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -52,6 +52,24 @@ async function listACLs(): Promise<ACL[]> {
|
||||
return (r.data.data as { acls?: ACL[] }).acls ?? []
|
||||
}
|
||||
|
||||
interface SquidStats {
|
||||
client_requests: number
|
||||
cache_hits: number
|
||||
cache_hit_pct: number
|
||||
client_errors: number
|
||||
bytes_in: number
|
||||
bytes_out: number
|
||||
server_requests: number
|
||||
server_errors: number
|
||||
}
|
||||
|
||||
function fmtBytes(n: number): string {
|
||||
if (n >= 1_073_741_824) return (n / 1_073_741_824).toFixed(1) + ' GB'
|
||||
if (n >= 1_048_576) return (n / 1_048_576).toFixed(1) + ' MB'
|
||||
if (n >= 1_024) return (n / 1_024).toFixed(0) + ' KB'
|
||||
return n + ' B'
|
||||
}
|
||||
|
||||
export default function ForwardProxyPage() {
|
||||
const { t } = useTranslation()
|
||||
const qc = useQueryClient()
|
||||
@@ -68,6 +86,18 @@ export default function ForwardProxyPage() {
|
||||
})
|
||||
const squid = services?.find(s => s.unit === 'squid.service' || s.unit === 'squid')
|
||||
|
||||
const { data: statsData, refetch: refetchStats } = useQuery({
|
||||
queryKey: ['fwd-proxy', 'stats'],
|
||||
queryFn: async () => {
|
||||
const r = await apiClient.get('/forward-proxy/stats')
|
||||
if (!isEnvelope(r.data)) return null
|
||||
const d = r.data.data as { stats?: SquidStats; error?: string }
|
||||
if (d.error) return null
|
||||
return d.stats ?? null
|
||||
},
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
|
||||
const [editing, setEditing] = useState<ACL | null>(null)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [form] = Form.useForm<FormValues>()
|
||||
@@ -206,6 +236,54 @@ export default function ForwardProxyPage() {
|
||||
description={t('fwd.helpBody')}
|
||||
/>
|
||||
|
||||
{statsData && (
|
||||
<Card
|
||||
size="small"
|
||||
className="mb-12"
|
||||
title={<><BarChartOutlined /> {t('fwd.statsCard.title')}</>}
|
||||
extra={
|
||||
<Space size={4}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>{t('fwd.statsCard.sinceRestart')}</Typography.Text>
|
||||
<Tooltip title={t('common.refresh')}>
|
||||
<Button size="small" icon={<ReloadOutlined />} onClick={() => { void refetchStats() }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Row gutter={[16, 8]}>
|
||||
<Col xs={12} sm={6} md={4}>
|
||||
<Statistic title={t('fwd.statsCard.clientRequests')} value={statsData.client_requests} />
|
||||
</Col>
|
||||
<Col xs={12} sm={6} md={4}>
|
||||
<Statistic title={t('fwd.statsCard.cacheHits')} value={statsData.cache_hits} />
|
||||
</Col>
|
||||
<Col xs={12} sm={6} md={4}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>{t('fwd.statsCard.cacheHitPct')}</Typography.Text>
|
||||
<Progress
|
||||
percent={Math.round(statsData.cache_hit_pct)}
|
||||
size="small"
|
||||
strokeColor={statsData.cache_hit_pct >= 50 ? '#16a34a' : statsData.cache_hit_pct >= 20 ? '#ca8a04' : '#dc2626'}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={6} md={4}>
|
||||
<Statistic title={t('fwd.statsCard.serverRequests')} value={statsData.server_requests} />
|
||||
</Col>
|
||||
<Col xs={12} sm={6} md={4}>
|
||||
<Statistic title={t('fwd.statsCard.bytesIn')} value={fmtBytes(statsData.bytes_in)} />
|
||||
</Col>
|
||||
<Col xs={12} sm={6} md={4}>
|
||||
<Statistic title={t('fwd.statsCard.bytesOut')} value={fmtBytes(statsData.bytes_out)} />
|
||||
</Col>
|
||||
</Row>
|
||||
{statsData.client_errors > 0 && (
|
||||
<Typography.Text type="danger" style={{ fontSize: 12 }}>
|
||||
{t('fwd.statsCard.clientErrors')}: {statsData.client_errors}
|
||||
{statsData.server_errors > 0 && ` · ${t('fwd.statsCard.serverErrors')}: ${statsData.server_errors}`}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<DataTable
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
|
||||
Reference in New Issue
Block a user