mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
feat: apikeys
This commit is contained in:
+10
-1
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user