fix(dashboard): Top 클라이언트 IP/ID 토글 및 메트릭 표시 오류 수정 #117
@ -208,6 +208,7 @@ export interface QueryMetricRow {
|
||||
cache_hit_days: number
|
||||
db_query_days: number
|
||||
client_ip: string | null
|
||||
client_id: string | null
|
||||
}
|
||||
|
||||
export interface QueryMetricsPage {
|
||||
@ -252,7 +253,8 @@ export interface TimeSeriesBucket {
|
||||
}
|
||||
|
||||
export interface TopClient {
|
||||
client_ip: string
|
||||
client: string
|
||||
client_ip?: string
|
||||
query_count: number
|
||||
avg_elapsed_ms: number
|
||||
}
|
||||
@ -261,6 +263,7 @@ export interface QueryMetricsTimeSeries {
|
||||
buckets: TimeSeriesBucket[]
|
||||
topClients: TopClient[]
|
||||
granularity: 'HOURLY' | 'DAILY'
|
||||
groupBy?: 'ip' | 'id'
|
||||
}
|
||||
|
||||
export interface QueryMetricsParams {
|
||||
|
||||
@ -136,6 +136,10 @@ export default function ApiMetrics() {
|
||||
key: 'client_ip', label: t('metrics.clientIp'), sortable: false,
|
||||
render: (row) => row.client_ip ? <span className="font-mono text-xs">{row.client_ip}</span> : '-',
|
||||
},
|
||||
{
|
||||
key: 'client_id', label: 'ID', sortable: false,
|
||||
render: (row) => row.client_id ? <span className="font-mono text-xs">{row.client_id}</span> : '-',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
@ -326,30 +326,31 @@ export default function Dashboard() {
|
||||
</div>
|
||||
|
||||
{/* Top Clients */}
|
||||
{queryTs.topClients.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted">{t('dashboard.topClients')}</span>
|
||||
<div className="flex overflow-hidden rounded-md border border-[var(--border-primary)] text-xs">
|
||||
<button
|
||||
type="button"
|
||||
className={`px-2 py-0.5 ${clientGroupBy === 'ip' ? 'bg-[var(--accent-primary)] text-white' : 'text-[var(--text-secondary)]'}`}
|
||||
onClick={() => setClientGroupBy('ip')}
|
||||
>IP</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`px-2 py-0.5 ${clientGroupBy === 'id' ? 'bg-[var(--accent-primary)] text-white' : 'text-[var(--text-secondary)]'}`}
|
||||
onClick={() => setClientGroupBy('id')}
|
||||
>ID</button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted">{t('dashboard.topClients')}</span>
|
||||
<div className="flex overflow-hidden rounded-md border border-[var(--border-primary)] text-xs">
|
||||
<button
|
||||
type="button"
|
||||
className={`px-2 py-0.5 transition-colors ${clientGroupBy === 'ip' ? 'bg-[var(--accent-primary)] text-white font-medium' : 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]'}`}
|
||||
onClick={() => setClientGroupBy('ip')}
|
||||
>IP</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`px-2 py-0.5 transition-colors ${clientGroupBy === 'id' ? 'bg-[var(--accent-primary)] text-white font-medium' : 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]'}`}
|
||||
onClick={() => setClientGroupBy('id')}
|
||||
>ID</button>
|
||||
</div>
|
||||
</div>
|
||||
{queryTs.topClients.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{queryTs.topClients.map((c, i) => {
|
||||
const maxCount = queryTs.topClients[0].query_count
|
||||
const pct = maxCount > 0 ? (c.query_count / maxCount) * 100 : 0
|
||||
const label = c.client ?? c.client_ip ?? '-'
|
||||
return (
|
||||
<div key={c.client_ip ?? i} className="flex items-center gap-3 text-sm">
|
||||
<span className="w-32 truncate font-mono text-xs">{c.client_ip ?? '-'}</span>
|
||||
<div key={label + i} className="flex items-center gap-3 text-sm">
|
||||
<span className="w-40 truncate font-mono text-xs" title={label}>{label}</span>
|
||||
<div className="flex-1">
|
||||
<div className="h-4 rounded bg-surface-hover">
|
||||
<div
|
||||
@ -365,8 +366,12 @@ export default function Dashboard() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="py-4 text-center text-xs text-muted">
|
||||
{clientGroupBy === 'id' ? '사용자 ID 데이터가 없습니다' : '클라이언트 데이터가 없습니다'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="py-8 text-center text-sm text-muted">{t('dashboard.noChartData')}</div>
|
||||
|
||||
@ -106,7 +106,7 @@ public class QueryMetricsController {
|
||||
zoom_level, requested_mmsi, unique_vessels, total_tracks,
|
||||
total_points, points_after_simplify, total_chunks,
|
||||
response_bytes, elapsed_ms, db_query_ms, simplify_ms,
|
||||
cache_hit_days, db_query_days, client_ip
|
||||
cache_hit_days, db_query_days, client_ip, client_id
|
||||
FROM signal.t_query_metrics
|
||||
""" + whereClause +
|
||||
" ORDER BY " + sortBy + " " + direction +
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user