diff --git a/config/routes.ts b/config/routes.ts index 3f1c0ed5..de65274b 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -70,7 +70,6 @@ export default [ name: 'models', path: '/models', key: 'models', - access: 'canSeeAdmin', routes: [ { path: '/models', @@ -103,7 +102,7 @@ export default [ icon: 'icon-models', selectedIcon: 'icon-models-filled', defaultIcon: 'icon-models', - access: 'canSeeAdmin', + access: 'canSeeUser', component: './llmodels/user-models' }, { diff --git a/src/access.ts b/src/access.ts index e2521d14..c5dd7dc1 100644 --- a/src/access.ts +++ b/src/access.ts @@ -4,8 +4,15 @@ export default (initialState: { currentUser?: Global.UserInfo }) => { initialState.currentUser && initialState.currentUser.is_admin ); + const canSeeUser = !!( + initialState && + initialState.currentUser && + !initialState.currentUser.is_admin + ); + return { canSeeAdmin, + canSeeUser, canDelete: true, canLogin: true }; diff --git a/src/locales/en-US/apikeys.ts b/src/locales/en-US/apikeys.ts index 5ed77840..97e96696 100644 --- a/src/locales/en-US/apikeys.ts +++ b/src/locales/en-US/apikeys.ts @@ -11,5 +11,6 @@ export default { 'apikeys.form.expiration.7days': '7 days', 'apikeys.form.expiration.1month': '1 month', 'apikeys.form.expiration.6months': '6 months', - 'apikeys.form.expiration.never': 'No expiration' + 'apikeys.form.expiration.never': 'No expiration', + 'apikeys.table.bindModels': 'Allowed Models' }; diff --git a/src/locales/ja-JP/apikeys.ts b/src/locales/ja-JP/apikeys.ts index 5306587c..8627e6bb 100644 --- a/src/locales/ja-JP/apikeys.ts +++ b/src/locales/ja-JP/apikeys.ts @@ -11,5 +11,6 @@ export default { 'apikeys.form.expiration.7days': '7日間', 'apikeys.form.expiration.1month': '1ヶ月', 'apikeys.form.expiration.6months': '6ヶ月', - 'apikeys.form.expiration.never': '無期限' + 'apikeys.form.expiration.never': '無期限', + 'apikeys.table.bindModels': '許可されたモデル' }; diff --git a/src/locales/ru-RU/apikeys.ts b/src/locales/ru-RU/apikeys.ts index df869dc1..b384f2b6 100644 --- a/src/locales/ru-RU/apikeys.ts +++ b/src/locales/ru-RU/apikeys.ts @@ -11,5 +11,6 @@ export default { 'apikeys.form.expiration.7days': '7 дней', 'apikeys.form.expiration.1month': '1 месяц', 'apikeys.form.expiration.6months': '6 месяцев', - 'apikeys.form.expiration.never': 'Без срока действия' + 'apikeys.form.expiration.never': 'Без срока действия', + 'apikeys.table.bindModels': 'Разрешенные модели' }; diff --git a/src/locales/zh-CN/apikeys.ts b/src/locales/zh-CN/apikeys.ts index 90303ad0..d5fcee61 100644 --- a/src/locales/zh-CN/apikeys.ts +++ b/src/locales/zh-CN/apikeys.ts @@ -10,5 +10,6 @@ export default { 'apikeys.form.expiration.7days': '7天', 'apikeys.form.expiration.1month': '1个月', 'apikeys.form.expiration.6months': '6个月', - 'apikeys.form.expiration.never': '永不过期' + 'apikeys.form.expiration.never': '永不过期', + 'apikeys.table.bindModels': '绑定模型' }; diff --git a/src/pages/_components/transfer.tsx b/src/pages/_components/transfer.tsx index ccdb4150..4b40885d 100644 --- a/src/pages/_components/transfer.tsx +++ b/src/pages/_components/transfer.tsx @@ -1,12 +1,18 @@ -import { Transfer, TransferProps } from 'antd'; +import { MoreOutlined } from '@ant-design/icons'; +import { Pagination, Transfer, TransferProps } from 'antd'; +import { useState } from 'react'; import styled from 'styled-components'; type TransferKey = string | number | bigint; +const PaginationWrapper = styled.div` + padding: 4px 16px; +`; + const TransferWrap = styled.div` .ant-transfer-list { width: 100%; - height: 300px; + height: 360px; } .ant-transfer-list-content { .ant-transfer-list-content-item { @@ -27,13 +33,48 @@ const TransferWrap = styled.div` `; interface TransferInnerProps extends TransferProps { + total?: number; + perPage?: number; + onPageChange?: (page: number, perPage?: number) => void; dataSource?: Array<{ key: TransferKey; title: string }>; targetKeys?: TransferKey[]; } const TransferInner: React.FC = (props) => { + const [page, setPage] = useState(1); + const { onPageChange, total, perPage = 30 } = props; + + const handleOnPageChange = (page: number, perPage?: number) => { + setPage(page); + onPageChange?.(page, perPage); + }; + + const renderFooter = (TransferProps: any, { direction }: any) => { + if (direction === 'left' && total && total > perPage!) { + return ( + + + + ); + } + return null; + }; + return ( - + + } + > ); }; diff --git a/src/pages/api-keys/apis/index.ts b/src/pages/api-keys/apis/index.ts index a339b379..34d26406 100644 --- a/src/pages/api-keys/apis/index.ts +++ b/src/pages/api-keys/apis/index.ts @@ -17,6 +17,13 @@ export async function createApisKey(params: { data: FormData }) { }); } +export async function updateApisKey(id: number, params: { data: FormData }) { + return request(`${APIS_KEYS_API}/${id}`, { + method: 'PUT', + data: params.data + }); +} + export async function deleteApisKey(id: number) { return request(`${APIS_KEYS_API}/${id}`, { method: 'DELETE' diff --git a/src/pages/api-keys/components/add-apikey-modal/allow-models.tsx b/src/pages/api-keys/components/add-apikey-modal/allow-models.tsx index 74e18de2..c321a278 100644 --- a/src/pages/api-keys/components/add-apikey-modal/allow-models.tsx +++ b/src/pages/api-keys/components/add-apikey-modal/allow-models.tsx @@ -39,7 +39,9 @@ const AllowModelsForm: React.FC = () => { }} render={(item) => item.title} titles={['Available Models', 'Allowed Models']} - showSearch + showSearch={{ + placeholder: 'Filter by model name' + }} filterOption={(inputValue, item) => item.title.toLowerCase().includes(inputValue.toLowerCase()) } diff --git a/src/pages/api-keys/components/add-apikey-modal/form.tsx b/src/pages/api-keys/components/add-apikey-modal/form.tsx new file mode 100644 index 00000000..9dbd6c53 --- /dev/null +++ b/src/pages/api-keys/components/add-apikey-modal/form.tsx @@ -0,0 +1,67 @@ +import SealInput from '@/components/seal-form/seal-input'; +import SealSelect from '@/components/seal-form/seal-select'; +import { useIntl } from '@umijs/max'; +import { Form } from 'antd'; +import React from 'react'; +import { expirationOptions } from '../../config'; +import { FormData } from '../../config/types'; +import AllowModelsForm from './allow-models'; + +const APIKeyForm: React.FC = () => { + const intl = useIntl(); + + return ( + <> + + name="name" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: intl.formatMessage({ id: 'common.table.name' }) + } + ) + } + ]} + > + + + + name="expires_in" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.select' }, + { + name: intl.formatMessage({ + id: 'apikeys.form.expiretime' + }) + } + ) + } + ]} + > + + + + name="description" rules={[{ required: false }]}> + + + + ); +}; + +export default APIKeyForm; diff --git a/src/pages/api-keys/components/add-apikey-modal/index.tsx b/src/pages/api-keys/components/add-apikey-modal/index.tsx index 2543dd23..7f93031e 100644 --- a/src/pages/api-keys/components/add-apikey-modal/index.tsx +++ b/src/pages/api-keys/components/add-apikey-modal/index.tsx @@ -2,17 +2,17 @@ import CopyButton from '@/components/copy-button'; import ModalFooter from '@/components/modal-footer'; import ScrollerModal from '@/components/scroller-modal'; import SealInput from '@/components/seal-form/seal-input'; -import SealSelect from '@/components/seal-form/seal-select'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import { useIntl } from '@umijs/max'; import { Button, Form, Tag } from 'antd'; import dayjs from 'dayjs'; import { useEffect, useState } from 'react'; -import { createApisKey } from '../../apis'; +import { createApisKey, updateApisKey } from '../../apis'; import { expirationOptions } from '../../config'; import { FormData, ListItem } from '../../config/types'; import AllowModelsForm from './allow-models'; +import APIKeyForm from './form'; type AddModalProps = { title: string; @@ -43,6 +43,13 @@ const AddModal: React.FC = ({ expires_in: 1 }); } + if (action === PageAction.EDIT && currentData && open) { + form.setFieldsValue({ + name: currentData.name, + description: currentData.description, + allowed_model_names: currentData.allowed_model_names || [] + }); + } }; useEffect(() => { @@ -67,16 +74,29 @@ const AddModal: React.FC = ({ return res; }; + const createAPIKey = async (data: FormData) => { + const params = { + ...data, + expires_in: getExpireValue(data.expires_in) + }; + const res = await createApisKey({ data: params }); + setAPIKeyValue(res.value); + setShowKey(true); + }; + + const updateAPIKey = async (data: FormData) => { + await updateApisKey(currentData?.id as number, { data }); + onOk(); + }; + const handleOnOk = async (data: FormData) => { try { setLoading(true); - const params = { - ...data, - expires_in: getExpireValue(data.expires_in) - }; - const res = await createApisKey({ data: params }); - setAPIKeyValue(res.value); - setShowKey(true); + if (action === PageAction.CREATE) { + await createAPIKey(data); + } else if (action === PageAction.EDIT && currentData?.id) { + await updateAPIKey(data); + } setLoading(false); } catch (error) { setLoading(false); @@ -109,7 +129,7 @@ const AddModal: React.FC = ({ closeIcon={false} maskClosable={false} keyboard={false} - width={600} + width={700} styles={{}} footer={ !showKey ? ( @@ -126,60 +146,9 @@ const AddModal: React.FC = ({ } >
- {!showKey ? ( - <> - - name="name" - rules={[ - { - required: true, - message: intl.formatMessage( - { id: 'common.form.rule.input' }, - { - name: intl.formatMessage({ id: 'common.table.name' }) - } - ) - } - ]} - > - - - - name="expires_in" - rules={[ - { - required: true, - message: intl.formatMessage( - { id: 'common.form.rule.select' }, - { - name: intl.formatMessage({ - id: 'apikeys.form.expiretime' - }) - } - ) - } - ]} - > - - - - - name="description" - rules={[{ required: false }]} - > - - - - ) : ( + {action === PageAction.EDIT && } + {!showKey && action === PageAction.CREATE && } + {showKey && action === PageAction.CREATE && (
) }, + { + title: intl.formatMessage({ id: 'apikeys.table.bindModels' }), + dataIndex: 'allowed_model_names', + key: 'allowed_model_names', + ellipsis: { + showTitle: false + }, + render: (text: string[], record: ListItem) => ( + {text?.join(', ')} + ) + }, { title: intl.formatMessage({ id: 'common.table.description' }), dataIndex: 'description', diff --git a/src/pages/api-keys/index.tsx b/src/pages/api-keys/index.tsx index 4a6cff71..534c5274 100644 --- a/src/pages/api-keys/index.tsx +++ b/src/pages/api-keys/index.tsx @@ -58,7 +58,7 @@ const APIKeys: React.FC = () => { const handleEditKey = (record: ListItem) => { setOpenAddModal({ open: true, - title: 'Edit API Key', + title: 'Edit Allowed Models', action: PageAction.EDIT, currentData: record }); @@ -79,7 +79,6 @@ const APIKeys: React.FC = () => { }; const handleModalCancel = () => { - console.log('handleModalCancel'); setOpenAddModal({ open: false, title: '', diff --git a/src/pages/llmodels/components/access-control-modal/form.tsx b/src/pages/llmodels/components/access-control-modal/form.tsx index 931b29a5..4caad31d 100644 --- a/src/pages/llmodels/components/access-control-modal/form.tsx +++ b/src/pages/llmodels/components/access-control-modal/form.tsx @@ -1,4 +1,3 @@ -import IconFont from '@/components/icon-font'; import CheckboxField from '@/components/seal-form/checkbox-field'; import TransferInner from '@/pages/_components/transfer'; import { queryUsersList } from '@/pages/users/apis'; @@ -33,7 +32,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { ); const [queryParams, setQueryParams] = useState({ page: 1, - perPage: 30 + perPage: 100 }); const getUserList = async (query: Global.SearchParams) => { @@ -62,6 +61,10 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { form.setFieldsValue({ users }); }; + const onSearch = (dir: 'left' | 'right', value: string) => { + console.log('search:', dir, value); + }; + useImperativeHandle(ref, () => ({ submit: () => { form.submit(); @@ -116,28 +119,34 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { > {setPublic && ( - - name="users" - rules={[ - { - required: true, - message: 'Please select at least one user' - } - ]} - > - 1} - titles={['Available Users', 'Users with Access']} - render={(item) => item.title} - selectAllLabels={[]} - selectionsIcon={} - onChange={handleOnChange} - /> - + <> + + + name="users" + rules={[ + { + required: true, + message: 'Please select at least one user' + } + ]} + > + item.title} + selectAllLabels={[]} + onSearch={onSearch} + onChange={handleOnChange} + /> + + )} ); diff --git a/src/pages/llmodels/components/access-control-modal/index.tsx b/src/pages/llmodels/components/access-control-modal/index.tsx index a6164beb..ee1bbe4b 100644 --- a/src/pages/llmodels/components/access-control-modal/index.tsx +++ b/src/pages/llmodels/components/access-control-modal/index.tsx @@ -45,7 +45,7 @@ const AccessControlModal: React.FC< closeIcon={true} maskClosable={false} keyboard={false} - width={600} + width={700} footer={ }