From 8fc11b2927fb8b2957cdde7bf805614370d2d50a Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:28:20 +0000 Subject: [PATCH] refactor(web): retire provider context plan state (#41925) --- oxlint-suppressions.json | 2 +- web/__mocks__/provider-context.ts | 46 --- .../billing/billing-integration.test.tsx | 274 +++++++--------- .../education-verification-flow.test.tsx | 72 ++--- .../(commonLayout)/external-service-sync.tsx | 14 + .../__tests__/test-utils.tsx | 4 - .../billing/__tests__/query-state.spec.tsx | 77 +++++ .../billing/annotation-full/usage.tsx | 16 +- .../components/billing/billing-page/index.tsx | 14 +- web/app/components/billing/config.ts | 28 -- .../billing/plan/__tests__/index.spec.tsx | 24 +- web/app/components/billing/plan/index.tsx | 58 ++-- web/app/components/billing/type.ts | 15 - .../billing/usage-info/apps-info.tsx | 27 -- .../billing/usage-info/vector-space-info.tsx | 24 +- .../billing/utils/__tests__/index.spec.ts | 296 ++---------------- web/app/components/billing/utils/index.ts | 38 +-- .../billing/vector-space-full/index.tsx | 4 +- .../__tests__/auto-disabled-document.spec.tsx | 28 +- .../auto-disabled-document.tsx | 9 +- .../documents/__tests__/index.spec.tsx | 18 -- .../__tests__/documents-header.spec.tsx | 11 - .../documents/components/documents-header.tsx | 4 +- .../components/datasets/documents/index.tsx | 4 - .../create-app-modal/__tests__/index.spec.tsx | 32 +- .../__tests__/compliance.spec.tsx | 97 ++---- .../header/account-dropdown/compliance.tsx | 21 +- .../__tests__/model-list-item.spec.tsx | 33 +- .../provider-added-card/model-list-item.tsx | 15 +- .../main-nav/__tests__/index.spec.tsx | 2 - .../__tests__/workspace-card.spec.tsx | 2 - .../__tests__/index.spec.tsx | 13 - .../__tests__/console-bootstrap.spec.tsx | 46 +++ web/context/provider-context-provider.tsx | 36 +-- web/context/provider-context.ts | 15 - web/service/use-common.ts | 9 - 36 files changed, 495 insertions(+), 933 deletions(-) create mode 100644 web/app/components/billing/__tests__/query-state.spec.tsx delete mode 100644 web/app/components/billing/usage-info/apps-info.tsx diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 06e0911c5a8..8ba12bd390c 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -5133,7 +5133,7 @@ }, "web/service/use-common.ts": { "@tanstack/query/prefer-query-options": { - "count": 12 + "count": 11 } }, "web/service/use-datasource.ts": { diff --git a/web/__mocks__/provider-context.ts b/web/__mocks__/provider-context.ts index 965a73a1ddd..406ef2cc306 100644 --- a/web/__mocks__/provider-context.ts +++ b/web/__mocks__/provider-context.ts @@ -1,9 +1,6 @@ -import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' -import type { UsagePlanInfo } from '@/app/components/billing/type' import type { ProviderContextState } from '@/context/provider-context' import { merge } from 'es-toolkit/compat' import { noop } from 'es-toolkit/function' -import { defaultPlan } from '@/app/components/billing/config' // Avoid being mocked in tests export const baseProviderContextValue: ProviderContextState = { @@ -13,15 +10,12 @@ export const baseProviderContextValue: ProviderContextState = { isLoadingModelProviders: false, isSuccessModelProviders: false, textGenerationModelList: [], - supportRetrievalMethods: [], isAPIKeySet: true, - plan: defaultPlan, enableSkill: false, enableReplaceWebAppLogo: false, modelLoadBalancingEnabled: false, enableEducationPlan: false, - webappCopyrightEnabled: false, isAllowTransferWorkspace: false, isAllowPublishAsCustomKnowledgePipelineTemplate: false, humanInputEmailDeliveryEnabled: false, @@ -37,43 +31,3 @@ export const createMockProviderContextValue = ( refreshModelProviders: merged.refreshModelProviders ?? noop, } } - -export const createMockPlan = (plan: CloudPlan): ProviderContextState => - createMockProviderContextValue({ - plan: merge({}, defaultPlan, { - type: plan, - }), - }) - -export const createMockPlanUsage = ( - usage: UsagePlanInfo, - ctx: Partial, -): ProviderContextState => - createMockProviderContextValue({ - ...ctx, - plan: merge(ctx.plan, { - usage, - }), - }) - -export const createMockPlanTotal = ( - total: UsagePlanInfo, - ctx: Partial, -): ProviderContextState => - createMockProviderContextValue({ - ...ctx, - plan: merge(ctx.plan, { - total, - }), - }) - -export const createMockPlanReset = ( - reset: Partial, - ctx: Partial, -): ProviderContextState => - createMockProviderContextValue({ - ...ctx, - plan: merge(ctx?.plan, { - reset, - }), - }) diff --git a/web/__tests__/billing/billing-integration.test.tsx b/web/__tests__/billing/billing-integration.test.tsx index 919055c07af..a1b65898357 100644 --- a/web/__tests__/billing/billing-integration.test.tsx +++ b/web/__tests__/billing/billing-integration.test.tsx @@ -1,15 +1,19 @@ -import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' +import type { + GetFeaturesResponse, + GetFeaturesVectorSpaceResponse, +} from '@dify/contracts/api/console/features/types.gen' import type { RenderOptions } from '@testing-library/react' import type { ReactElement } from 'react' -import type { UsagePlanInfo, UsageResetInfo } from '@/app/components/billing/type' +import type { DeepPartial } from '@/test/console/system-features' import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import dayjs from 'dayjs' import * as React from 'react' import AnnotationFull from '@/app/components/billing/annotation-full' import AnnotationFullModal from '@/app/components/billing/annotation-full/modal' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import Billing from '@/app/components/billing/billing-page' -import { defaultPlan, NUM_INFINITE } from '@/app/components/billing/config' +import { NUM_INFINITE } from '@/app/components/billing/config' import PlanComp from '@/app/components/billing/plan' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' import PriorityLabel from '@/app/components/billing/priority-label' @@ -24,16 +28,15 @@ import { } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' -let mockProviderCtx: Record = {} +let mockFeatures: DeepPartial = {} +let mockVectorSpace: GetFeaturesVectorSpaceResponse = { size: 0, limit: 50, usage_unknown: false } let mockConsoleState: Record = {} let mockEducationStatus = { is_student: false, allow_refresh: false, expire_at: null } const render = (ui: ReactElement, options: RenderOptions = {}, vectorSpaceUsageUnknown = false) => { const queryClient = createConsoleQueryClient() - const plan = mockProviderCtx.plan as ReturnType queryClient.setQueryData(consoleQuery.features.vectorSpace.get.queryOptions().queryKey, { - size: plan.usage.vectorSpace, - limit: plan.total.vectorSpace, + ...mockVectorSpace, usage_unknown: vectorSpaceUsageUnknown, }) queryClient.setQueryData(consoleQuery.billing.invoices.get.queryOptions().queryKey, { @@ -44,10 +47,7 @@ const render = (ui: ReactElement, options: RenderOptions = {}, vectorSpaceUsageU accountProfile: mockConsoleState.userProfile as { email?: string }, accountProfileMeta: { currentVersion: '1.0.0' }, systemFeatures: { deployment_edition: 'CLOUD' }, - features: { - billing: { subscription: { plan: plan.type } }, - apps: { size: plan.usage.buildApps, limit: plan.total.buildApps }, - }, + features: mockFeatures, queryClient, }) return renderWithConsoleState(ui, { ...options, wrapper }) @@ -55,10 +55,6 @@ const render = (ui: ReactElement, options: RenderOptions = {}, vectorSpaceUsageU const mockSetShowPricingModal = vi.fn() -vi.mock('@/context/provider-context', () => ({ - useProviderContext: () => mockProviderCtx, -})) - vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState) @@ -87,25 +83,13 @@ vi.mock('@/app/components/header/utils/util', () => ({ })) // ─── Test data factories ──────────────────────────────────────────────────── -type PlanOverrides = { - type?: CloudPlan - usage?: Partial - total?: Partial - reset?: Partial +type BillingOverrides = DeepPartial & { + vectorSpace?: Partial } -const createPlanData = (overrides: PlanOverrides = {}) => ({ - ...defaultPlan, - ...overrides, - type: overrides.type ?? defaultPlan.type, - usage: { ...defaultPlan.usage, ...overrides.usage }, - total: { ...defaultPlan.total, ...overrides.total }, - reset: { ...defaultPlan.reset, ...overrides.reset }, -}) - -const setupProviderContext = ( - planOverrides: PlanOverrides = {}, - extra: Record = {}, +const setupBilling = ( + { vectorSpace, ...features }: BillingOverrides = {}, + education: { enableEducationPlan?: boolean } = {}, educationStatus: Partial = {}, ) => { mockEducationStatus = { @@ -114,11 +98,8 @@ const setupProviderContext = ( expire_at: null, ...educationStatus, } - mockProviderCtx = { - plan: createPlanData(planOverrides), - enableEducationPlan: false, - ...extra, - } + mockFeatures = { ...features, education: { enabled: education.enableEducationPlan ?? false } } + mockVectorSpace = { size: 0, limit: 50, usage_unknown: false, ...vectorSpace } } const setupConsoleState = (overrides: Record = {}) => { @@ -146,26 +127,15 @@ describe('Billing Page + Plan Integration', () => { // Verify that the billing page renders PlanComp with all 7 usage items describe('Rendering complete plan information', () => { it('should display all 7 usage metrics for sandbox plan', () => { - setupProviderContext({ - type: 'sandbox', - usage: { - buildApps: 3, - teamMembers: 1, - documentsUploadQuota: 10, - vectorSpace: 20, - annotatedResponse: 5, - triggerEvents: 1000, - apiRateLimit: 2000, - }, - total: { - buildApps: 5, - teamMembers: 1, - documentsUploadQuota: 50, - vectorSpace: 50, - annotatedResponse: 10, - triggerEvents: 3000, - apiRateLimit: 5000, - }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + apps: { size: 3, limit: 5 }, + members: { size: 1, limit: 1 }, + documents_upload_quota: { size: 10, limit: 50 }, + annotation_quota_limit: { size: 5, limit: 10 }, + trigger_event: { usage: 1000, limit: 3000 }, + api_rate_limit: { usage: 2000, limit: 5000 }, + vectorSpace: { size: 20, limit: 50 }, }) render() @@ -184,10 +154,9 @@ describe('Billing Page + Plan Integration', () => { }) it('should expose each quota card and its value through stable semantics', () => { - setupProviderContext({ - type: 'sandbox', - usage: { teamMembers: 3 }, - total: { teamMembers: 5 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + members: { size: 3, limit: 5 }, }) render() @@ -202,10 +171,9 @@ describe('Billing Page + Plan Integration', () => { }) it('should display unknown vector space usage as a placeholder', () => { - setupProviderContext({ - type: 'sandbox', - usage: { vectorSpace: 0 }, - total: { vectorSpace: 50 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + vectorSpace: { size: 0, limit: 50 }, }) render(, {}, true) @@ -217,9 +185,9 @@ describe('Billing Page + Plan Integration', () => { }) it('should show "unlimited" for infinite quotas (professional API rate limit)', () => { - setupProviderContext({ - type: 'professional', - total: { apiRateLimit: NUM_INFINITE }, + setupBilling({ + billing: { subscription: { plan: 'professional' } }, + api_rate_limit: { limit: NUM_INFINITE }, }) render() @@ -228,10 +196,9 @@ describe('Billing Page + Plan Integration', () => { }) it('should display reset days for trigger events when applicable', () => { - setupProviderContext({ - type: 'professional', - total: { triggerEvents: 20000 }, - reset: { triggerEvents: 7 }, + setupBilling({ + billing: { subscription: { plan: 'professional' } }, + trigger_event: { limit: 20000, reset_date: dayjs().add(7, 'day').startOf('day').unix() }, }) render() @@ -244,7 +211,7 @@ describe('Billing Page + Plan Integration', () => { // Verify billing URL button visibility and behavior describe('Billing URL button', () => { it('should show billing button to managers without billing permission keys', () => { - setupProviderContext({ type: 'sandbox' }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } } }) setupConsoleState({ isCurrentWorkspaceManager: true, workspacePermissionKeys: [], @@ -257,7 +224,7 @@ describe('Billing Page + Plan Integration', () => { }) it('should hide billing button from non-manager members', () => { - setupProviderContext({ type: 'sandbox' }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } } }) setupConsoleState({ isCurrentWorkspaceManager: false, }) @@ -268,7 +235,7 @@ describe('Billing Page + Plan Integration', () => { }) it('should show billing button when a manager has no billing permission keys', () => { - setupProviderContext({ type: 'sandbox' }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } } }) setupConsoleState({ isCurrentWorkspaceManager: true, workspacePermissionKeys: [], @@ -292,7 +259,7 @@ describe('Plan Type Display Integration', () => { }) it('should render sandbox plan with upgrade button (premium badge)', () => { - setupProviderContext({ type: 'sandbox' }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } } }) render() @@ -303,7 +270,7 @@ describe('Plan Type Display Integration', () => { }) it('should render professional plan with plain upgrade button', () => { - setupProviderContext({ type: 'professional' }) + setupBilling({ billing: { subscription: { plan: 'professional' } } }) render() @@ -313,7 +280,7 @@ describe('Plan Type Display Integration', () => { }) it('should render team plan with plain-style upgrade button', () => { - setupProviderContext({ type: 'team' }) + setupBilling({ billing: { subscription: { plan: 'team' } } }) render() @@ -323,7 +290,7 @@ describe('Plan Type Display Integration', () => { }) it('should show education verify button when enableEducationPlan is true and not yet verified', () => { - setupProviderContext({ type: 'sandbox' }, { enableEducationPlan: true }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } } }, { enableEducationPlan: true }) render() @@ -331,7 +298,11 @@ describe('Plan Type Display Integration', () => { }) it('should show education discount to managers without billing permission keys', () => { - setupProviderContext({ type: 'sandbox' }, { enableEducationPlan: true }, { is_student: true }) + setupBilling( + { billing: { subscription: { plan: 'sandbox' } } }, + { enableEducationPlan: true }, + { is_student: true }, + ) setupConsoleState({ isCurrentWorkspaceManager: true, workspacePermissionKeys: [] }) render() @@ -340,7 +311,11 @@ describe('Plan Type Display Integration', () => { }) it('should hide education discount from non-manager members', () => { - setupProviderContext({ type: 'sandbox' }, { enableEducationPlan: true }, { is_student: true }) + setupBilling( + { billing: { subscription: { plan: 'sandbox' } } }, + { enableEducationPlan: true }, + { is_student: true }, + ) setupConsoleState({ isCurrentWorkspaceManager: false, workspacePermissionKeys: ['billing.manage'], @@ -361,7 +336,7 @@ describe('Upgrade Flow Integration', () => { beforeEach(() => { vi.clearAllMocks() setupConsoleState() - setupProviderContext({ type: 'sandbox' }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } } }) }) // UpgradeBtn triggers pricing modal @@ -487,7 +462,7 @@ describe('Upgrade Flow Integration', () => { describe('PlanComp upgrade button triggers pricing', () => { it('should open pricing modal when clicking upgrade in sandbox plan', async () => { const user = userEvent.setup() - setupProviderContext({ type: 'sandbox' }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } } }) render() @@ -513,11 +488,7 @@ describe('Capacity Full Components Integration', () => { // AppsFull renders with correct messaging and components describe('AppsFull integration', () => { it('should display upgrade tip and upgrade button for sandbox plan at capacity', () => { - setupProviderContext({ - type: 'sandbox', - usage: { buildApps: 5 }, - total: { buildApps: 5 }, - }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } }, apps: { size: 5, limit: 5 } }) render() @@ -532,10 +503,9 @@ describe('Capacity Full Components Integration', () => { }) it('should display upgrade tip and upgrade button for professional plan', () => { - setupProviderContext({ - type: 'professional', - usage: { buildApps: 48 }, - total: { buildApps: 50 }, + setupBilling({ + billing: { subscription: { plan: 'professional' } }, + apps: { size: 48, limit: 50 }, }) render() @@ -545,11 +515,7 @@ describe('Capacity Full Components Integration', () => { }) it('should display contact tip and contact button for team plan', () => { - setupProviderContext({ - type: 'team', - usage: { buildApps: 200 }, - total: { buildApps: 200 }, - }) + setupBilling({ billing: { subscription: { plan: 'team' } }, apps: { size: 200, limit: 200 } }) render() @@ -562,11 +528,7 @@ describe('Capacity Full Components Integration', () => { it('should render progress bar with correct color based on usage percentage', () => { // 100% usage should show error color - setupProviderContext({ - type: 'sandbox', - usage: { buildApps: 5 }, - total: { buildApps: 5 }, - }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } }, apps: { size: 5, limit: 5 } }) const { container } = render() @@ -577,10 +539,9 @@ describe('Capacity Full Components Integration', () => { // VectorSpaceFull renders with VectorSpaceInfo and UpgradeBtn describe('VectorSpaceFull integration', () => { it('should display full tip, upgrade button, and vector space usage info', () => { - setupProviderContext({ - type: 'sandbox', - usage: { vectorSpace: 50 }, - total: { vectorSpace: 50 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + vectorSpace: { size: 50, limit: 50 }, }) render() @@ -598,10 +559,9 @@ describe('Capacity Full Components Integration', () => { // AnnotationFull renders with Usage component and UpgradeBtn describe('AnnotationFull integration', () => { it('should display annotation full tip, upgrade button, and usage info', () => { - setupProviderContext({ - type: 'sandbox', - usage: { annotatedResponse: 10 }, - total: { annotatedResponse: 10 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + annotation_quota_limit: { size: 10, limit: 10 }, }) render() @@ -618,10 +578,9 @@ describe('Capacity Full Components Integration', () => { // AnnotationFullModal shows modal with usage and upgrade button describe('AnnotationFullModal integration', () => { it('should render modal with annotation info and upgrade button when show is true', () => { - setupProviderContext({ - type: 'sandbox', - usage: { annotatedResponse: 10 }, - total: { annotatedResponse: 10 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + annotation_quota_limit: { size: 10, limit: 10 }, }) render() @@ -632,10 +591,9 @@ describe('Capacity Full Components Integration', () => { }) it('should not render content when show is false', () => { - setupProviderContext({ - type: 'sandbox', - usage: { annotatedResponse: 10 }, - total: { annotatedResponse: 10 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + annotation_quota_limit: { size: 10, limit: 10 }, }) render() @@ -647,7 +605,7 @@ describe('Capacity Full Components Integration', () => { // TriggerEventsLimitModal renders PlanUpgradeModal with embedded UsageInfo describe('TriggerEventsLimitModal integration', () => { it('should display trigger limit title, usage info, and upgrade button', () => { - setupProviderContext({ type: 'professional' }) + setupBilling({ billing: { subscription: { plan: 'professional' } } }) render( { const user = userEvent.setup() const onClose = vi.fn() const onUpgrade = vi.fn() - setupProviderContext({ type: 'professional' }) + setupBilling({ billing: { subscription: { plan: 'professional' } } }) render( { }) it('should display "standard" priority for sandbox plan', () => { - setupProviderContext({ type: 'sandbox' }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } } }) render() @@ -718,7 +676,7 @@ describe('PriorityLabel Integration', () => { }) it('should display "priority" for professional plan with icon', () => { - setupProviderContext({ type: 'professional' }) + setupBilling({ billing: { subscription: { plan: 'professional' } } }) const { container } = render() @@ -728,7 +686,7 @@ describe('PriorityLabel Integration', () => { }) it('should display "top-priority" for team plan with icon', () => { - setupProviderContext({ type: 'team' }) + setupBilling({ billing: { subscription: { plan: 'team' } } }) const { container } = render() @@ -750,10 +708,9 @@ describe('Usage Display Edge Cases', () => { // Vector space storage mode behavior describe('VectorSpace storage mode in PlanComp', () => { it('should show "< 50" for sandbox plan with low vector space usage', () => { - setupProviderContext({ - type: 'sandbox', - usage: { vectorSpace: 10 }, - total: { vectorSpace: 50 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + vectorSpace: { size: 10, limit: 50 }, }) render() @@ -763,10 +720,9 @@ describe('Usage Display Edge Cases', () => { }) it('should show indeterminate progress bar for usage below threshold', () => { - setupProviderContext({ - type: 'sandbox', - usage: { vectorSpace: 10 }, - total: { vectorSpace: 50 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + vectorSpace: { size: 10, limit: 50 }, }) render() @@ -776,10 +732,9 @@ describe('Usage Display Edge Cases', () => { }) it('should show actual usage for pro plan above threshold', () => { - setupProviderContext({ - type: 'professional', - usage: { vectorSpace: 1024 }, - total: { vectorSpace: 5120 }, + setupBilling({ + billing: { subscription: { plan: 'professional' } }, + vectorSpace: { size: 1024, limit: 5120 }, }) render() @@ -792,11 +747,7 @@ describe('Usage Display Edge Cases', () => { // Progress bar color logic through real components describe('Progress bar color reflects usage severity', () => { it('should show normal color for low usage percentage', () => { - setupProviderContext({ - type: 'sandbox', - usage: { buildApps: 1 }, - total: { buildApps: 5 }, - }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } }, apps: { size: 1, limit: 5 } }) const { container } = render() @@ -810,10 +761,9 @@ describe('Usage Display Edge Cases', () => { // Reset days calculation in PlanComp describe('Reset days integration', () => { it('should not show reset for sandbox trigger events (no reset_date)', () => { - setupProviderContext({ - type: 'sandbox', - total: { triggerEvents: 3000 }, - reset: { triggerEvents: null }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + trigger_event: { limit: 3000, reset_date: 0 }, }) render() @@ -826,10 +776,9 @@ describe('Usage Display Edge Cases', () => { }) it('should show reset for professional trigger events with reset date', () => { - setupProviderContext({ - type: 'professional', - total: { triggerEvents: 20000 }, - reset: { triggerEvents: 14 }, + setupBilling({ + billing: { subscription: { plan: 'professional' } }, + trigger_event: { limit: 20000, reset_date: dayjs().add(14, 'day').startOf('day').unix() }, }) render() @@ -853,11 +802,7 @@ describe('Cross-Component Upgrade Flow', () => { it('should trigger pricing from AppsFull upgrade button', async () => { const user = userEvent.setup() - setupProviderContext({ - type: 'sandbox', - usage: { buildApps: 5 }, - total: { buildApps: 5 }, - }) + setupBilling({ billing: { subscription: { plan: 'sandbox' } }, apps: { size: 5, limit: 5 } }) render() @@ -869,10 +814,9 @@ describe('Cross-Component Upgrade Flow', () => { it('should trigger pricing from VectorSpaceFull upgrade button', async () => { const user = userEvent.setup() - setupProviderContext({ - type: 'sandbox', - usage: { vectorSpace: 50 }, - total: { vectorSpace: 50 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + vectorSpace: { size: 50, limit: 50 }, }) render() @@ -885,10 +829,9 @@ describe('Cross-Component Upgrade Flow', () => { it('should trigger pricing from AnnotationFull upgrade button', async () => { const user = userEvent.setup() - setupProviderContext({ - type: 'sandbox', - usage: { annotatedResponse: 10 }, - total: { annotatedResponse: 10 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + annotation_quota_limit: { size: 10, limit: 10 }, }) render() @@ -902,7 +845,7 @@ describe('Cross-Component Upgrade Flow', () => { it('should trigger pricing from TriggerEventsLimitModal through PlanUpgradeModal', async () => { const user = userEvent.setup() const onClose = vi.fn() - setupProviderContext({ type: 'professional' }) + setupBilling({ billing: { subscription: { plan: 'professional' } } }) render( { it('should trigger pricing from AnnotationFullModal upgrade button', async () => { const user = userEvent.setup() - setupProviderContext({ - type: 'sandbox', - usage: { annotatedResponse: 10 }, - total: { annotatedResponse: 10 }, + setupBilling({ + billing: { subscription: { plan: 'sandbox' } }, + annotation_quota_limit: { size: 10, limit: 10 }, }) render() diff --git a/web/__tests__/billing/education-verification-flow.test.tsx b/web/__tests__/billing/education-verification-flow.test.tsx index a9322ece4fe..061d8602903 100644 --- a/web/__tests__/billing/education-verification-flow.test.tsx +++ b/web/__tests__/billing/education-verification-flow.test.tsx @@ -1,9 +1,12 @@ +import type { + GetFeaturesResponse, + GetFeaturesVectorSpaceResponse, +} from '@dify/contracts/api/console/features/types.gen' import type { RenderOptions } from '@testing-library/react' import type { ReactElement } from 'react' -import type { UsagePlanInfo, UsageResetInfo } from '@/app/components/billing/type' +import type { DeepPartial } from '@/test/console/system-features' import { cleanup, screen } from '@testing-library/react' import * as React from 'react' -import { defaultPlan } from '@/app/components/billing/config' import PlanComp from '@/app/components/billing/plan' import { consoleQuery } from '@/service/client' import { @@ -13,19 +16,15 @@ import { } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' -let mockProviderCtx: Record = {} +let mockFeatures: DeepPartial = {} +let mockVectorSpace: GetFeaturesVectorSpaceResponse = { size: 0, limit: 50, usage_unknown: false } let mockConsoleState: Record = {} let mockEducationStatus = { is_student: false, allow_refresh: false, expire_at: null } const render = (ui: ReactElement, options: RenderOptions = {}) => { const queryClient = createConsoleQueryClient() - const plan = mockProviderCtx.plan as { - usage: { vectorSpace: number } - total: { vectorSpace: number } - } queryClient.setQueryData(consoleQuery.features.vectorSpace.get.queryOptions().queryKey, { - size: plan.usage.vectorSpace, - limit: plan.total.vectorSpace, + ...mockVectorSpace, usage_unknown: false, }) seedEducationStatus(queryClient, mockEducationStatus) @@ -33,6 +32,7 @@ const render = (ui: ReactElement, options: RenderOptions = {}) => { accountProfile: mockConsoleState.userProfile as { email?: string }, accountProfileMeta: { currentVersion: '1.0.0' }, systemFeatures: { deployment_edition: 'CLOUD' }, + features: mockFeatures, queryClient, }) return renderWithConsoleState(ui, { ...options, wrapper }) @@ -42,9 +42,6 @@ const render = (ui: ReactElement, options: RenderOptions = {}) => { const mockSetShowPricingModal = vi.fn() // ─── Context mocks ─────────────────────────────────────────────────────────── -vi.mock('@/context/provider-context', () => ({ - useProviderContext: () => mockProviderCtx, -})) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') @@ -63,25 +60,13 @@ vi.mock('@/next/navigation', () => ({ })) // ─── Test data factories ──────────────────────────────────────────────────── -type PlanOverrides = { - type?: string - usage?: Partial - total?: Partial - reset?: Partial +type BillingOverrides = DeepPartial & { + vectorSpace?: Partial } -const createPlanData = (overrides: PlanOverrides = {}) => ({ - ...defaultPlan, - ...overrides, - type: overrides.type ?? defaultPlan.type, - usage: { ...defaultPlan.usage, ...overrides.usage }, - total: { ...defaultPlan.total, ...overrides.total }, - reset: { ...defaultPlan.reset, ...overrides.reset }, -}) - -const setupContexts = ( - planOverrides: PlanOverrides = {}, - providerOverrides: Record = {}, +const setupBilling = ( + { vectorSpace, ...features }: BillingOverrides = {}, + education: { enableEducationPlan?: boolean } = {}, appOverrides: Record = {}, educationStatus: Partial = {}, ) => { @@ -91,11 +76,8 @@ const setupContexts = ( expire_at: null, ...educationStatus, } - mockProviderCtx = { - plan: createPlanData(planOverrides), - enableEducationPlan: false, - ...providerOverrides, - } + mockFeatures = { ...features, education: { enabled: education.enableEducationPlan ?? false } } + mockVectorSpace = { size: 0, limit: 50, usage_unknown: false, ...vectorSpace } mockConsoleState = { isCurrentWorkspaceManager: true, userProfile: { email: 'student@university.edu' }, @@ -109,13 +91,13 @@ describe('Education Verification Flow', () => { beforeEach(() => { vi.clearAllMocks() cleanup() - setupContexts() + setupBilling() }) // ─── 1. Education Button Visibility ───────────────────────────────────── describe('Education button visibility', () => { it('should not show verify button when enableEducationPlan is false', () => { - setupContexts({}, { enableEducationPlan: false }) + setupBilling({}, { enableEducationPlan: false }) render() @@ -123,7 +105,7 @@ describe('Education Verification Flow', () => { }) it('should show verify button when enableEducationPlan is true and not yet verified', () => { - setupContexts({}, { enableEducationPlan: true }) + setupBilling({}, { enableEducationPlan: true }) render() @@ -134,7 +116,7 @@ describe('Education Verification Flow', () => { }) it('should not show verify button when already verified and not about to expire', () => { - setupContexts({}, { enableEducationPlan: true }, {}, { is_student: true }) + setupBilling({}, { enableEducationPlan: true }, {}, { is_student: true }) render() @@ -142,12 +124,7 @@ describe('Education Verification Flow', () => { }) it('should show verify button when the education status allows refresh', () => { - setupContexts( - {}, - { enableEducationPlan: true }, - {}, - { is_student: true, allow_refresh: true }, - ) + setupBilling({}, { enableEducationPlan: true }, {}, { is_student: true, allow_refresh: true }) render() @@ -158,7 +135,10 @@ describe('Education Verification Flow', () => { // ─── 2. Education + Upgrade Coexistence ───────────────────────────────── describe('Education and upgrade button coexistence', () => { it('should show both education verify and upgrade buttons for sandbox user', () => { - setupContexts({ type: 'sandbox' }, { enableEducationPlan: true }) + setupBilling( + { billing: { subscription: { plan: 'sandbox' } } }, + { enableEducationPlan: true }, + ) render() @@ -167,7 +147,7 @@ describe('Education Verification Flow', () => { }) it('should show team plan with plain upgrade button and education button', () => { - setupContexts({ type: 'team' }, { enableEducationPlan: true }) + setupBilling({ billing: { subscription: { plan: 'team' } } }, { enableEducationPlan: true }) render() diff --git a/web/app/(commonLayout)/external-service-sync.tsx b/web/app/(commonLayout)/external-service-sync.tsx index 4e27aba7551..82a988953a0 100644 --- a/web/app/(commonLayout)/external-service-sync.tsx +++ b/web/app/(commonLayout)/external-service-sync.tsx @@ -133,6 +133,20 @@ function ZendeskConversationSync() { deploymentEdition: data.deployment_edition, }), }) + const { data: plan } = useQuery( + consoleQuery.features.get.queryOptions({ + enabled: systemFeatures.deploymentEdition === 'CLOUD' && Boolean(ZENDESK_FIELD_IDS.PLAN), + select: (data) => data.billing.subscription.plan, + }), + ) + useEffect(() => { + if (systemFeatures.deploymentEdition !== 'CLOUD' || !plan || !ZENDESK_FIELD_IDS.PLAN) return + zendeskRuntime.setConversationFields( + [{ id: ZENDESK_FIELD_IDS.PLAN, value: `${plan}-plan` }], + systemFeatures.deploymentEdition, + ) + }, [plan, systemFeatures.deploymentEdition]) + const currentWorkspace = useAtomValue(currentWorkspaceAtom) const { data: versionData } = useQuery( consoleQuery.version.get.queryOptions({ 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 944be50ca2d..8c7e45a9640 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 @@ -2,7 +2,6 @@ import type { DeploymentEdition } from '@dify/contracts/api/console/system-featu import type { RenderOptions } from '@testing-library/react' import type { MockedFunction } from 'vite-plus/test' import { fireEvent, screen } from '@testing-library/react' -import { defaultPlan } from '@/app/components/billing/config' import { useProviderContext as actualUseProviderContext } from '@/context/provider-context' import { renderWithConsoleQuery } from '@/test/console/query-data' import APIKeyInfoPanel from '../index' @@ -43,15 +42,12 @@ const defaultProviderContext = { isLoadingModelProviders: false, isSuccessModelProviders: false, textGenerationModelList: [], - supportRetrievalMethods: [], isAPIKeySet: false, - plan: defaultPlan, enableSkill: false, enableReplaceWebAppLogo: false, modelLoadBalancingEnabled: false, enableEducationPlan: false, - webappCopyrightEnabled: false, isAllowTransferWorkspace: false, isAllowPublishAsCustomKnowledgePipelineTemplate: false, humanInputEmailDeliveryEnabled: false, diff --git a/web/app/components/billing/__tests__/query-state.spec.tsx b/web/app/components/billing/__tests__/query-state.spec.tsx new file mode 100644 index 00000000000..07b3084caee --- /dev/null +++ b/web/app/components/billing/__tests__/query-state.spec.tsx @@ -0,0 +1,77 @@ +import { act, render, screen, within } from '@testing-library/react' +import { consoleQuery } from '@/service/client' +import { + createConsoleQueryClient, + createConsoleQueryWrapper, + seedFeatures, +} from '@/test/console/query-data' +import AnnotationUsage from '../annotation-full/usage' +import Billing from '../billing-page' + +vi.mock('@/service/base', () => ({ + request: vi.fn(() => new Promise(() => {})), + sseGeneratorPost: vi.fn(), +})) +vi.mock('../upgrade-btn', () => ({ default: () => null })) +vi.mock('../hooks/use-education-discount', () => ({ + useEducationDiscount: () => ({ + handleEducationDiscount: vi.fn(), + isEducationDiscountLoading: false, + }), +})) + +it('reveals the plan and all usage together when both billing queries have data', async () => { + const queryClient = createConsoleQueryClient() + const { wrapper } = createConsoleQueryWrapper({ + queryClient, + systemFeatures: { deployment_edition: 'CLOUD' }, + }) + render(, { wrapper }) + expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument() + expect(screen.queryByText('billing.plans.sandbox.name')).not.toBeInTheDocument() + expect( + screen.queryByRole('group', { name: 'billing.usagePage.buildApps' }), + ).not.toBeInTheDocument() + await act(async () => { + seedFeatures(queryClient, { + billing: { subscription: { plan: 'professional' } }, + apps: { size: 17, limit: 50 }, + }) + }) + expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument() + expect( + screen.queryByRole('group', { name: 'billing.usagePage.buildApps' }), + ).not.toBeInTheDocument() + await act(async () => { + queryClient.setQueryData(consoleQuery.features.vectorSpace.get.queryKey(), { + size: 256, + limit: 900, + usage_unknown: false, + }) + }) + const apps = await screen.findByRole('group', { name: 'billing.usagePage.buildApps' }) + expect(within(apps).getByText('17')).toBeInTheDocument() + expect(within(apps).getByText('50')).toBeInTheDocument() + expect(screen.getByText('billing.plans.professional.name')).toBeInTheDocument() + const storage = screen.getByRole('group', { name: 'billing.usagePage.vectorSpace' }) + expect(within(storage).getByText('256')).toBeInTheDocument() + expect(within(storage).getByText('900MB')).toBeInTheDocument() + expect(screen.queryByRole('status', { name: 'appApi.loading' })).not.toBeInTheDocument() +}) + +it('renders annotation usage only from returned data and preserves zero as an unlimited quota', async () => { + const queryClient = createConsoleQueryClient() + const { wrapper } = createConsoleQueryWrapper({ queryClient }) + render(, { wrapper }) + expect( + screen.queryByRole('group', { name: 'billing.annotatedResponse.quotaTitle' }), + ).not.toBeInTheDocument() + await act(async () => { + seedFeatures(queryClient, { annotation_quota_limit: { size: 4, limit: 0 } }) + }) + const annotation = await screen.findByRole('group', { + name: 'billing.annotatedResponse.quotaTitle', + }) + expect(within(annotation).getByText('4')).toBeInTheDocument() + expect(within(annotation).getByText('billing.plansCommon.unlimited')).toBeInTheDocument() +}) diff --git a/web/app/components/billing/annotation-full/usage.tsx b/web/app/components/billing/annotation-full/usage.tsx index 614d27f67bb..be6ee035737 100644 --- a/web/app/components/billing/annotation-full/usage.tsx +++ b/web/app/components/billing/annotation-full/usage.tsx @@ -1,10 +1,12 @@ 'use client' import type { FC } from 'react' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useProviderContext } from '@/context/provider-context' +import { consoleQuery } from '@/service/client' import { MessageFastPlus } from '../../base/icons/src/vender/line/communication' import UsageInfo from '../usage-info' +import { parseLimit } from '../utils' type Props = Readonly<{ className?: string @@ -12,15 +14,19 @@ type Props = Readonly<{ const Usage: FC = ({ className }) => { const { t } = useTranslation() - const { plan } = useProviderContext() - const { usage, total } = plan + const { data: annotationQuota } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.annotation_quota_limit, + }), + ) + if (!annotationQuota) return null return ( $['annotatedResponse.quotaTitle'], { ns: 'billing' })} - usage={usage.annotatedResponse} - total={total.annotatedResponse} + usage={annotationQuota.size} + total={parseLimit(annotationQuota.limit)} /> ) } diff --git a/web/app/components/billing/billing-page/index.tsx b/web/app/components/billing/billing-page/index.tsx index ae5aaa51ff6..8da90b3831d 100644 --- a/web/app/components/billing/billing-page/index.tsx +++ b/web/app/components/billing/billing-page/index.tsx @@ -1,10 +1,11 @@ 'use client' import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' -import { useQuery } from '@tanstack/react-query' +import { usePrefetchQuery, useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useTranslation } from 'react-i18next' +import Loading from '@/app/components/base/loading' import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state' import { deploymentEditionAtom } from '@/features/system-features/state' import { consoleQuery } from '@/service/client' @@ -14,6 +15,7 @@ const Billing: FC = () => { const { t } = useTranslation() const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom) const deploymentEdition = useAtomValue(deploymentEditionAtom) + usePrefetchQuery(consoleQuery.features.vectorSpace.get.queryOptions()) const { data: billing } = useQuery( consoleQuery.billing.invoices.get.queryOptions({ enabled: deploymentEdition === 'CLOUD' && isCurrentWorkspaceManager, @@ -23,7 +25,15 @@ const Billing: FC = () => { return (
- +
+ + } + > + + +
{deploymentEdition === 'CLOUD' && isCurrentWorkspaceManager && ( = { logHistory: NUM_INFINITE, }, } - -export const defaultPlan = { - type: 'sandbox' as const, - usage: { - documents: 50, - vectorSpace: 1, - buildApps: 1, - teamMembers: 1, - annotatedResponse: 1, - documentsUploadQuota: 0, - apiRateLimit: 0, - triggerEvents: 0, - }, - total: { - documents: 50, - vectorSpace: 10, - buildApps: 10, - teamMembers: 1, - annotatedResponse: 10, - documentsUploadQuota: 0, - apiRateLimit: ALL_PLANS.sandbox.apiRateLimit, - triggerEvents: ALL_PLANS.sandbox.triggerEvents, - }, - reset: { - apiRateLimit: null, - triggerEvents: null, - }, -} diff --git a/web/app/components/billing/plan/__tests__/index.spec.tsx b/web/app/components/billing/plan/__tests__/index.spec.tsx index f9ecda11e9a..2d975695aab 100644 --- a/web/app/components/billing/plan/__tests__/index.spec.tsx +++ b/web/app/components/billing/plan/__tests__/index.spec.tsx @@ -1,5 +1,4 @@ import { screen } from '@testing-library/react' -import { baseProviderContextValue } from '@/context/provider-context' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' import PlanComp from '../index' @@ -11,24 +10,6 @@ vi.mock('@/context/workspace-state', async () => { })) }) -vi.mock('@/context/provider-context', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useProviderContext: () => ({ - ...baseProviderContextValue, - enableEducationPlan: true, - }), - } -}) - -vi.mock('@/app/components/billing/hooks/use-education-discount', () => ({ - useEducationDiscount: () => ({ - handleEducationDiscount: vi.fn(), - isEducationDiscountLoading: false, - }), -})) - vi.mock('@/app/components/billing/upgrade-btn', () => ({ default: () => , })) @@ -37,10 +18,6 @@ vi.mock('@/app/components/billing/usage-info', () => ({ default: () => null, })) -vi.mock('@/app/components/billing/usage-info/apps-info', () => ({ - default: () => null, -})) - vi.mock('@/app/components/billing/usage-info/vector-space-info', () => ({ default: () => null, })) @@ -54,6 +31,7 @@ vi.mock('../assets', () => ({ const renderPlan = (educationStatus = { allow_refresh: false, is_student: false }) => { const { wrapper } = createConsoleQueryWrapper({ educationStatus, + features: { education: { enabled: true } }, systemFeatures: { deployment_edition: 'CLOUD' }, }) diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx index d544275c54b..783693472a4 100644 --- a/web/app/components/billing/plan/index.tsx +++ b/web/app/components/billing/plan/index.tsx @@ -2,14 +2,13 @@ import type { EducationStatusResponse } from '@dify/contracts/api/console/account/types.gen' import type { FC } from 'react' import { Button, buttonVariants } from '@langgenius/dify-ui/button' -import { RiBook2Line, RiFileEditLine, RiGroupLine } from '@remixicon/react' +import { RiApps2Line, RiBook2Line, RiFileEditLine, RiGroupLine } from '@remixicon/react' import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useTranslation } from 'react-i18next' import { ApiAggregate, TriggerAll } from '@/app/components/base/icons/src/vender/workflow' import UsageInfo from '@/app/components/billing/usage-info' -import { useProviderContext } from '@/context/provider-context' import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import Link from '@/next/link' @@ -19,8 +18,8 @@ import Loading from '../../base/icons/src/public/thought/Loading' import { NUM_INFINITE } from '../config' import { useEducationDiscount } from '../hooks/use-education-discount' import UpgradeBtn from '../upgrade-btn' -import AppsInfo from '../usage-info/apps-info' import VectorSpaceInfo from '../usage-info/vector-space-info' +import { getResetInDaysFromDate, parseLimit, parseRateLimit } from '../utils' import { Professional, Sandbox, Team } from './assets' type Props = Readonly<{ @@ -40,7 +39,8 @@ const PlanComp: FC = ({ loc }) => { }) const isCloudEdition = deploymentEdition === 'CLOUD' const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom) - const { plan, enableEducationPlan } = useProviderContext() + const { data: features } = useSuspenseQuery(consoleQuery.features.get.queryOptions()) + const enableEducationPlan = features.education.enabled const { data: educationStatus } = useQuery( consoleQuery.account.education.get.queryOptions({ enabled: enableEducationPlan, @@ -48,16 +48,17 @@ const PlanComp: FC = ({ loc }) => { }), ) const { isAboutToExpire = false, isEducationAccount = false } = educationStatus ?? {} - const { type } = plan - - const { usage, total, reset } = plan + const type = features.billing.subscription.plan + const triggerEventsLimit = parseRateLimit(features.trigger_event.limit) + const apiRateLimit = parseRateLimit(features.api_rate_limit.limit) + const apiRateLimitReset = getResetInDaysFromDate(features.api_rate_limit.reset_date) const triggerEventsResetInDays = - type === 'professional' && total.triggerEvents !== NUM_INFINITE - ? (reset.triggerEvents ?? undefined) + type === 'professional' && triggerEventsLimit !== NUM_INFINITE + ? (getResetInDaysFromDate(features.trigger_event.reset_date) ?? undefined) : undefined const apiRateLimitResetInDays = (() => { - if (total.apiRateLimit === NUM_INFINITE) return undefined - if (typeof reset.apiRateLimit === 'number') return reset.apiRateLimit + if (apiRateLimit === NUM_INFINITE) return undefined + if (typeof apiRateLimitReset === 'number') return apiRateLimitReset if (type === 'sandbox') return getDaysUntilEndOfMonth() return undefined })() @@ -66,9 +67,9 @@ const PlanComp: FC = ({ loc }) => { return (
- {plan.type === 'sandbox' && } - {plan.type === 'professional' && } - {plan.type === 'team' && } + {type === 'sandbox' && } + {type === 'professional' && } + {type === 'team' && }
@@ -110,41 +111,46 @@ const PlanComp: FC = ({ loc }) => {
{/* Plan detail */}
- + $['usagePage.buildApps'], { ns: 'billing' })} + usage={features.apps.size} + total={parseLimit(features.apps.limit)} + /> $['usagePage.teamMembers'], { ns: 'billing' })} - usage={usage.teamMembers} - total={total.teamMembers} + usage={features.members.size} + total={parseLimit(features.members.limit)} /> $['usagePage.documentsUploadQuota'], { ns: 'billing' })} - usage={usage.documentsUploadQuota} - total={total.documentsUploadQuota} + usage={features.documents_upload_quota.size} + total={parseLimit(features.documents_upload_quota.limit)} /> $['usagePage.annotationQuota'], { ns: 'billing' })} - usage={usage.annotatedResponse} - total={total.annotatedResponse} + usage={features.annotation_quota_limit.size} + total={parseLimit(features.annotation_quota_limit.limit)} /> $['usagePage.triggerEvents'], { ns: 'billing' })} - usage={usage.triggerEvents} - total={total.triggerEvents} + usage={features.trigger_event.usage} + total={triggerEventsLimit} tooltip={t(($) => $['plansCommon.triggerEvents.tooltip'], { ns: 'billing' }) as string} resetInDays={triggerEventsResetInDays} /> $['plansCommon.apiRateLimit'], { ns: 'billing' })} - usage={usage.apiRateLimit} - total={total.apiRateLimit} + usage={features.api_rate_limit.usage} + total={apiRateLimit} tooltip={ - total.apiRateLimit === NUM_INFINITE + apiRateLimit === NUM_INFINITE ? undefined : (t(($) => $['plansCommon.apiRateLimitTooltip'], { ns: 'billing' }) as string) } diff --git a/web/app/components/billing/type.ts b/web/app/components/billing/type.ts index 22e62c02fa0..6adddc29f0f 100644 --- a/web/app/components/billing/type.ts +++ b/web/app/components/billing/type.ts @@ -18,18 +18,3 @@ export type PlanInfo = { triggerEvents: number annotatedResponse: number } - -export type UsagePlanInfo = { - buildApps: number - teamMembers: number - annotatedResponse: number - documentsUploadQuota: number - apiRateLimit: number - triggerEvents: number - vectorSpace: number -} - -export type UsageResetInfo = { - apiRateLimit?: number | null - triggerEvents?: number | null -} diff --git a/web/app/components/billing/usage-info/apps-info.tsx b/web/app/components/billing/usage-info/apps-info.tsx deleted file mode 100644 index 8aab1a4273d..00000000000 --- a/web/app/components/billing/usage-info/apps-info.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client' -import type { FC } from 'react' -import { RiApps2Line } from '@remixicon/react' -import * as React from 'react' -import { useTranslation } from 'react-i18next' -import { useProviderContext } from '@/context/provider-context' -import UsageInfo from '../usage-info' - -type Props = Readonly<{ - className?: string -}> - -const AppsInfo: FC = ({ className }) => { - const { t } = useTranslation() - const { plan } = useProviderContext() - const { usage, total } = plan - return ( - $['usagePage.buildApps'], { ns: 'billing' })} - usage={usage.buildApps} - total={total.buildApps} - /> - ) -} -export default React.memo(AppsInfo) diff --git a/web/app/components/billing/usage-info/vector-space-info.tsx b/web/app/components/billing/usage-info/vector-space-info.tsx index 1a039cb5640..7ad3096964f 100644 --- a/web/app/components/billing/usage-info/vector-space-info.tsx +++ b/web/app/components/billing/usage-info/vector-space-info.tsx @@ -1,10 +1,9 @@ 'use client' import type { FC } from 'react' import { RiHardDrive3Line } from '@remixicon/react' -import { useQuery } from '@tanstack/react-query' +import { useSuspenseQueries } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useProviderContext } from '@/context/provider-context' import { consoleQuery } from '@/service/client' import UsageInfo from '../usage-info' import { getPlanVectorSpaceLimitMB } from '../utils' @@ -18,11 +17,14 @@ const STORAGE_THRESHOLD_MB = getPlanVectorSpaceLimitMB('sandbox') const VectorSpaceInfo: FC = ({ className }) => { const { t } = useTranslation() - const { plan } = useProviderContext() - const { data: vectorSpace } = useQuery(consoleQuery.features.vectorSpace.get.queryOptions()) - const vectorSpaceUsage = vectorSpace?.size ?? plan.usage.vectorSpace - const vectorSpaceLimit = vectorSpace?.limit ?? getPlanVectorSpaceLimitMB(plan.type) - const isSandbox = plan.type === 'sandbox' + const [{ data: plan }, { data: vectorSpace }] = useSuspenseQueries({ + queries: [ + consoleQuery.features.get.queryOptions({ + select: (features) => features.billing.subscription.plan, + }), + consoleQuery.features.vectorSpace.get.queryOptions(), + ], + }) return ( = ({ className }) => { Icon={RiHardDrive3Line} name={t(($) => $['usagePage.vectorSpace'], { ns: 'billing' })} tooltip={t(($) => $['usagePage.vectorSpaceTooltip'], { ns: 'billing' }) as string} - usage={vectorSpaceUsage} - total={vectorSpaceLimit} + usage={vectorSpace.size} + total={vectorSpace.limit} unit="MB" unitPosition="inline" storageMode storageThreshold={STORAGE_THRESHOLD_MB} storageTooltip={t(($) => $['usagePage.storageThresholdTooltip'], { ns: 'billing' }) as string} - isSandboxPlan={isSandbox} - usageUnknown={vectorSpace?.usage_unknown} + isSandboxPlan={plan === 'sandbox'} + usageUnknown={vectorSpace.usage_unknown} /> ) } diff --git a/web/app/components/billing/utils/__tests__/index.spec.ts b/web/app/components/billing/utils/__tests__/index.spec.ts index 2d8bf044d81..e910ac13abd 100644 --- a/web/app/components/billing/utils/__tests__/index.spec.ts +++ b/web/app/components/billing/utils/__tests__/index.spec.ts @@ -1,5 +1,10 @@ -import type { GetFeaturesResponse } from '@dify/contracts/api/console/features/types.gen' -import { getPlanVectorSpaceLimitMB, parseCurrentPlan, parseVectorSpaceToMB } from '../index' +import { + getPlanVectorSpaceLimitMB, + getResetInDaysFromDate, + parseLimit, + parseRateLimit, + parseVectorSpaceToMB, +} from '../index' describe('billing utils', () => { // parseVectorSpaceToMB tests @@ -42,275 +47,34 @@ describe('billing utils', () => { }) }) - // parseCurrentPlan tests - describe('parseCurrentPlan', () => { - const createMockPlanData = ( - overrides: Partial = {}, - ): GetFeaturesResponse => ({ - annotation_quota_limit: { - size: 5, - limit: 10, - }, - api_rate_limit: { - usage: 0, - limit: 5000, - reset_date: -1, - }, - apps: { - size: 2, - limit: 5, - }, - billing: { - subscription: { - interval: '', - plan: 'sandbox', - }, - }, - can_replace_logo: false, - dataset_operator_enabled: false, - docs_processing: '', - documents_upload_quota: { - size: 20, - limit: 0, - }, - education: { - activated: false, - enabled: false, - }, - enable_skill: false, - human_input_email_delivery_enabled: false, - is_allow_transfer_workspace: false, - knowledge_pipeline: { - publish_enabled: false, - }, - knowledge_rate_limit: 0, - members: { - size: 1, - limit: 1, - }, - model_load_balancing_enabled: false, - next_credit_reset_date: 0, - trigger_event: { - usage: 0, - limit: 3000, - reset_date: -1, - }, - vector_space: null, - webapp_copyright_enabled: false, - workspace_members: { - enabled: false, - size: 0, - limit: 0, - }, - ...overrides, + describe('quota reset dates', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 8, 7, 12)) }) - - it('should parse plan type correctly', () => { - const data = createMockPlanData() - const result = parseCurrentPlan(data) - expect(result.type).toBe('sandbox') + afterEach(() => vi.useRealTimers()) + it('handles Unix timestamps in seconds and milliseconds', () => { + const resetDate = new Date(2026, 8, 14).getTime() + expect(getResetInDaysFromDate(resetDate)).toBe(7) + expect(getResetInDaysFromDate(resetDate / 1000)).toBe(7) }) - - it('should parse usage values correctly', () => { - const data = createMockPlanData() - const result = parseCurrentPlan(data) - - expect(result.usage.vectorSpace).toBe(0) - expect(result.usage.buildApps).toBe(2) - expect(result.usage.teamMembers).toBe(1) - expect(result.usage.annotatedResponse).toBe(5) - expect(result.usage.documentsUploadQuota).toBe(20) + it('handles calendar dates and omits absent or past resets', () => { + expect(getResetInDaysFromDate(20260914)).toBe(7) + expect(getResetInDaysFromDate(0)).toBeNull() + expect(getResetInDaysFromDate(-1)).toBeNull() + expect(getResetInDaysFromDate(20260901)).toBeNull() }) + }) - it('should parse total limits correctly', () => { - const data = createMockPlanData() - const result = parseCurrentPlan(data) - - expect(result.total.vectorSpace).toBe(50) - expect(result.total.buildApps).toBe(5) - expect(result.total.teamMembers).toBe(1) - expect(result.total.annotatedResponse).toBe(10) + describe('quota display', () => { + it('displays zero count limits as unlimited', () => { + expect(parseLimit(0)).toBe(-1) + expect(parseLimit(10)).toBe(10) }) - - it('should not read vector space usage from current plan info', () => { - const data = createMockPlanData() - const result = parseCurrentPlan(data) - - expect(result.usage.vectorSpace).toBe(0) - expect(result.total.vectorSpace).toBe(50) - }) - - it('should derive vector space total from plan config', () => { - const data = createMockPlanData({ - billing: { - subscription: { - interval: '', - plan: 'professional', - }, - }, - }) - const result = parseCurrentPlan(data) - - expect(result.usage.vectorSpace).toBe(0) - expect(result.total.vectorSpace).toBe(5 * 1024) - }) - - it('should convert 0 limits to NUM_INFINITE (-1)', () => { - const data = createMockPlanData({ - documents_upload_quota: { - size: 20, - limit: 0, - }, - }) - const result = parseCurrentPlan(data) - expect(result.total.documentsUploadQuota).toBe(-1) - }) - - it('should handle api_rate_limit quota', () => { - const data = createMockPlanData({ - api_rate_limit: { - usage: 100, - limit: 5000, - reset_date: 0, - }, - }) - const result = parseCurrentPlan(data) - - expect(result.usage.apiRateLimit).toBe(100) - expect(result.total.apiRateLimit).toBe(5000) - }) - - it('should handle trigger_event quota', () => { - const data = createMockPlanData({ - trigger_event: { - usage: 50, - limit: 3000, - reset_date: 0, - }, - }) - const result = parseCurrentPlan(data) - - expect(result.usage.triggerEvents).toBe(50) - expect(result.total.triggerEvents).toBe(3000) - }) - - it('should convert 0 or -1 rate limits to NUM_INFINITE', () => { - const data = createMockPlanData({ - api_rate_limit: { - usage: 0, - limit: 0, - reset_date: 0, - }, - }) - const result = parseCurrentPlan(data) - expect(result.total.apiRateLimit).toBe(-1) - - const data2 = createMockPlanData({ - api_rate_limit: { - usage: 0, - limit: -1, - reset_date: 0, - }, - }) - const result2 = parseCurrentPlan(data2) - expect(result2.total.apiRateLimit).toBe(-1) - }) - - it('should handle reset dates with milliseconds timestamp', () => { - const futureDate = Date.now() + 86400000 // Tomorrow in ms - const data = createMockPlanData({ - api_rate_limit: { - usage: 100, - limit: 5000, - reset_date: futureDate, - }, - }) - const result = parseCurrentPlan(data) - - expect(result.reset.apiRateLimit).toBe(1) - }) - - it('should handle reset dates with seconds timestamp', () => { - const futureDate = Math.floor(Date.now() / 1000) + 86400 // Tomorrow in seconds - const data = createMockPlanData({ - api_rate_limit: { - usage: 100, - limit: 5000, - reset_date: futureDate, - }, - }) - const result = parseCurrentPlan(data) - - expect(result.reset.apiRateLimit).toBe(1) - }) - - it('should handle reset dates in YYYYMMDD format', () => { - const tomorrow = new Date() - tomorrow.setDate(tomorrow.getDate() + 1) - const year = tomorrow.getFullYear() - const month = String(tomorrow.getMonth() + 1).padStart(2, '0') - const day = String(tomorrow.getDate()).padStart(2, '0') - const dateNumber = Number.parseInt(`${year}${month}${day}`, 10) - - const data = createMockPlanData({ - api_rate_limit: { - usage: 100, - limit: 5000, - reset_date: dateNumber, - }, - }) - const result = parseCurrentPlan(data) - - expect(result.reset.apiRateLimit).toBe(1) - }) - - it('should return null for invalid reset dates', () => { - const data = createMockPlanData({ - api_rate_limit: { - usage: 100, - limit: 5000, - reset_date: 0, - }, - }) - const result = parseCurrentPlan(data) - expect(result.reset.apiRateLimit).toBeNull() - }) - - it('should return null for negative reset dates', () => { - const data = createMockPlanData({ - api_rate_limit: { - usage: 100, - limit: 5000, - reset_date: -1, - }, - }) - const result = parseCurrentPlan(data) - expect(result.reset.apiRateLimit).toBeNull() - }) - - it('should return null when reset date is in the past', () => { - const pastDate = Date.now() - 86400000 // Yesterday - const data = createMockPlanData({ - api_rate_limit: { - usage: 100, - limit: 5000, - reset_date: pastDate, - }, - }) - const result = parseCurrentPlan(data) - expect(result.reset.apiRateLimit).toBeNull() - }) - - it('should return null for unrecognized date format', () => { - const data = createMockPlanData({ - api_rate_limit: { - usage: 100, - limit: 5000, - reset_date: 12345, // Unrecognized format - }, - }) - const result = parseCurrentPlan(data) - expect(result.reset.apiRateLimit).toBeNull() + it('preserves unlimited rate limits in either API representation', () => { + expect(parseRateLimit(0)).toBe(-1) + expect(parseRateLimit(-1)).toBe(-1) + expect(parseRateLimit(5000)).toBe(5000) }) }) }) diff --git a/web/app/components/billing/utils/index.ts b/web/app/components/billing/utils/index.ts index 704f96be6b4..ce8791d49ae 100644 --- a/web/app/components/billing/utils/index.ts +++ b/web/app/components/billing/utils/index.ts @@ -1,4 +1,4 @@ -import type { CloudPlan, GetFeaturesResponse } from '@dify/contracts/api/console/features/types.gen' +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import dayjs from 'dayjs' import { ALL_PLANS, NUM_INFINITE } from '@/app/components/billing/config' @@ -23,13 +23,14 @@ export const getPlanVectorSpaceLimitMB = (planType: CloudPlan): number => { return parseVectorSpaceToMB(ALL_PLANS[planType].vectorSpace) } -const parseLimit = (limit: number) => { +// The API uses 0 for unlimited count quotas. +export const parseLimit = (limit: number) => { if (limit === 0) return NUM_INFINITE return limit } -const parseRateLimit = (limit: number) => { +export const parseRateLimit = (limit: number) => { if (limit === 0 || limit === -1) return NUM_INFINITE return limit @@ -63,34 +64,3 @@ export const getResetInDaysFromDate = (resetDate: number) => { return diff } - -export const parseCurrentPlan = (data: GetFeaturesResponse) => { - const planType = data.billing.subscription.plan - const vectorSpaceLimit = getPlanVectorSpaceLimitMB(planType) - - return { - type: planType, - usage: { - vectorSpace: 0, - buildApps: data.apps.size, - teamMembers: data.members.size, - annotatedResponse: data.annotation_quota_limit.size, - documentsUploadQuota: data.documents_upload_quota.size, - apiRateLimit: data.api_rate_limit.usage, - triggerEvents: data.trigger_event.usage, - }, - total: { - vectorSpace: vectorSpaceLimit, - buildApps: parseLimit(data.apps.limit), - teamMembers: parseLimit(data.members.limit), - annotatedResponse: parseLimit(data.annotation_quota_limit.limit), - documentsUploadQuota: parseLimit(data.documents_upload_quota.limit), - apiRateLimit: parseRateLimit(data.api_rate_limit.limit), - triggerEvents: parseRateLimit(data.trigger_event.limit), - }, - reset: { - apiRateLimit: getResetInDaysFromDate(data.api_rate_limit.reset_date), - triggerEvents: getResetInDaysFromDate(data.trigger_event.reset_date), - }, - } -} diff --git a/web/app/components/billing/vector-space-full/index.tsx b/web/app/components/billing/vector-space-full/index.tsx index 7d98709891d..b19d5d01663 100644 --- a/web/app/components/billing/vector-space-full/index.tsx +++ b/web/app/components/billing/vector-space-full/index.tsx @@ -25,7 +25,9 @@ const VectorSpaceFull: FC = () => {
- + + +
) diff --git a/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx b/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx index 780d2fb6ec8..619dc6a5922 100644 --- a/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx +++ b/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx @@ -1,7 +1,10 @@ import { toast } from '@langgenius/dify-ui/toast' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { queryOptions, useQuery } from '@tanstack/react-query' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { useAutoDisabledDocuments } from '@/service/knowledge/use-document' +import { createConsoleQueryWrapper } from '@/test/console/query-data' import AutoDisabledDocument from '../auto-disabled-document' const { mockToastSuccess } = vi.hoisted(() => ({ @@ -89,6 +92,29 @@ describe('AutoDisabledDocument', () => { }) }) + it('enables the current document IDs after the initial query completes', async () => { + const user = userEvent.setup() + mockUseAutoDisabledDocuments.mockImplementation(() => + useQuery( + queryOptions({ + queryKey: ['disabled-documents'], + queryFn: () => new Promise(() => {}), + }), + ), + ) + const { wrapper, queryClient } = createConsoleQueryWrapper() + render(, { wrapper }) + expect(screen.queryByRole('button', { name: /enable/i })).not.toBeInTheDocument() + await act(async () => { + queryClient.setQueryData(['disabled-documents'], { document_ids: ['returned-document'] }) + }) + await user.click(await screen.findByRole('button', { name: /enable/i })) + expect(mockMutateAsync).toHaveBeenCalledWith({ + datasetId: 'dataset', + documentIds: ['returned-document'], + }) + }) + describe('User Interactions', () => { it('should call enableDocument when action button is clicked', async () => { mockUseAutoDisabledDocuments.mockReturnValue( diff --git a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx index 63765d25702..ee034e8810e 100644 --- a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx +++ b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx @@ -2,7 +2,6 @@ import type { FC } from 'react' import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' -import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useAutoDisabledDocuments, @@ -20,14 +19,14 @@ const AutoDisabledDocument: FC = ({ datasetId }) => { const { data, isLoading } = useAutoDisabledDocuments(datasetId) const invalidDisabledDocument = useInvalidDisabledDocument() const documentIds = data?.document_ids - const hasDisabledDocument = documentIds && documentIds.length > 0 const { mutateAsync: enableDocument } = useDocumentEnable() - const handleEnableDocuments = useCallback(async () => { + if (!documentIds?.length || isLoading) return null + + const handleEnableDocuments = async () => { await enableDocument({ datasetId, documentIds }) invalidDisabledDocument() toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) - }, []) - if (!hasDisabledDocument || isLoading) return null + } return ( ({ }), })) -vi.mock('@/context/provider-context', () => ({ - useProviderContext: vi.fn(() => ({ - plan: { type: 'professional' }, - })), -})) - vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') @@ -193,7 +186,6 @@ vi.mock('../components/documents-header', () => ({ datasetId: string dataSourceType?: string embeddingAvailable: boolean - isFreePlan: boolean statusFilterValue: string sortValue: string inputValue: string @@ -719,16 +711,6 @@ describe('Documents', () => { expect(screen.getByTestId('header-embedding-available')).toHaveTextContent('false') }) - - it('should handle free plan user', () => { - vi.mocked(useProviderContext).mockReturnValueOnce({ - plan: { type: 'sandbox' }, - } as ReturnType) - - render() - - expect(screen.getByTestId('documents-header')).toBeInTheDocument() - }) }) describe('Pagination', () => { diff --git a/web/app/components/datasets/documents/components/__tests__/documents-header.spec.tsx b/web/app/components/datasets/documents/components/__tests__/documents-header.spec.tsx index 17bc7f7f6b1..a230dfd7951 100644 --- a/web/app/components/datasets/documents/components/__tests__/documents-header.spec.tsx +++ b/web/app/components/datasets/documents/components/__tests__/documents-header.spec.tsx @@ -36,7 +36,6 @@ describe('DocumentsHeader', () => { canManageMetadata: true, canAddDocument: true, canEditDocument: true, - isFreePlan: false, statusFilterValue: 'all', sortValue: 'created_at' as SortType, inputValue: '', @@ -104,16 +103,6 @@ describe('DocumentsHeader', () => { }) describe('AutoDisabledDocument', () => { - it('should show AutoDisabledDocument when not free plan', () => { - render() - expect(screen.getByTestId('auto-disabled-document')).toBeInTheDocument() - }) - - it('should not show AutoDisabledDocument when on free plan', () => { - render() - expect(screen.queryByTestId('auto-disabled-document')).not.toBeInTheDocument() - }) - it('should not show AutoDisabledDocument without document edit permission', () => { render() expect(screen.queryByTestId('auto-disabled-document')).not.toBeInTheDocument() diff --git a/web/app/components/datasets/documents/components/documents-header.tsx b/web/app/components/datasets/documents/components/documents-header.tsx index 14dc12b5dcf..35b5e3f6e18 100644 --- a/web/app/components/datasets/documents/components/documents-header.tsx +++ b/web/app/components/datasets/documents/components/documents-header.tsx @@ -32,7 +32,6 @@ type DocumentsHeaderProps = { canManageMetadata?: boolean canAddDocument?: boolean canEditDocument?: boolean - isFreePlan: boolean // Filter & sort statusFilterValue: string @@ -66,7 +65,6 @@ const DocumentsHeader: FC = ({ canManageMetadata = false, canAddDocument = false, canEditDocument = false, - isFreePlan, statusFilterValue, sortValue, inputValue, @@ -174,7 +172,7 @@ const DocumentsHeader: FC = ({ {/* Right: Actions */}
- {!isFreePlan && canEditDocument && } + {canEditDocument && } {canEditDocument && } {!embeddingAvailable && ( = ({ datasetId }) => { const router = useRouter() - const { plan } = useProviderContext() - const isFreePlan = plan.type === 'sandbox' const dataset = useDatasetDetailContextWithSelector((s) => s.dataset) const { data: currentUserId } = useSuspenseQuery({ @@ -185,7 +182,6 @@ const Documents: FC = ({ datasetId }) => { canManageMetadata={datasetACLCapabilities.canEdit} canAddDocument={datasetACLCapabilities.canUse} canEditDocument={datasetACLCapabilities.canEdit} - isFreePlan={isFreePlan} statusFilterValue={statusFilterValue} sortValue={sortValue} inputValue={inputValue} diff --git a/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx b/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx index 1f917566dd1..6702fc20cf3 100644 --- a/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx @@ -1,7 +1,6 @@ import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { ReactElement } from 'react' import type { CreateAppModalProps } from '../index' -import type { UsagePlanInfo } from '@/app/components/billing/type' import { act, fireEvent, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' @@ -43,20 +42,10 @@ vi.mock('@/next/navigation', () => ({ useParams: () => ({}), })) -const createPlanInfo = (buildApps: number): UsagePlanInfo => ({ - vectorSpace: 0, - buildApps, - teamMembers: 0, - annotatedResponse: 0, - documentsUploadQuota: 0, - apiRateLimit: 0, - triggerEvents: 0, -}) - let deploymentEdition: 'CLOUD' | 'COMMUNITY' = 'COMMUNITY' let mockPlanType: CloudPlan = 'team' -let mockUsagePlanInfo: UsagePlanInfo = createPlanInfo(1) -let mockTotalPlanInfo: UsagePlanInfo = createPlanInfo(10) +let mockAppCount = 1 +const mockAppLimit = 10 type ConfirmPayload = Parameters[0] @@ -107,7 +96,7 @@ function render(ui: ReactElement) { systemFeatures: { deployment_edition: deploymentEdition }, features: { billing: { subscription: { plan: mockPlanType } }, - apps: { size: mockUsagePlanInfo.buildApps, limit: mockTotalPlanInfo.buildApps }, + apps: { size: mockAppCount, limit: mockAppLimit }, }, }) } @@ -117,8 +106,7 @@ describe('CreateAppModal', () => { vi.clearAllMocks() deploymentEdition = 'COMMUNITY' mockPlanType = 'team' - mockUsagePlanInfo = createPlanInfo(1) - mockTotalPlanInfo = createPlanInfo(10) + mockAppCount = 1 hotkeyMocks.handlers.clear() }) @@ -222,8 +210,7 @@ describe('CreateAppModal', () => { it('should show AppsFull and disable create when apps quota is reached', async () => { deploymentEdition = 'CLOUD' mockPlanType = 'team' - mockUsagePlanInfo = createPlanInfo(10) - mockTotalPlanInfo = createPlanInfo(10) + mockAppCount = 10 await setup({ isEditModal: false }) @@ -234,8 +221,7 @@ describe('CreateAppModal', () => { it('should allow saving when apps quota is reached in edit mode', async () => { deploymentEdition = 'CLOUD' mockPlanType = 'team' - mockUsagePlanInfo = createPlanInfo(10) - mockTotalPlanInfo = createPlanInfo(10) + mockAppCount = 10 await setup({ isEditModal: true }) @@ -280,8 +266,7 @@ describe('CreateAppModal', () => { it('should not submit when apps quota is reached in create mode', async () => { deploymentEdition = 'CLOUD' mockPlanType = 'team' - mockUsagePlanInfo = createPlanInfo(10) - mockTotalPlanInfo = createPlanInfo(10) + mockAppCount = 10 const { onConfirm, onHide } = await setup({ isEditModal: false }) @@ -297,8 +282,7 @@ describe('CreateAppModal', () => { it('should submit when apps quota is reached in edit mode', async () => { deploymentEdition = 'CLOUD' mockPlanType = 'team' - mockUsagePlanInfo = createPlanInfo(10) - mockTotalPlanInfo = createPlanInfo(10) + mockAppCount = 10 const { onConfirm, onHide } = await setup({ isEditModal: true }) diff --git a/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx b/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx index 275ee2d38ac..2f3eeeae541 100644 --- a/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx +++ b/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx @@ -8,29 +8,19 @@ import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { useModalContext } from '@/context/modal-context' -import { baseProviderContextValue, useProviderContext } from '@/context/provider-context' import { getDocDownloadUrl } from '@/service/common' +import { seedFeatures } from '@/test/console/query-data' import { downloadUrl } from '@/utils/download' import Compliance from '../compliance' -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 { - ...actual, - useModalContext: vi.fn(), - } + return { ...actual, useModalContext: vi.fn() } }) - -vi.mock('@/service/common', () => ({ - getDocDownloadUrl: vi.fn(), +vi.mock('@/service/common', () => ({ getDocDownloadUrl: vi.fn() })) +vi.mock('@/service/base', () => ({ + request: vi.fn(() => new Promise(() => {})), + sseGeneratorPost: vi.fn(), })) vi.mock('@/utils/download', () => ({ @@ -55,17 +45,11 @@ describe('Compliance', () => { toastErrorSpy.mockClear() queryClient = new QueryClient({ defaultOptions: { - queries: { retry: false }, + queries: { retry: false, staleTime: Infinity }, mutations: { retry: false }, }, }) - vi.mocked(useProviderContext).mockReturnValue({ - ...baseProviderContextValue, - plan: { - ...baseProviderContextValue.plan, - type: 'sandbox', - }, - }) + seedFeatures(queryClient, { billing: { subscription: { plan: 'sandbox' } } }) vi.mocked(useModalContext).mockReturnValue({ setShowPricingModal: mockSetShowPricingModal, } as unknown as ModalContextState) @@ -95,6 +79,23 @@ describe('Compliance', () => { return screen.getByText(label).closest('[role="menuitem"]') } + it('keeps GDPR downloadable while plan-dependent documents wait for features', async () => { + queryClient.clear() + vi.mocked(getDocDownloadUrl).mockResolvedValue({ url: 'https://example.com/gdpr.pdf' }) + openMenuAndRender() + expect(screen.queryByText('common.compliance.soc2Type1')).not.toBeInTheDocument() + expect(screen.queryByText('common.compliance.soc2Type2')).not.toBeInTheDocument() + expect(screen.queryByText('common.compliance.iso27001')).not.toBeInTheDocument() + expect(getComplianceMenuItem('common.compliance.gdpr')).not.toHaveAttribute( + 'aria-disabled', + 'true', + ) + expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument() + fireEvent.click(screen.getByText('common.compliance.gdpr')) + await waitFor(() => expect(getDocDownloadUrl).toHaveBeenCalledWith('GDPR')) + expect(mockSetShowPricingModal).not.toHaveBeenCalled() + }) + describe('Rendering', () => { it('should render compliance menu trigger', () => { // Act @@ -130,13 +131,7 @@ describe('Compliance', () => { it('should show Download button for plan that allows it', () => { // Arrange - vi.mocked(useProviderContext).mockReturnValue({ - ...baseProviderContextValue, - plan: { - ...baseProviderContextValue.plan, - type: 'team', - }, - }) + seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } }) // Act openMenuAndRender() @@ -154,13 +149,7 @@ describe('Compliance', () => { // Arrange const mockUrl = 'http://example.com/doc.pdf' vi.mocked(getDocDownloadUrl).mockResolvedValue({ url: mockUrl }) - vi.mocked(useProviderContext).mockReturnValue({ - ...baseProviderContextValue, - plan: { - ...baseProviderContextValue.plan, - type: 'team', - }, - }) + seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } }) // Act openMenuAndRender() @@ -178,13 +167,7 @@ describe('Compliance', () => { it('should handle download mutation error', async () => { // Arrange vi.mocked(getDocDownloadUrl).mockRejectedValue(new Error('Download failed')) - vi.mocked(useProviderContext).mockReturnValue({ - ...baseProviderContextValue, - plan: { - ...baseProviderContextValue.plan, - type: 'team', - }, - }) + seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } }) const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // Act @@ -213,13 +196,7 @@ describe('Compliance', () => { it('should handle upgrade click on badge for non-sandbox plan', () => { // Arrange - vi.mocked(useProviderContext).mockReturnValue({ - ...baseProviderContextValue, - plan: { - ...baseProviderContextValue.plan, - type: 'professional', - }, - }) + seedFeatures(queryClient, { billing: { subscription: { plan: 'professional' } } }) // Act openMenuAndRender() @@ -241,13 +218,7 @@ describe('Compliance', () => { resolveDownload = resolve }), ) - vi.mocked(useProviderContext).mockReturnValue({ - ...baseProviderContextValue, - plan: { - ...baseProviderContextValue.plan, - type: 'team', - }, - }) + seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } }) // Act openMenuAndRender() @@ -279,13 +250,7 @@ describe('Compliance', () => { resolveDownload = resolve }), ) - vi.mocked(useProviderContext).mockReturnValue({ - ...baseProviderContextValue, - plan: { - ...baseProviderContextValue.plan, - type: 'team', - }, - }) + seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } }) openMenuAndRender() const menuItem = getComplianceMenuItem('common.compliance.soc2Type1') diff --git a/web/app/components/header/account-dropdown/compliance.tsx b/web/app/components/header/account-dropdown/compliance.tsx index a5c0bf43206..63d54911416 100644 --- a/web/app/components/header/account-dropdown/compliance.tsx +++ b/web/app/components/header/account-dropdown/compliance.tsx @@ -10,7 +10,7 @@ import { } from '@langgenius/dify-ui/dropdown-menu' import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' import { useQueryState } from 'nuqs' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' @@ -19,7 +19,7 @@ import { settingsQueryParser, } from '@/app/components/header/account-setting/query-params' import { useModalContext } from '@/context/modal-context' -import { useProviderContext } from '@/context/provider-context' +import { consoleQuery } from '@/service/client' import { getDocDownloadUrl } from '@/service/common' import { downloadUrl } from '@/utils/download' import Gdpr from '../../base/icons/src/public/common/Gdpr' @@ -101,10 +101,15 @@ type ComplianceDocRowItemProps = { function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProps) { const { t } = useTranslation() - const { plan } = useProviderContext() + const { data: plan } = useQuery( + consoleQuery.features.get.queryOptions({ + enabled: docName !== DocName.GDPR, + select: (data) => data.billing.subscription.plan, + }), + ) const { setShowPricingModal } = useModalContext() const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser) - const isFreePlan = plan.type === 'sandbox' + const isFreePlan = plan === 'sandbox' const { isPending, mutate: downloadCompliance } = useMutation({ mutationKey: ['downloadCompliance', docName], @@ -127,7 +132,9 @@ function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProp [DocName.GDPR]: ['team', 'professional', 'sandbox'], } - const isCurrentPlanCanDownload = whichPlanCanDownloadCompliance[docName].includes(plan.type) + const isCurrentPlanCanDownload = + docName === DocName.GDPR || + (plan !== undefined && whichPlanCanDownloadCompliance[docName].includes(plan)) const handleSelect = useCallback(() => { if (isCurrentPlanCanDownload) { @@ -153,6 +160,8 @@ function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProp } const labelTitle = typeof label === 'string' ? label : undefined + if (docName !== DocName.GDPR && plan === undefined) return null + return ( $['operation.download'], { ns: 'common' })} upgradeText={t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })} /> diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx index c2f35ad934e..3284996dddc 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx @@ -1,22 +1,23 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { ModelItem, ModelProvider } from '../../declarations' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, screen, waitFor } from '@testing-library/react' import { disableModel, enableModel } from '@/service/common' +import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' import { ModelStatusEnum } from '../../declarations' import ModelListItem from '../model-list-item' -function createWrapper() { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - return ({ children }: { children: React.ReactNode }) => ( - {children} - ) -} - let mockModelLoadBalancingEnabled = false -let mockPlanType: string = 'pro' +let mockPlanType: CloudPlan = 'professional' let mockWorkspacePermissionKeys: string[] = ['plugin.model_config'] +function createWrapper(deploymentEdition: 'CLOUD' | 'COMMUNITY' = 'CLOUD') { + return createConsoleQueryWrapper({ + systemFeatures: { deployment_edition: deploymentEdition }, + features: { billing: { subscription: { plan: mockPlanType } } }, + }).wrapper +} + vi.mock('@/context/permission-state', async () => { const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') return createPermissionStateModuleMock(() => ({ @@ -25,9 +26,6 @@ vi.mock('@/context/permission-state', async () => { }) vi.mock('@/context/provider-context', () => ({ - useProviderContext: () => ({ - plan: { type: mockPlanType }, - }), useProviderContextSelector: () => mockModelLoadBalancingEnabled, })) @@ -78,10 +76,17 @@ describe('ModelListItem', () => { beforeEach(() => { vi.clearAllMocks() mockModelLoadBalancingEnabled = false - mockPlanType = 'pro' + mockPlanType = 'professional' mockWorkspacePermissionKeys = ['plugin.model_config'] }) + it('keeps model configuration available outside Cloud without a Sandbox placeholder', () => { + render(, { + wrapper: createWrapper('COMMUNITY'), + }) + expect(screen.getByRole('button', { name: 'modify load balancing' })).toBeInTheDocument() + }) + it('should render model item with icon and name', () => { render(, { wrapper: createWrapper(), @@ -247,7 +252,7 @@ describe('ModelListItem', () => { it('should hide ConfigModel for non-sandbox plan without load balancing enabled', () => { // Arrange - set plan type to non-sandbox and keep load balancing disabled mockModelLoadBalancingEnabled = false - mockPlanType = 'pro' + mockPlanType = 'professional' // Act render(, { diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx index f49ff602d41..c96749fd0ab 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx @@ -3,7 +3,7 @@ import type { ModelItem, ModelProvider } from '../declarations' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { Switch } from '@langgenius/dify-ui/switch' -import { useQueryClient } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' import { useAtomValue } from 'jotai' import { memo, useCallback } from 'react' @@ -11,7 +11,8 @@ import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' import { workspacePermissionKeysAtom } from '@/context/permission-state' -import { useProviderContext, useProviderContextSelector } from '@/context/provider-context' +import { useProviderContextSelector } from '@/context/provider-context' +import { deploymentEditionAtom } from '@/features/system-features/state' import { consoleQuery } from '@/service/client' import { disableModel, enableModel } from '@/service/common' import { hasPermission } from '@/utils/permission' @@ -41,7 +42,13 @@ const ModelListItem = ({ onModifyLoadBalancing, }: ModelListItemProps) => { const { t } = useTranslation() - const { plan } = useProviderContext() + const deploymentEdition = useAtomValue(deploymentEditionAtom) + const { data: plan } = useQuery( + consoleQuery.features.get.queryOptions({ + enabled: deploymentEdition === 'CLOUD', + select: (features) => features.billing.subscription.plan, + }), + ) const modelLoadBalancingEnabled = useProviderContextSelector( (state) => state.modelLoadBalancingEnabled, ) @@ -131,7 +138,7 @@ const ModelListItem = ({ )} {canConfigureModels && - (modelLoadBalancingEnabled || plan.type === 'sandbox') && + (deploymentEdition !== 'CLOUD' || modelLoadBalancingEnabled || plan === 'sandbox') && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status) && ( { } ;(useProviderContext as Mock).mockReturnValue({ enableEducationPlan: false, - plan: { type: 'sandbox' }, } as ProviderContextState) ;(useModalContext as Mock).mockReturnValue({ setShowPricingModal: mockSetShowPricingModal, @@ -835,7 +834,6 @@ describe('MainNav', () => { it('shows the user education badge in the account popup without adding the workspace plan there', async () => { ;(useProviderContext as Mock).mockReturnValue({ enableEducationPlan: true, - plan: { type: 'sandbox' }, } as ProviderContextState) renderMainNav(defaultMainNavSystemFeatures, { 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 0d53f884595..3ab0be0ad20 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 @@ -178,7 +178,6 @@ describe('WorkspaceCard', () => { mockCurrentWorkspaceQuery() vi.mocked(useProviderContext).mockReturnValue({ enableEducationPlan: false, - plan: { type: 'sandbox' }, } as ProviderContextState) mockWorkspacePermissionKeys(['workspace.member.manage']) vi.mocked(useModalContext).mockReturnValue({ @@ -343,7 +342,6 @@ describe('WorkspaceCard', () => { }) vi.mocked(useProviderContext).mockReturnValue({ enableEducationPlan: false, - plan: { type: 'sandbox' }, } as ProviderContextState) renderWorkspaceCard({ systemFeatures: { deployment_edition: 'CLOUD' } }) diff --git a/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx b/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx index 3ea1a51be75..8cdb13f4e36 100644 --- a/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx @@ -1,5 +1,4 @@ import type { ModalContextState } from '@/context/modal-context' -import type { ProviderContextState } from '@/context/provider-context' import { toast } from '@langgenius/dify-ui/toast' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' @@ -35,11 +34,6 @@ vi.mock('@/context/modal-context', () => ({ }), })) -const mockUseProviderContext = vi.fn() -vi.mock('@/context/provider-context', () => ({ - useProviderContext: () => mockUseProviderContext(), -})) - vi.mock('@/context/i18n', async () => { const actual = await vi.importActual('@/context/i18n') return { @@ -62,13 +56,6 @@ describe('EditCustomCollectionModal', () => { parameters_schema: [], schema_type: 'openapi', }) - mockUseProviderContext.mockReturnValue({ - plan: { - type: 'sandbox', - }, - - webappCopyrightEnabled: true, - } as ProviderContextState) }) const renderModal = (props?: { diff --git a/web/context/__tests__/console-bootstrap.spec.tsx b/web/context/__tests__/console-bootstrap.spec.tsx index c23e52866ea..dd051b4e37c 100644 --- a/web/context/__tests__/console-bootstrap.spec.tsx +++ b/web/context/__tests__/console-bootstrap.spec.tsx @@ -96,6 +96,7 @@ vi.mock('@/config', async (importOriginal) => { return { ...actual, ZENDESK_FIELD_IDS: { + PLAN: '', ENVIRONMENT: 'environment-field', VERSION: 'version-field', EMAIL: 'email-field', @@ -113,6 +114,15 @@ vi.mock('@/features/account-profile/client', () => ({ vi.mock('@/service/client', () => ({ consoleQuery: { + features: { + get: { + queryOptions: (options: object) => ({ + queryKey: ['features'], + queryFn: () => new Promise(() => {}), + ...options, + }), + }, + }, systemFeatures: { get: { queryOptions: () => ({ @@ -514,6 +524,42 @@ describe('Console bootstrap', () => { }) }) + it('syncs the actual plan only after features arrive and follows plan changes', async () => { + ZENDESK_FIELD_IDS.PLAN = 'plan-field' + try { + const { queryClient } = renderConsoleBootstrap() + await waitFor(() => expect(zendeskRuntime.setConversationFields).toHaveBeenCalled()) + expect( + vi + .mocked(zendeskRuntime.setConversationFields) + .mock.calls.flatMap(([fields]) => fields) + .some((field) => field.id === 'plan-field'), + ).toBe(false) + act(() => + queryClient.setQueryData(['features'], { + billing: { subscription: { plan: 'professional' } }, + }), + ) + await waitFor(() => + expect(zendeskRuntime.setConversationFields).toHaveBeenCalledWith( + [{ id: 'plan-field', value: 'professional-plan' }], + 'CLOUD', + ), + ) + act(() => + queryClient.setQueryData(['features'], { billing: { subscription: { plan: 'team' } } }), + ) + await waitFor(() => + expect(zendeskRuntime.setConversationFields).toHaveBeenCalledWith( + [{ id: 'plan-field', value: 'team-plan' }], + 'CLOUD', + ), + ) + } finally { + ZENDESK_FIELD_IDS.PLAN = '' + } + }) + it('should not sync Zendesk fields outside cloud deployments', async () => { mockSystemFeaturesState.data = createSystemFeaturesFixture({ deployment_edition: 'COMMUNITY', diff --git a/web/context/provider-context-provider.tsx b/web/context/provider-context-provider.tsx index 4f478b9f2ab..04103f98431 100644 --- a/web/context/provider-context-provider.tsx +++ b/web/context/provider-context-provider.tsx @@ -2,23 +2,12 @@ import type { ReactNode } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { useAtomValue } from 'jotai' -import { useEffect } from 'react' -import { zendeskRuntime } from '@/app/components/base/zendesk/runtime' -import { defaultPlan } from '@/app/components/billing/config' -import { parseCurrentPlan } from '@/app/components/billing/utils' import { ModelStatusEnum, ModelTypeEnum, } from '@/app/components/header/account-setting/model-provider-page/declarations' -import { ZENDESK_FIELD_IDS } from '@/config' -import { deploymentEditionAtom } from '@/features/system-features/state' import { consoleQuery } from '@/service/client' -import { - commonQueryKeys, - useModelListByType, - useSupportRetrievalMethods, -} from '@/service/use-common' +import { commonQueryKeys, useModelListByType } from '@/service/use-common' import { ProviderContext } from './provider-context' type ProviderContextProviderProps = { @@ -26,7 +15,6 @@ type ProviderContextProviderProps = { } export const ProviderContextProvider = ({ children }: ProviderContextProviderProps) => { - const deploymentEdition = useAtomValue(deploymentEditionAtom) const queryClient = useQueryClient() const featuresQuery = useQuery(consoleQuery.features.get.queryOptions()) const { @@ -35,15 +23,12 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro isSuccess: isSuccessModelProviders, } = useQuery(consoleQuery.workspaces.current.modelProviders.summary.get.queryOptions()) const { data: textGenerationModelList } = useModelListByType(ModelTypeEnum.textGeneration) - const { data: supportRetrievalMethods } = useSupportRetrievalMethods() const features = featuresQuery.data - const plan = deploymentEdition === 'CLOUD' && features ? parseCurrentPlan(features) : defaultPlan const enableEducationPlan = features?.education.enabled ?? false const enableSkill = features?.enable_skill ?? false const enableReplaceWebAppLogo = features?.can_replace_logo ?? false const modelLoadBalancingEnabled = features?.model_load_balancing_enabled ?? false - const webappCopyrightEnabled = features?.webapp_copyright_enabled ?? false const isAllowTransferWorkspace = features?.is_allow_transfer_workspace ?? false const isAllowPublishAsCustomKnowledgePipelineTemplate = features?.knowledge_pipeline.publish_enabled ?? false @@ -57,22 +42,6 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro queryClient.invalidateQueries({ queryKey: commonQueryKeys.modelProviderDetails }), ]).then(() => undefined) - // #region Zendesk conversation fields - useEffect(() => { - if (ZENDESK_FIELD_IDS.PLAN && plan.type) { - zendeskRuntime.setConversationFields( - [ - { - id: ZENDESK_FIELD_IDS.PLAN, - value: `${plan.type}-plan`, - }, - ], - deploymentEdition, - ) - } - }, [deploymentEdition, plan.type]) - // #endregion Zendesk conversation fields - return ( model.status === ModelStatusEnum.active, ), - supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [], - plan, enableSkill, enableReplaceWebAppLogo, modelLoadBalancingEnabled, enableEducationPlan, - webappCopyrightEnabled, isAllowTransferWorkspace, isAllowPublishAsCustomKnowledgePipelineTemplate, humanInputEmailDeliveryEnabled, diff --git a/web/context/provider-context.ts b/web/context/provider-context.ts index 0a54aee4739..da2a8523482 100644 --- a/web/context/provider-context.ts +++ b/web/context/provider-context.ts @@ -1,15 +1,11 @@ 'use client' -import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { ModelProviderPluginSummaryResponse, ModelProviderSummaryResponse, } from '@dify/contracts/api/console/workspaces/types.gen' -import type { UsagePlanInfo, UsageResetInfo } from '@/app/components/billing/type' import type { Model } from '@/app/components/header/account-setting/model-provider-page/declarations' -import type { RETRIEVE_METHOD } from '@/types/app' import { createContext, useContext, useContextSelector } from 'use-context-selector' -import { defaultPlan } from '@/app/components/billing/config' export type ProviderContextState = { modelProviders: ModelProviderSummaryResponse[] @@ -18,19 +14,11 @@ export type ProviderContextState = { isSuccessModelProviders: boolean refreshModelProviders: () => Promise textGenerationModelList: Model[] - supportRetrievalMethods: RETRIEVE_METHOD[] isAPIKeySet: boolean - plan: { - type: CloudPlan - usage: UsagePlanInfo - total: UsagePlanInfo - reset: UsageResetInfo - } enableSkill: boolean enableReplaceWebAppLogo: boolean modelLoadBalancingEnabled: boolean enableEducationPlan: boolean - webappCopyrightEnabled: boolean isAllowTransferWorkspace: boolean isAllowPublishAsCustomKnowledgePipelineTemplate: boolean humanInputEmailDeliveryEnabled: boolean @@ -43,14 +31,11 @@ export const baseProviderContextValue: ProviderContextState = { isSuccessModelProviders: false, refreshModelProviders: async () => {}, textGenerationModelList: [], - supportRetrievalMethods: [], isAPIKeySet: true, - plan: defaultPlan, enableSkill: false, enableReplaceWebAppLogo: false, modelLoadBalancingEnabled: false, enableEducationPlan: false, - webappCopyrightEnabled: false, isAllowTransferWorkspace: false, isAllowPublishAsCustomKnowledgePipelineTemplate: false, humanInputEmailDeliveryEnabled: false, diff --git a/web/service/use-common.ts b/web/service/use-common.ts index 524a38bfd0f..a2ab3e8db9d 100644 --- a/web/service/use-common.ts +++ b/web/service/use-common.ts @@ -15,7 +15,6 @@ import type { StructuredOutputRulesRequestBody, StructuredOutputRulesResponse, } from '@/models/common' -import type { RETRIEVE_METHOD } from '@/types/app' import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { discardRegistrationSessionState } from '@/app/components/base/amplitude/registration-session-state' // oxlint-disable-next-line no-restricted-imports @@ -32,7 +31,6 @@ export const commonQueryKeys = { modelProviders: [NAME_SPACE, 'model-providers'] as const, modelProviderDetails: [NAME_SPACE, 'model-provider-details'] as const, defaultModel: (type: ModelTypeEnum) => [NAME_SPACE, 'default-model', type] as const, - retrievalMethods: [NAME_SPACE, 'support-retrieval-methods'] as const, accountIntegrates: [NAME_SPACE, 'account-integrates'] as const, notionConnection: [NAME_SPACE, 'notion-connection'] as const, codeBasedExtensions: (module?: string) => [NAME_SPACE, 'code-based-extensions', module] as const, @@ -227,13 +225,6 @@ export const useModelListByType = (type: ModelTypeEnum | ModelType, enabled = tr }) } -export const useSupportRetrievalMethods = () => { - return useQuery<{ retrieval_method: RETRIEVE_METHOD[] }>({ - queryKey: commonQueryKeys.retrievalMethods, - queryFn: () => get<{ retrieval_method: RETRIEVE_METHOD[] }>('/datasets/retrieval-setting'), - }) -} - export const useCodeBasedExtensions = (module: string) => { return useQuery({ queryKey: commonQueryKeys.codeBasedExtensions(module),