LogsTable.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import React, { useEffect, useState } from 'react';
  2. import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react';
  3. import { API, isAdmin, showError, timestamp2string } from '../helpers';
  4. import { ITEMS_PER_PAGE } from '../constants';
  5. import { renderQuota } from '../helpers/render';
  6. function renderTimestamp(timestamp) {
  7. return (
  8. <>
  9. {timestamp2string(timestamp)}
  10. </>
  11. );
  12. }
  13. const MODE_OPTIONS = [
  14. { key: 'all', text: '全部用户', value: 'all' },
  15. { key: 'self', text: '当前用户', value: 'self' }
  16. ];
  17. const LOG_OPTIONS = [
  18. { key: '0', text: '全部', value: 0 },
  19. { key: '1', text: '充值', value: 1 },
  20. { key: '2', text: '消费', value: 2 },
  21. { key: '3', text: '管理', value: 3 },
  22. { key: '4', text: '系统', value: 4 }
  23. ];
  24. function renderType(type) {
  25. switch (type) {
  26. case 1:
  27. return <Label basic color='green'> 充值 </Label>;
  28. case 2:
  29. return <Label basic color='olive'> 消费 </Label>;
  30. case 3:
  31. return <Label basic color='orange'> 管理 </Label>;
  32. case 4:
  33. return <Label basic color='purple'> 系统 </Label>;
  34. default:
  35. return <Label basic color='black'> 未知 </Label>;
  36. }
  37. }
  38. const LogsTable = () => {
  39. const [logs, setLogs] = useState([]);
  40. const [showStat, setShowStat] = useState(false);
  41. const [loading, setLoading] = useState(true);
  42. const [activePage, setActivePage] = useState(1);
  43. const [searchKeyword, setSearchKeyword] = useState('');
  44. const [searching, setSearching] = useState(false);
  45. const [logType, setLogType] = useState(0);
  46. const isAdminUser = isAdmin();
  47. let now = new Date();
  48. const [inputs, setInputs] = useState({
  49. username: '',
  50. token_name: '',
  51. model_name: '',
  52. start_timestamp: timestamp2string(0),
  53. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600)
  54. });
  55. const { username, token_name, model_name, start_timestamp, end_timestamp } = inputs;
  56. const [stat, setStat] = useState({
  57. quota: 0,
  58. token: 0
  59. });
  60. const handleInputChange = (e, { name, value }) => {
  61. setInputs((inputs) => ({ ...inputs, [name]: value }));
  62. };
  63. const getLogSelfStat = async () => {
  64. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  65. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  66. let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
  67. const { success, message, data } = res.data;
  68. if (success) {
  69. setStat(data);
  70. } else {
  71. showError(message);
  72. }
  73. };
  74. const getLogStat = async () => {
  75. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  76. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  77. let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
  78. const { success, message, data } = res.data;
  79. if (success) {
  80. setStat(data);
  81. } else {
  82. showError(message);
  83. }
  84. };
  85. const handleEyeClick = async () => {
  86. if (!showStat) {
  87. if (isAdminUser) {
  88. await getLogStat();
  89. } else {
  90. await getLogSelfStat();
  91. }
  92. }
  93. setShowStat(!showStat);
  94. };
  95. const loadLogs = async (startIdx) => {
  96. let url = '';
  97. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  98. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  99. if (isAdminUser) {
  100. url = `/api/log/?p=${startIdx}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  101. } else {
  102. url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  103. }
  104. const res = await API.get(url);
  105. const { success, message, data } = res.data;
  106. if (success) {
  107. if (startIdx === 0) {
  108. setLogs(data);
  109. } else {
  110. let newLogs = [...logs];
  111. newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
  112. setLogs(newLogs);
  113. }
  114. } else {
  115. showError(message);
  116. }
  117. setLoading(false);
  118. };
  119. const onPaginationChange = (e, { activePage }) => {
  120. (async () => {
  121. if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
  122. // In this case we have to load more data and then append them.
  123. await loadLogs(activePage - 1);
  124. }
  125. setActivePage(activePage);
  126. })();
  127. };
  128. const refresh = async () => {
  129. setLoading(true);
  130. setActivePage(1);
  131. await loadLogs(0);
  132. };
  133. useEffect(() => {
  134. refresh().then();
  135. }, [logType]);
  136. const searchLogs = async () => {
  137. if (searchKeyword === '') {
  138. // if keyword is blank, load files instead.
  139. await loadLogs(0);
  140. setActivePage(1);
  141. return;
  142. }
  143. setSearching(true);
  144. const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
  145. const { success, message, data } = res.data;
  146. if (success) {
  147. setLogs(data);
  148. setActivePage(1);
  149. } else {
  150. showError(message);
  151. }
  152. setSearching(false);
  153. };
  154. const handleKeywordChange = async (e, { value }) => {
  155. setSearchKeyword(value.trim());
  156. };
  157. const sortLog = (key) => {
  158. if (logs.length === 0) return;
  159. setLoading(true);
  160. let sortedLogs = [...logs];
  161. if (typeof sortedLogs[0][key] === 'string') {
  162. sortedLogs.sort((a, b) => {
  163. return ('' + a[key]).localeCompare(b[key]);
  164. });
  165. } else {
  166. sortedLogs.sort((a, b) => {
  167. if (a[key] === b[key]) return 0;
  168. if (a[key] > b[key]) return -1;
  169. if (a[key] < b[key]) return 1;
  170. });
  171. }
  172. if (sortedLogs[0].id === logs[0].id) {
  173. sortedLogs.reverse();
  174. }
  175. setLogs(sortedLogs);
  176. setLoading(false);
  177. };
  178. return (
  179. <>
  180. <Segment>
  181. <Header as='h3'>
  182. 使用明细(总消耗额度:
  183. {showStat && renderQuota(stat.quota)}
  184. {!showStat && <span onClick={handleEyeClick} style={{ cursor: 'pointer', color: 'gray' }}>点击查看</span>}
  185. </Header>
  186. <Form>
  187. <Form.Group>
  188. {
  189. isAdminUser && (
  190. <Form.Input fluid label={'用户名称'} width={2} value={username}
  191. placeholder={'可选值'} name='username'
  192. onChange={handleInputChange} />
  193. )
  194. }
  195. <Form.Input fluid label={'令牌名称'} width={isAdminUser ? 2 : 3} value={token_name}
  196. placeholder={'可选值'} name='token_name' onChange={handleInputChange} />
  197. <Form.Input fluid label='模型名称' width={isAdminUser ? 2 : 3} value={model_name} placeholder='可选值'
  198. name='model_name'
  199. onChange={handleInputChange} />
  200. <Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local'
  201. name='start_timestamp'
  202. onChange={handleInputChange} />
  203. <Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local'
  204. name='end_timestamp'
  205. onChange={handleInputChange} />
  206. <Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
  207. </Form.Group>
  208. </Form>
  209. <Table basic compact size='small'>
  210. <Table.Header>
  211. <Table.Row>
  212. <Table.HeaderCell
  213. style={{ cursor: 'pointer' }}
  214. onClick={() => {
  215. sortLog('created_time');
  216. }}
  217. width={3}
  218. >
  219. 时间
  220. </Table.HeaderCell>
  221. {
  222. isAdminUser && <Table.HeaderCell
  223. style={{ cursor: 'pointer' }}
  224. onClick={() => {
  225. sortLog('username');
  226. }}
  227. width={1}
  228. >
  229. 用户
  230. </Table.HeaderCell>
  231. }
  232. <Table.HeaderCell
  233. style={{ cursor: 'pointer' }}
  234. onClick={() => {
  235. sortLog('token_name');
  236. }}
  237. width={1}
  238. >
  239. 令牌
  240. </Table.HeaderCell>
  241. <Table.HeaderCell
  242. style={{ cursor: 'pointer' }}
  243. onClick={() => {
  244. sortLog('type');
  245. }}
  246. width={1}
  247. >
  248. 类型
  249. </Table.HeaderCell>
  250. <Table.HeaderCell
  251. style={{ cursor: 'pointer' }}
  252. onClick={() => {
  253. sortLog('model_name');
  254. }}
  255. width={2}
  256. >
  257. 模型
  258. </Table.HeaderCell>
  259. <Table.HeaderCell
  260. style={{ cursor: 'pointer' }}
  261. onClick={() => {
  262. sortLog('prompt_tokens');
  263. }}
  264. width={1}
  265. >
  266. 提示
  267. </Table.HeaderCell>
  268. <Table.HeaderCell
  269. style={{ cursor: 'pointer' }}
  270. onClick={() => {
  271. sortLog('completion_tokens');
  272. }}
  273. width={1}
  274. >
  275. 补全
  276. </Table.HeaderCell>
  277. <Table.HeaderCell
  278. style={{ cursor: 'pointer' }}
  279. onClick={() => {
  280. sortLog('quota');
  281. }}
  282. width={2}
  283. >
  284. 消耗额度
  285. </Table.HeaderCell>
  286. <Table.HeaderCell
  287. style={{ cursor: 'pointer' }}
  288. onClick={() => {
  289. sortLog('content');
  290. }}
  291. width={isAdminUser ? 4 : 5}
  292. >
  293. 详情
  294. </Table.HeaderCell>
  295. </Table.Row>
  296. </Table.Header>
  297. <Table.Body>
  298. {logs
  299. .slice(
  300. (activePage - 1) * ITEMS_PER_PAGE,
  301. activePage * ITEMS_PER_PAGE
  302. )
  303. .map((log, idx) => {
  304. if (log.deleted) return <></>;
  305. return (
  306. <Table.Row key={log.id}>
  307. <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
  308. {
  309. isAdminUser && (
  310. <Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell>
  311. )
  312. }
  313. <Table.Cell>{log.token_name ? <Label basic>{log.token_name}</Label> : ''}</Table.Cell>
  314. <Table.Cell>{renderType(log.type)}</Table.Cell>
  315. <Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell>
  316. <Table.Cell>{log.prompt_tokens ? log.prompt_tokens : ''}</Table.Cell>
  317. <Table.Cell>{log.completion_tokens ? log.completion_tokens : ''}</Table.Cell>
  318. <Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell>
  319. <Table.Cell>{log.content}</Table.Cell>
  320. </Table.Row>
  321. );
  322. })}
  323. </Table.Body>
  324. <Table.Footer>
  325. <Table.Row>
  326. <Table.HeaderCell colSpan={'9'}>
  327. <Select
  328. placeholder='选择明细分类'
  329. options={LOG_OPTIONS}
  330. style={{ marginRight: '8px' }}
  331. name='logType'
  332. value={logType}
  333. onChange={(e, { name, value }) => {
  334. setLogType(value);
  335. }}
  336. />
  337. <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
  338. <Pagination
  339. floated='right'
  340. activePage={activePage}
  341. onPageChange={onPaginationChange}
  342. size='small'
  343. siblingRange={1}
  344. totalPages={
  345. Math.ceil(logs.length / ITEMS_PER_PAGE) +
  346. (logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
  347. }
  348. />
  349. </Table.HeaderCell>
  350. </Table.Row>
  351. </Table.Footer>
  352. </Table>
  353. </Segment>
  354. </>
  355. );
  356. };
  357. export default LogsTable;