From 1fa2124efd53207d1a0b03198058963a6bcbc912 Mon Sep 17 00:00:00 2001 From: jialin Date: Tue, 30 Sep 2025 18:41:34 +0800 Subject: [PATCH] feat: add allowed models --- src/config/global.d.ts | 12 +- src/pages/_components/transfer.tsx | 41 +++ .../add-apikey-modal/allow-models.tsx | 51 ++++ .../index.tsx} | 10 +- src/pages/api-keys/config/types.ts | 1 + src/pages/api-keys/hooks/use-keys-columns.tsx | 106 ++++++++ src/pages/api-keys/index.tsx | 247 +++++------------- .../components/access-control-modal/form.tsx | 62 ++--- .../components/access-control-modal/index.tsx | 2 +- src/pages/llmodels/user-models.tsx | 6 +- 10 files changed, 311 insertions(+), 227 deletions(-) create mode 100644 src/pages/_components/transfer.tsx create mode 100644 src/pages/api-keys/components/add-apikey-modal/allow-models.tsx rename src/pages/api-keys/components/{add-apikey.tsx => add-apikey-modal/index.tsx} (94%) create mode 100644 src/pages/api-keys/hooks/use-keys-columns.tsx diff --git a/src/config/global.d.ts b/src/config/global.d.ts index 4fbe9d63..693ecb76 100644 --- a/src/config/global.d.ts +++ b/src/config/global.d.ts @@ -1,4 +1,3 @@ -import { PageActionType } from './types'; declare namespace Global { type WithFalse = T | false; interface Pagination { @@ -57,12 +56,21 @@ declare namespace Global { interface ScrollerModalProps { title?: string; - action?: PageActionType; + action?: 'create' | 'update' | 'view' | 'edit'; open: boolean; currentData?: T | null; onOk?: (values: U) => void; onCancel: () => void; } + + interface ActionItem { + label: string; + key: string; + icon: React.ReactNode; + props?: { + danger?: boolean; + }; + } } interface Window { diff --git a/src/pages/_components/transfer.tsx b/src/pages/_components/transfer.tsx new file mode 100644 index 00000000..ccdb4150 --- /dev/null +++ b/src/pages/_components/transfer.tsx @@ -0,0 +1,41 @@ +import { Transfer, TransferProps } from 'antd'; +import styled from 'styled-components'; + +type TransferKey = string | number | bigint; + +const TransferWrap = styled.div` + .ant-transfer-list { + width: 100%; + height: 300px; + } + .ant-transfer-list-content { + .ant-transfer-list-content-item { + &:hover { + background-color: var(--ant-control-item-bg-hover); + } + &.ant-transfer-list-content-item-checked { + background-color: unset; + &:hover { + background-color: var(--ant-control-item-bg-hover); + } + } + } + } + .ant-pagination { + justify-content: center; + } +`; + +interface TransferInnerProps extends TransferProps { + dataSource?: Array<{ key: TransferKey; title: string }>; + targetKeys?: TransferKey[]; +} +const TransferInner: React.FC = (props) => { + return ( + + + + ); +}; + +export default TransferInner; 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 new file mode 100644 index 00000000..74e18de2 --- /dev/null +++ b/src/pages/api-keys/components/add-apikey-modal/allow-models.tsx @@ -0,0 +1,51 @@ +import TransferInner from '@/pages/_components/transfer'; +import { queryModelsList } from '@/pages/llmodels/apis'; +import { Form } from 'antd'; +import { useEffect, useState } from 'react'; + +const AllowModelsForm: React.FC = () => { + const form = Form.useFormInstance(); + const targetKeys = Form.useWatch('allowed_model_names', form); + const [modelList, setModelList] = useState<{ key: string; title: string }[]>( + [] + ); + const [queryParams, setQueryParams] = useState({ + page: 1, + perPage: 100 + }); + + const getModelList = async () => { + try { + const res = await queryModelsList(queryParams); + const options = res.items.map((item) => ({ + title: item.name, + key: item.name + })); + setModelList(options); + } catch (error) {} + }; + + useEffect(() => { + getModelList(); + }, []); + + return ( + + { + form.setFieldsValue({ allowed_model_names: nextTargetKeys }); + }} + render={(item) => item.title} + titles={['Available Models', 'Allowed Models']} + showSearch + filterOption={(inputValue, item) => + item.title.toLowerCase().includes(inputValue.toLowerCase()) + } + > + + ); +}; + +export default AllowModelsForm; diff --git a/src/pages/api-keys/components/add-apikey.tsx b/src/pages/api-keys/components/add-apikey-modal/index.tsx similarity index 94% rename from src/pages/api-keys/components/add-apikey.tsx rename to src/pages/api-keys/components/add-apikey-modal/index.tsx index 2a253bed..2543dd23 100644 --- a/src/pages/api-keys/components/add-apikey.tsx +++ b/src/pages/api-keys/components/add-apikey-modal/index.tsx @@ -9,14 +9,16 @@ 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 { expirationOptions } from '../config'; -import { FormData } from '../config/types'; +import { createApisKey } from '../../apis'; +import { expirationOptions } from '../../config'; +import { FormData, ListItem } from '../../config/types'; +import AllowModelsForm from './allow-models'; type AddModalProps = { title: string; action: PageActionType; open: boolean; + currentData?: Partial | null; onOk: () => void; onCancel: () => void; }; @@ -25,6 +27,7 @@ const AddModal: React.FC = ({ title, action, open, + currentData, onOk, onCancel }) => { @@ -166,6 +169,7 @@ const AddModal: React.FC = ({ required > + name="description" rules={[{ required: false }]} diff --git a/src/pages/api-keys/config/types.ts b/src/pages/api-keys/config/types.ts index e3bf24bb..93462ff3 100644 --- a/src/pages/api-keys/config/types.ts +++ b/src/pages/api-keys/config/types.ts @@ -11,5 +11,6 @@ export interface ListItem { export interface FormData { name: string; description: string; + allowed_model_names: string[]; expires_in: number | null; } diff --git a/src/pages/api-keys/hooks/use-keys-columns.tsx b/src/pages/api-keys/hooks/use-keys-columns.tsx new file mode 100644 index 00000000..443493a6 --- /dev/null +++ b/src/pages/api-keys/hooks/use-keys-columns.tsx @@ -0,0 +1,106 @@ +// columns.ts +import AutoTooltip from '@/components/auto-tooltip'; +import DropdownButtons from '@/components/drop-down-buttons'; +import icons from '@/components/icon-font/icons'; +import { useIntl } from '@umijs/max'; +import type { SortOrder } from 'antd/es/table/interface'; +import { ColumnsType } from 'antd/lib/table'; +import dayjs from 'dayjs'; +import { useMemo } from 'react'; +import { ListItem } from '../config/types'; + +interface ColumnsHookProps { + handleSelect: (val: string, record: ListItem) => void; + sortOrder: SortOrder; +} + +const actionList: Global.ActionItem[] = [ + { + label: 'common.button.edit', + key: 'edit', + icon: icons.EditOutlined + }, + { + label: 'common.button.delete', + key: 'delete', + icon: icons.DeleteOutlined, + props: { danger: true } + } +]; + +const useModelsColumns = ({ + handleSelect, + sortOrder +}: ColumnsHookProps): ColumnsType => { + const intl = useIntl(); + + return useMemo(() => { + return [ + { + title: intl.formatMessage({ id: 'common.table.name' }), + dataIndex: 'name', + key: 'name', + render: (text: string, record: ListItem) => ( + + {text} + + ) + }, + { + title: intl.formatMessage({ id: 'apikeys.form.expiretime' }), + dataIndex: 'expires_at', + key: 'expires_at', + render: (text: string, record: ListItem) => ( + + {text + ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') + : intl.formatMessage({ + id: 'apikeys.form.expiration.never' + })} + + ) + }, + { + title: intl.formatMessage({ id: 'common.table.description' }), + dataIndex: 'description', + key: 'description', + ellipsis: { + showTitle: false + }, + render: (text: string, record: ListItem) => ( + {text} + ) + }, + { + title: intl.formatMessage({ id: 'common.table.createTime' }), + dataIndex: 'created_at', + key: 'created_at', + defaultSortOrder: 'descend', + sortOrder, + sorter: false, + ellipsis: { + showTitle: false + }, + render: (text: number) => ( + + {dayjs(text).format('YYYY-MM-DD HH:mm:ss')} + + ) + }, + { + title: intl.formatMessage({ id: 'common.table.operation' }), + key: 'operation', + dataIndex: 'operation', + span: 3, + render: (text, record) => ( + handleSelect(val, record)} + /> + ) + } + ]; + }, [sortOrder, intl, handleSelect]); +}; + +export default useModelsColumns; diff --git a/src/pages/api-keys/index.tsx b/src/pages/api-keys/index.tsx index 22dde7b4..4a6cff71 100644 --- a/src/pages/api-keys/index.tsx +++ b/src/pages/api-keys/index.tsx @@ -1,35 +1,17 @@ -import AutoTooltip from '@/components/auto-tooltip'; import DeleteModal from '@/components/delete-modal'; -import PageTools from '@/components/page-tools'; +import { FilterBar } from '@/components/page-tools'; import { PageAction } from '@/config'; -import HotKeys from '@/config/hotkeys'; import type { PageActionType } from '@/config/types'; import useTableFetch from '@/hooks/use-table-fetch'; -import { - DeleteOutlined, - PlusOutlined, - SearchOutlined, - SyncOutlined -} from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; import { useIntl } from '@umijs/max'; -import { - Button, - ConfigProvider, - Empty, - Input, - Space, - Table, - Tooltip -} from 'antd'; -import dayjs from 'dayjs'; +import useMemoizedFn from 'ahooks/lib/useMemoizedFn'; +import { ConfigProvider, Empty, Table } from 'antd'; import { useState } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; import { deleteApisKey, queryApisKeysList } from './apis'; -import AddAPIKeyModal from './components/add-apikey'; +import AddAPIKeyModal from './components/add-apikey-modal'; import { ListItem } from './config/types'; - -const { Column } = Table; +import useKeysColumns from './hooks/use-keys-columns'; const APIKeys: React.FC = () => { const { @@ -52,19 +34,45 @@ const APIKeys: React.FC = () => { }); const intl = useIntl(); + const [openAddModal, setOpenAddModal] = useState<{ + open: boolean; + action: PageActionType; + title: string; + currentData?: Partial | null; + }>({ + open: false, + action: PageAction.CREATE, + title: '', + currentData: null + }); - const [openAddModal, setOpenAddModal] = useState(false); - const [action, setAction] = useState(PageAction.CREATE); + const handleAddKey = () => { + setOpenAddModal({ + open: true, + title: intl.formatMessage({ id: 'apikeys.button.create' }), + action: PageAction.CREATE, + currentData: null + }); + }; - const handleAddUser = () => { - setOpenAddModal(true); - setAction(PageAction.CREATE); + const handleEditKey = (record: ListItem) => { + setOpenAddModal({ + open: true, + title: 'Edit API Key', + action: PageAction.EDIT, + currentData: record + }); }; const handleModalOk = async () => { try { await fetchData(); - setOpenAddModal(false); + setOpenAddModal({ + open: false, + title: '', + action: PageAction.CREATE, + currentData: null + }); } catch (error) { // do nothing } @@ -72,9 +80,22 @@ const APIKeys: React.FC = () => { const handleModalCancel = () => { console.log('handleModalCancel'); - setOpenAddModal(false); + setOpenAddModal({ + open: false, + title: '', + action: PageAction.CREATE, + currentData: null + }); }; + const onSelect = useMemoizedFn(async (val: string, record: ListItem) => { + if (val === 'delete') { + handleDelete(record); + } else if (val === 'edit') { + handleEditKey(record); + } + }); + const renderEmpty = (type?: string) => { if (type !== 'Table') return; if ( @@ -87,15 +108,7 @@ const APIKeys: React.FC = () => { return
; }; - useHotkeys( - HotKeys.CREATE, - () => { - handleAddUser(); - }, - { - enabled: !openAddModal - } - ); + const columns = useKeysColumns({ handleSelect: onSelect, sortOrder }); return ( <> @@ -110,56 +123,20 @@ const APIKeys: React.FC = () => { }} extra={[]} > - - - } - placeholder={intl.formatMessage({ id: 'common.filter.name' })} - style={{ width: 300 }} - allowClear - onChange={handleNameChange} - > - - - } - right={ - - - - - } - > + marginTop={30} + buttonText={intl.formatMessage({ id: 'apikeys.button.create' })} + handleSearch={handleSearch} + handleDeleteByBatch={handleDeleteBatch} + handleClickPrimary={handleAddKey} + handleInputChange={handleNameChange} + rowSelection={rowSelection} + width={{ input: 300 }} + > { hideOnSinglePage: queryParams.perPage === 10, onChange: handlePageChange }} - > - { - return ( - - {text} - - ); - }} - /> - - { - return ( - - {text - ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') - : intl.formatMessage({ - id: 'apikeys.form.expiration.never' - })} - - ); - }} - /> - { - return {text}; - }} - /> - { - return ( - - {dayjs(text).format('YYYY-MM-DD HH:mm:ss')} - - ); - }} - /> - { - return ( - - - - - - ); - }} - /> -
+ >
diff --git a/src/pages/llmodels/components/access-control-modal/form.tsx b/src/pages/llmodels/components/access-control-modal/form.tsx index a1a665a2..931b29a5 100644 --- a/src/pages/llmodels/components/access-control-modal/form.tsx +++ b/src/pages/llmodels/components/access-control-modal/form.tsx @@ -1,7 +1,8 @@ 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'; -import { Form, Transfer } from 'antd'; +import { Form } from 'antd'; import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import styled from 'styled-components'; import { queryModelAccessUserList } from '../../apis'; @@ -9,29 +10,6 @@ import { AccessControlFormData, ListItem } from '../../config/types'; type TransferKey = string | number | bigint; -const TransferWrap = styled.div` - .ant-transfer-list { - width: 100%; - height: 400px; - } - .ant-transfer-list-content { - .ant-transfer-list-content-item { - &:hover { - background-color: var(--ant-control-item-bg-hover); - } - &.ant-transfer-list-content-item-checked { - background-color: unset; - &:hover { - background-color: var(--ant-control-item-bg-hover); - } - } - } - } - .ant-pagination { - justify-content: center; - } -`; - const Label = styled.div` font-weight: 500; margin-bottom: 16px; @@ -138,21 +116,27 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { > {setPublic && ( - name="users"> - - 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' + } + ]} + > + 1} + titles={['Available Users', 'Users with Access']} + render={(item) => item.title} + selectAllLabels={[]} + selectionsIcon={} + 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 68563c6e..a6164beb 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={800} + width={600} footer={ } diff --git a/src/pages/llmodels/user-models.tsx b/src/pages/llmodels/user-models.tsx index 69ab4fa1..00ad2f9e 100644 --- a/src/pages/llmodels/user-models.tsx +++ b/src/pages/llmodels/user-models.tsx @@ -9,7 +9,7 @@ import { PageContainer } from '@ant-design/pro-components'; import { useIntl, useNavigate } from '@umijs/max'; import { Button, Input, Space } from 'antd'; import React from 'react'; -import { MODELS_API, queryModelsList } from './apis'; +import { MY_MODELS_API, queryMyModels } from './apis'; import ModelItem from './components/model-item'; import { categoryOptions, modelCategoriesMap } from './config'; import { categoryToPathMap } from './config/button-actions'; @@ -24,8 +24,8 @@ const UserModels: React.FC = () => { handleQueryChange, handleNameChange } = useTableFetch({ - fetchAPI: queryModelsList, - API: MODELS_API, + fetchAPI: queryMyModels, + API: MY_MODELS_API, watch: false }); const intl = useIntl();