feat: Enhance API Key Management in Provider Settings
- Add single key checking functionality - Implement ability to remove individual API keys - Improve UI with remove and check buttons for each key - Disable actions during checking to prevent conflicts - Add styled remove icon for key deletion
This commit is contained in:
parent
f890da0cda
commit
b42da9f154
@ -1,4 +1,4 @@
|
|||||||
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons'
|
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined, MinusCircleOutlined } from '@ant-design/icons'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { checkApi } from '@renderer/services/ApiService'
|
import { checkApi } from '@renderer/services/ApiService'
|
||||||
@ -8,6 +8,7 @@ import { maskApiKey } from '@renderer/utils/api'
|
|||||||
import { Button, List, Modal, Space, Spin, Typography } from 'antd'
|
import { Button, List, Modal, Space, Spin, Typography } from 'antd'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface ShowParams {
|
interface ShowParams {
|
||||||
title: string
|
title: string
|
||||||
@ -34,6 +35,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, reso
|
|||||||
})
|
})
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isChecking, setIsChecking] = useState(false)
|
const [isChecking, setIsChecking] = useState(false)
|
||||||
|
const [isCheckingSingle, setIsCheckingSingle] = useState(false)
|
||||||
|
|
||||||
const checkAllKeys = async () => {
|
const checkAllKeys = async () => {
|
||||||
setIsChecking(true)
|
setIsChecking(true)
|
||||||
@ -54,10 +56,37 @@ const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, reso
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkSingleKey = async (keyIndex: number) => {
|
||||||
|
if (isChecking || keyStatuses[keyIndex].checking) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCheckingSingle(true)
|
||||||
|
setKeyStatuses((prev) => prev.map((status, idx) => (idx === keyIndex ? { ...status, checking: true } : status)))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { valid } = await checkApi({ ...provider, apiKey: keyStatuses[keyIndex].key }, model)
|
||||||
|
|
||||||
|
setKeyStatuses((prev) =>
|
||||||
|
prev.map((status, idx) => (idx === keyIndex ? { ...status, checking: false, isValid: valid } : status))
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
setKeyStatuses((prev) =>
|
||||||
|
prev.map((status, idx) => (idx === keyIndex ? { ...status, checking: false, isValid: false } : status))
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setIsCheckingSingle(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const removeInvalidKeys = () => {
|
const removeInvalidKeys = () => {
|
||||||
setKeyStatuses((prev) => prev.filter((status) => status.isValid !== false))
|
setKeyStatuses((prev) => prev.filter((status) => status.isValid !== false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removeKey = (keyIndex: number) => {
|
||||||
|
setKeyStatuses((prev) => prev.filter((_, idx) => idx !== keyIndex))
|
||||||
|
}
|
||||||
|
|
||||||
const onOk = () => {
|
const onOk = () => {
|
||||||
const allKeys = keyStatuses.map((status) => status.key)
|
const allKeys = keyStatuses.map((status) => status.key)
|
||||||
resolve({ validKeys: allKeys })
|
resolve({ validKeys: allKeys })
|
||||||
@ -84,12 +113,12 @@ const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, reso
|
|||||||
footer={
|
footer={
|
||||||
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Button key="remove" danger onClick={removeInvalidKeys}>
|
<Button key="remove" danger onClick={removeInvalidKeys} disabled={isChecking || isCheckingSingle}>
|
||||||
{t('settings.provider.remove_invalid_keys')}
|
{t('settings.provider.remove_invalid_keys')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<Button key="check" type="primary" ghost onClick={checkAllKeys} disabled={isChecking}>
|
<Button key="check" type="primary" ghost onClick={checkAllKeys} disabled={isChecking || isCheckingSingle}>
|
||||||
{t('settings.provider.check_all_keys')}
|
{t('settings.provider.check_all_keys')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button key="save" type="primary" onClick={onOk}>
|
<Button key="save" type="primary" onClick={onOk}>
|
||||||
@ -101,7 +130,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, reso
|
|||||||
<Scrollbar style={{ maxHeight: '70vh', overflowX: 'hidden' }}>
|
<Scrollbar style={{ maxHeight: '70vh', overflowX: 'hidden' }}>
|
||||||
<List
|
<List
|
||||||
dataSource={keyStatuses}
|
dataSource={keyStatuses}
|
||||||
renderItem={(status) => (
|
renderItem={(status, index) => (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||||
<Typography.Text copyable={{ text: status.key }}>{maskApiKey(status.key)}</Typography.Text>
|
<Typography.Text copyable={{ text: status.key }}>{maskApiKey(status.key)}</Typography.Text>
|
||||||
@ -111,11 +140,21 @@ const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, reso
|
|||||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} />
|
<Spin indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} />
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
{status.isValid === true && <CheckCircleFilled style={{ color: '#52c41a' }} />}
|
{status.isValid === true && !status.checking && <CheckCircleFilled style={{ color: '#52c41a' }} />}
|
||||||
{status.isValid === false && <CloseCircleFilled style={{ color: '#ff4d4f' }} />}
|
{status.isValid === false && !status.checking && <CloseCircleFilled style={{ color: '#ff4d4f' }} />}
|
||||||
{status.isValid === undefined && !status.checking && (
|
{status.isValid === undefined && !status.checking && (
|
||||||
<span>{t('settings.provider.not_checked')}</span>
|
<span>{t('settings.provider.not_checked')}</span>
|
||||||
)}
|
)}
|
||||||
|
<Button size="small" onClick={() => checkSingleKey(index)} disabled={isChecking || isCheckingSingle}>
|
||||||
|
{t('settings.provider.check')}
|
||||||
|
</Button>
|
||||||
|
<RemoveIcon
|
||||||
|
onClick={() => !isChecking && !isCheckingSingle && removeKey(index)}
|
||||||
|
style={{
|
||||||
|
cursor: isChecking || isCheckingSingle ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: isChecking || isCheckingSingle ? 0.5 : 1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
@ -146,3 +185,13 @@ export default class ApiCheckPopup {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RemoveIcon = styled(MinusCircleOutlined)`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--color-error);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user