style: allowed models ux

This commit is contained in:
jialin
2025-10-13 15:21:17 +08:00
parent 1fa2124efd
commit 663b6c7b4e
16 changed files with 218 additions and 102 deletions
+1 -2
View File
@@ -70,7 +70,6 @@ export default [
name: 'models', name: 'models',
path: '/models', path: '/models',
key: 'models', key: 'models',
access: 'canSeeAdmin',
routes: [ routes: [
{ {
path: '/models', path: '/models',
@@ -103,7 +102,7 @@ export default [
icon: 'icon-models', icon: 'icon-models',
selectedIcon: 'icon-models-filled', selectedIcon: 'icon-models-filled',
defaultIcon: 'icon-models', defaultIcon: 'icon-models',
access: 'canSeeAdmin', access: 'canSeeUser',
component: './llmodels/user-models' component: './llmodels/user-models'
}, },
{ {
+7
View File
@@ -4,8 +4,15 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
initialState.currentUser && initialState.currentUser &&
initialState.currentUser.is_admin initialState.currentUser.is_admin
); );
const canSeeUser = !!(
initialState &&
initialState.currentUser &&
!initialState.currentUser.is_admin
);
return { return {
canSeeAdmin, canSeeAdmin,
canSeeUser,
canDelete: true, canDelete: true,
canLogin: true canLogin: true
}; };
+2 -1
View File
@@ -11,5 +11,6 @@ export default {
'apikeys.form.expiration.7days': '7 days', 'apikeys.form.expiration.7days': '7 days',
'apikeys.form.expiration.1month': '1 month', 'apikeys.form.expiration.1month': '1 month',
'apikeys.form.expiration.6months': '6 months', 'apikeys.form.expiration.6months': '6 months',
'apikeys.form.expiration.never': 'No expiration' 'apikeys.form.expiration.never': 'No expiration',
'apikeys.table.bindModels': 'Allowed Models'
}; };
+2 -1
View File
@@ -11,5 +11,6 @@ export default {
'apikeys.form.expiration.7days': '7日間', 'apikeys.form.expiration.7days': '7日間',
'apikeys.form.expiration.1month': '1ヶ月', 'apikeys.form.expiration.1month': '1ヶ月',
'apikeys.form.expiration.6months': '6ヶ月', 'apikeys.form.expiration.6months': '6ヶ月',
'apikeys.form.expiration.never': '無期限' 'apikeys.form.expiration.never': '無期限',
'apikeys.table.bindModels': '許可されたモデル'
}; };
+2 -1
View File
@@ -11,5 +11,6 @@ export default {
'apikeys.form.expiration.7days': '7 дней', 'apikeys.form.expiration.7days': '7 дней',
'apikeys.form.expiration.1month': '1 месяц', 'apikeys.form.expiration.1month': '1 месяц',
'apikeys.form.expiration.6months': '6 месяцев', 'apikeys.form.expiration.6months': '6 месяцев',
'apikeys.form.expiration.never': 'Без срока действия' 'apikeys.form.expiration.never': 'Без срока действия',
'apikeys.table.bindModels': 'Разрешенные модели'
}; };
+2 -1
View File
@@ -10,5 +10,6 @@ export default {
'apikeys.form.expiration.7days': '7天', 'apikeys.form.expiration.7days': '7天',
'apikeys.form.expiration.1month': '1个月', 'apikeys.form.expiration.1month': '1个月',
'apikeys.form.expiration.6months': '6个月', 'apikeys.form.expiration.6months': '6个月',
'apikeys.form.expiration.never': '永不过期' 'apikeys.form.expiration.never': '永不过期',
'apikeys.table.bindModels': '绑定模型'
}; };
+44 -3
View File
@@ -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'; import styled from 'styled-components';
type TransferKey = string | number | bigint; type TransferKey = string | number | bigint;
const PaginationWrapper = styled.div`
padding: 4px 16px;
`;
const TransferWrap = styled.div` const TransferWrap = styled.div`
.ant-transfer-list { .ant-transfer-list {
width: 100%; width: 100%;
height: 300px; height: 360px;
} }
.ant-transfer-list-content { .ant-transfer-list-content {
.ant-transfer-list-content-item { .ant-transfer-list-content-item {
@@ -27,13 +33,48 @@ const TransferWrap = styled.div`
`; `;
interface TransferInnerProps extends TransferProps { interface TransferInnerProps extends TransferProps {
total?: number;
perPage?: number;
onPageChange?: (page: number, perPage?: number) => void;
dataSource?: Array<{ key: TransferKey; title: string }>; dataSource?: Array<{ key: TransferKey; title: string }>;
targetKeys?: TransferKey[]; targetKeys?: TransferKey[];
} }
const TransferInner: React.FC<TransferInnerProps> = (props) => { const TransferInner: React.FC<TransferInnerProps> = (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 (
<PaginationWrapper>
<Pagination
simple={{ readOnly: true }}
size="small"
total={total}
onChange={handleOnPageChange}
pageSize={perPage}
current={page}
showSizeChanger={false}
/>
</PaginationWrapper>
);
}
return null;
};
return ( return (
<TransferWrap> <TransferWrap>
<Transfer {...props}></Transfer> <Transfer
{...props}
selectionsIcon={
<MoreOutlined style={{ fontSize: 14, marginBottom: 3 }} />
}
></Transfer>
</TransferWrap> </TransferWrap>
); );
}; };
+7
View File
@@ -17,6 +17,13 @@ export async function createApisKey(params: { data: FormData }) {
}); });
} }
export async function updateApisKey(id: number, params: { data: FormData }) {
return request<ListItem>(`${APIS_KEYS_API}/${id}`, {
method: 'PUT',
data: params.data
});
}
export async function deleteApisKey(id: number) { export async function deleteApisKey(id: number) {
return request(`${APIS_KEYS_API}/${id}`, { return request(`${APIS_KEYS_API}/${id}`, {
method: 'DELETE' method: 'DELETE'
@@ -39,7 +39,9 @@ const AllowModelsForm: React.FC = () => {
}} }}
render={(item) => item.title} render={(item) => item.title}
titles={['Available Models', 'Allowed Models']} titles={['Available Models', 'Allowed Models']}
showSearch showSearch={{
placeholder: 'Filter by model name'
}}
filterOption={(inputValue, item) => filterOption={(inputValue, item) =>
item.title.toLowerCase().includes(inputValue.toLowerCase()) item.title.toLowerCase().includes(inputValue.toLowerCase())
} }
@@ -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 (
<>
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{
name: intl.formatMessage({ id: 'common.table.name' })
}
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'common.table.name' })}
required
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="expires_in"
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.select' },
{
name: intl.formatMessage({
id: 'apikeys.form.expiretime'
})
}
)
}
]}
>
<SealSelect
options={expirationOptions}
label={intl.formatMessage({ id: 'apikeys.form.expiretime' })}
required
></SealSelect>
</Form.Item>
<AllowModelsForm></AllowModelsForm>
<Form.Item<FormData> name="description" rules={[{ required: false }]}>
<SealInput.TextArea
scaleSize={true}
label={intl.formatMessage({ id: 'common.table.description' })}
></SealInput.TextArea>
</Form.Item>
</>
);
};
export default APIKeyForm;
@@ -2,17 +2,17 @@ import CopyButton from '@/components/copy-button';
import ModalFooter from '@/components/modal-footer'; import ModalFooter from '@/components/modal-footer';
import ScrollerModal from '@/components/scroller-modal'; import ScrollerModal from '@/components/scroller-modal';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Form, Tag } from 'antd'; import { Button, Form, Tag } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { createApisKey } from '../../apis'; import { createApisKey, updateApisKey } from '../../apis';
import { expirationOptions } from '../../config'; import { expirationOptions } from '../../config';
import { FormData, ListItem } from '../../config/types'; import { FormData, ListItem } from '../../config/types';
import AllowModelsForm from './allow-models'; import AllowModelsForm from './allow-models';
import APIKeyForm from './form';
type AddModalProps = { type AddModalProps = {
title: string; title: string;
@@ -43,6 +43,13 @@ const AddModal: React.FC<AddModalProps> = ({
expires_in: 1 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(() => { useEffect(() => {
@@ -67,16 +74,29 @@ const AddModal: React.FC<AddModalProps> = ({
return res; 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) => { const handleOnOk = async (data: FormData) => {
try { try {
setLoading(true); setLoading(true);
const params = { if (action === PageAction.CREATE) {
...data, await createAPIKey(data);
expires_in: getExpireValue(data.expires_in) } else if (action === PageAction.EDIT && currentData?.id) {
}; await updateAPIKey(data);
const res = await createApisKey({ data: params }); }
setAPIKeyValue(res.value);
setShowKey(true);
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
setLoading(false); setLoading(false);
@@ -109,7 +129,7 @@ const AddModal: React.FC<AddModalProps> = ({
closeIcon={false} closeIcon={false}
maskClosable={false} maskClosable={false}
keyboard={false} keyboard={false}
width={600} width={700}
styles={{}} styles={{}}
footer={ footer={
!showKey ? ( !showKey ? (
@@ -126,60 +146,9 @@ const AddModal: React.FC<AddModalProps> = ({
} }
> >
<Form name="addAPIKey" form={form} onFinish={handleOnOk} preserve={false}> <Form name="addAPIKey" form={form} onFinish={handleOnOk} preserve={false}>
{!showKey ? ( {action === PageAction.EDIT && <AllowModelsForm></AllowModelsForm>}
<> {!showKey && action === PageAction.CREATE && <APIKeyForm></APIKeyForm>}
<Form.Item<FormData> {showKey && action === PageAction.CREATE && (
name="name"
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.input' },
{
name: intl.formatMessage({ id: 'common.table.name' })
}
)
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'common.table.name' })}
required
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="expires_in"
rules={[
{
required: true,
message: intl.formatMessage(
{ id: 'common.form.rule.select' },
{
name: intl.formatMessage({
id: 'apikeys.form.expiretime'
})
}
)
}
]}
>
<SealSelect
options={expirationOptions}
label={intl.formatMessage({ id: 'apikeys.form.expiretime' })}
required
></SealSelect>
</Form.Item>
<AllowModelsForm></AllowModelsForm>
<Form.Item<FormData>
name="description"
rules={[{ required: false }]}
>
<SealInput.TextArea
label={intl.formatMessage({ id: 'common.table.description' })}
></SealInput.TextArea>
</Form.Item>
</>
) : (
<Form.Item> <Form.Item>
<div> <div>
<Tag <Tag
+1
View File
@@ -6,6 +6,7 @@ export interface ListItem {
created_at: string; created_at: string;
updated_at: string; updated_at: string;
expires_at: string; expires_at: string;
allowed_model_names: string[];
} }
export interface FormData { export interface FormData {
+12 -1
View File
@@ -16,7 +16,7 @@ interface ColumnsHookProps {
const actionList: Global.ActionItem[] = [ const actionList: Global.ActionItem[] = [
{ {
label: 'common.button.edit', label: 'Edit Allowed Models',
key: 'edit', key: 'edit',
icon: icons.EditOutlined icon: icons.EditOutlined
}, },
@@ -60,6 +60,17 @@ const useModelsColumns = ({
</AutoTooltip> </AutoTooltip>
) )
}, },
{
title: intl.formatMessage({ id: 'apikeys.table.bindModels' }),
dataIndex: 'allowed_model_names',
key: 'allowed_model_names',
ellipsis: {
showTitle: false
},
render: (text: string[], record: ListItem) => (
<AutoTooltip ghost>{text?.join(', ')}</AutoTooltip>
)
},
{ {
title: intl.formatMessage({ id: 'common.table.description' }), title: intl.formatMessage({ id: 'common.table.description' }),
dataIndex: 'description', dataIndex: 'description',
+1 -2
View File
@@ -58,7 +58,7 @@ const APIKeys: React.FC = () => {
const handleEditKey = (record: ListItem) => { const handleEditKey = (record: ListItem) => {
setOpenAddModal({ setOpenAddModal({
open: true, open: true,
title: 'Edit API Key', title: 'Edit Allowed Models',
action: PageAction.EDIT, action: PageAction.EDIT,
currentData: record currentData: record
}); });
@@ -79,7 +79,6 @@ const APIKeys: React.FC = () => {
}; };
const handleModalCancel = () => { const handleModalCancel = () => {
console.log('handleModalCancel');
setOpenAddModal({ setOpenAddModal({
open: false, open: false,
title: '', title: '',
@@ -1,4 +1,3 @@
import IconFont from '@/components/icon-font';
import CheckboxField from '@/components/seal-form/checkbox-field'; import CheckboxField from '@/components/seal-form/checkbox-field';
import TransferInner from '@/pages/_components/transfer'; import TransferInner from '@/pages/_components/transfer';
import { queryUsersList } from '@/pages/users/apis'; import { queryUsersList } from '@/pages/users/apis';
@@ -33,7 +32,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
); );
const [queryParams, setQueryParams] = useState<Global.SearchParams>({ const [queryParams, setQueryParams] = useState<Global.SearchParams>({
page: 1, page: 1,
perPage: 30 perPage: 100
}); });
const getUserList = async (query: Global.SearchParams) => { const getUserList = async (query: Global.SearchParams) => {
@@ -62,6 +61,10 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
form.setFieldsValue({ users }); form.setFieldsValue({ users });
}; };
const onSearch = (dir: 'left' | 'right', value: string) => {
console.log('search:', dir, value);
};
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
submit: () => { submit: () => {
form.submit(); form.submit();
@@ -116,28 +119,34 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
></CheckboxField> ></CheckboxField>
</Form.Item> </Form.Item>
{setPublic && ( {setPublic && (
<Form.Item<AccessControlFormData> <>
name="users" <Label>User Select</Label>
rules={[ <Form.Item<AccessControlFormData>
{ name="users"
required: true, rules={[
message: 'Please select at least one user' {
} required: true,
]} message: 'Please select at least one user'
> }
<TransferInner ]}
dataSource={userList} >
targetKeys={targetKeys} <TransferInner
showSelectAll total={100}
showSearch dataSource={userList}
pagination={totalPages > 1} targetKeys={targetKeys}
titles={['Available Users', 'Users with Access']} showSelectAll
render={(item) => item.title} pagination={false}
selectAllLabels={[]} titles={['Available Users', 'Users with Access']}
selectionsIcon={<IconFont type="icon-down"></IconFont>} showSearch={{
onChange={handleOnChange} placeholder: 'Filter by username'
/> }}
</Form.Item> render={(item) => item.title}
selectAllLabels={[]}
onSearch={onSearch}
onChange={handleOnChange}
/>
</Form.Item>
</>
)} )}
</Form> </Form>
); );
@@ -45,7 +45,7 @@ const AccessControlModal: React.FC<
closeIcon={true} closeIcon={true}
maskClosable={false} maskClosable={false}
keyboard={false} keyboard={false}
width={600} width={700}
footer={ footer={
<ModalFooter onOk={handleSumit} onCancel={onCancel}></ModalFooter> <ModalFooter onOk={handleSumit} onCancel={onCancel}></ModalFooter>
} }