From 1ad9305732530491f7ce52a205573fdbe488c91f Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 4 Mar 2026 18:43:01 +0800 Subject: [PATCH] fix(web): avoid quota panel flicker on account-setting tab switch - remove mount-time workspace invalidate in model provider page - read quota with useCurrentWorkspace and keep loading only for initial empty fetch - reuse existing useSystemFeaturesQuery for marketplace and trial models - update model provider and quota panel tests for new query/loading behavior --- .../model-provider-page/index.spec.tsx | 21 ++++------ .../model-provider-page/index.tsx | 17 +++----- .../provider-added-card/quota-panel.spec.tsx | 42 ++++++++++++------- .../provider-added-card/quota-panel.tsx | 20 ++++----- 4 files changed, 52 insertions(+), 48 deletions(-) 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 = () => {