feat: add model access control
This commit is contained in:
@@ -49,7 +49,8 @@ const icons = {
|
|||||||
EditContent: React.createElement(IconFont, { type: 'icon-edit-content' }),
|
EditContent: React.createElement(IconFont, { type: 'icon-edit-content' }),
|
||||||
Yaml: React.createElement(IconFont, { type: 'icon-code_block' }),
|
Yaml: React.createElement(IconFont, { type: 'icon-code_block' }),
|
||||||
Version: React.createElement(IconFont, { type: 'icon-version' }),
|
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;
|
export default icons;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
|
|||||||
// import './iconfont/iconfont.js';
|
// import './iconfont/iconfont.js';
|
||||||
|
|
||||||
const IconFont = createFromIconfontCN({
|
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;
|
export default IconFont;
|
||||||
|
|||||||
Vendored
+10
@@ -1,3 +1,4 @@
|
|||||||
|
import { PageActionType } from './types';
|
||||||
declare namespace Global {
|
declare namespace Global {
|
||||||
type WithFalse<T> = T | false;
|
type WithFalse<T> = T | false;
|
||||||
interface Pagination {
|
interface Pagination {
|
||||||
@@ -53,6 +54,15 @@ declare namespace Global {
|
|||||||
type SearchParams = Pagination & { search?: string };
|
type SearchParams = Pagination & { search?: string };
|
||||||
|
|
||||||
type MessageType = 'transition' | 'warning' | 'danger' | 'success' | 'info';
|
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 {
|
interface Window {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export const MODEL_EVALUATIONS = '/model-evaluations';
|
|||||||
|
|
||||||
export const BACKEND_LIST_API = '/inference-backends/list';
|
export const BACKEND_LIST_API = '/inference-backends/list';
|
||||||
|
|
||||||
|
export const MY_MODELS_API = '/my-models';
|
||||||
|
|
||||||
const setProxyUrl = (url: string) => {
|
const setProxyUrl = (url: string) => {
|
||||||
return `/proxy?url=${encodeURIComponent(url)}`;
|
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 BaseSelect from '@/components/seal-form/base/select';
|
||||||
import SealTable from '@/components/seal-table';
|
import SealTable from '@/components/seal-table';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
|
import { PageActionType } from '@/config/types';
|
||||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||||
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
|
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
|
||||||
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
||||||
@@ -56,6 +57,7 @@ import {
|
|||||||
} from '../config/types';
|
} from '../config/types';
|
||||||
import useFormInitialValues from '../hooks/use-form-initial-values';
|
import useFormInitialValues from '../hooks/use-form-initial-values';
|
||||||
import useModelsColumns from '../hooks/use-models-columns';
|
import useModelsColumns from '../hooks/use-models-columns';
|
||||||
|
import AccessControlModal from './access-control-modal';
|
||||||
import APIAccessInfoModal from './api-access-info';
|
import APIAccessInfoModal from './api-access-info';
|
||||||
import DeployModal from './deploy-modal';
|
import DeployModal from './deploy-modal';
|
||||||
import Instances from './instances';
|
import Instances from './instances';
|
||||||
@@ -177,6 +179,17 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
url: '',
|
url: '',
|
||||||
status: ''
|
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);
|
const modalRef = useRef<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
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) {
|
} catch (error) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -557,6 +579,16 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleCancelAccessControl = () => {
|
||||||
|
setOpenAccessControlModal({
|
||||||
|
...openAccessControlModal,
|
||||||
|
currentData: null,
|
||||||
|
open: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitAccessControl = (data: any) => {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageContainer
|
<PageContainer
|
||||||
@@ -723,6 +755,14 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
></APIAccessInfoModal>
|
></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',
|
key: 'api',
|
||||||
icon: icons.ApiOutlined
|
icon: icons.ApiOutlined
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Access Control',
|
||||||
|
key: 'accessControl',
|
||||||
|
icon: icons.Private
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'common.button.stop',
|
label: 'common.button.stop',
|
||||||
key: 'stop',
|
key: 'stop',
|
||||||
|
|||||||
@@ -252,3 +252,8 @@ export interface BackendOption {
|
|||||||
default_version: string;
|
default_version: string;
|
||||||
versions: { label: string; value: string }[];
|
versions: { label: string; value: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccessControlFormData {
|
||||||
|
set_public: boolean;
|
||||||
|
users: { id: number }[];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user