TokensTable.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. import React, { useEffect, useState } from 'react';
  2. import {
  3. API,
  4. copy,
  5. showError,
  6. showSuccess,
  7. timestamp2string,
  8. } from '../helpers';
  9. import { ITEMS_PER_PAGE } from '../constants';
  10. import { renderGroup, renderQuota } from '../helpers/render';
  11. import {
  12. Button,
  13. Divider,
  14. Dropdown,
  15. Form,
  16. Modal,
  17. Popconfirm,
  18. Popover,
  19. Space,
  20. SplitButtonGroup,
  21. Table,
  22. Tag,
  23. } from '@douyinfe/semi-ui';
  24. import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
  25. import EditToken from '../pages/Token/EditToken';
  26. import { useTranslation } from 'react-i18next';
  27. function renderTimestamp(timestamp) {
  28. return <>{timestamp2string(timestamp)}</>;
  29. }
  30. const TokensTable = () => {
  31. const { t } = useTranslation();
  32. const renderStatus = (status, model_limits_enabled = false) => {
  33. switch (status) {
  34. case 1:
  35. if (model_limits_enabled) {
  36. return (
  37. <Tag color='green' size='large'>
  38. {t('已启用:限制模型')}
  39. </Tag>
  40. );
  41. } else {
  42. return (
  43. <Tag color='green' size='large'>
  44. {t('已启用')}
  45. </Tag>
  46. );
  47. }
  48. case 2:
  49. return (
  50. <Tag color='red' size='large'>
  51. {t('已禁用')}
  52. </Tag>
  53. );
  54. case 3:
  55. return (
  56. <Tag color='yellow' size='large'>
  57. {t('已过期')}
  58. </Tag>
  59. );
  60. case 4:
  61. return (
  62. <Tag color='grey' size='large'>
  63. {t('已耗尽')}
  64. </Tag>
  65. );
  66. default:
  67. return (
  68. <Tag color='black' size='large'>
  69. {t('未知状态')}
  70. </Tag>
  71. );
  72. }
  73. };
  74. const columns = [
  75. {
  76. title: t('名称'),
  77. dataIndex: 'name',
  78. },
  79. {
  80. title: t('状态'),
  81. dataIndex: 'status',
  82. key: 'status',
  83. render: (text, record, index) => {
  84. return (
  85. <div>
  86. <Space>
  87. {renderStatus(text, record.model_limits_enabled)}
  88. {renderGroup(record.group)}
  89. </Space>
  90. </div>
  91. );
  92. },
  93. },
  94. {
  95. title: t('已用额度'),
  96. dataIndex: 'used_quota',
  97. render: (text, record, index) => {
  98. return <div>{renderQuota(parseInt(text))}</div>;
  99. },
  100. },
  101. {
  102. title: t('剩余额度'),
  103. dataIndex: 'remain_quota',
  104. render: (text, record, index) => {
  105. return (
  106. <div>
  107. {record.unlimited_quota ? (
  108. <Tag size={'large'} color={'white'}>
  109. {t('无限制')}
  110. </Tag>
  111. ) : (
  112. <Tag size={'large'} color={'light-blue'}>
  113. {renderQuota(parseInt(text))}
  114. </Tag>
  115. )}
  116. </div>
  117. );
  118. },
  119. },
  120. {
  121. title: t('创建时间'),
  122. dataIndex: 'created_time',
  123. render: (text, record, index) => {
  124. return <div>{renderTimestamp(text)}</div>;
  125. },
  126. },
  127. {
  128. title: t('过期时间'),
  129. dataIndex: 'expired_time',
  130. render: (text, record, index) => {
  131. return (
  132. <div>
  133. {record.expired_time === -1 ? t('永不过期') : renderTimestamp(text)}
  134. </div>
  135. );
  136. },
  137. },
  138. {
  139. title: '',
  140. dataIndex: 'operate',
  141. render: (text, record, index) => {
  142. let chats = localStorage.getItem('chats');
  143. let chatsArray = [];
  144. let shouldUseCustom = true;
  145. if (shouldUseCustom) {
  146. try {
  147. // console.log(chats);
  148. chats = JSON.parse(chats);
  149. // check chats is array
  150. if (Array.isArray(chats)) {
  151. for (let i = 0; i < chats.length; i++) {
  152. let chat = {};
  153. chat.node = 'item';
  154. // c is a map
  155. // chat.key = chats[i].name;
  156. // console.log(chats[i])
  157. for (let key in chats[i]) {
  158. if (chats[i].hasOwnProperty(key)) {
  159. chat.key = i;
  160. chat.name = key;
  161. chat.onClick = () => {
  162. onOpenLink(key, chats[i][key], record);
  163. };
  164. }
  165. }
  166. chatsArray.push(chat);
  167. }
  168. }
  169. } catch (e) {
  170. console.log(e);
  171. showError(t('聊天链接配置错误,请联系管理员'));
  172. }
  173. }
  174. return (
  175. <div>
  176. <Popover
  177. content={'sk-' + record.key}
  178. style={{ padding: 20 }}
  179. position='top'
  180. >
  181. <Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
  182. {t('查看')}
  183. </Button>
  184. </Popover>
  185. <Button
  186. theme='light'
  187. type='secondary'
  188. style={{ marginRight: 1 }}
  189. onClick={async (text) => {
  190. await copyText('sk-' + record.key);
  191. }}
  192. >
  193. {t('复制')}
  194. </Button>
  195. <SplitButtonGroup
  196. style={{ marginRight: 1 }}
  197. aria-label={t('项目操作按钮组')}
  198. >
  199. <Button
  200. theme='light'
  201. style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
  202. onClick={() => {
  203. if (chatsArray.length === 0) {
  204. showError(t('请联系管理员配置聊天链接'));
  205. } else {
  206. onOpenLink(
  207. 'default',
  208. chats[0][Object.keys(chats[0])[0]],
  209. record,
  210. );
  211. }
  212. }}
  213. >
  214. {t('聊天')}
  215. </Button>
  216. <Dropdown
  217. trigger='click'
  218. position='bottomRight'
  219. menu={chatsArray}
  220. >
  221. <Button
  222. style={{
  223. padding: '8px 4px',
  224. color: 'rgba(var(--semi-teal-7), 1)',
  225. }}
  226. type='primary'
  227. icon={<IconTreeTriangleDown />}
  228. ></Button>
  229. </Dropdown>
  230. </SplitButtonGroup>
  231. <Popconfirm
  232. title={t('确定是否要删除此令牌?')}
  233. content={t('此修改将不可逆')}
  234. okType={'danger'}
  235. position={'left'}
  236. onConfirm={() => {
  237. manageToken(record.id, 'delete', record).then(() => {
  238. removeRecord(record.key);
  239. });
  240. }}
  241. >
  242. <Button theme='light' type='danger' style={{ marginRight: 1 }}>
  243. {t('删除')}
  244. </Button>
  245. </Popconfirm>
  246. {record.status === 1 ? (
  247. <Button
  248. theme='light'
  249. type='warning'
  250. style={{ marginRight: 1 }}
  251. onClick={async () => {
  252. manageToken(record.id, 'disable', record);
  253. }}
  254. >
  255. {t('禁用')}
  256. </Button>
  257. ) : (
  258. <Button
  259. theme='light'
  260. type='secondary'
  261. style={{ marginRight: 1 }}
  262. onClick={async () => {
  263. manageToken(record.id, 'enable', record);
  264. }}
  265. >
  266. {t('启用')}
  267. </Button>
  268. )}
  269. <Button
  270. theme='light'
  271. type='tertiary'
  272. style={{ marginRight: 1 }}
  273. onClick={() => {
  274. setEditingToken(record);
  275. setShowEdit(true);
  276. }}
  277. >
  278. {t('编辑')}
  279. </Button>
  280. </div>
  281. );
  282. },
  283. },
  284. ];
  285. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  286. const [showEdit, setShowEdit] = useState(false);
  287. const [tokens, setTokens] = useState([]);
  288. const [selectedKeys, setSelectedKeys] = useState([]);
  289. const [tokenCount, setTokenCount] = useState(pageSize);
  290. const [loading, setLoading] = useState(true);
  291. const [activePage, setActivePage] = useState(1);
  292. const [searchKeyword, setSearchKeyword] = useState('');
  293. const [searchToken, setSearchToken] = useState('');
  294. const [searching, setSearching] = useState(false);
  295. const [chats, setChats] = useState([]);
  296. const [editingToken, setEditingToken] = useState({
  297. id: undefined,
  298. });
  299. const closeEdit = () => {
  300. setShowEdit(false);
  301. setTimeout(() => {
  302. setEditingToken({
  303. id: undefined,
  304. });
  305. }, 500);
  306. };
  307. const setTokensFormat = (tokens) => {
  308. setTokens(tokens);
  309. if (tokens.length >= pageSize) {
  310. setTokenCount(tokens.length + pageSize);
  311. } else {
  312. setTokenCount(tokens.length);
  313. }
  314. };
  315. let pageData = tokens.slice(
  316. (activePage - 1) * pageSize,
  317. activePage * pageSize,
  318. );
  319. const loadTokens = async (startIdx) => {
  320. setLoading(true);
  321. const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`);
  322. const { success, message, data } = res.data;
  323. if (success) {
  324. if (startIdx === 0) {
  325. setTokensFormat(data);
  326. } else {
  327. let newTokens = [...tokens];
  328. newTokens.splice(startIdx * pageSize, data.length, ...data);
  329. setTokensFormat(newTokens);
  330. }
  331. } else {
  332. showError(message);
  333. }
  334. setLoading(false);
  335. };
  336. const refresh = async () => {
  337. await loadTokens(activePage - 1);
  338. };
  339. const copyText = async (text) => {
  340. if (await copy(text)) {
  341. showSuccess(t('已复制到剪贴板!'));
  342. } else {
  343. Modal.error({
  344. title: t('无法复制到剪贴板,请手动复制'),
  345. content: text,
  346. size: 'large',
  347. });
  348. }
  349. };
  350. const onOpenLink = async (type, url, record) => {
  351. // console.log(type, url, key);
  352. let status = localStorage.getItem('status');
  353. let serverAddress = '';
  354. if (status) {
  355. status = JSON.parse(status);
  356. serverAddress = status.server_address;
  357. }
  358. if (serverAddress === '') {
  359. serverAddress = window.location.origin;
  360. }
  361. let encodedServerAddress = encodeURIComponent(serverAddress);
  362. url = url.replaceAll('{address}', encodedServerAddress);
  363. url = url.replaceAll('{key}', 'sk-' + record.key);
  364. window.open(url, '_blank');
  365. };
  366. useEffect(() => {
  367. loadTokens(0)
  368. .then()
  369. .catch((reason) => {
  370. showError(reason);
  371. });
  372. }, [pageSize]);
  373. const removeRecord = (key) => {
  374. let newDataSource = [...tokens];
  375. if (key != null) {
  376. let idx = newDataSource.findIndex((data) => data.key === key);
  377. if (idx > -1) {
  378. newDataSource.splice(idx, 1);
  379. setTokensFormat(newDataSource);
  380. }
  381. }
  382. };
  383. const manageToken = async (id, action, record) => {
  384. setLoading(true);
  385. let data = { id };
  386. let res;
  387. switch (action) {
  388. case 'delete':
  389. res = await API.delete(`/api/token/${id}/`);
  390. break;
  391. case 'enable':
  392. data.status = 1;
  393. res = await API.put('/api/token/?status_only=true', data);
  394. break;
  395. case 'disable':
  396. data.status = 2;
  397. res = await API.put('/api/token/?status_only=true', data);
  398. break;
  399. }
  400. const { success, message } = res.data;
  401. if (success) {
  402. showSuccess('操作成功完成!');
  403. let token = res.data.data;
  404. let newTokens = [...tokens];
  405. // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
  406. if (action === 'delete') {
  407. } else {
  408. record.status = token.status;
  409. // newTokens[realIdx].status = token.status;
  410. }
  411. setTokensFormat(newTokens);
  412. } else {
  413. showError(message);
  414. }
  415. setLoading(false);
  416. };
  417. const searchTokens = async () => {
  418. if (searchKeyword === '' && searchToken === '') {
  419. // if keyword is blank, load files instead.
  420. await loadTokens(0);
  421. setActivePage(1);
  422. return;
  423. }
  424. setSearching(true);
  425. const res = await API.get(
  426. `/api/token/search?keyword=${searchKeyword}&token=${searchToken}`,
  427. );
  428. const { success, message, data } = res.data;
  429. if (success) {
  430. setTokensFormat(data);
  431. setActivePage(1);
  432. } else {
  433. showError(message);
  434. }
  435. setSearching(false);
  436. };
  437. const handleKeywordChange = async (value) => {
  438. setSearchKeyword(value.trim());
  439. };
  440. const handleSearchTokenChange = async (value) => {
  441. setSearchToken(value.trim());
  442. };
  443. const sortToken = (key) => {
  444. if (tokens.length === 0) return;
  445. setLoading(true);
  446. let sortedTokens = [...tokens];
  447. sortedTokens.sort((a, b) => {
  448. return ('' + a[key]).localeCompare(b[key]);
  449. });
  450. if (sortedTokens[0].id === tokens[0].id) {
  451. sortedTokens.reverse();
  452. }
  453. setTokens(sortedTokens);
  454. setLoading(false);
  455. };
  456. const handlePageChange = (page) => {
  457. setActivePage(page);
  458. if (page === Math.ceil(tokens.length / pageSize) + 1) {
  459. // In this case we have to load more data and then append them.
  460. loadTokens(page - 1).then((r) => {});
  461. }
  462. };
  463. const rowSelection = {
  464. onSelect: (record, selected) => {},
  465. onSelectAll: (selected, selectedRows) => {},
  466. onChange: (selectedRowKeys, selectedRows) => {
  467. setSelectedKeys(selectedRows);
  468. },
  469. };
  470. const handleRow = (record, index) => {
  471. if (record.status !== 1) {
  472. return {
  473. style: {
  474. background: 'var(--semi-color-disabled-border)',
  475. },
  476. };
  477. } else {
  478. return {};
  479. }
  480. };
  481. return (
  482. <>
  483. <EditToken
  484. refresh={refresh}
  485. editingToken={editingToken}
  486. visiable={showEdit}
  487. handleClose={closeEdit}
  488. ></EditToken>
  489. <Form
  490. layout='horizontal'
  491. style={{ marginTop: 10 }}
  492. labelPosition={'left'}
  493. >
  494. <Form.Input
  495. field='keyword'
  496. label={t('搜索关键字')}
  497. placeholder={t('令牌名称')}
  498. value={searchKeyword}
  499. loading={searching}
  500. onChange={handleKeywordChange}
  501. />
  502. <Form.Input
  503. field='token'
  504. label={t('密钥')}
  505. placeholder={t('密钥')}
  506. value={searchToken}
  507. loading={searching}
  508. onChange={handleSearchTokenChange}
  509. />
  510. <Button
  511. label={t('查询')}
  512. type='primary'
  513. htmlType='submit'
  514. className='btn-margin-right'
  515. onClick={searchTokens}
  516. style={{ marginRight: 8 }}
  517. >
  518. {t('查询')}
  519. </Button>
  520. </Form>
  521. <Divider style={{ margin: '15px 0' }} />
  522. <div>
  523. <Button
  524. theme='light'
  525. type='primary'
  526. style={{ marginRight: 8 }}
  527. onClick={() => {
  528. setEditingToken({
  529. id: undefined,
  530. });
  531. setShowEdit(true);
  532. }}
  533. >
  534. {t('添加令牌')}
  535. </Button>
  536. <Button
  537. label={t('复制所选令牌')}
  538. type='warning'
  539. onClick={async () => {
  540. if (selectedKeys.length === 0) {
  541. showError(t('请至少选择一个令牌!'));
  542. return;
  543. }
  544. let keys = '';
  545. for (let i = 0; i < selectedKeys.length; i++) {
  546. keys +=
  547. selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
  548. }
  549. await copyText(keys);
  550. }}
  551. >
  552. {t('复制所选令牌到剪贴板')}
  553. </Button>
  554. </div>
  555. <Table
  556. style={{ marginTop: 20 }}
  557. columns={columns}
  558. dataSource={pageData}
  559. pagination={{
  560. currentPage: activePage,
  561. pageSize: pageSize,
  562. total: tokenCount,
  563. showSizeChanger: true,
  564. pageSizeOptions: [10, 20, 50, 100],
  565. formatPageText: (page) =>
  566. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  567. start: page.currentStart,
  568. end: page.currentEnd,
  569. total: tokens.length,
  570. }),
  571. onPageSizeChange: (size) => {
  572. setPageSize(size);
  573. setActivePage(1);
  574. },
  575. onPageChange: handlePageChange,
  576. }}
  577. loading={loading}
  578. rowSelection={rowSelection}
  579. onRow={handleRow}
  580. ></Table>
  581. </>
  582. );
  583. };
  584. export default TokensTable;