diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.spec.tsx
index d60c985b99..3e6804bf5a 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.spec.tsx
@@ -2,12 +2,6 @@ import type { Credential } from '../../declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import CredentialItem from './credential-item'
-vi.mock('@remixicon/react', () => ({
- RiCheckLine: () =>
,
- RiDeleteBinLine: () => ,
- RiEqualizer2Line: () => ,
-}))
-
vi.mock('@/app/components/header/indicator', () => ({
default: () => ,
}))
@@ -61,8 +55,12 @@ describe('CredentialItem', () => {
render()
- fireEvent.click(screen.getByTestId('edit-icon').closest('button') as HTMLButtonElement)
- fireEvent.click(screen.getByTestId('delete-icon').closest('button') as HTMLButtonElement)
+ const buttons = screen.getAllByRole('button')
+ const editButton = buttons.find(b => b.querySelector('.i-ri-equalizer-2-line'))!
+ const deleteButton = buttons.find(b => b.querySelector('.i-ri-delete-bin-line'))!
+
+ fireEvent.click(editButton)
+ fireEvent.click(deleteButton)
expect(onEdit).toHaveBeenCalledWith(credential)
expect(onDelete).toHaveBeenCalledWith(credential)
@@ -81,7 +79,10 @@ describe('CredentialItem', () => {
/>,
)
- fireEvent.click(screen.getByTestId('delete-icon').closest('button') as HTMLButtonElement)
+ const deleteButton = screen.getAllByRole('button')
+ .find(b => b.querySelector('.i-ri-delete-bin-line'))!
+
+ fireEvent.click(deleteButton)
expect(onDelete).not.toHaveBeenCalled()
})
diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx
index 11fb65c3e5..95f84e5908 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx
@@ -1,9 +1,4 @@
import type { Credential } from '../../declarations'
-import {
- RiCheckLine,
- RiDeleteBinLine,
- RiEqualizer2Line,
-} from '@remixicon/react'
import {
memo,
useMemo,
@@ -11,7 +6,7 @@ import {
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge'
-import Tooltip from '@/app/components/base/tooltip'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
import Indicator from '@/app/components/header/indicator'
import { cn } from '@/utils/classnames'
@@ -56,7 +51,7 @@ const CredentialItem = ({
key={credential.credential_id}
className={cn(
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
- (disabled || credential.not_allowed_to_use) && 'cursor-not-allowed opacity-50',
+ (disabled || credential.not_allowed_to_use) ? 'cursor-not-allowed opacity-50' : onItemClick && 'cursor-pointer',
)}
onClick={() => {
if (disabled || credential.not_allowed_to_use)
@@ -70,7 +65,7 @@ const CredentialItem = ({
{
selectedCredentialId === credential.credential_id && (
-
+
)
}
@@ -78,7 +73,7 @@ const CredentialItem = ({
}
{credential.credential_name}
@@ -96,38 +91,50 @@ const CredentialItem = ({
{
!disableEdit && !credential.not_allowed_to_use && (
-
- {
- e.stopPropagation()
- onEdit?.(credential)
- }}
- >
-
-
+
+ {
+ e.stopPropagation()
+ onEdit?.(credential)
+ }}
+ >
+
+
+ )}
+ />
+ {t('operation.edit', { ns: 'common' })}
)
}
{
!disableDelete && (
-
- {
- if (disabled || disableDeleteWhenSelected)
- return
- e.stopPropagation()
- onDelete?.(credential)
- }}
- >
-
+ {
+ if (disabled || disableDeleteWhenSelected)
+ return
+ e.stopPropagation()
+ onDelete?.(credential)
+ }}
+ >
+
+
)}
- />
-
+ />
+
+ {disableDeleteWhenSelected ? disableDeleteTip : t('operation.delete', { ns: 'common' })}
+
)
}
@@ -139,8 +146,9 @@ const CredentialItem = ({
if (credential.not_allowed_to_use) {
return (
-
- {Item}
+
+
+ {t('auth.customCredentialUnavailable', { ns: 'plugin' })}
)
}
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx
index 1b235c1e2b..79b53b2eb9 100644
--- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx
@@ -8,6 +8,7 @@ type ApiKeySectionProps = {
provider: ModelProvider
credentials: Credential[]
selectedCredentialId: string | undefined
+ isActivating?: boolean
onItemClick: (credential: Credential, model?: CustomModel) => void
onEdit: (credential?: Credential) => void
onDelete: (credential?: Credential) => void
@@ -18,6 +19,7 @@ function ApiKeySection({
provider,
credentials,
selectedCredentialId,
+ isActivating,
onItemClick,
onEdit,
onDelete,
@@ -62,6 +64,7 @@ function ApiKeySection({
({
useTrialCredits: () => ({ credits: 0, totalCredits: 10_000, isExhausted: true, isLoading: false }),
}))
+vi.mock('./use-activate-credential', () => ({
+ useActivateCredential: () => ({
+ selectedCredentialId: 'cred-1',
+ isActivating: false,
+ activate: mockActivate,
+ }),
+}))
+
vi.mock('../../model-auth/hooks', () => ({
useAuth: () => ({
openConfirmDelete: mockOpenConfirmDelete,
closeConfirmDelete: mockCloseConfirmDelete,
doingAction: false,
- handleActiveCredential: mockHandleActiveCredential,
handleConfirmDelete: mockHandleConfirmDelete,
deleteCredentialId: mockDeleteCredentialId,
handleOpenModal: mockHandleOpenModal,
@@ -300,7 +307,7 @@ describe('DropdownContent', () => {
expect(screen.getByText(/noApiKeysDescription/)).toBeInTheDocument()
})
- it('should call handleActiveCredential and close on credential item click', () => {
+ it('should call activate without closing on credential item click', () => {
render(
{
/>,
)
- fireEvent.click(screen.getByTestId('click-cred-1'))
+ fireEvent.click(screen.getByTestId('click-cred-2'))
- expect(mockHandleActiveCredential).toHaveBeenCalledWith(
- expect.objectContaining({ credential_id: 'cred-1' }),
+ expect(mockActivate).toHaveBeenCalledWith(
+ expect.objectContaining({ credential_id: 'cred-2' }),
)
- expect(onClose).toHaveBeenCalled()
+ expect(onClose).not.toHaveBeenCalled()
})
it('should call handleOpenModal and close on edit credential', () => {
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/dropdown-content.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/dropdown-content.tsx
index c6588d1f81..526cdf1dc9 100644
--- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/dropdown-content.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/dropdown-content.tsx
@@ -17,6 +17,7 @@ import ApiKeySection from './api-key-section'
import CreditsExhaustedAlert from './credits-exhausted-alert'
import CreditsFallbackAlert from './credits-fallback-alert'
import UsagePrioritySection from './usage-priority-section'
+import { useActivateCredential } from './use-activate-credential'
const EMPTY_CREDENTIALS: Credential[] = []
@@ -36,25 +37,18 @@ function DropdownContent({
onClose,
}: DropdownContentProps) {
const { t } = useTranslation()
- const {
- current_credential_id,
- available_credentials,
- } = provider.custom_configuration
+ const { available_credentials } = provider.custom_configuration
const {
openConfirmDelete,
closeConfirmDelete,
doingAction,
- handleActiveCredential,
handleConfirmDelete,
deleteCredentialId,
handleOpenModal,
} = useAuth(provider, ConfigurationMethodEnum.predefinedModel)
- const handleItemClick = useCallback((credential: Credential) => {
- handleActiveCredential(credential)
- onClose()
- }, [handleActiveCredential, onClose])
+ const { selectedCredentialId, isActivating, activate } = useActivateCredential(provider)
const handleEdit = useCallback((credential?: Credential) => {
handleOpenModal(credential)
@@ -98,8 +92,9 @@ function DropdownContent({
({
openConfirmDelete: vi.fn(),
closeConfirmDelete: vi.fn(),
doingAction: false,
- handleActiveCredential: vi.fn(),
handleConfirmDelete: vi.fn(),
deleteCredentialId: null,
handleOpenModal: vi.fn(),
}),
}))
+vi.mock('./use-activate-credential', () => ({
+ useActivateCredential: () => ({
+ selectedCredentialId: undefined,
+ isActivating: false,
+ activate: vi.fn(),
+ }),
+}))
+
vi.mock('../use-trial-credits', () => ({
useTrialCredits: () => ({ credits: 0, totalCredits: 10_000, isExhausted: true, isLoading: false }),
}))
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts
new file mode 100644
index 0000000000..63705f799e
--- /dev/null
+++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts
@@ -0,0 +1,51 @@
+import type { Credential, ModelProvider } from '../../declarations'
+import { useCallback, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import Toast from '@/app/components/base/toast'
+import { useActiveProviderCredential } from '@/service/use-models'
+import {
+ useUpdateModelList,
+ useUpdateModelProviders,
+} from '../../hooks'
+
+export function useActivateCredential(provider: ModelProvider) {
+ const { t } = useTranslation()
+ const updateModelProviders = useUpdateModelProviders()
+ const updateModelList = useUpdateModelList()
+ const { mutate, isPending } = useActiveProviderCredential(provider.provider)
+ const [optimisticId, setOptimisticId] = useState()
+
+ const currentId = provider.custom_configuration.current_credential_id
+ const selectedCredentialId = optimisticId ?? currentId
+
+ const selectedIdRef = useRef(selectedCredentialId)
+ selectedIdRef.current = selectedCredentialId
+
+ const supportedModelTypes = provider.supported_model_types
+
+ const activate = useCallback((credential: Credential) => {
+ if (credential.credential_id === selectedIdRef.current)
+ return
+ setOptimisticId(credential.credential_id)
+ mutate(
+ { credential_id: credential.credential_id },
+ {
+ onSuccess: () => {
+ Toast.notify({ type: 'success', message: t('api.actionSuccess', { ns: 'common' }) })
+ updateModelProviders()
+ supportedModelTypes.forEach(type => updateModelList(type))
+ },
+ onError: () => {
+ setOptimisticId(undefined)
+ Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
+ },
+ },
+ )
+ }, [mutate, t, updateModelProviders, updateModelList, supportedModelTypes])
+
+ return {
+ selectedCredentialId,
+ isActivating: isPending,
+ activate,
+ }
+}
diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json
index 3ef6ce9ff6..d51661e3ce 100644
--- a/web/eslint-suppressions.json
+++ b/web/eslint-suppressions.json
@@ -4705,14 +4705,6 @@
"count": 1
}
},
- "app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx": {
- "no-restricted-imports": {
- "count": 1
- },
- "tailwindcss/enforce-consistent-class-order": {
- "count": 1
- }
- },
"app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx": {
"no-restricted-imports": {
"count": 3