diff --git a/web/app/components/header/account-setting/model-provider-page/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/index.spec.tsx
index feb0a40069..ede9c1f7fe 100644
--- a/web/app/components/header/account-setting/model-provider-page/index.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/index.spec.tsx
@@ -7,16 +7,7 @@ import {
} from './declarations'
import ModelProviderPage from './index'
-vi.mock('@/context/app-context', () => ({
- useAppContext: () => ({
- mutateCurrentWorkspace: vi.fn(),
- isValidatingCurrentWorkspace: false,
- }),
-}))
-
-const mockGlobalState = {
- systemFeatures: { enable_marketplace: true },
-}
+let mockEnableMarketplace = true
const mockQuotaConfig = {
quota_type: CurrentSystemQuotaTypeEnum.free,
@@ -28,7 +19,11 @@ const mockQuotaConfig = {
}
vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: (selector: (s: { systemFeatures: { enable_marketplace: boolean } }) => unknown) => selector(mockGlobalState),
+ useSystemFeaturesQuery: () => ({
+ data: {
+ enable_marketplace: mockEnableMarketplace,
+ },
+ }),
}))
const mockProviders = [
@@ -92,7 +87,7 @@ describe('ModelProviderPage', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.clearAllMocks()
- mockGlobalState.systemFeatures.enable_marketplace = true
+ mockEnableMarketplace = true
Object.keys(mockDefaultModels).forEach((key) => {
mockDefaultModels[key] = { data: null, isLoading: false }
})
@@ -153,7 +148,7 @@ describe('ModelProviderPage', () => {
})
it('should hide marketplace section when marketplace feature is disabled', () => {
- mockGlobalState.systemFeatures.enable_marketplace = false
+ mockEnableMarketplace = false
render()
diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx
index cda81d276a..b84fe68358 100644
--- a/web/app/components/header/account-setting/model-provider-page/index.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/index.tsx
@@ -2,11 +2,10 @@ import type {
ModelProvider,
} from './declarations'
import { useDebounce } from 'ahooks'
-import { useEffect, useMemo } from 'react'
+import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { IS_CLOUD_EDITION } from '@/config'
-import { useAppContext } from '@/context/app-context'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { useSystemFeaturesQuery } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { cn } from '@/utils/classnames'
import {
@@ -32,14 +31,14 @@ const FixedModelProvider = ['langgenius/openai/openai', 'langgenius/anthropic/an
const ModelProviderPage = ({ searchText }: Props) => {
const debouncedSearchText = useDebounce(searchText, { wait: 500 })
const { t } = useTranslation()
- const { mutateCurrentWorkspace, isValidatingCurrentWorkspace } = useAppContext()
const { data: textGenerationDefaultModel, isLoading: isTextGenerationDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textGeneration)
const { data: embeddingsDefaultModel, isLoading: isEmbeddingsDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textEmbedding)
const { data: rerankDefaultModel, isLoading: isRerankDefaultModelLoading } = useDefaultModel(ModelTypeEnum.rerank)
const { data: speech2textDefaultModel, isLoading: isSpeech2textDefaultModelLoading } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: ttsDefaultModel, isLoading: isTTSDefaultModelLoading } = useDefaultModel(ModelTypeEnum.tts)
const { modelProviders: providers } = useProviderContext()
- const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
+ const { data: systemFeatures } = useSystemFeaturesQuery()
+ const enableMarketplace = systemFeatures?.enable_marketplace ?? false
const isDefaultModelLoading = isTextGenerationDefaultModelLoading
|| isEmbeddingsDefaultModelLoading
|| isRerankDefaultModelLoading
@@ -109,10 +108,6 @@ const ModelProviderPage = ({ searchText }: Props) => {
return [filteredConfiguredProviders, filteredNotConfiguredProviders]
}, [configuredProviders, debouncedSearchText, notConfiguredProviders])
- useEffect(() => {
- mutateCurrentWorkspace()
- }, [mutateCurrentWorkspace])
-
return (
@@ -140,7 +135,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
/>
- {IS_CLOUD_EDITION && }
+ {IS_CLOUD_EDITION && }
{!filteredConfiguredProviders?.length && (
@@ -175,7 +170,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
>
)}
{
- enable_marketplace && (
+ enableMarketplace && (
{
}
})
-vi.mock('@/context/app-context', () => ({
- useAppContext: () => ({
- currentWorkspace: mockWorkspace,
+vi.mock('@/service/use-common', () => ({
+ useCurrentWorkspace: () => ({
+ data: mockWorkspaceData,
+ isPending: mockWorkspaceIsPending,
}),
}))
vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: (selector: (state: { systemFeatures: { trial_models: string[] } }) => unknown) => selector({
- systemFeatures: {
+ useSystemFeaturesQuery: () => ({
+ data: {
trial_models: mockTrialModels,
},
}),
@@ -71,22 +77,21 @@ describe('QuotaPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockWorkspace = {
+ mockWorkspaceData = {
trial_credits: 100,
trial_credits_used: 30,
next_credit_reset_date: '2024-12-31',
}
+ mockWorkspaceIsPending = false
mockTrialModels = ['langgenius/openai/openai']
mockPlugins = [{ plugin_id: 'langgenius/openai', latest_package_identifier: 'openai@1.0.0' }]
})
it('should render loading state', () => {
- render(
- ,
- )
+ mockWorkspaceData = undefined
+ mockWorkspaceIsPending = true
+
+ render()
expect(screen.getByRole('status')).toBeInTheDocument()
})
@@ -102,8 +107,17 @@ describe('QuotaPanel', () => {
expect(screen.getByText(/modelProvider\.resetDate/)).toBeInTheDocument()
})
+ it('should keep quota content during background refetch when cached workspace exists', () => {
+ mockWorkspaceIsPending = true
+
+ render()
+
+ expect(screen.queryByRole('status')).not.toBeInTheDocument()
+ expect(screen.getByText('70')).toBeInTheDocument()
+ })
+
it('should floor credits at zero when usage is higher than quota', () => {
- mockWorkspace = {
+ mockWorkspaceData = {
trial_credits: 10,
trial_credits_used: 999,
next_credit_reset_date: '',
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx
index d44c201e0e..62815169ac 100644
--- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx
@@ -9,9 +9,9 @@ import { AnthropicShortLight, Deepseek, Gemini, Grok, OpenaiSmall, Tongyi } from
import Loading from '@/app/components/base/loading'
import Tooltip from '@/app/components/base/tooltip'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
-import { useAppContext } from '@/context/app-context'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { useSystemFeaturesQuery } from '@/context/global-public-context'
import useTimestamp from '@/hooks/use-timestamp'
+import { useCurrentWorkspace } from '@/service/use-common'
import { ModelProviderQuotaGetPaid } from '@/types/model-provider'
import { cn } from '@/utils/classnames'
import { formatNumber } from '@/utils/format'
@@ -48,16 +48,16 @@ const providerKeyToPluginId: Record = {
type QuotaPanelProps = {
providers: ModelProvider[]
- isLoading?: boolean
}
const QuotaPanel: FC = ({
providers,
- isLoading = false,
}) => {
const { t } = useTranslation()
- const { currentWorkspace } = useAppContext()
- const { trial_models } = useGlobalPublicStore(s => s.systemFeatures)
- const credits = Math.max((currentWorkspace.trial_credits - currentWorkspace.trial_credits_used) || 0, 0)
+ const { data: currentWorkspace, isPending: isPendingWorkspace } = useCurrentWorkspace()
+ const { data: systemFeatures } = useSystemFeaturesQuery()
+ const trialModels = systemFeatures?.trial_models ?? []
+ const credits = Math.max(((currentWorkspace?.trial_credits ?? 0) - (currentWorkspace?.trial_credits_used ?? 0)) || 0, 0)
+ const isLoading = isPendingWorkspace && !currentWorkspace
const providerMap = useMemo(() => new Map(
providers.map(p => [p.provider, p.preferred_provider_type]),
), [providers])
@@ -110,13 +110,13 @@ const QuotaPanel: FC = ({
{t('modelProvider.quota', { ns: 'common' })}
- modelNameMap[key as keyof typeof modelNameMap]).filter(Boolean).join(', ') })} />
+ modelNameMap[key as keyof typeof modelNameMap]).filter(Boolean).join(', ') })} />
{formatNumber(credits)}
{t('modelProvider.credits', { ns: 'common' })}
- {currentWorkspace.next_credit_reset_date
+ {currentWorkspace?.next_credit_reset_date
? (
<>
ยท
@@ -132,7 +132,7 @@ const QuotaPanel: FC = ({
: null}
- {allProviders.filter(({ key }) => trial_models.includes(key)).map(({ key, Icon }) => {
+ {allProviders.filter(({ key }) => trialModels.includes(key)).map(({ key, Icon }) => {
const providerType = providerMap.get(key)
const isConfigured = (installedProvidersMap.get(key)?.length ?? 0) > 0 // means the provider is configured API key
const getTooltipKey = () => {