From bcd1a339f9027c67c321b1d793ea20e685b0bfde Mon Sep 17 00:00:00 2001 From: jialin Date: Tue, 30 Sep 2025 17:02:26 +0800 Subject: [PATCH] feat: add model access control --- src/components/icon-font/icons.ts | 3 +- src/components/icon-font/index.tsx | 2 +- src/config/global.d.ts | 10 ++ src/pages/llmodels/apis/index.ts | 2 + .../components/access-control-modal/form.tsx | 162 ++++++++++++++++++ .../components/access-control-modal/index.tsx | 62 +++++++ src/pages/llmodels/components/table-list.tsx | 40 +++++ src/pages/llmodels/config/button-actions.ts | 5 + src/pages/llmodels/config/types.ts | 5 + 9 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 src/pages/llmodels/components/access-control-modal/form.tsx create mode 100644 src/pages/llmodels/components/access-control-modal/index.tsx diff --git a/src/components/icon-font/icons.ts b/src/components/icon-font/icons.ts index 823d10f6..51af4dd3 100644 --- a/src/components/icon-font/icons.ts +++ b/src/components/icon-font/icons.ts @@ -49,7 +49,8 @@ const icons = { EditContent: React.createElement(IconFont, { type: 'icon-edit-content' }), Yaml: React.createElement(IconFont, { type: 'icon-code_block' }), Version: React.createElement(IconFont, { type: 'icon-version' }), - Parameter: React.createElement(IconFont, { type: 'icon-parameters' }) + Parameter: React.createElement(IconFont, { type: 'icon-parameters' }), + Private: React.createElement(IconFont, { type: 'icon-private' }) }; export default icons; diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index d31ea97a..5e231a14 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; // import './iconfont/iconfont.js'; const IconFont = createFromIconfontCN({ - scriptUrl: '//at.alicdn.com/t/c/font_4613488_gacm1yqzw1w.js' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_759c66g4c2g.js' }); export default IconFont; diff --git a/src/config/global.d.ts b/src/config/global.d.ts index 1ae72730..4fbe9d63 100644 --- a/src/config/global.d.ts +++ b/src/config/global.d.ts @@ -1,3 +1,4 @@ +import { PageActionType } from './types'; declare namespace Global { type WithFalse = T | false; interface Pagination { @@ -53,6 +54,15 @@ declare namespace Global { type SearchParams = Pagination & { search?: string }; type MessageType = 'transition' | 'warning' | 'danger' | 'success' | 'info'; + + interface ScrollerModalProps { + title?: string; + action?: PageActionType; + open: boolean; + currentData?: T | null; + onOk?: (values: U) => void; + onCancel: () => void; + } } interface Window { diff --git a/src/pages/llmodels/apis/index.ts b/src/pages/llmodels/apis/index.ts index 7131cacc..2634bbf1 100644 --- a/src/pages/llmodels/apis/index.ts +++ b/src/pages/llmodels/apis/index.ts @@ -22,6 +22,8 @@ export const MODEL_EVALUATIONS = '/model-evaluations'; export const BACKEND_LIST_API = '/inference-backends/list'; +export const MY_MODELS_API = '/my-models'; + const setProxyUrl = (url: string) => { return `/proxy?url=${encodeURIComponent(url)}`; }; diff --git a/src/pages/llmodels/components/access-control-modal/form.tsx b/src/pages/llmodels/components/access-control-modal/form.tsx new file mode 100644 index 00000000..a1a665a2 --- /dev/null +++ b/src/pages/llmodels/components/access-control-modal/form.tsx @@ -0,0 +1,162 @@ +import IconFont from '@/components/icon-font'; +import CheckboxField from '@/components/seal-form/checkbox-field'; +import { queryUsersList } from '@/pages/users/apis'; +import { Form, Transfer } from 'antd'; +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import styled from 'styled-components'; +import { queryModelAccessUserList } from '../../apis'; +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; + margin-top: 16px; + font-size: 14px; +`; + +interface AccessControlFormProps { + currentData?: ListItem | null; + onFinish: (values: AccessControlFormData) => void; +} + +const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { + const { currentData, onFinish } = props; + const [form] = Form.useForm(); + const setPublic = Form.useWatch('set_public', form); + const [targetKeys, setTargetKeys] = useState([]); + const [totalPages, setTotalPages] = useState(0); + const [userList, setUserList] = useState<{ title: string; key: number }[]>( + [] + ); + const [queryParams, setQueryParams] = useState({ + page: 1, + perPage: 30 + }); + + const getUserList = async (query: Global.SearchParams) => { + try { + const res = await queryUsersList(query); + const options = res.items.map((item) => ({ + title: item.username, + key: item.id + })); + console.log('options', options); + setTotalPages(res.pagination.totalPage); + setUserList(options); + } catch (error) { + setUserList([]); + } + }; + + const handleOnChange = ( + nextTargetKeys: TransferKey[], + direction: string, + removeKeys: TransferKey[] + ) => { + setTargetKeys(nextTargetKeys); + console.log('nextTargetKeys', nextTargetKeys); + const users = nextTargetKeys.map((key) => ({ id: key })); + form.setFieldsValue({ users }); + }; + + useImperativeHandle(ref, () => ({ + submit: () => { + form.submit(); + }, + setFieldsValue: (values: Partial) => { + form.setFieldsValue(values); + }, + getFieldsValue: () => { + return form.getFieldsValue(); + }, + resetFields: () => { + form.resetFields(); + } + })); + + useEffect(() => { + if (currentData?.id) { + queryModelAccessUserList(currentData.id).then((res) => { + const keys = res.items.map((item) => item.id); + setTargetKeys(keys); + form.setFieldsValue({ + set_public: res.items.length > 0, + users: res.items.map((item) => ({ id: item.id })) + }); + }); + } else { + setTargetKeys([]); + form.setFieldsValue({ users: [] }); + } + getUserList(queryParams); + }, [currentData?.id]); + + return ( +
+ + valuePropName="checked" + name="set_public" + style={{ marginBottom: 24, paddingLeft: 6 }} + > + + + {setPublic && ( + name="users"> + + 1} + titles={['Available Users', 'Users with Access']} + render={(item) => item.title} + selectAllLabels={[]} + selectionsIcon={} + onChange={handleOnChange} + /> + + + )} + + ); +}); + +export default AccessControlForm; diff --git a/src/pages/llmodels/components/access-control-modal/index.tsx b/src/pages/llmodels/components/access-control-modal/index.tsx new file mode 100644 index 00000000..68563c6e --- /dev/null +++ b/src/pages/llmodels/components/access-control-modal/index.tsx @@ -0,0 +1,62 @@ +import ModalFooter from '@/components/modal-footer'; +import ScrollerModal from '@/components/scroller-modal'; +import { useIntl } from '@umijs/max'; +import { message } from 'antd'; +import { useRef } from 'react'; +import { updateModelAccessUser } from '../../apis'; +import { AccessControlFormData, ListItem } from '../../config/types'; +import AccessControlForm from './form'; + +const AccessControlModal: React.FC< + Global.ScrollerModalProps +> = ({ open, title, currentData, action, onOk, onCancel }) => { + const intl = useIntl(); + const form = useRef(null); + + const handleSumit = () => { + form.current?.submit(); + }; + + const handleOnFinish = async (values: AccessControlFormData) => { + console.log('onFinish', values); + try { + const data = { + set_public: !values.set_public, + users: values.users || [] + }; + await updateModelAccessUser({ + id: currentData?.id as number, + data: data + }); + message.success(intl.formatMessage({ id: 'common.message.success' })); + onCancel?.(); + } catch (error) { + message.error(intl.formatMessage({ id: 'common.message.failed' })); + } + }; + + return ( + + } + > + + + ); +}; + +export default AccessControlModal; diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 22dcb7c8..6780dcd3 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -7,6 +7,7 @@ import PageTools from '@/components/page-tools'; import BaseSelect from '@/components/seal-form/base/select'; import SealTable from '@/components/seal-table'; import { PageAction } from '@/config'; +import { PageActionType } from '@/config/types'; import useBodyScroll from '@/hooks/use-body-scroll'; import useExpandedRowKeys from '@/hooks/use-expanded-row-keys'; import useTableRowSelection from '@/hooks/use-table-row-selection'; @@ -56,6 +57,7 @@ import { } from '../config/types'; import useFormInitialValues from '../hooks/use-form-initial-values'; import useModelsColumns from '../hooks/use-models-columns'; +import AccessControlModal from './access-control-modal'; import APIAccessInfoModal from './api-access-info'; import DeployModal from './deploy-modal'; import Instances from './instances'; @@ -177,6 +179,17 @@ const Models: React.FC = ({ url: '', status: '' }); + const [openAccessControlModal, setOpenAccessControlModal] = useState<{ + open: boolean; + currentData: ListItem | null; + title: string; + action: PageActionType; + }>({ + open: false, + currentData: null, + title: '', + action: PageAction.CREATE + }); const modalRef = useRef(null); useEffect(() => { @@ -451,6 +464,15 @@ const Models: React.FC = ({ } }); } + + if (val === 'accessControl') { + setOpenAccessControlModal({ + title: 'Edit Access Control', + action: PageAction.EDIT, + currentData: row, + open: true + }); + } } catch (error) { // ignore } @@ -557,6 +579,16 @@ const Models: React.FC = ({ } }); + const handleCancelAccessControl = () => { + setOpenAccessControlModal({ + ...openAccessControlModal, + currentData: null, + open: false + }); + }; + + const handleSubmitAccessControl = (data: any) => {}; + return ( <> = ({ }); }} > + ); }; diff --git a/src/pages/llmodels/config/button-actions.ts b/src/pages/llmodels/config/button-actions.ts index 52b93cfe..860b71ec 100644 --- a/src/pages/llmodels/config/button-actions.ts +++ b/src/pages/llmodels/config/button-actions.ts @@ -61,6 +61,11 @@ export const ActionList: ActionItem[] = [ key: 'api', icon: icons.ApiOutlined }, + { + label: 'Access Control', + key: 'accessControl', + icon: icons.Private + }, { label: 'common.button.stop', key: 'stop', diff --git a/src/pages/llmodels/config/types.ts b/src/pages/llmodels/config/types.ts index 7c961d16..81576db3 100644 --- a/src/pages/llmodels/config/types.ts +++ b/src/pages/llmodels/config/types.ts @@ -252,3 +252,8 @@ export interface BackendOption { default_version: string; versions: { label: string; value: string }[]; } + +export interface AccessControlFormData { + set_public: boolean; + users: { id: number }[]; +}