dashboard.jsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React from 'react';
  16. import { Progress, Divider, Empty } from '@douyinfe/semi-ui';
  17. import {
  18. IllustrationConstruction,
  19. IllustrationConstructionDark,
  20. } from '@douyinfe/semi-illustrations';
  21. import {
  22. timestamp2string,
  23. timestamp2string1,
  24. copy,
  25. showSuccess,
  26. } from './utils';
  27. import {
  28. STORAGE_KEYS,
  29. DEFAULT_TIME_INTERVALS,
  30. DEFAULTS,
  31. ILLUSTRATION_SIZE,
  32. } from '../constants/dashboard.constants';
  33. // ========== 时间相关工具函数 ==========
  34. export const getDefaultTime = () => {
  35. return localStorage.getItem(STORAGE_KEYS.DATA_EXPORT_DEFAULT_TIME) || 'hour';
  36. };
  37. export const getTimeInterval = (timeType, isSeconds = false) => {
  38. const intervals =
  39. DEFAULT_TIME_INTERVALS[timeType] || DEFAULT_TIME_INTERVALS.hour;
  40. return isSeconds ? intervals.seconds : intervals.minutes;
  41. };
  42. export const getInitialTimestamp = () => {
  43. const defaultTime = getDefaultTime();
  44. const now = new Date().getTime() / 1000;
  45. switch (defaultTime) {
  46. case 'hour':
  47. return timestamp2string(now - 86400);
  48. case 'week':
  49. return timestamp2string(now - 86400 * 30);
  50. default:
  51. return timestamp2string(now - 86400 * 7);
  52. }
  53. };
  54. // ========== 数据处理工具函数 ==========
  55. export const updateMapValue = (map, key, value) => {
  56. if (!map.has(key)) {
  57. map.set(key, 0);
  58. }
  59. map.set(key, map.get(key) + value);
  60. };
  61. export const initializeMaps = (key, ...maps) => {
  62. maps.forEach((map) => {
  63. if (!map.has(key)) {
  64. map.set(key, 0);
  65. }
  66. });
  67. };
  68. // ========== 图表相关工具函数 ==========
  69. export const updateChartSpec = (
  70. setterFunc,
  71. newData,
  72. subtitle,
  73. newColors,
  74. dataId,
  75. ) => {
  76. setterFunc((prev) => ({
  77. ...prev,
  78. data: [{ id: dataId, values: newData }],
  79. title: {
  80. ...prev.title,
  81. subtext: subtitle,
  82. },
  83. color: {
  84. specified: newColors,
  85. },
  86. }));
  87. };
  88. export const getTrendSpec = (data, color) => ({
  89. type: 'line',
  90. data: [{ id: 'trend', values: data.map((val, idx) => ({ x: idx, y: val })) }],
  91. xField: 'x',
  92. yField: 'y',
  93. height: 40,
  94. width: 100,
  95. axes: [
  96. {
  97. orient: 'bottom',
  98. visible: false,
  99. },
  100. {
  101. orient: 'left',
  102. visible: false,
  103. },
  104. ],
  105. padding: 0,
  106. autoFit: false,
  107. legends: { visible: false },
  108. tooltip: { visible: false },
  109. crosshair: { visible: false },
  110. line: {
  111. style: {
  112. stroke: color,
  113. lineWidth: 2,
  114. },
  115. },
  116. point: {
  117. visible: false,
  118. },
  119. background: {
  120. fill: 'transparent',
  121. },
  122. });
  123. // ========== UI 工具函数 ==========
  124. export const createSectionTitle = (Icon, text) => (
  125. <div className='flex items-center gap-2'>
  126. <Icon size={16} />
  127. {text}
  128. </div>
  129. );
  130. export const createFormField = (Component, props, FORM_FIELD_PROPS) => (
  131. <Component {...FORM_FIELD_PROPS} {...props} />
  132. );
  133. // ========== 操作处理函数 ==========
  134. export const handleCopyUrl = async (url, t) => {
  135. if (await copy(url)) {
  136. showSuccess(t('复制成功'));
  137. }
  138. };
  139. export const handleSpeedTest = (apiUrl) => {
  140. const encodedUrl = encodeURIComponent(apiUrl);
  141. const speedTestUrl = `https://www.tcptest.cn/http/${encodedUrl}`;
  142. window.open(speedTestUrl, '_blank', 'noopener,noreferrer');
  143. };
  144. // ========== 状态映射函数 ==========
  145. export const getUptimeStatusColor = (status, uptimeStatusMap) =>
  146. uptimeStatusMap[status]?.color || '#8b9aa7';
  147. export const getUptimeStatusText = (status, uptimeStatusMap, t) =>
  148. uptimeStatusMap[status]?.text || t('未知');
  149. // ========== 监控列表渲染函数 ==========
  150. export const renderMonitorList = (
  151. monitors,
  152. getUptimeStatusColor,
  153. getUptimeStatusText,
  154. t,
  155. ) => {
  156. if (!monitors || monitors.length === 0) {
  157. return (
  158. <div className='flex justify-center items-center py-4'>
  159. <Empty
  160. image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
  161. darkModeImage={
  162. <IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
  163. }
  164. title={t('暂无监控数据')}
  165. />
  166. </div>
  167. );
  168. }
  169. const grouped = {};
  170. monitors.forEach((m) => {
  171. const g = m.group || '';
  172. if (!grouped[g]) grouped[g] = [];
  173. grouped[g].push(m);
  174. });
  175. const renderItem = (monitor, idx) => (
  176. <div key={idx} className='p-2 hover:bg-white rounded-lg transition-colors'>
  177. <div className='flex items-center justify-between mb-1'>
  178. <div className='flex items-center gap-2'>
  179. <div
  180. className='w-2 h-2 rounded-full flex-shrink-0'
  181. style={{ backgroundColor: getUptimeStatusColor(monitor.status) }}
  182. />
  183. <span className='text-sm font-medium text-gray-900'>
  184. {monitor.name}
  185. </span>
  186. </div>
  187. <span className='text-xs text-gray-500'>
  188. {((monitor.uptime || 0) * 100).toFixed(2)}%
  189. </span>
  190. </div>
  191. <div className='flex items-center gap-2'>
  192. <span className='text-xs text-gray-500'>
  193. {getUptimeStatusText(monitor.status)}
  194. </span>
  195. <div className='flex-1'>
  196. <Progress
  197. percent={(monitor.uptime || 0) * 100}
  198. showInfo={false}
  199. aria-label={`${monitor.name} uptime`}
  200. stroke={getUptimeStatusColor(monitor.status)}
  201. />
  202. </div>
  203. </div>
  204. </div>
  205. );
  206. return Object.entries(grouped).map(([gname, list]) => (
  207. <div key={gname || 'default'} className='mb-2'>
  208. {gname && (
  209. <>
  210. <div className='text-md font-semibold text-gray-500 px-2 py-1'>
  211. {gname}
  212. </div>
  213. <Divider />
  214. </>
  215. )}
  216. {list.map(renderItem)}
  217. </div>
  218. ));
  219. };
  220. // ========== 数据处理函数 ==========
  221. export const processRawData = (
  222. data,
  223. dataExportDefaultTime,
  224. initializeMaps,
  225. updateMapValue,
  226. ) => {
  227. const result = {
  228. totalQuota: 0,
  229. totalTimes: 0,
  230. totalTokens: 0,
  231. uniqueModels: new Set(),
  232. timePoints: [],
  233. timeQuotaMap: new Map(),
  234. timeTokensMap: new Map(),
  235. timeCountMap: new Map(),
  236. };
  237. data.forEach((item) => {
  238. result.uniqueModels.add(item.model_name);
  239. result.totalTokens += item.token_used;
  240. result.totalQuota += item.quota;
  241. result.totalTimes += item.count;
  242. const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
  243. if (!result.timePoints.includes(timeKey)) {
  244. result.timePoints.push(timeKey);
  245. }
  246. initializeMaps(
  247. timeKey,
  248. result.timeQuotaMap,
  249. result.timeTokensMap,
  250. result.timeCountMap,
  251. );
  252. updateMapValue(result.timeQuotaMap, timeKey, item.quota);
  253. updateMapValue(result.timeTokensMap, timeKey, item.token_used);
  254. updateMapValue(result.timeCountMap, timeKey, item.count);
  255. });
  256. result.timePoints.sort();
  257. return result;
  258. };
  259. export const calculateTrendData = (
  260. timePoints,
  261. timeQuotaMap,
  262. timeTokensMap,
  263. timeCountMap,
  264. dataExportDefaultTime,
  265. ) => {
  266. const quotaTrend = timePoints.map((time) => timeQuotaMap.get(time) || 0);
  267. const tokensTrend = timePoints.map((time) => timeTokensMap.get(time) || 0);
  268. const countTrend = timePoints.map((time) => timeCountMap.get(time) || 0);
  269. const rpmTrend = [];
  270. const tpmTrend = [];
  271. if (timePoints.length >= 2) {
  272. const interval = getTimeInterval(dataExportDefaultTime);
  273. for (let i = 0; i < timePoints.length; i++) {
  274. rpmTrend.push(timeCountMap.get(timePoints[i]) / interval);
  275. tpmTrend.push(timeTokensMap.get(timePoints[i]) / interval);
  276. }
  277. }
  278. return {
  279. balance: [],
  280. usedQuota: [],
  281. requestCount: [],
  282. times: countTrend,
  283. consumeQuota: quotaTrend,
  284. tokens: tokensTrend,
  285. rpm: rpmTrend,
  286. tpm: tpmTrend,
  287. };
  288. };
  289. export const aggregateDataByTimeAndModel = (data, dataExportDefaultTime) => {
  290. const aggregatedData = new Map();
  291. data.forEach((item) => {
  292. const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
  293. const modelKey = item.model_name;
  294. const key = `${timeKey}-${modelKey}`;
  295. if (!aggregatedData.has(key)) {
  296. aggregatedData.set(key, {
  297. time: timeKey,
  298. model: modelKey,
  299. quota: 0,
  300. count: 0,
  301. });
  302. }
  303. const existing = aggregatedData.get(key);
  304. existing.quota += item.quota;
  305. existing.count += item.count;
  306. });
  307. return aggregatedData;
  308. };
  309. export const generateChartTimePoints = (
  310. aggregatedData,
  311. data,
  312. dataExportDefaultTime,
  313. ) => {
  314. let chartTimePoints = Array.from(
  315. new Set([...aggregatedData.values()].map((d) => d.time)),
  316. );
  317. if (chartTimePoints.length < DEFAULTS.MAX_TREND_POINTS) {
  318. const lastTime = Math.max(...data.map((item) => item.created_at));
  319. const interval = getTimeInterval(dataExportDefaultTime, true);
  320. chartTimePoints = Array.from(
  321. { length: DEFAULTS.MAX_TREND_POINTS },
  322. (_, i) =>
  323. timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime),
  324. );
  325. }
  326. return chartTimePoints;
  327. };