mirror of https://github.com/langgenius/dify.git
feat(web): add credits-fallback variant for API Key priority with available credits
When API Key is selected but unavailable/unconfigured and credits are available, the card now shows "AI credits in use" with a warning icon instead of "API key required". When both credits are exhausted and no API key exists, it shows "No available usage" (destructive). New deriveVariant logic for priority=apiKey: - !exhausted + !authorized → credits-fallback (was api-required-*) - exhausted + no credential → no-usage (was api-required-add) - exhausted + named unauthorized → api-unavailable (unchanged)
This commit is contained in:
parent
ad9ac6978e
commit
ce34937a1c
|
|
@ -147,7 +147,7 @@ describe('CredentialPanel', () => {
|
|||
expect(screen.getByText(/noAvailableUsage/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show "API key required" for api-required-add variant (custom priority, no credentials)', () => {
|
||||
it('should show "AI credits in use" with warning for credits-fallback (custom priority, no credentials, credits available)', () => {
|
||||
renderWithQueryClient(createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
|
|
@ -155,10 +155,10 @@ describe('CredentialPanel', () => {
|
|||
available_credentials: [],
|
||||
},
|
||||
}))
|
||||
expect(screen.getByText(/apiKeyRequired/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/aiCreditsInUse/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show "API key required" for api-required-configure variant (custom priority, credential exists but name missing)', () => {
|
||||
it('should show "AI credits in use" with warning for credits-fallback (custom priority, credential unauthorized, credits available)', () => {
|
||||
renderWithQueryClient(createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
|
|
@ -168,7 +168,18 @@ describe('CredentialPanel', () => {
|
|||
available_credentials: [{ credential_id: 'cred-1' }],
|
||||
},
|
||||
}))
|
||||
expect(screen.getByText(/apiKeyRequired/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/aiCreditsInUse/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show warning icon for credits-fallback variant', () => {
|
||||
const { container } = renderWithQueryClient(createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
status: CustomConfigurationStatusEnum.noConfigure,
|
||||
available_credentials: [],
|
||||
},
|
||||
}))
|
||||
expect(container.querySelector('.i-ri-error-warning-fill')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -201,7 +212,8 @@ describe('CredentialPanel', () => {
|
|||
expect(container.querySelector('.i-ri-error-warning-fill')).toBeNull()
|
||||
})
|
||||
|
||||
it('should show red indicator and "Unavailable" for api-unavailable', () => {
|
||||
it('should show red indicator and "Unavailable" for api-unavailable (exhausted + named unauthorized key)', () => {
|
||||
mockTrialCredits.isExhausted = true
|
||||
renderWithQueryClient(createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
|
|
@ -243,6 +255,7 @@ describe('CredentialPanel', () => {
|
|||
})
|
||||
|
||||
it('should apply destructive container for api-unavailable variant', () => {
|
||||
mockTrialCredits.isExhausted = true
|
||||
const { container } = renderWithQueryClient(createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
|
|
@ -356,7 +369,7 @@ describe('CredentialPanel', () => {
|
|||
expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'api-active')
|
||||
})
|
||||
|
||||
it('should pass api-required-add variant for custom priority with no credentials', () => {
|
||||
it('should pass credits-fallback variant for custom priority with no credentials and credits available', () => {
|
||||
renderWithQueryClient(createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
|
|
@ -364,10 +377,10 @@ describe('CredentialPanel', () => {
|
|||
available_credentials: [],
|
||||
},
|
||||
}))
|
||||
expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'api-required-add')
|
||||
expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'credits-fallback')
|
||||
})
|
||||
|
||||
it('should pass api-unavailable variant for custom priority with named but unauthorized key', () => {
|
||||
it('should pass credits-fallback variant for custom priority with named unauthorized key and credits available', () => {
|
||||
renderWithQueryClient(createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
|
|
@ -377,7 +390,7 @@ describe('CredentialPanel', () => {
|
|||
available_credentials: [{ credential_id: 'cred-1', credential_name: 'Bad Key' }],
|
||||
},
|
||||
}))
|
||||
expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'api-unavailable')
|
||||
expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'credits-fallback')
|
||||
})
|
||||
|
||||
it('should pass no-usage variant when exhausted + credential but unauthorized', () => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ type CredentialPanelProps = {
|
|||
|
||||
const TEXT_LABEL_VARIANTS = new Set<CardVariant>([
|
||||
'credits-active',
|
||||
'credits-fallback',
|
||||
'credits-exhausted',
|
||||
'no-usage',
|
||||
'api-required-add',
|
||||
|
|
@ -70,10 +71,11 @@ const CredentialPanel = ({
|
|||
const { variant, credentialName } = state
|
||||
const isDestructive = isDestructiveVariant(variant)
|
||||
const isTextLabel = TEXT_LABEL_VARIANTS.has(variant)
|
||||
const needsGap = !isTextLabel || variant === 'credits-fallback'
|
||||
|
||||
return (
|
||||
<SystemQuotaCard variant={isDestructive ? 'destructive' : 'default'}>
|
||||
<SystemQuotaCard.Label className={isTextLabel ? undefined : 'gap-1'}>
|
||||
<SystemQuotaCard.Label className={needsGap ? 'gap-1' : undefined}>
|
||||
{isTextLabel
|
||||
? <TextLabel variant={variant} />
|
||||
: <StatusLabel variant={variant} credentialName={credentialName} />}
|
||||
|
|
@ -92,6 +94,7 @@ const CredentialPanel = ({
|
|||
|
||||
const TEXT_LABEL_KEYS = {
|
||||
'credits-active': 'modelProvider.card.aiCreditsInUse',
|
||||
'credits-fallback': 'modelProvider.card.aiCreditsInUse',
|
||||
'credits-exhausted': 'modelProvider.card.quotaExhausted',
|
||||
'no-usage': 'modelProvider.card.noAvailableUsage',
|
||||
'api-required-add': 'modelProvider.card.apiKeyRequired',
|
||||
|
|
@ -104,9 +107,14 @@ function TextLabel({ variant }: { variant: CardVariant }) {
|
|||
const labelKey = TEXT_LABEL_KEYS[variant as keyof typeof TEXT_LABEL_KEYS]
|
||||
|
||||
return (
|
||||
<span className={isDestructive ? 'text-text-destructive' : 'text-text-secondary'}>
|
||||
{t(labelKey, { ns: 'common' })}
|
||||
</span>
|
||||
<>
|
||||
<span className={isDestructive ? 'text-text-destructive' : 'text-text-secondary'}>
|
||||
{t(labelKey, { ns: 'common' })}
|
||||
</span>
|
||||
{variant === 'credits-fallback' && (
|
||||
<span className="i-ri-error-warning-fill h-3 w-3 shrink-0 text-text-warning" />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ describe('useCredentialPanelState', () => {
|
|||
expect(result.current.priority).toBe('apiKey')
|
||||
})
|
||||
|
||||
it('should return api-unavailable when API key unauthorized', () => {
|
||||
it('should return credits-fallback when API key unauthorized and credits available', () => {
|
||||
const provider = createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
|
|
@ -117,10 +117,10 @@ describe('useCredentialPanelState', () => {
|
|||
|
||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
||||
|
||||
expect(result.current.variant).toBe('api-required-configure')
|
||||
expect(result.current.variant).toBe('credits-fallback')
|
||||
})
|
||||
|
||||
it('should return api-required-add when no credentials exist', () => {
|
||||
it('should return credits-fallback when no credentials and credits available', () => {
|
||||
const provider = createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
|
|
@ -131,7 +131,41 @@ describe('useCredentialPanelState', () => {
|
|||
|
||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
||||
|
||||
expect(result.current.variant).toBe('api-required-add')
|
||||
expect(result.current.variant).toBe('credits-fallback')
|
||||
})
|
||||
|
||||
it('should return no-usage when no credentials and credits exhausted', () => {
|
||||
mockTrialCredits.isExhausted = true
|
||||
mockTrialCredits.credits = 0
|
||||
const provider = createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
status: CustomConfigurationStatusEnum.noConfigure,
|
||||
available_credentials: [],
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
||||
|
||||
expect(result.current.variant).toBe('no-usage')
|
||||
})
|
||||
|
||||
it('should return api-unavailable when credential with name unauthorized and credits exhausted', () => {
|
||||
mockTrialCredits.isExhausted = true
|
||||
mockTrialCredits.credits = 0
|
||||
const provider = createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
custom_configuration: {
|
||||
status: CustomConfigurationStatusEnum.active,
|
||||
current_credential_id: undefined,
|
||||
current_credential_name: 'Bad Key',
|
||||
available_credentials: [{ credential_id: 'cred-1', credential_name: 'Bad Key' }],
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
||||
|
||||
expect(result.current.variant).toBe('api-unavailable')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export type UsagePriority = 'credits' | 'apiKey' | 'apiKeyOnly'
|
|||
|
||||
export type CardVariant
|
||||
= | 'credits-active'
|
||||
| 'credits-fallback'
|
||||
| 'credits-exhausted'
|
||||
| 'no-usage'
|
||||
| 'api-fallback'
|
||||
|
|
@ -56,6 +57,13 @@ function deriveVariant(
|
|||
|
||||
if (hasCredential && authorized)
|
||||
return 'api-active'
|
||||
|
||||
if (priority === 'apiKey' && !isExhausted)
|
||||
return 'credits-fallback'
|
||||
|
||||
if (priority === 'apiKey' && !hasCredential)
|
||||
return 'no-usage'
|
||||
|
||||
if (hasCredential && !authorized)
|
||||
return credentialName ? 'api-unavailable' : 'api-required-configure'
|
||||
return 'api-required-add'
|
||||
|
|
|
|||
Loading…
Reference in New Issue