feat: apikeys

This commit is contained in:
scarqin
2024-12-24 18:14:50 +08:00
parent 0f30d85ae8
commit 4733b3c919
4 changed files with 313 additions and 5 deletions
+10 -1
View File
@@ -14,4 +14,13 @@ Create detailed components with these requirements:
- Update current packages/core/src/pages/Root.tsx with new comprehensive code
- Don't forget root route (page.tsx) handling
- You MUST complete the entire prompt before stopping
11. Table component should use <PageList>
12. use `const { fetchData } = useFetch()` to fetch http data,such as ``` fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' }).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setUserInfo(data.profile)
dispatch({ type: 'UPDATE_USERDATA', userData: data.profile })
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})```
@@ -0,0 +1,67 @@
import { $t } from '@common/locales'
import { DatePicker, Form, Input, Modal, Switch, theme } from 'antd'
import dayjs from 'dayjs'
import React from 'react'
interface EditKeyModalProps {
visible: boolean
onCancel: () => void
onSave: (values: any) => void
initialValues?: {
key: string
expirationDate: string
enabled: boolean
}
}
const EditKeyModal: React.FC<EditKeyModalProps> = ({ visible, onCancel, onSave, initialValues }) => {
const [form] = Form.useForm()
const { token } = theme.useToken()
const handleOk = async () => {
try {
const values = await form.validateFields()
onSave({
...values,
expirationDate: values.expirationDate.format('YYYY-MM-DD')
})
} catch (error) {
console.error('Validation failed:', error)
}
}
return (
<Modal title={$t('Edit API Key')} open={visible} onCancel={onCancel} onOk={handleOk} destroyOnClose>
<Form
form={form}
layout="vertical"
initialValues={{
...initialValues,
expirationDate: initialValues?.expirationDate ? dayjs(initialValues.expirationDate) : undefined
}}
>
<Form.Item
name="key"
label={$t('API Key')}
rules={[{ required: true, message: $t('Please input the API key') }]}
>
<Input.Password />
</Form.Item>
<Form.Item
name="expirationDate"
label={$t('Expiration Date')}
rules={[{ required: true, message: $t('Please select expiration date') }]}
>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="enabled" label={$t('Enabled')} valuePropName="checked">
<Switch checkedChildren={$t('Yes')} unCheckedChildren={$t('No')} />
</Form.Item>
</Form>
</Modal>
)
}
export default EditKeyModal
@@ -0,0 +1,40 @@
import { $t } from '@common/locales'
import { Select, Space, theme } from 'antd'
import React from 'react'
interface StatusFilterProps {
value: string[]
onChange: (value: string[]) => void
}
const StatusFilter: React.FC<StatusFilterProps> = ({ value, onChange }) => {
const { token } = theme.useToken()
const options = [
{ label: $t('Normal'), value: 'normal', color: token.colorSuccess },
{ label: $t('Exceeded'), value: 'exceeded', color: token.colorError },
{ label: $t('Expired'), value: 'expired', color: token.colorWarning },
{ label: $t('Disabled'), value: 'disabled', color: token.colorTextDisabled },
{ label: $t('Error'), value: 'error', color: token.colorError }
]
return (
<Space>
<span>{$t('Status')}:</span>
<Select
mode="multiple"
value={value}
onChange={onChange}
style={{ width: 300 }}
placeholder={$t('Filter by status')}
allowClear
options={options.map((option) => ({
...option,
label: <span style={{ color: option.color }}>{option.label}</span>
}))}
/>
</Space>
)
}
export default StatusFilter
@@ -1,16 +1,208 @@
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { ActionType } from '@ant-design/pro-components'
import InsidePage from '@common/components/aoplatform/InsidePage'
import PageList from '@common/components/aoplatform/PageList'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import React from 'react'
import { Badge, Button, message, Select, Space, Tooltip } from 'antd'
import React, { useEffect, useRef, useState } from 'react'
import EditKeyModal from './components/EditKeyModal'
import StatusFilter from './components/StatusFilter'
interface APIKey {
id: string
key: string
status: 'normal' | 'exceeded' | 'expired' | 'disabled' | 'error'
expirationDate: string
priority: number
isDefault: boolean
}
const KeySettings: React.FC = () => {
const actionRef = useRef<ActionType>()
const [selectedProvider, setSelectedProvider] = useState<string>('openai')
const [statusFilter, setStatusFilter] = useState<string[]>([])
const [editModalVisible, setEditModalVisible] = useState(false)
const [editingKey, setEditingKey] = useState<APIKey | null>(null)
const [apiKeys, setApiKeys] = useState<APIKey[]>([])
const { fetchData } = useFetch()
useEffect(() => {
fetchData<BasicResponse<{ data: APIKey[] }>>('ai/resource/keys', {
method: 'GET',
eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setApiKeys(data.keys)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}, [])
const statusColors = {
normal: '#52c41a',
exceeded: '#ff4d4f',
expired: '#faad14',
disabled: '#d9d9d9',
error: '#ff4d4f'
}
const handleEdit = (record: APIKey) => {
setEditingKey(record)
setEditModalVisible(true)
}
const handleSave = (values: any) => {
if (editingKey) {
const newKeys = apiKeys.map((key) =>
key.id === editingKey.id
? {
...key,
key: values.key,
expirationDate: values.expirationDate,
status: values.enabled ? 'normal' : 'disabled'
}
: key
)
setApiKeys(newKeys)
}
setEditModalVisible(false)
setEditingKey(null)
}
const handleDelete = (id: string) => {
setApiKeys(apiKeys.filter((key) => key.id !== id))
}
const handleDragSortEnd = async (beforeIndex: number, afterIndex: number, newDataSource: APIKey[]) => {
setApiKeys(
newDataSource.map((item, index) => ({
...item,
priority: index + 1
}))
)
}
const columns = [
{
title: $t('API Key'),
dataIndex: 'key',
render: (text: string, record: APIKey) => (
<Space>
{text}
{record.isDefault && <Badge count={$t('Default')} style={{ backgroundColor: statusColors.normal }} />}
</Space>
)
},
{
title: $t('状态'),
dataIndex: 'status',
render: (status: keyof typeof statusColors) => (
<Badge
status="processing"
text={status.charAt(0).toUpperCase() + status.slice(1)}
style={{ color: statusColors[status] }}
/>
)
},
{
title: $t('过期时间'),
dataIndex: 'expirationDate'
},
{
title: $t('优先级'),
dataIndex: 'priority'
},
{
title: $t('操作'),
key: 'actions',
render: (_: unknown, record: APIKey) => (
<Space>
<Tooltip title={$t('Edit')}>
<Button
type="text"
icon={<EditOutlined />}
disabled={record.isDefault}
onClick={() => handleEdit(record)}
/>
</Tooltip>
<Tooltip title={$t('Delete')}>
<Button
type="text"
icon={<DeleteOutlined />}
disabled={record.isDefault}
danger
onClick={() => handleDelete(record.id)}
/>
</Tooltip>
</Space>
),
width: 120,
btnNums: 2
}
]
// const filteredKeys = statusFilter.length > 0 ? apiKeys.filter((key) => statusFilter.includes(key.status)) : apiKeys
const filteredKeys = apiKeys
const beforeSearchNode = [
<Select
key="provider"
value={selectedProvider}
onChange={setSelectedProvider}
style={{ width: 200 }}
options={[
{ label: 'OpenAI', value: 'openai' },
{ label: 'Anthropic', value: 'anthropic' }
]}
/>,
<StatusFilter key="status" value={statusFilter} onChange={setStatusFilter} />
]
return (
<InsidePage
className="overflow-y-auto pb-PAGE_INSIDE_B"
pageTitle={$t('key 模型')}
description={$t('配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务')}
pageTitle={$t('APIKey 资源池')}
description={$t('支持单个 API 模型供应商下创建多个 APIKey,并可对多个 APIKEY 进行智能负载均衡')}
showBorder={false}
scrollPage={false}
></InsidePage>
>
<PageList
actionRef={actionRef}
columns={columns}
dataSource={filteredKeys}
dragSortKey="sort"
onDragSortEnd={handleDragSortEnd}
beforeSearchNode={beforeSearchNode}
addNewBtnTitle={$t('添加 API Key')}
onAddNewBtnClick={() => {
setEditingKey(null)
setEditModalVisible(true)
}}
rowKey="id"
/>
<EditKeyModal
visible={editModalVisible}
onCancel={() => {
setEditModalVisible(false)
setEditingKey(null)
}}
onSave={handleSave}
initialValues={
editingKey
? {
key: editingKey.key,
expirationDate: editingKey.expirationDate,
enabled: editingKey.status !== 'disabled'
}
: undefined
}
/>
</InsidePage>
)
}