LogsTable.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. import React, { useEffect, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. API,
  5. copy,
  6. getTodayStartTimestamp,
  7. isAdmin,
  8. showError,
  9. showSuccess,
  10. timestamp2string,
  11. } from '../helpers';
  12. import {
  13. Avatar,
  14. Button, Descriptions,
  15. Form,
  16. Layout,
  17. Modal,
  18. Select,
  19. Space,
  20. Spin,
  21. Table,
  22. Tag,
  23. Tooltip
  24. } from '@douyinfe/semi-ui';
  25. import { ITEMS_PER_PAGE } from '../constants';
  26. import {
  27. renderAudioModelPrice,
  28. renderModelPrice, renderModelPriceSimple,
  29. renderNumber,
  30. renderQuota,
  31. stringToColor
  32. } from '../helpers/render';
  33. import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
  34. import { getLogOther } from '../helpers/other.js';
  35. const { Header } = Layout;
  36. function renderTimestamp(timestamp) {
  37. return <>{timestamp2string(timestamp)}</>;
  38. }
  39. const MODE_OPTIONS = [
  40. { key: 'all', text: 'all', value: 'all' },
  41. { key: 'self', text: 'current user', value: 'self' },
  42. ];
  43. const colors = [
  44. 'amber',
  45. 'blue',
  46. 'cyan',
  47. 'green',
  48. 'grey',
  49. 'indigo',
  50. 'light-blue',
  51. 'lime',
  52. 'orange',
  53. 'pink',
  54. 'purple',
  55. 'red',
  56. 'teal',
  57. 'violet',
  58. 'yellow',
  59. ];
  60. const LogsTable = () => {
  61. const { t } = useTranslation();
  62. function renderType(type) {
  63. switch (type) {
  64. case 1:
  65. return <Tag color='cyan' size='large'>{t('充值')}</Tag>;
  66. case 2:
  67. return <Tag color='lime' size='large'>{t('消费')}</Tag>;
  68. case 3:
  69. return <Tag color='orange' size='large'>{t('管理')}</Tag>;
  70. case 4:
  71. return <Tag color='purple' size='large'>{t('系统')}</Tag>;
  72. default:
  73. return <Tag color='black' size='large'>{t('未知')}</Tag>;
  74. }
  75. }
  76. function renderIsStream(bool) {
  77. if (bool) {
  78. return <Tag color='blue' size='large'>{t('流')}</Tag>;
  79. } else {
  80. return <Tag color='purple' size='large'>{t('非流')}</Tag>;
  81. }
  82. }
  83. function renderUseTime(type) {
  84. const time = parseInt(type);
  85. if (time < 101) {
  86. return (
  87. <Tag color='green' size='large'>
  88. {' '}
  89. {time} s{' '}
  90. </Tag>
  91. );
  92. } else if (time < 300) {
  93. return (
  94. <Tag color='orange' size='large'>
  95. {' '}
  96. {time} s{' '}
  97. </Tag>
  98. );
  99. } else {
  100. return (
  101. <Tag color='red' size='large'>
  102. {' '}
  103. {time} s{' '}
  104. </Tag>
  105. );
  106. }
  107. }
  108. function renderFirstUseTime(type) {
  109. let time = parseFloat(type) / 1000.0;
  110. time = time.toFixed(1);
  111. if (time < 3) {
  112. return (
  113. <Tag color='green' size='large'>
  114. {' '}
  115. {time} s{' '}
  116. </Tag>
  117. );
  118. } else if (time < 10) {
  119. return (
  120. <Tag color='orange' size='large'>
  121. {' '}
  122. {time} s{' '}
  123. </Tag>
  124. );
  125. } else {
  126. return (
  127. <Tag color='red' size='large'>
  128. {' '}
  129. {time} s{' '}
  130. </Tag>
  131. );
  132. }
  133. }
  134. const columns = [
  135. {
  136. title: t('时间'),
  137. dataIndex: 'timestamp2string',
  138. },
  139. {
  140. title: t('渠道'),
  141. dataIndex: 'channel',
  142. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  143. render: (text, record, index) => {
  144. return isAdminUser ? (
  145. record.type === 0 || record.type === 2 ? (
  146. <div>
  147. {
  148. <Tag
  149. color={colors[parseInt(text) % colors.length]}
  150. size='large'
  151. >
  152. {' '}
  153. {text}{' '}
  154. </Tag>
  155. }
  156. </div>
  157. ) : (
  158. <></>
  159. )
  160. ) : (
  161. <></>
  162. );
  163. },
  164. },
  165. {
  166. title: t('用户'),
  167. dataIndex: 'username',
  168. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  169. render: (text, record, index) => {
  170. return isAdminUser ? (
  171. <div>
  172. <Avatar
  173. size='small'
  174. color={stringToColor(text)}
  175. style={{ marginRight: 4 }}
  176. onClick={() => showUserInfo(record.user_id)}
  177. >
  178. {typeof text === 'string' && text.slice(0, 1)}
  179. </Avatar>
  180. {text}
  181. </div>
  182. ) : (
  183. <></>
  184. );
  185. },
  186. },
  187. {
  188. title: t('令牌'),
  189. dataIndex: 'token_name',
  190. render: (text, record, index) => {
  191. return record.type === 0 || record.type === 2 ? (
  192. <div>
  193. <Tag
  194. color='grey'
  195. size='large'
  196. onClick={() => {
  197. copyText(text);
  198. }}
  199. >
  200. {' '}
  201. {t(text)}{' '}
  202. </Tag>
  203. </div>
  204. ) : (
  205. <></>
  206. );
  207. },
  208. },
  209. {
  210. title: t('分组'),
  211. dataIndex: 'group',
  212. render: (text, record, index) => {
  213. if (record.type === 0 || record.type === 2) {
  214. let other = JSON.parse(record.other);
  215. if (other === null) {
  216. return <></>;
  217. }
  218. if (other.group !== undefined) {
  219. return (
  220. <Tag color='blue' size='large'>
  221. {' '}
  222. {other.group}{' '}
  223. </Tag>
  224. );
  225. } else {
  226. return <></>;
  227. }
  228. } else {
  229. return <></>;
  230. }
  231. },
  232. },
  233. {
  234. title: t('类型'),
  235. dataIndex: 'type',
  236. render: (text, record, index) => {
  237. return <>{renderType(text)}</>;
  238. },
  239. },
  240. {
  241. title: t('模型'),
  242. dataIndex: 'model_name',
  243. render: (text, record, index) => {
  244. return record.type === 0 || record.type === 2 ? (
  245. <>
  246. <Tag
  247. color={stringToColor(text)}
  248. size='large'
  249. onClick={() => {
  250. copyText(text);
  251. }}
  252. >
  253. {' '}
  254. {text}{' '}
  255. </Tag>
  256. </>
  257. ) : (
  258. <></>
  259. );
  260. },
  261. },
  262. {
  263. title: t('用时/首字'),
  264. dataIndex: 'use_time',
  265. render: (text, record, index) => {
  266. if (record.is_stream) {
  267. let other = getLogOther(record.other);
  268. return (
  269. <>
  270. <Space>
  271. {renderUseTime(text)}
  272. {renderFirstUseTime(other.frt)}
  273. {renderIsStream(record.is_stream)}
  274. </Space>
  275. </>
  276. );
  277. } else {
  278. return (
  279. <>
  280. <Space>
  281. {renderUseTime(text)}
  282. {renderIsStream(record.is_stream)}
  283. </Space>
  284. </>
  285. );
  286. }
  287. },
  288. },
  289. {
  290. title: t('提示'),
  291. dataIndex: 'prompt_tokens',
  292. render: (text, record, index) => {
  293. return record.type === 0 || record.type === 2 ? (
  294. <>{<span> {text} </span>}</>
  295. ) : (
  296. <></>
  297. );
  298. },
  299. },
  300. {
  301. title: t('补全'),
  302. dataIndex: 'completion_tokens',
  303. render: (text, record, index) => {
  304. return parseInt(text) > 0 &&
  305. (record.type === 0 || record.type === 2) ? (
  306. <>{<span> {text} </span>}</>
  307. ) : (
  308. <></>
  309. );
  310. },
  311. },
  312. {
  313. title: t('花费'),
  314. dataIndex: 'quota',
  315. render: (text, record, index) => {
  316. return record.type === 0 || record.type === 2 ? (
  317. <>{renderQuota(text, 6)}</>
  318. ) : (
  319. <></>
  320. );
  321. },
  322. },
  323. {
  324. title: t('重试'),
  325. dataIndex: 'retry',
  326. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  327. render: (text, record, index) => {
  328. let content = t('渠道') + `:${record.channel}`;
  329. if (record.other !== '') {
  330. let other = JSON.parse(record.other);
  331. if (other === null) {
  332. return <></>;
  333. }
  334. if (other.admin_info !== undefined) {
  335. if (
  336. other.admin_info.use_channel !== null &&
  337. other.admin_info.use_channel !== undefined &&
  338. other.admin_info.use_channel !== ''
  339. ) {
  340. // channel id array
  341. let useChannel = other.admin_info.use_channel;
  342. let useChannelStr = useChannel.join('->');
  343. content = t('渠道') + `:${useChannelStr}`;
  344. }
  345. }
  346. }
  347. return isAdminUser ? <div>{content}</div> : <></>;
  348. },
  349. },
  350. {
  351. title: t('详情'),
  352. dataIndex: 'content',
  353. render: (text, record, index) => {
  354. let other = getLogOther(record.other);
  355. if (other == null || record.type !== 2) {
  356. return (
  357. <Paragraph
  358. ellipsis={{
  359. rows: 2,
  360. showTooltip: {
  361. type: 'popover',
  362. opts: { style: { width: 240 } },
  363. },
  364. }}
  365. style={{ maxWidth: 240 }}
  366. >
  367. {text}
  368. </Paragraph>
  369. );
  370. }
  371. let content = renderModelPriceSimple(
  372. other.model_ratio,
  373. other.model_price,
  374. other.group_ratio,
  375. );
  376. return (
  377. <Paragraph
  378. ellipsis={{
  379. rows: 2,
  380. }}
  381. style={{ maxWidth: 240 }}
  382. >
  383. {content}
  384. </Paragraph>
  385. );
  386. },
  387. },
  388. ];
  389. const [logs, setLogs] = useState([]);
  390. const [expandData, setExpandData] = useState({});
  391. const [showStat, setShowStat] = useState(false);
  392. const [loading, setLoading] = useState(false);
  393. const [loadingStat, setLoadingStat] = useState(false);
  394. const [activePage, setActivePage] = useState(1);
  395. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  396. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  397. const [logType, setLogType] = useState(0);
  398. const isAdminUser = isAdmin();
  399. let now = new Date();
  400. // 初始化start_timestamp为今天0点
  401. const [inputs, setInputs] = useState({
  402. username: '',
  403. token_name: '',
  404. model_name: '',
  405. start_timestamp: timestamp2string(getTodayStartTimestamp()),
  406. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  407. channel: '',
  408. });
  409. const {
  410. username,
  411. token_name,
  412. model_name,
  413. start_timestamp,
  414. end_timestamp,
  415. channel,
  416. } = inputs;
  417. const [stat, setStat] = useState({
  418. quota: 0,
  419. token: 0,
  420. });
  421. const handleInputChange = (value, name) => {
  422. setInputs((inputs) => ({ ...inputs, [name]: value }));
  423. };
  424. const getLogSelfStat = async () => {
  425. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  426. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  427. let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  428. url = encodeURI(url);
  429. let res = await API.get(url);
  430. const { success, message, data } = res.data;
  431. if (success) {
  432. setStat(data);
  433. } else {
  434. showError(message);
  435. }
  436. };
  437. const getLogStat = async () => {
  438. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  439. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  440. let url = `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
  441. url = encodeURI(url);
  442. let res = await API.get(url);
  443. const { success, message, data } = res.data;
  444. if (success) {
  445. setStat(data);
  446. } else {
  447. showError(message);
  448. }
  449. };
  450. const handleEyeClick = async () => {
  451. if (loadingStat) {
  452. return;
  453. }
  454. setLoadingStat(true);
  455. if (isAdminUser) {
  456. await getLogStat();
  457. } else {
  458. await getLogSelfStat();
  459. }
  460. setShowStat(true);
  461. setLoadingStat(false);
  462. };
  463. const showUserInfo = async (userId) => {
  464. if (!isAdminUser) {
  465. return;
  466. }
  467. const res = await API.get(`/api/user/${userId}`);
  468. const { success, message, data } = res.data;
  469. if (success) {
  470. Modal.info({
  471. title: t('用户信息'),
  472. content: (
  473. <div style={{ padding: 12 }}>
  474. <p>{t('用户名')}: {data.username}</p>
  475. <p>{t('余额')}: {renderQuota(data.quota)}</p>
  476. <p>{t('已用额度')}:{renderQuota(data.used_quota)}</p>
  477. <p>{t('请求次数')}:{renderNumber(data.request_count)}</p>
  478. </div>
  479. ),
  480. centered: true,
  481. });
  482. } else {
  483. showError(message);
  484. }
  485. };
  486. const setLogsFormat = (logs) => {
  487. let expandDatesLocal = {};
  488. for (let i = 0; i < logs.length; i++) {
  489. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  490. logs[i].key = i;
  491. let other = getLogOther(logs[i].other);
  492. let expandDataLocal = [];
  493. if (isAdmin()) {
  494. // let content = '渠道:' + logs[i].channel;
  495. // if (other.admin_info !== undefined) {
  496. // if (
  497. // other.admin_info.use_channel !== null &&
  498. // other.admin_info.use_channel !== undefined &&
  499. // other.admin_info.use_channel !== ''
  500. // ) {
  501. // // channel id array
  502. // let useChannel = other.admin_info.use_channel;
  503. // let useChannelStr = useChannel.join('->');
  504. // content = `渠道:${useChannelStr}`;
  505. // }
  506. // }
  507. // expandDataLocal.push({
  508. // key: '渠道重试',
  509. // value: content,
  510. // })
  511. }
  512. if (other?.ws || other?.audio) {
  513. expandDataLocal.push({
  514. key: t('语音输入'),
  515. value: other.audio_input,
  516. });
  517. expandDataLocal.push({
  518. key: t('语音输出'),
  519. value: other.audio_output,
  520. });
  521. expandDataLocal.push({
  522. key: t('文字输入'),
  523. value: other.text_input,
  524. });
  525. expandDataLocal.push({
  526. key: t('文字输出'),
  527. value: other.text_output,
  528. });
  529. }
  530. expandDataLocal.push({
  531. key: t('日志详情'),
  532. value: logs[i].content,
  533. });
  534. if (logs[i].type === 2) {
  535. let content = '';
  536. if (other?.ws || other?.audio) {
  537. content = renderAudioModelPrice(
  538. other.text_input,
  539. other.text_output,
  540. other.model_ratio,
  541. other.model_price,
  542. other.completion_ratio,
  543. other.audio_input,
  544. other.audio_output,
  545. other?.audio_ratio,
  546. other?.audio_completion_ratio,
  547. other.group_ratio,
  548. );
  549. } else {
  550. content = renderModelPrice(
  551. logs[i].prompt_tokens,
  552. logs[i].completion_tokens,
  553. other.model_ratio,
  554. other.model_price,
  555. other.completion_ratio,
  556. other.group_ratio,
  557. );
  558. }
  559. expandDataLocal.push({
  560. key: t('计费过程'),
  561. value: content,
  562. });
  563. }
  564. expandDatesLocal[logs[i].key] = expandDataLocal;
  565. }
  566. setExpandData(expandDatesLocal);
  567. setLogs(logs);
  568. };
  569. const loadLogs = async (startIdx, pageSize, logType = 0) => {
  570. setLoading(true);
  571. let url = '';
  572. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  573. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  574. if (isAdminUser) {
  575. url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
  576. } else {
  577. url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  578. }
  579. url = encodeURI(url);
  580. const res = await API.get(url);
  581. const { success, message, data } = res.data;
  582. if (success) {
  583. const newPageData = data.items;
  584. setActivePage(data.page);
  585. setPageSize(data.page_size);
  586. setLogCount(data.total);
  587. setLogsFormat(newPageData);
  588. } else {
  589. showError(message);
  590. }
  591. setLoading(false);
  592. };
  593. const handlePageChange = (page) => {
  594. setActivePage(page);
  595. loadLogs(page, pageSize, logType).then((r) => {});
  596. };
  597. const handlePageSizeChange = async (size) => {
  598. localStorage.setItem('page-size', size + '');
  599. setPageSize(size);
  600. setActivePage(1);
  601. loadLogs(activePage, size)
  602. .then()
  603. .catch((reason) => {
  604. showError(reason);
  605. });
  606. };
  607. const refresh = async () => {
  608. setActivePage(1);
  609. handleEyeClick();
  610. await loadLogs(activePage, pageSize, logType);
  611. };
  612. const copyText = async (text) => {
  613. if (await copy(text)) {
  614. showSuccess('已复制:' + text);
  615. } else {
  616. Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
  617. }
  618. };
  619. useEffect(() => {
  620. const localPageSize =
  621. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  622. setPageSize(localPageSize);
  623. loadLogs(activePage, localPageSize)
  624. .then()
  625. .catch((reason) => {
  626. showError(reason);
  627. });
  628. handleEyeClick();
  629. }, []);
  630. const expandRowRender = (record, index) => {
  631. return <Descriptions data={expandData[record.key]} />;
  632. };
  633. return (
  634. <>
  635. <Layout>
  636. <Header>
  637. <Spin spinning={loadingStat}>
  638. <Space>
  639. <Tag color='green' size='large' style={{ padding: 15 }}>
  640. {t('总消耗额度')}: {renderQuota(stat.quota)}
  641. </Tag>
  642. <Tag color='blue' size='large' style={{ padding: 15 }}>
  643. RPM: {stat.rpm}
  644. </Tag>
  645. <Tag color='purple' size='large' style={{ padding: 15 }}>
  646. TPM: {stat.tpm}
  647. </Tag>
  648. </Space>
  649. </Spin>
  650. </Header>
  651. <Form layout='horizontal' style={{ marginTop: 10 }}>
  652. <>
  653. <Form.Input
  654. field='token_name'
  655. label={t('令牌名称')}
  656. style={{ width: 176 }}
  657. value={token_name}
  658. placeholder={t('可选值')}
  659. name='token_name'
  660. onChange={(value) => handleInputChange(value, 'token_name')}
  661. />
  662. <Form.Input
  663. field='model_name'
  664. label={t('模型名称')}
  665. style={{ width: 176 }}
  666. value={model_name}
  667. placeholder={t('可选值')}
  668. name='model_name'
  669. onChange={(value) => handleInputChange(value, 'model_name')}
  670. />
  671. <Form.DatePicker
  672. field='start_timestamp'
  673. label={t('起始时间')}
  674. style={{ width: 272 }}
  675. initValue={start_timestamp}
  676. value={start_timestamp}
  677. type='dateTime'
  678. name='start_timestamp'
  679. onChange={(value) => handleInputChange(value, 'start_timestamp')}
  680. />
  681. <Form.DatePicker
  682. field='end_timestamp'
  683. fluid
  684. label={t('结束时间')}
  685. style={{ width: 272 }}
  686. initValue={end_timestamp}
  687. value={end_timestamp}
  688. type='dateTime'
  689. name='end_timestamp'
  690. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  691. />
  692. {isAdminUser && (
  693. <>
  694. <Form.Input
  695. field='channel'
  696. label={t('渠道 ID')}
  697. style={{ width: 176 }}
  698. value={channel}
  699. placeholder={t('可选值')}
  700. name='channel'
  701. onChange={(value) => handleInputChange(value, 'channel')}
  702. />
  703. <Form.Input
  704. field='username'
  705. label={t('用户名称')}
  706. style={{ width: 176 }}
  707. value={username}
  708. placeholder={t('可选值')}
  709. name='username'
  710. onChange={(value) => handleInputChange(value, 'username')}
  711. />
  712. </>
  713. )}
  714. <Button
  715. label={t('查询')}
  716. type='primary'
  717. htmlType='submit'
  718. className='btn-margin-right'
  719. onClick={refresh}
  720. loading={loading}
  721. style={{ marginTop: 24 }}
  722. >
  723. {t('查询')}
  724. </Button>
  725. <Form.Section></Form.Section>
  726. </>
  727. </Form>
  728. <div style={{marginTop:10}}>
  729. <Select
  730. defaultValue='0'
  731. style={{ width: 120 }}
  732. onChange={(value) => {
  733. setLogType(parseInt(value));
  734. loadLogs(0, pageSize, parseInt(value));
  735. }}
  736. >
  737. <Select.Option value='0'>{t('全部')}</Select.Option>
  738. <Select.Option value='1'>{t('充值')}</Select.Option>
  739. <Select.Option value='2'>{t('消费')}</Select.Option>
  740. <Select.Option value='3'>{t('管理')}</Select.Option>
  741. <Select.Option value='4'>{t('系统')}</Select.Option>
  742. </Select>
  743. </div>
  744. <Table
  745. style={{ marginTop: 5 }}
  746. columns={columns}
  747. expandedRowRender={expandRowRender}
  748. expandRowByClick={true}
  749. dataSource={logs}
  750. rowKey="key"
  751. pagination={{
  752. currentPage: activePage,
  753. pageSize: pageSize,
  754. total: logCount,
  755. pageSizeOpts: [10, 20, 50, 100],
  756. showSizeChanger: true,
  757. onPageSizeChange: (size) => {
  758. handlePageSizeChange(size);
  759. },
  760. onPageChange: handlePageChange,
  761. }}
  762. />
  763. </Layout>
  764. </>
  765. );
  766. };
  767. export default LogsTable;