LogsTable.js 21 KB

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