|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import React from 'react'; |
|
|
import { Progress, Divider, Empty } from '@douyinfe/semi-ui'; |
|
|
import { |
|
|
IllustrationConstruction, |
|
|
IllustrationConstructionDark, |
|
|
} from '@douyinfe/semi-illustrations'; |
|
|
import { |
|
|
timestamp2string, |
|
|
timestamp2string1, |
|
|
copy, |
|
|
showSuccess, |
|
|
} from './utils'; |
|
|
import { |
|
|
STORAGE_KEYS, |
|
|
DEFAULT_TIME_INTERVALS, |
|
|
DEFAULTS, |
|
|
ILLUSTRATION_SIZE, |
|
|
} from '../constants/dashboard.constants'; |
|
|
|
|
|
|
|
|
export const getDefaultTime = () => { |
|
|
return localStorage.getItem(STORAGE_KEYS.DATA_EXPORT_DEFAULT_TIME) || 'hour'; |
|
|
}; |
|
|
|
|
|
export const getTimeInterval = (timeType, isSeconds = false) => { |
|
|
const intervals = |
|
|
DEFAULT_TIME_INTERVALS[timeType] || DEFAULT_TIME_INTERVALS.hour; |
|
|
return isSeconds ? intervals.seconds : intervals.minutes; |
|
|
}; |
|
|
|
|
|
export const getInitialTimestamp = () => { |
|
|
const defaultTime = getDefaultTime(); |
|
|
const now = new Date().getTime() / 1000; |
|
|
|
|
|
switch (defaultTime) { |
|
|
case 'hour': |
|
|
return timestamp2string(now - 86400); |
|
|
case 'week': |
|
|
return timestamp2string(now - 86400 * 30); |
|
|
default: |
|
|
return timestamp2string(now - 86400 * 7); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
export const updateMapValue = (map, key, value) => { |
|
|
if (!map.has(key)) { |
|
|
map.set(key, 0); |
|
|
} |
|
|
map.set(key, map.get(key) + value); |
|
|
}; |
|
|
|
|
|
export const initializeMaps = (key, ...maps) => { |
|
|
maps.forEach((map) => { |
|
|
if (!map.has(key)) { |
|
|
map.set(key, 0); |
|
|
} |
|
|
}); |
|
|
}; |
|
|
|
|
|
|
|
|
export const updateChartSpec = ( |
|
|
setterFunc, |
|
|
newData, |
|
|
subtitle, |
|
|
newColors, |
|
|
dataId, |
|
|
) => { |
|
|
setterFunc((prev) => ({ |
|
|
...prev, |
|
|
data: [{ id: dataId, values: newData }], |
|
|
title: { |
|
|
...prev.title, |
|
|
subtext: subtitle, |
|
|
}, |
|
|
color: { |
|
|
specified: newColors, |
|
|
}, |
|
|
})); |
|
|
}; |
|
|
|
|
|
export const getTrendSpec = (data, color) => ({ |
|
|
type: 'line', |
|
|
data: [{ id: 'trend', values: data.map((val, idx) => ({ x: idx, y: val })) }], |
|
|
xField: 'x', |
|
|
yField: 'y', |
|
|
height: 40, |
|
|
width: 100, |
|
|
axes: [ |
|
|
{ |
|
|
orient: 'bottom', |
|
|
visible: false, |
|
|
}, |
|
|
{ |
|
|
orient: 'left', |
|
|
visible: false, |
|
|
}, |
|
|
], |
|
|
padding: 0, |
|
|
autoFit: false, |
|
|
legends: { visible: false }, |
|
|
tooltip: { visible: false }, |
|
|
crosshair: { visible: false }, |
|
|
line: { |
|
|
style: { |
|
|
stroke: color, |
|
|
lineWidth: 2, |
|
|
}, |
|
|
}, |
|
|
point: { |
|
|
visible: false, |
|
|
}, |
|
|
background: { |
|
|
fill: 'transparent', |
|
|
}, |
|
|
}); |
|
|
|
|
|
|
|
|
export const createSectionTitle = (Icon, text) => ( |
|
|
<div className='flex items-center gap-2'> |
|
|
<Icon size={16} /> |
|
|
{text} |
|
|
</div> |
|
|
); |
|
|
|
|
|
export const createFormField = (Component, props, FORM_FIELD_PROPS) => ( |
|
|
<Component {...FORM_FIELD_PROPS} {...props} /> |
|
|
); |
|
|
|
|
|
|
|
|
export const handleCopyUrl = async (url, t) => { |
|
|
if (await copy(url)) { |
|
|
showSuccess(t('复制成功')); |
|
|
} |
|
|
}; |
|
|
|
|
|
export const handleSpeedTest = (apiUrl) => { |
|
|
const encodedUrl = encodeURIComponent(apiUrl); |
|
|
const speedTestUrl = `https://www.tcptest.cn/http/${encodedUrl}`; |
|
|
window.open(speedTestUrl, '_blank', 'noopener,noreferrer'); |
|
|
}; |
|
|
|
|
|
|
|
|
export const getUptimeStatusColor = (status, uptimeStatusMap) => |
|
|
uptimeStatusMap[status]?.color || '#8b9aa7'; |
|
|
|
|
|
export const getUptimeStatusText = (status, uptimeStatusMap, t) => |
|
|
uptimeStatusMap[status]?.text || t('未知'); |
|
|
|
|
|
|
|
|
export const renderMonitorList = ( |
|
|
monitors, |
|
|
getUptimeStatusColor, |
|
|
getUptimeStatusText, |
|
|
t, |
|
|
) => { |
|
|
if (!monitors || monitors.length === 0) { |
|
|
return ( |
|
|
<div className='flex justify-center items-center py-4'> |
|
|
<Empty |
|
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />} |
|
|
darkModeImage={ |
|
|
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} /> |
|
|
} |
|
|
title={t('暂无监控数据')} |
|
|
/> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
const grouped = {}; |
|
|
monitors.forEach((m) => { |
|
|
const g = m.group || ''; |
|
|
if (!grouped[g]) grouped[g] = []; |
|
|
grouped[g].push(m); |
|
|
}); |
|
|
|
|
|
const renderItem = (monitor, idx) => ( |
|
|
<div key={idx} className='p-2 hover:bg-white rounded-lg transition-colors'> |
|
|
<div className='flex items-center justify-between mb-1'> |
|
|
<div className='flex items-center gap-2'> |
|
|
<div |
|
|
className='w-2 h-2 rounded-full flex-shrink-0' |
|
|
style={{ backgroundColor: getUptimeStatusColor(monitor.status) }} |
|
|
/> |
|
|
<span className='text-sm font-medium text-gray-900'> |
|
|
{monitor.name} |
|
|
</span> |
|
|
</div> |
|
|
<span className='text-xs text-gray-500'> |
|
|
{((monitor.uptime || 0) * 100).toFixed(2)}% |
|
|
</span> |
|
|
</div> |
|
|
<div className='flex items-center gap-2'> |
|
|
<span className='text-xs text-gray-500'> |
|
|
{getUptimeStatusText(monitor.status)} |
|
|
</span> |
|
|
<div className='flex-1'> |
|
|
<Progress |
|
|
percent={(monitor.uptime || 0) * 100} |
|
|
showInfo={false} |
|
|
aria-label={`${monitor.name} uptime`} |
|
|
stroke={getUptimeStatusColor(monitor.status)} |
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
|
|
|
return Object.entries(grouped).map(([gname, list]) => ( |
|
|
<div key={gname || 'default'} className='mb-2'> |
|
|
{gname && ( |
|
|
<> |
|
|
<div className='text-md font-semibold text-gray-500 px-2 py-1'> |
|
|
{gname} |
|
|
</div> |
|
|
<Divider /> |
|
|
</> |
|
|
)} |
|
|
{list.map(renderItem)} |
|
|
</div> |
|
|
)); |
|
|
}; |
|
|
|
|
|
|
|
|
export const processRawData = ( |
|
|
data, |
|
|
dataExportDefaultTime, |
|
|
initializeMaps, |
|
|
updateMapValue, |
|
|
) => { |
|
|
const result = { |
|
|
totalQuota: 0, |
|
|
totalTimes: 0, |
|
|
totalTokens: 0, |
|
|
uniqueModels: new Set(), |
|
|
timePoints: [], |
|
|
timeQuotaMap: new Map(), |
|
|
timeTokensMap: new Map(), |
|
|
timeCountMap: new Map(), |
|
|
}; |
|
|
|
|
|
data.forEach((item) => { |
|
|
result.uniqueModels.add(item.model_name); |
|
|
result.totalTokens += item.token_used; |
|
|
result.totalQuota += item.quota; |
|
|
result.totalTimes += item.count; |
|
|
|
|
|
const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime); |
|
|
if (!result.timePoints.includes(timeKey)) { |
|
|
result.timePoints.push(timeKey); |
|
|
} |
|
|
|
|
|
initializeMaps( |
|
|
timeKey, |
|
|
result.timeQuotaMap, |
|
|
result.timeTokensMap, |
|
|
result.timeCountMap, |
|
|
); |
|
|
updateMapValue(result.timeQuotaMap, timeKey, item.quota); |
|
|
updateMapValue(result.timeTokensMap, timeKey, item.token_used); |
|
|
updateMapValue(result.timeCountMap, timeKey, item.count); |
|
|
}); |
|
|
|
|
|
result.timePoints.sort(); |
|
|
return result; |
|
|
}; |
|
|
|
|
|
export const calculateTrendData = ( |
|
|
timePoints, |
|
|
timeQuotaMap, |
|
|
timeTokensMap, |
|
|
timeCountMap, |
|
|
dataExportDefaultTime, |
|
|
) => { |
|
|
const quotaTrend = timePoints.map((time) => timeQuotaMap.get(time) || 0); |
|
|
const tokensTrend = timePoints.map((time) => timeTokensMap.get(time) || 0); |
|
|
const countTrend = timePoints.map((time) => timeCountMap.get(time) || 0); |
|
|
|
|
|
const rpmTrend = []; |
|
|
const tpmTrend = []; |
|
|
|
|
|
if (timePoints.length >= 2) { |
|
|
const interval = getTimeInterval(dataExportDefaultTime); |
|
|
|
|
|
for (let i = 0; i < timePoints.length; i++) { |
|
|
rpmTrend.push(timeCountMap.get(timePoints[i]) / interval); |
|
|
tpmTrend.push(timeTokensMap.get(timePoints[i]) / interval); |
|
|
} |
|
|
} |
|
|
|
|
|
return { |
|
|
balance: [], |
|
|
usedQuota: [], |
|
|
requestCount: [], |
|
|
times: countTrend, |
|
|
consumeQuota: quotaTrend, |
|
|
tokens: tokensTrend, |
|
|
rpm: rpmTrend, |
|
|
tpm: tpmTrend, |
|
|
}; |
|
|
}; |
|
|
|
|
|
export const aggregateDataByTimeAndModel = (data, dataExportDefaultTime) => { |
|
|
const aggregatedData = new Map(); |
|
|
|
|
|
data.forEach((item) => { |
|
|
const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime); |
|
|
const modelKey = item.model_name; |
|
|
const key = `${timeKey}-${modelKey}`; |
|
|
|
|
|
if (!aggregatedData.has(key)) { |
|
|
aggregatedData.set(key, { |
|
|
time: timeKey, |
|
|
model: modelKey, |
|
|
quota: 0, |
|
|
count: 0, |
|
|
}); |
|
|
} |
|
|
|
|
|
const existing = aggregatedData.get(key); |
|
|
existing.quota += item.quota; |
|
|
existing.count += item.count; |
|
|
}); |
|
|
|
|
|
return aggregatedData; |
|
|
}; |
|
|
|
|
|
export const generateChartTimePoints = ( |
|
|
aggregatedData, |
|
|
data, |
|
|
dataExportDefaultTime, |
|
|
) => { |
|
|
let chartTimePoints = Array.from( |
|
|
new Set([...aggregatedData.values()].map((d) => d.time)), |
|
|
); |
|
|
|
|
|
if (chartTimePoints.length < DEFAULTS.MAX_TREND_POINTS) { |
|
|
const lastTime = Math.max(...data.map((item) => item.created_at)); |
|
|
const interval = getTimeInterval(dataExportDefaultTime, true); |
|
|
|
|
|
chartTimePoints = Array.from( |
|
|
{ length: DEFAULTS.MAX_TREND_POINTS }, |
|
|
(_, i) => |
|
|
timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime), |
|
|
); |
|
|
} |
|
|
|
|
|
return chartTimePoints; |
|
|
}; |
|
|
|