feat: add model access control

This commit is contained in:
jialin
2025-10-13 15:21:17 +08:00
parent c41e5d9c55
commit bcd1a339f9
9 changed files with 289 additions and 2 deletions
+2 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+10
View File
@@ -1,3 +1,4 @@
import { PageActionType } from './types';
declare namespace Global {
type WithFalse<T> = 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<T = unknown, U = unknown> {
title?: string;
action?: PageActionType;
open: boolean;
currentData?: T | null;
onOk?: (values: U) => void;
onCancel: () => void;
}
}
interface Window {
+2
View File
@@ -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)}`;
};
@@ -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<TransferKey[]>([]);
const [totalPages, setTotalPages] = useState(0);
const [userList, setUserList] = useState<{ title: string; key: number }[]>(
[]
);
const [queryParams, setQueryParams] = useState<Global.SearchParams>({
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<AccessControlFormData>) => {
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 (
<Form
form={form}
onFinish={onFinish}
preserve={false}
clearOnDestroy={true}
scrollToFirstError={true}
initialValues={{
public: true
}}
>
<Form.Item<AccessControlFormData>
valuePropName="checked"
name="set_public"
style={{ marginBottom: 24, paddingLeft: 6 }}
>
<CheckboxField
description="Only authorized users can access"
label={'Restricted'}
></CheckboxField>
</Form.Item>
{setPublic && (
<Form.Item<AccessControlFormData> name="users">
<TransferWrap>
<Transfer
dataSource={userList}
targetKeys={targetKeys}
showSelectAll
showSearch
pagination={totalPages > 1}
titles={['Available Users', 'Users with Access']}
render={(item) => item.title}
selectAllLabels={[]}
selectionsIcon={<IconFont type="icon-down"></IconFont>}
onChange={handleOnChange}
/>
</TransferWrap>
</Form.Item>
)}
</Form>
);
});
export default AccessControlForm;
@@ -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<ListItem, AccessControlFormData>
> = ({ open, title, currentData, action, onOk, onCancel }) => {
const intl = useIntl();
const form = useRef<any>(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 (
<ScrollerModal
title={title}
open={open}
centered={true}
onCancel={onCancel}
destroyOnHidden={true}
closeIcon={true}
maskClosable={false}
keyboard={false}
width={800}
footer={
<ModalFooter onOk={handleSumit} onCancel={onCancel}></ModalFooter>
}
>
<AccessControlForm
ref={form}
currentData={currentData}
onFinish={handleOnFinish}
/>
</ScrollerModal>
);
};
export default AccessControlModal;
@@ -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<ModelsProps> = ({
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<any>(null);
useEffect(() => {
@@ -451,6 +464,15 @@ const Models: React.FC<ModelsProps> = ({
}
});
}
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<ModelsProps> = ({
}
});
const handleCancelAccessControl = () => {
setOpenAccessControlModal({
...openAccessControlModal,
currentData: null,
open: false
});
};
const handleSubmitAccessControl = (data: any) => {};
return (
<>
<PageContainer
@@ -723,6 +755,14 @@ const Models: React.FC<ModelsProps> = ({
});
}}
></APIAccessInfoModal>
<AccessControlModal
onCancel={handleCancelAccessControl}
onOk={handleSubmitAccessControl}
title={openAccessControlModal.title}
open={openAccessControlModal.open}
currentData={openAccessControlModal.currentData}
action={openAccessControlModal.action}
></AccessControlModal>
</>
);
};
@@ -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',
+5
View File
@@ -252,3 +252,8 @@ export interface BackendOption {
default_version: string;
versions: { label: string; value: string }[];
}
export interface AccessControlFormData {
set_public: boolean;
users: { id: number }[];
}