From 5c0d3c4393937b77bc9ff997228ecb2dcd853a11 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:37:26 +0000 Subject: [PATCH] refactor(web): remove provider plan query flags (#41908) --- oxlint-suppressions.json | 2 +- web/__mocks__/provider-context.ts | 2 - .../billing/billing-integration.test.tsx | 1 - .../education-verification-flow.test.tsx | 1 - .../app/log/__tests__/filter.spec.tsx | 123 ++++++------ .../retention-upgrade-notice.spec.tsx | 72 +++---- .../app/log/cloud-sandbox-retention.ts | 17 +- .../__tests__/test-utils.tsx | 2 - .../workflow-log/__tests__/filter.spec.tsx | 68 +++---- web/app/components/billing/utils/index.ts | 2 +- .../main-nav/__tests__/index.spec.tsx | 2 - .../__tests__/workspace-card.spec.tsx | 2 - .../__tests__/features-trigger.spec.tsx | 86 ++++----- .../workflow-header/features-trigger.tsx | 19 +- .../nodes/llm/__tests__/panel.spec.tsx | 1 - .../hooks/use-trigger-events-limit-modal.ts | 148 ++++++--------- web/context/modal-context-provider.tsx | 12 +- web/context/modal-context.test.tsx | 177 ++++++++---------- web/context/provider-context-provider.tsx | 4 - web/context/provider-context.ts | 4 - 20 files changed, 311 insertions(+), 434 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 623a6061d0c..06e0911c5a8 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -4796,7 +4796,7 @@ }, "web/context/hooks/use-trigger-events-limit-modal.ts": { "eslint-react/set-state-in-effect": { - "count": 3 + "count": 1 }, "no-restricted-globals": { "count": 2 diff --git a/web/__mocks__/provider-context.ts b/web/__mocks__/provider-context.ts index f9eb2ffc8dd..e6136db2e7e 100644 --- a/web/__mocks__/provider-context.ts +++ b/web/__mocks__/provider-context.ts @@ -16,8 +16,6 @@ export const baseProviderContextValue: ProviderContextState = { supportRetrievalMethods: [], isAPIKeySet: true, plan: defaultPlan, - isFetchedPlan: false, - isFetchedPlanInfo: false, enableBilling: false, enableSkill: false, enableReplaceWebAppLogo: false, diff --git a/web/__tests__/billing/billing-integration.test.tsx b/web/__tests__/billing/billing-integration.test.tsx index 30531a83d98..e3f2f7a961e 100644 --- a/web/__tests__/billing/billing-integration.test.tsx +++ b/web/__tests__/billing/billing-integration.test.tsx @@ -115,7 +115,6 @@ const setupProviderContext = ( mockProviderCtx = { plan: createPlanData(planOverrides), enableBilling: true, - isFetchedPlan: true, enableEducationPlan: false, ...extra, } diff --git a/web/__tests__/billing/education-verification-flow.test.tsx b/web/__tests__/billing/education-verification-flow.test.tsx index e9f79a71697..f9bea585ea4 100644 --- a/web/__tests__/billing/education-verification-flow.test.tsx +++ b/web/__tests__/billing/education-verification-flow.test.tsx @@ -94,7 +94,6 @@ const setupContexts = ( mockProviderCtx = { plan: createPlanData(planOverrides), enableBilling: true, - isFetchedPlan: true, enableEducationPlan: false, ...providerOverrides, } diff --git a/web/app/components/app/log/__tests__/filter.spec.tsx b/web/app/components/app/log/__tests__/filter.spec.tsx index 8f3389c01bf..b06f5bce7c1 100644 --- a/web/app/components/app/log/__tests__/filter.spec.tsx +++ b/web/app/components/app/log/__tests__/filter.spec.tsx @@ -1,37 +1,37 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' +import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen' +import type { ReactElement } from 'react' import type { QueryParam } from '../index' -import { fireEvent, render, screen, within } from '@testing-library/react' +import { act, fireEvent, screen, waitFor, within } from '@testing-library/react' +import { consoleQuery } from '@/service/client' +import { + createConsoleQueryClient, + renderWithConsoleQuery, + seedFeatures, +} from '@/test/console/query-data' import Filter, { TIME_PERIOD_MAPPING } from '../filter' let mockAnnotationsCountLoading = false let mockAnnotationsCountData: { count: number } | null = { count: 10 } -const mockRuntime = vi.hoisted(() => ({ - deploymentEdition: 'CLOUD', - enableBilling: true, - isFetchedPlan: true, - isFetchedPlanInfo: true, - planType: 'professional', -})) +const scenario = { + deploymentEdition: 'CLOUD' as DeploymentEdition, + pending: false, + planType: 'professional' as CloudPlan, +} -vi.mock('@tanstack/react-query', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useSuspenseQuery: () => ({ data: mockRuntime.deploymentEdition }), - } -}) - -vi.mock('@/context/provider-context', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useProviderContext: () => ({ - enableBilling: mockRuntime.enableBilling, - isFetchedPlan: mockRuntime.isFetchedPlan, - isFetchedPlanInfo: mockRuntime.isFetchedPlanInfo, - plan: { type: mockRuntime.planType }, - }), - } -}) +const render = (ui: ReactElement) => { + const queryClient = createConsoleQueryClient() + if (scenario.pending) { + void queryClient.query({ + queryKey: consoleQuery.features.get.queryKey(), + queryFn: () => new Promise(() => {}), + }) + } else seedFeatures(queryClient, { billing: { subscription: { plan: scenario.planType } } }) + return renderWithConsoleQuery(ui, { + queryClient, + systemFeatures: { deployment_edition: scenario.deploymentEdition }, + }) +} vi.mock('@/service/use-log', () => ({ useAnnotationsCount: () => ({ @@ -102,11 +102,9 @@ describe('Filter', () => { vi.clearAllMocks() mockAnnotationsCountLoading = false mockAnnotationsCountData = { count: 10 } - mockRuntime.deploymentEdition = 'CLOUD' - mockRuntime.enableBilling = true - mockRuntime.isFetchedPlan = true - mockRuntime.isFetchedPlanInfo = true - mockRuntime.planType = 'professional' + scenario.deploymentEdition = 'CLOUD' + scenario.pending = false + scenario.planType = 'professional' }) describe('Rendering', () => { @@ -179,8 +177,8 @@ describe('Filter', () => { describe('User Interactions', () => { it('should only show supported periods for Cloud sandbox workspaces', () => { - mockRuntime.deploymentEdition = 'CLOUD' - mockRuntime.planType = 'sandbox' + scenario.deploymentEdition = 'CLOUD' + scenario.planType = 'sandbox' render() @@ -194,11 +192,12 @@ describe('Filter', () => { ]) }) - it('should only show supported periods while the Cloud plan is pending', () => { - mockRuntime.isFetchedPlan = false - mockRuntime.isFetchedPlanInfo = false + it('should keep periods restricted until the Cloud plan resolves, then follow cache updates', async () => { + scenario.pending = true - render() + const { queryClient } = render( + , + ) fireEvent.click(screen.getByRole('button', { name: 'open-options-1' })) @@ -208,36 +207,36 @@ describe('Filter', () => { expect.stringMatching(/(?:^|\.)filter\.period\.last7days(?=$|:)/), expect.stringMatching(/(?:^|\.)filter\.period\.last30days(?=$|:)/), ]) + + act(() => { + seedFeatures(queryClient, { billing: { subscription: { plan: 'professional' } } }) + }) + await waitFor(() => expect(periodOptions.getAllByRole('listitem')).toHaveLength(9)) + + act(() => { + seedFeatures(queryClient, { billing: { subscription: { plan: 'sandbox' } } }) + }) + await waitFor(() => expect(periodOptions.getAllByRole('listitem')).toHaveLength(3)) }) - it('should keep all periods when Cloud billing is known to be disabled', () => { - mockRuntime.enableBilling = false - mockRuntime.isFetchedPlan = false - mockRuntime.isFetchedPlanInfo = true + it.each(['COMMUNITY', 'ENTERPRISE'] as const)( + 'should keep all periods for sandbox workspaces in %s', + (edition) => { + scenario.deploymentEdition = edition + scenario.planType = 'sandbox' - render() + render() - fireEvent.click(screen.getByRole('button', { name: 'open-options-1' })) + fireEvent.click(screen.getByRole('button', { name: 'open-options-1' })) - const periodOptions = within(screen.getByRole('list', { name: 'options-1' })) - expect(periodOptions.getAllByRole('listitem')).toHaveLength(9) - }) - - it('should keep all periods for sandbox workspaces outside Cloud', () => { - mockRuntime.deploymentEdition = 'COMMUNITY' - mockRuntime.planType = 'sandbox' - - render() - - fireEvent.click(screen.getByRole('button', { name: 'open-options-1' })) - - const periodOptions = within(screen.getByRole('list', { name: 'options-1' })) - expect(periodOptions.getAllByRole('listitem')).toHaveLength(9) - }) + const periodOptions = within(screen.getByRole('list', { name: 'options-1' })) + expect(periodOptions.getAllByRole('listitem')).toHaveLength(9) + }, + ) it('should reset the Cloud sandbox period to today when cleared', () => { - mockRuntime.deploymentEdition = 'CLOUD' - mockRuntime.planType = 'sandbox' + scenario.deploymentEdition = 'CLOUD' + scenario.planType = 'sandbox' render() diff --git a/web/app/components/app/log/__tests__/retention-upgrade-notice.spec.tsx b/web/app/components/app/log/__tests__/retention-upgrade-notice.spec.tsx index b94f5ad2fd0..a27bce8618c 100644 --- a/web/app/components/app/log/__tests__/retention-upgrade-notice.spec.tsx +++ b/web/app/components/app/log/__tests__/retention-upgrade-notice.spec.tsx @@ -2,22 +2,12 @@ import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen' import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { createMockProviderContextValue } from '@/__mocks__/provider-context' -import { defaultPlan } from '@/app/components/billing/config' import { useModalContext } from '@/context/modal-context' -import { useProviderContext } from '@/context/provider-context' -import { createConsoleQueryWrapper } from '@/test/console/query-data' +import { consoleQuery } from '@/service/client' +import { createConsoleQueryWrapper, seedFeatures } from '@/test/console/query-data' import { render } from '@/test/console/render' import { RetentionUpgradeNotice } from '../retention-upgrade-notice' -vi.mock('@/context/provider-context', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useProviderContext: vi.fn(), - } -}) - vi.mock('@/context/modal-context', async (importOriginal) => { const actual = await importOriginal() return { @@ -26,46 +16,30 @@ vi.mock('@/context/modal-context', async (importOriginal) => { } }) -const mockUseProviderContext = vi.mocked(useProviderContext) const mockUseModalContext = vi.mocked(useModalContext) describe('RetentionUpgradeNotice', () => { const setShowPricingModal = vi.fn() - function mockProvider({ - enableBilling = true, - isFetchedPlan = true, - isFetchedPlanInfo = true, - planType = 'sandbox', - }: { - enableBilling?: boolean - isFetchedPlan?: boolean - isFetchedPlanInfo?: boolean - planType?: CloudPlan - } = {}) { - mockUseProviderContext.mockReturnValue( - createMockProviderContextValue({ - enableBilling, - isFetchedPlan, - isFetchedPlanInfo, - plan: { - ...defaultPlan, - type: planType, - }, - }), - ) - } - - function renderNotice(deploymentEdition: DeploymentEdition = 'CLOUD') { - const { wrapper } = createConsoleQueryWrapper({ + function renderNotice( + deploymentEdition: DeploymentEdition = 'CLOUD', + plan: CloudPlan | null = 'sandbox', + ) { + const { wrapper, queryClient } = createConsoleQueryWrapper({ systemFeatures: { deployment_edition: deploymentEdition }, }) + if (plan) seedFeatures(queryClient, { billing: { subscription: { plan } } }) + else { + void queryClient.query({ + queryKey: consoleQuery.features.get.queryKey(), + queryFn: () => new Promise(() => {}), + }) + } return render(, { wrapper }) } beforeEach(() => { vi.clearAllMocks() - mockProvider() mockUseModalContext.mockReturnValue({ setShowPricingModal, } as unknown as ReturnType) @@ -89,28 +63,26 @@ describe('RetentionUpgradeNotice', () => { it.each([ { name: 'paid Cloud workspaces', - provider: { planType: 'professional' }, + plan: 'professional', deploymentEdition: 'CLOUD', }, { name: 'self-hosted sandbox workspaces', - provider: { planType: 'sandbox' }, + plan: 'sandbox', deploymentEdition: 'COMMUNITY', }, { - name: 'workspaces without billing', - provider: { enableBilling: false }, - deploymentEdition: 'CLOUD', + name: 'Enterprise workspaces', + plan: 'sandbox', + deploymentEdition: 'ENTERPRISE', }, { name: 'workspaces before plan loading completes', - provider: { isFetchedPlan: false, isFetchedPlanInfo: false }, + plan: null, deploymentEdition: 'CLOUD', }, - ] as const)('should not show guidance for $name', ({ provider, deploymentEdition }) => { - mockProvider(provider) - - renderNotice(deploymentEdition) + ] as const)('should not show guidance for $name', ({ plan, deploymentEdition }) => { + renderNotice(deploymentEdition, plan) expect(screen.queryByRole('status')).not.toBeInTheDocument() }) diff --git a/web/app/components/app/log/cloud-sandbox-retention.ts b/web/app/components/app/log/cloud-sandbox-retention.ts index 29bc15398b7..0938009169d 100644 --- a/web/app/components/app/log/cloud-sandbox-retention.ts +++ b/web/app/components/app/log/cloud-sandbox-retention.ts @@ -1,8 +1,8 @@ 'use client' -import { useSuspenseQuery } from '@tanstack/react-query' -import { useProviderContext } from '@/context/provider-context' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { systemFeaturesQueryOptions } from '@/features/system-features/client' +import { consoleQuery } from '@/service/client' export const CLOUD_SANDBOX_TIME_PERIOD_KEYS = new Set(['1', '2', '3']) export const CLOUD_SANDBOX_CLEARED_TIME_PERIOD = '1' @@ -42,12 +42,15 @@ export function useCloudSandboxPlanStatus(): CloudSandboxPlanState { ...systemFeaturesQueryOptions(), select: ({ deployment_edition }) => deployment_edition, }) - const { enableBilling, isFetchedPlan, isFetchedPlanInfo, plan } = useProviderContext() + const { data: plan } = useQuery( + consoleQuery.features.get.queryOptions({ + enabled: deploymentEdition === 'CLOUD', + select: (features) => features.billing.subscription.plan, + }), + ) if (deploymentEdition !== 'CLOUD') return 'unrestricted' - if (!isFetchedPlanInfo) return 'pending' - if (!enableBilling) return 'unrestricted' - if (!isFetchedPlan) return 'pending' + if (!plan) return 'pending' - return plan.type === 'sandbox' ? 'sandbox' : 'unrestricted' + return plan === 'sandbox' ? 'sandbox' : 'unrestricted' } diff --git a/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx b/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx index 2fd88ee7ed2..a00915264a8 100644 --- a/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx +++ b/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx @@ -46,8 +46,6 @@ const defaultProviderContext = { supportRetrievalMethods: [], isAPIKeySet: false, plan: defaultPlan, - isFetchedPlan: false, - isFetchedPlanInfo: false, enableBilling: false, enableSkill: false, enableReplaceWebAppLogo: false, diff --git a/web/app/components/app/workflow-log/__tests__/filter.spec.tsx b/web/app/components/app/workflow-log/__tests__/filter.spec.tsx index b6a8d6c2c0f..d050ecf9717 100644 --- a/web/app/components/app/workflow-log/__tests__/filter.spec.tsx +++ b/web/app/components/app/workflow-log/__tests__/filter.spec.tsx @@ -7,44 +7,37 @@ * - Keyword search */ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' +import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen' +import type { ReactElement } from 'react' import type { QueryParam } from '../index' -import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' +import { fireEvent, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' +import { + createConsoleQueryClient, + renderWithConsoleQuery, + seedFeatures, +} from '@/test/console/query-data' import Filter, { TIME_PERIOD_MAPPING } from '../filter' // ============================================================================ // Mocks // ============================================================================ -const mockRuntime = vi.hoisted(() => ({ - deploymentEdition: 'CLOUD', - enableBilling: true, - isFetchedPlan: true, - isFetchedPlanInfo: true, - planType: 'professional', -})) +const scenario = { + deploymentEdition: 'CLOUD' as DeploymentEdition, + planType: 'professional' as CloudPlan, +} -vi.mock('@tanstack/react-query', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useSuspenseQuery: () => ({ data: mockRuntime.deploymentEdition }), - } -}) - -vi.mock('@/context/provider-context', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useProviderContext: () => ({ - enableBilling: mockRuntime.enableBilling, - isFetchedPlan: mockRuntime.isFetchedPlan, - isFetchedPlanInfo: mockRuntime.isFetchedPlanInfo, - plan: { type: mockRuntime.planType }, - }), - } -}) +const render = (ui: ReactElement) => { + const queryClient = createConsoleQueryClient() + seedFeatures(queryClient, { billing: { subscription: { plan: scenario.planType } } }) + return renderWithConsoleQuery(ui, { + queryClient, + systemFeatures: { deployment_edition: scenario.deploymentEdition }, + }) +} const mockTrackEvent = vi.fn() vi.mock('@/app/components/base/amplitude/utils', () => ({ @@ -70,11 +63,8 @@ describe('Filter', () => { beforeEach(() => { vi.clearAllMocks() - mockRuntime.deploymentEdition = 'CLOUD' - mockRuntime.enableBilling = true - mockRuntime.isFetchedPlan = true - mockRuntime.isFetchedPlanInfo = true - mockRuntime.planType = 'professional' + scenario.deploymentEdition = 'CLOUD' + scenario.planType = 'professional' }) // -------------------------------------------------------------------------- @@ -214,8 +204,8 @@ describe('Filter', () => { describe('Time Period Filter', () => { it('should only show supported periods for Cloud sandbox workspaces', async () => { const user = userEvent.setup() - mockRuntime.deploymentEdition = 'CLOUD' - mockRuntime.planType = 'sandbox' + scenario.deploymentEdition = 'CLOUD' + scenario.planType = 'sandbox' render( , @@ -237,8 +227,8 @@ describe('Filter', () => { it('should keep all periods for sandbox workspaces outside Cloud', async () => { const user = userEvent.setup() - mockRuntime.deploymentEdition = 'COMMUNITY' - mockRuntime.planType = 'sandbox' + scenario.deploymentEdition = 'COMMUNITY' + scenario.planType = 'sandbox' render( , @@ -253,8 +243,8 @@ describe('Filter', () => { it('should reset the Cloud sandbox period to today when cleared', async () => { const user = userEvent.setup() const setQueryParams = vi.fn() - mockRuntime.deploymentEdition = 'CLOUD' - mockRuntime.planType = 'sandbox' + scenario.deploymentEdition = 'CLOUD' + scenario.planType = 'sandbox' render( { return null } -const getResetInDaysFromDate = (resetDate: number) => { +export const getResetInDaysFromDate = (resetDate: number) => { const resetDay = normalizeResetDate(resetDate) if (!resetDay) return null diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index 648b20861aa..e59439197bb 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -657,7 +657,6 @@ describe('MainNav', () => { ;(useProviderContext as Mock).mockReturnValue({ enableBilling: true, enableEducationPlan: false, - isFetchedPlan: true, plan: { type: 'sandbox' }, } as ProviderContextState) ;(useModalContext as Mock).mockReturnValue({ @@ -837,7 +836,6 @@ describe('MainNav', () => { ;(useProviderContext as Mock).mockReturnValue({ enableBilling: true, enableEducationPlan: true, - isFetchedPlan: true, plan: { type: 'sandbox' }, } as ProviderContextState) diff --git a/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx b/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx index 91ccdeef403..15d5f40c297 100644 --- a/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx +++ b/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx @@ -179,7 +179,6 @@ describe('WorkspaceCard', () => { vi.mocked(useProviderContext).mockReturnValue({ enableBilling: true, enableEducationPlan: false, - isFetchedPlan: true, plan: { type: 'sandbox' }, } as ProviderContextState) mockWorkspacePermissionKeys(['workspace.member.manage']) @@ -346,7 +345,6 @@ describe('WorkspaceCard', () => { vi.mocked(useProviderContext).mockReturnValue({ enableBilling: false, enableEducationPlan: false, - isFetchedPlan: true, plan: { type: 'sandbox' }, } as ProviderContextState) renderWorkspaceCard({ systemFeatures: { deployment_edition: 'CLOUD' } }) diff --git a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx index 54ddf212a8f..454ecab1c47 100644 --- a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx @@ -1,13 +1,14 @@ import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' +import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen' import type { ReactElement } from 'react' import type { AppPublisherProps } from '@/app/components/app/app-publisher/types' import type { App } from '@/types/app' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useStore as useAppStore } from '@/app/components/app/store' import { BlockEnum, InputVarType } from '@/app/components/workflow/types' import { consoleQuery } from '@/service/client' +import { createConsoleQueryWrapper, seedFeatures } from '@/test/console/query-data' import FeaturesTrigger from '../features-trigger' const mockUseIsChatMode = vi.fn() @@ -17,7 +18,6 @@ const mockUseChecklist = vi.fn() const mockUseChecklistBeforePublish = vi.fn() const mockUseNodesSyncDraft = vi.fn() const mockUseFeatures = vi.fn() -const mockUseProviderContext = vi.fn() const mockUseNodes = vi.fn() const mockUseEdges = vi.fn() @@ -112,24 +112,10 @@ vi.mock('@/app/components/workflow/hooks-store', () => ({ }), })) -vi.mock('@tanstack/react-query', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useQueryClient: () => ({ - invalidateQueries: mockInvalidateQueries, - }), - } -}) - vi.mock('@/app/components/base/features/hooks', () => ({ useFeatures: (selector: (state: Record) => unknown) => mockUseFeatures(selector), })) -vi.mock('@/context/provider-context', () => ({ - useProviderContext: () => mockUseProviderContext(), -})) - vi.mock('@/app/components/workflow/store/workflow/use-nodes', () => ({ default: () => mockUseNodes(), })) @@ -253,23 +239,16 @@ vi.mock('@/hooks/use-theme', () => ({ // Use real app store - global zustand mock will auto-reset between tests -const createProviderContext = ({ - type = 'sandbox', - isFetchedPlan = true, -}: { - type?: CloudPlan - isFetchedPlan?: boolean -}) => ({ - plan: { type }, - isFetchedPlan, -}) - -const renderWithToast = (ui: ReactElement) => { - const queryClient = new QueryClient() - return { - queryClient, - ...render({ui}), - } +const renderWithToast = ( + ui: ReactElement, + { edition = 'CLOUD', plan = 'sandbox' }: { edition?: DeploymentEdition; plan?: CloudPlan } = {}, +) => { + const { queryClient, wrapper } = createConsoleQueryWrapper({ + systemFeatures: { deployment_edition: edition }, + }) + seedFeatures(queryClient, { billing: { subscription: { plan } } }) + vi.spyOn(queryClient, 'invalidateQueries').mockImplementation(mockInvalidateQueries) + return { queryClient, ...render(ui, { wrapper }) } } describe('FeaturesTrigger', () => { @@ -295,7 +274,6 @@ describe('FeaturesTrigger', () => { mockUseFeatures.mockImplementation((selector: (state: Record) => unknown) => selector({ features: { file: {} } }), ) - mockUseProviderContext.mockReturnValue(createProviderContext({})) mockUseNodes.mockReturnValue([]) mockUseEdges.mockReturnValue([]) // Set up app store state @@ -467,24 +445,32 @@ describe('FeaturesTrigger', () => { }) }) - it('should set startNodeLimitExceeded when sandbox entry limit is exceeded', () => { - // Arrange - mockUseNodes.mockReturnValue([ - { id: 'start', data: { type: BlockEnum.Start } }, - { id: 'trigger-1', data: { type: BlockEnum.TriggerWebhook } }, - { id: 'trigger-2', data: { type: BlockEnum.TriggerSchedule } }, - { id: 'end', data: { type: BlockEnum.End } }, - ]) + it.each([ + { edition: 'CLOUD', plan: 'sandbox', restricted: true }, + { edition: 'CLOUD', plan: 'professional', restricted: false }, + { edition: 'COMMUNITY', plan: 'sandbox', restricted: false }, + { edition: 'ENTERPRISE', plan: 'sandbox', restricted: false }, + ] as const)( + 'should apply the entry limit for $edition / $plan', + ({ edition, plan, restricted }) => { + // Arrange + mockUseNodes.mockReturnValue([ + { id: 'start', data: { type: BlockEnum.Start } }, + { id: 'trigger-1', data: { type: BlockEnum.TriggerWebhook } }, + { id: 'trigger-2', data: { type: BlockEnum.TriggerSchedule } }, + { id: 'end', data: { type: BlockEnum.End } }, + ]) - // Act - renderWithToast() + // Act + renderWithToast(, { edition, plan }) - // Assert - const publisher = screen.getByTestId('app-publisher') - expect(publisher).toHaveAttribute('data-start-node-limit-exceeded', 'true') - expect(publisher).toHaveAttribute('data-publish-disabled', 'true') - expect(publisher).toHaveAttribute('data-has-trigger-node', 'true') - }) + // Assert + const publisher = screen.getByTestId('app-publisher') + expect(publisher).toHaveAttribute('data-start-node-limit-exceeded', String(restricted)) + expect(publisher).toHaveAttribute('data-publish-disabled', String(restricted)) + expect(publisher).toHaveAttribute('data-has-trigger-node', 'true') + }, + ) }) // Verifies callbacks wired from AppPublisher to stores and draft syncing. diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index 659936e4b08..e7d89862aea 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -9,7 +9,7 @@ import type { CommonEdgeType, Node } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' -import { useQueryClient } from '@tanstack/react-query' +import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { memo, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useEdges } from 'reactflow' @@ -28,7 +28,7 @@ import { isAgentV2NodeData } from '@/app/components/workflow/nodes/agent-v2/type import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { BlockEnum, InputVarType, isTriggerNode } from '@/app/components/workflow/types' -import { useProviderContext } from '@/context/provider-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useTheme from '@/hooks/use-theme' import { fetchAppDetail } from '@/service/apps' import { consoleQuery } from '@/service/client' @@ -50,7 +50,16 @@ const FeaturesTrigger = () => { const appID = appDetail?.id const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly() const canReleaseAndVersion = useHooksStore((s) => s.accessControl.canReleaseAndVersion) - const { plan, isFetchedPlan } = useProviderContext() + const { data: deploymentEdition } = useSuspenseQuery({ + ...systemFeaturesQueryOptions(), + select: ({ deployment_edition }) => deployment_edition, + }) + const { data: plan } = useQuery( + consoleQuery.features.get.queryOptions({ + enabled: deploymentEdition === 'CLOUD', + select: (features) => features.billing.subscription.plan, + }), + ) const publishedAt = useStore((s) => s.publishedAt) const draftUpdatedAt = useStore((s) => s.draftUpdatedAt) const toolPublished = useStore((s) => s.toolPublished) @@ -135,8 +144,8 @@ const FeaturesTrigger = () => { if (nodeType === BlockEnum.Start || isTriggerNode(nodeType)) return count + 1 return count }, 0) - return isFetchedPlan && plan.type === 'sandbox' && entryCount > 2 - }, [nodes, plan.type, isFetchedPlan]) + return deploymentEdition === 'CLOUD' && plan === 'sandbox' && entryCount > 2 + }, [nodes, plan, deploymentEdition]) const hasHumanInputNode = useMemo(() => { return nodes.some((node) => node.data.type === BlockEnum.HumanInput) diff --git a/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx index fb7943b65a8..ac1887a7688 100644 --- a/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx @@ -187,7 +187,6 @@ const renderPanelElement = (data?: Partial) => ( plugin_id: 'langgenius/openai', } as unknown as ModelProviderSummaryResponse, ], - isFetchedPlan: true, })} > diff --git a/web/context/hooks/use-trigger-events-limit-modal.ts b/web/context/hooks/use-trigger-events-limit-modal.ts index 8b017697983..da90ee8ae41 100644 --- a/web/context/hooks/use-trigger-events-limit-modal.ts +++ b/web/context/hooks/use-trigger-events-limit-modal.ts @@ -1,10 +1,11 @@ -import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import dayjs from 'dayjs' -import { useCallback, useEffect, useRef, useState } from 'react' -import { NUM_INFINITE } from '@/app/components/billing/config' +import { useAtomValue } from 'jotai' +import { useEffect, useState } from 'react' +import { getResetInDaysFromDate } from '@/app/components/billing/utils' +import { currentWorkspaceIdAtom } from '@/context/workspace-state' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { isServer } from '@/utils/client' +import { consoleQuery } from '@/service/client' type TriggerEventsLimitModalContent = { usage: number @@ -12,24 +13,6 @@ type TriggerEventsLimitModalContent = { resetInDays?: number } -type TriggerEventsLimitModalState = TriggerEventsLimitModalContent & { - storageKey: string - persistDismiss: boolean -} - -type TriggerPlanInfo = { - type: CloudPlan - usage: { triggerEvents: number } - total: { triggerEvents: number } - reset: { triggerEvents?: number | null } -} - -type UseTriggerEventsLimitModalOptions = { - plan: TriggerPlanInfo - isFetchedPlan: boolean - currentWorkspaceId?: string -} - type UseTriggerEventsLimitModalResult = { triggerEventsLimitModal: TriggerEventsLimitModalContent | null dismissTriggerEventsLimitModal: () => void @@ -37,89 +20,72 @@ type UseTriggerEventsLimitModalResult = { const TRIGGER_EVENTS_LOCALSTORAGE_PREFIX = 'trigger-events-limit-dismissed' -export const useTriggerEventsLimitModal = ({ - plan, - isFetchedPlan, - currentWorkspaceId, -}: UseTriggerEventsLimitModalOptions): UseTriggerEventsLimitModalResult => { +export const useTriggerEventsLimitModal = (): UseTriggerEventsLimitModalResult => { + const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom) const { data: deploymentEdition } = useSuspenseQuery({ ...systemFeaturesQueryOptions(), select: ({ deployment_edition }) => deployment_edition, }) - const [triggerEventsLimitModal, setTriggerEventsLimitModal] = - useState(null) - const dismissedTriggerEventsLimitStorageKeysRef = useRef>({}) + const { data: quota } = useQuery( + consoleQuery.features.get.queryOptions({ + enabled: deploymentEdition === 'CLOUD', + select: ({ billing, trigger_event }) => ({ + plan: billing.subscription.plan, + ...trigger_event, + }), + }), + ) + const [dismissedCycles, setDismissedCycles] = useState>({}) + const resetInDays = quota ? getResetInDaysFromDate(quota.reset_date) : null + const cycleTag = + resetInDays !== null + ? dayjs().startOf('day').add(resetInDays, 'day').format('YYYY-MM-DD') + : quota?.plan === 'sandbox' + ? dayjs().endOf('month').format('YYYY-MM-DD') + : 'none' + const storageKey = + deploymentEdition === 'CLOUD' && + currentWorkspaceId && + quota && + quota.plan !== 'team' && + quota.limit > 0 && + quota.usage >= quota.limit + ? `${TRIGGER_EVENTS_LOCALSTORAGE_PREFIX}-${currentWorkspaceId}-${quota.plan}-${quota.limit}-${cycleTag}` + : null + const dismissed = storageKey ? dismissedCycles[storageKey] : undefined useEffect(() => { - if (deploymentEdition !== 'CLOUD') return - if (isServer) return - if (!currentWorkspaceId) return - if (!isFetchedPlan) { - setTriggerEventsLimitModal(null) - return - } + if (!storageKey || dismissed !== undefined) return - const { type, usage, total, reset } = plan - const isUnlimited = total.triggerEvents === NUM_INFINITE - const reachedLimit = total.triggerEvents > 0 && usage.triggerEvents >= total.triggerEvents - - if (type === 'team' || isUnlimited || !reachedLimit) { - if (triggerEventsLimitModal) setTriggerEventsLimitModal(null) - return - } - - const triggerResetInDays = - type === 'professional' && total.triggerEvents !== NUM_INFINITE - ? (reset.triggerEvents ?? undefined) - : undefined - const cycleTag = (() => { - if (typeof reset.triggerEvents === 'number') - return dayjs().startOf('day').add(reset.triggerEvents, 'day').format('YYYY-MM-DD') - if (type === 'sandbox') return dayjs().endOf('month').format('YYYY-MM-DD') - return 'none' - })() - const storageKey = `${TRIGGER_EVENTS_LOCALSTORAGE_PREFIX}-${currentWorkspaceId}-${type}-${total.triggerEvents}-${cycleTag}` - if (dismissedTriggerEventsLimitStorageKeysRef.current[storageKey]) return - - let persistDismiss = true - let hasDismissed = false + let storedDismissal = false try { - if (localStorage.getItem(storageKey) === '1') hasDismissed = true + storedDismissal = localStorage.getItem(storageKey) === '1' } catch { - persistDismiss = false + // Storage can be unavailable; dismissal still lasts for this mounted session. } - if (hasDismissed) return + setDismissedCycles((current) => ({ ...current, [storageKey]: storedDismissal })) + }, [storageKey, dismissed]) - if (triggerEventsLimitModal?.storageKey === storageKey) return + const dismissTriggerEventsLimitModal = () => { + if (!storageKey) return - setTriggerEventsLimitModal({ - usage: usage.triggerEvents, - total: total.triggerEvents, - resetInDays: triggerResetInDays, - storageKey, - persistDismiss, - }) - }, [plan, isFetchedPlan, triggerEventsLimitModal, currentWorkspaceId, deploymentEdition]) - - const dismissTriggerEventsLimitModal = useCallback(() => { - if (!triggerEventsLimitModal) return - - const { storageKey, persistDismiss } = triggerEventsLimitModal - if (persistDismiss) { - try { - localStorage.setItem(storageKey, '1') - setTriggerEventsLimitModal(null) - return - } catch { - // ignore error and fall back to in-memory guard - } + setDismissedCycles((current) => ({ ...current, [storageKey]: true })) + try { + localStorage.setItem(storageKey, '1') + } catch { + // The in-memory dismissal above also covers failed storage writes. } - dismissedTriggerEventsLimitStorageKeysRef.current[storageKey] = true - setTriggerEventsLimitModal(null) - }, [triggerEventsLimitModal]) + } return { - triggerEventsLimitModal, + triggerEventsLimitModal: + storageKey && dismissed === false && quota + ? { + usage: quota.usage, + total: quota.limit, + resetInDays: quota.plan === 'professional' ? (resetInDays ?? undefined) : undefined, + } + : null, dismissTriggerEventsLimitModal, } } diff --git a/web/context/modal-context-provider.tsx b/web/context/modal-context-provider.tsx index b88b0fed621..c726694336a 100644 --- a/web/context/modal-context-provider.tsx +++ b/web/context/modal-context-provider.tsx @@ -8,11 +8,8 @@ import type { UpdatePluginPayload } from '@/app/components/plugins/types' import type { InputVar } from '@/app/components/workflow/types' import type { ExternalDataTool } from '@/models/common' import type { ModerationConfig, PromptVariable } from '@/models/debug' -import { useAtomValue } from 'jotai' import { useCallback, useState } from 'react' import { PluginCategoryEnum } from '@/app/components/plugins/types' -import { useProviderContext } from '@/context/provider-context' -import { currentWorkspaceIdAtom } from '@/context/workspace-state' import { usePricingModal } from '@/hooks/use-query-params' import dynamic from '@/next/dynamic' import { useTriggerEventsLimitModal } from './hooks/use-trigger-events-limit-modal' @@ -93,15 +90,8 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) => > | null>(null) const [showUpdatePluginModal, setShowUpdatePluginModal] = useState | null>(null) - const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom) - const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false) - const { plan, isFetchedPlan } = useProviderContext() - const { triggerEventsLimitModal, dismissTriggerEventsLimitModal } = useTriggerEventsLimitModal({ - plan, - isFetchedPlan, - currentWorkspaceId, - }) + const { triggerEventsLimitModal, dismissTriggerEventsLimitModal } = useTriggerEventsLimitModal() const handleCancelModerationSettingModal = () => { setShowModerationSettingModal(null) diff --git a/web/context/modal-context.test.tsx b/web/context/modal-context.test.tsx index 3d32d8dea74..7dec0ac2c83 100644 --- a/web/context/modal-context.test.tsx +++ b/web/context/modal-context.test.tsx @@ -1,12 +1,12 @@ -import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' -import { screen, waitFor } from '@testing-library/react' +import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen' +import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import dayjs from 'dayjs' import * as React from 'react' -import { defaultPlan } from '@/app/components/billing/config' import { PluginCategoryEnum, PluginSource } from '@/app/components/plugins/types' import { useModalContextSelector } from '@/context/modal-context' import { ModalContextProvider } from '@/context/modal-context-provider' -import { createConsoleQueryWrapper } from '@/test/console/query-data' +import { createConsoleQueryWrapper, seedFeatures } from '@/test/console/query-data' import { render } from '@/test/console/render' import { createNuqsTestWrapper } from '@/test/nuqs-testing' @@ -29,11 +29,6 @@ vi.mock('@/app/components/plugins/update-plugin', () => ({ ), })) -const mockUseProviderContext = vi.fn() -vi.mock('@/context/provider-context', () => ({ - useProviderContext: () => mockUseProviderContext(), -})) - const mockConsoleStateReader = vi.fn() vi.mock('@/context/workspace-state', async () => { @@ -41,39 +36,6 @@ vi.mock('@/context/workspace-state', async () => { return createWorkspaceStateModuleMock(() => mockConsoleStateReader()) }) -type DefaultPlanShape = typeof defaultPlan -type ResetShape = { - apiRateLimit: number | null - triggerEvents: number | null -} -type PlanShape = Omit & { - type: CloudPlan - reset: ResetShape -} -type PlanOverrides = Partial> & { - type?: CloudPlan - usage?: Partial - total?: Partial - reset?: Partial -} - -const createPlan = (overrides: PlanOverrides = {}): PlanShape => ({ - ...defaultPlan, - ...overrides, - usage: { - ...defaultPlan.usage, - ...overrides.usage, - }, - total: { - ...defaultPlan.total, - ...overrides.total, - }, - reset: { - ...defaultPlan.reset, - ...overrides.reset, - }, -}) - const ModalBlockingState = () => { const hasBlockingModalOpen = useModalContextSelector((state) => state.hasBlockingModalOpen) @@ -117,10 +79,15 @@ const UpdatePluginTrigger = ({ ) } -const renderProvider = (children: React.ReactNode = ) => { - const { wrapper: QueryWrapper } = createConsoleQueryWrapper({ - systemFeatures: { deployment_edition: 'CLOUD' }, +const renderProvider = ( + children: React.ReactNode = , + features: Parameters[1] = {}, + edition: DeploymentEdition = 'CLOUD', +) => { + const { wrapper: QueryWrapper, queryClient } = createConsoleQueryWrapper({ + systemFeatures: { deployment_edition: edition }, }) + seedFeatures(queryClient, features) const { wrapper: NuqsWrapper } = createNuqsTestWrapper() const wrapper = ({ children: wrapperChildren }: { children: React.ReactNode }) => ( @@ -128,13 +95,15 @@ const renderProvider = (children: React.ReactNode = ) => { ) - return render({children}, { wrapper }) + return { + queryClient, + ...render({children}, { wrapper }), + } } describe('ModalContextProvider trigger events limit modal', () => { beforeEach(() => { mockConsoleStateReader.mockReset() - mockUseProviderContext.mockReset() window.localStorage.clear() mockConsoleStateReader.mockReturnValue({ currentWorkspace: { @@ -147,23 +116,60 @@ describe('ModalContextProvider trigger events limit modal', () => { vi.restoreAllMocks() }) + it('updates the visible quota and closes the modal when usage drops below the limit', async () => { + const features = { + billing: { subscription: { plan: 'professional' as const } }, + trigger_event: { usage: 200, limit: 200, reset_date: dayjs().add(3, 'day').unix() }, + } + const { queryClient } = renderProvider(undefined, features) + + expect(await screen.findByRole('dialog')).toBeInTheDocument() + act(() => { + seedFeatures(queryClient, { + ...features, + trigger_event: { ...features.trigger_event, usage: 250 }, + }) + }) + expect(await screen.findByText('250')).toBeInTheDocument() + + act(() => { + seedFeatures(queryClient, { + ...features, + trigger_event: { ...features.trigger_event, usage: 100 }, + }) + }) + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()) + expect(screen.getByText('clear')).toBeInTheDocument() + }) + + it.each(['COMMUNITY', 'ENTERPRISE'] as const)( + 'does not show Cloud quota prompts in %s', + (edition) => { + renderProvider( + undefined, + { + billing: { subscription: { plan: 'sandbox' } }, + trigger_event: { usage: 200, limit: 200 }, + }, + edition, + ) + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + expect(screen.getByText('clear')).toBeInTheDocument() + }, + ) + it('opens the trigger events limit modal and persists dismissal in localStorage', async () => { - const plan = createPlan({ - type: 'professional', - usage: { triggerEvents: 3000 }, - total: { triggerEvents: 3000 }, - reset: { triggerEvents: 5 }, - }) - mockUseProviderContext.mockReturnValue({ - plan, - isFetchedPlan: true, - }) + const features = { + billing: { subscription: { plan: 'professional' as const } }, + trigger_event: { usage: 3000, limit: 3000, reset_date: dayjs().add(5, 'day').unix() }, + } // Note: vitest.setup.ts replaces localStorage with a mock object that has vi.fn() methods // We need to spy on the mock's setItem, not Storage.prototype.setItem const setItemSpy = vi.spyOn(localStorage, 'setItem') const user = userEvent.setup() - renderProvider() + renderProvider(undefined, features) await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()) expect(screen.getAllByText('3000')).toHaveLength(2) @@ -182,23 +188,16 @@ describe('ModalContextProvider trigger events limit modal', () => { }) it('relies on the in-memory guard when localStorage reads throw', async () => { - const plan = createPlan({ - type: 'professional', - usage: { triggerEvents: 200 }, - total: { triggerEvents: 200 }, - reset: { triggerEvents: 3 }, - }) - mockUseProviderContext.mockReturnValue({ - plan, - isFetchedPlan: true, - }) + const features = { + billing: { subscription: { plan: 'professional' as const } }, + trigger_event: { usage: 200, limit: 200, reset_date: dayjs().add(3, 'day').unix() }, + } vi.spyOn(localStorage, 'getItem').mockImplementation(() => { throw new Error('Storage disabled') }) - const setItemSpy = vi.spyOn(localStorage, 'setItem') const user = userEvent.setup() - const { rerender } = renderProvider() + const { rerender } = renderProvider(undefined, features) await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()) @@ -212,26 +211,19 @@ describe('ModalContextProvider trigger events limit modal', () => { ) await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()) expect(screen.getByText('clear')).toBeInTheDocument() - expect(setItemSpy).not.toHaveBeenCalled() }) it('falls back to the in-memory guard when localStorage.setItem fails', async () => { - const plan = createPlan({ - type: 'professional', - usage: { triggerEvents: 120 }, - total: { triggerEvents: 120 }, - reset: { triggerEvents: 2 }, - }) - mockUseProviderContext.mockReturnValue({ - plan, - isFetchedPlan: true, - }) + const features = { + billing: { subscription: { plan: 'professional' as const } }, + trigger_event: { usage: 120, limit: 120, reset_date: dayjs().add(2, 'day').unix() }, + } vi.spyOn(localStorage, 'setItem').mockImplementation(() => { throw new Error('Quota exceeded') }) const user = userEvent.setup() - const { rerender } = renderProvider() + const { rerender } = renderProvider(undefined, features) await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()) @@ -248,19 +240,13 @@ describe('ModalContextProvider trigger events limit modal', () => { }) it('closes the trigger events limit modal and opens pricing when upgrading', async () => { - const plan = createPlan({ - type: 'professional', - usage: { triggerEvents: 400 }, - total: { triggerEvents: 400 }, - reset: { triggerEvents: 6 }, - }) - mockUseProviderContext.mockReturnValue({ - plan, - isFetchedPlan: true, - }) + const features = { + billing: { subscription: { plan: 'professional' as const } }, + trigger_event: { usage: 400, limit: 400, reset_date: dayjs().add(6, 'day').unix() }, + } const user = userEvent.setup() - renderProvider() + renderProvider(undefined, features) await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()) @@ -277,16 +263,11 @@ describe('ModalContextProvider trigger events limit modal', () => { describe('ModalContextProvider plugin update modal', () => { beforeEach(() => { mockConsoleStateReader.mockReset() - mockUseProviderContext.mockReset() mockConsoleStateReader.mockReturnValue({ currentWorkspace: { id: 'workspace-1', }, }) - mockUseProviderContext.mockReturnValue({ - plan: createPlan(), - isFetchedPlan: false, - }) }) it('keeps a model plugin update open until its refresh callback finishes', async () => { diff --git a/web/context/provider-context-provider.tsx b/web/context/provider-context-provider.tsx index f8c2939e864..9af1a3d8d48 100644 --- a/web/context/provider-context-provider.tsx +++ b/web/context/provider-context-provider.tsx @@ -40,8 +40,6 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro const features = featuresQuery.data const enableBilling = features?.billing.enabled ?? false const plan = enableBilling && features ? parseCurrentPlan(features) : defaultPlan - const isFetchedPlan = featuresQuery.isSuccess && enableBilling - const isFetchedPlanInfo = featuresQuery.isFetched const enableEducationPlan = features?.education.enabled ?? false const enableSkill = features?.enable_skill ?? false const enableReplaceWebAppLogo = features?.can_replace_logo ?? false @@ -90,8 +88,6 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro ), supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [], plan, - isFetchedPlan, - isFetchedPlanInfo, enableBilling, enableSkill, enableReplaceWebAppLogo, diff --git a/web/context/provider-context.ts b/web/context/provider-context.ts index ad53a5b4f38..7189b7b4c75 100644 --- a/web/context/provider-context.ts +++ b/web/context/provider-context.ts @@ -26,8 +26,6 @@ export type ProviderContextState = { total: UsagePlanInfo reset: UsageResetInfo } - isFetchedPlan: boolean - isFetchedPlanInfo: boolean enableBilling: boolean enableSkill: boolean enableReplaceWebAppLogo: boolean @@ -49,8 +47,6 @@ export const baseProviderContextValue: ProviderContextState = { supportRetrievalMethods: [], isAPIKeySet: true, plan: defaultPlan, - isFetchedPlan: false, - isFetchedPlanInfo: false, enableBilling: false, enableSkill: false, enableReplaceWebAppLogo: false,