refactor(web): retire provider context plan state (#41925)

This commit is contained in:
yyh 2026-09-07 13:28:20 +00:00 committed by GitHub
parent a9cb17eb6c
commit 8fc11b2927
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 495 additions and 933 deletions

View File

@ -5133,7 +5133,7 @@
},
"web/service/use-common.ts": {
"@tanstack/query/prefer-query-options": {
"count": 12
"count": 11
}
},
"web/service/use-datasource.ts": {

View File

@ -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>,
): ProviderContextState =>
createMockProviderContextValue({
...ctx,
plan: merge(ctx.plan, {
usage,
}),
})
export const createMockPlanTotal = (
total: UsagePlanInfo,
ctx: Partial<ProviderContextState>,
): ProviderContextState =>
createMockProviderContextValue({
...ctx,
plan: merge(ctx.plan, {
total,
}),
})
export const createMockPlanReset = (
reset: Partial<ProviderContextState['plan']['reset']>,
ctx: Partial<ProviderContextState>,
): ProviderContextState =>
createMockProviderContextValue({
...ctx,
plan: merge(ctx?.plan, {
reset,
}),
})

View File

@ -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<string, unknown> = {}
let mockFeatures: DeepPartial<GetFeaturesResponse> = {}
let mockVectorSpace: GetFeaturesVectorSpaceResponse = { size: 0, limit: 50, usage_unknown: false }
let mockConsoleState: Record<string, unknown> = {}
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<typeof createPlanData>
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<UsagePlanInfo>
total?: Partial<UsagePlanInfo>
reset?: Partial<UsageResetInfo>
type BillingOverrides = DeepPartial<GetFeaturesResponse> & {
vectorSpace?: Partial<GetFeaturesVectorSpaceResponse>
}
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<string, unknown> = {},
const setupBilling = (
{ vectorSpace, ...features }: BillingOverrides = {},
education: { enableEducationPlan?: boolean } = {},
educationStatus: Partial<typeof mockEducationStatus> = {},
) => {
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<string, unknown> = {}) => {
@ -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(<Billing />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />, {}, 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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test-loc" />)
@ -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(<AppsFull loc="test" />)
@ -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(<AppsFull loc="test" />)
@ -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(<AppsFull loc="test" />)
@ -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(<AppsFull loc="test" />)
@ -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(<VectorSpaceFull />)
@ -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(<AnnotationFull />)
@ -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(<AnnotationFullModal show={true} onHide={vi.fn()} />)
@ -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(<AnnotationFullModal show={false} onHide={vi.fn()} />)
@ -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(
<TriggerEventsLimitModal
@ -678,7 +636,7 @@ describe('Capacity Full Components Integration', () => {
const user = userEvent.setup()
const onClose = vi.fn()
const onUpgrade = vi.fn()
setupProviderContext({ type: 'professional' })
setupBilling({ billing: { subscription: { plan: 'professional' } } })
render(
<TriggerEventsLimitModal
@ -710,7 +668,7 @@ describe('PriorityLabel Integration', () => {
})
it('should display "standard" priority for sandbox plan', () => {
setupProviderContext({ type: 'sandbox' })
setupBilling({ billing: { subscription: { plan: 'sandbox' } } })
render(<PriorityLabel />)
@ -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(<PriorityLabel />)
@ -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(<PriorityLabel />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<AppsFull loc="app-create" />)
@ -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(<VectorSpaceFull />)
@ -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(<AnnotationFull />)
@ -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(
<TriggerEventsLimitModal
@ -924,10 +867,9 @@ describe('Cross-Component Upgrade Flow', () => {
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(<AnnotationFullModal show={true} onHide={vi.fn()} />)

View File

@ -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<string, unknown> = {}
let mockFeatures: DeepPartial<GetFeaturesResponse> = {}
let mockVectorSpace: GetFeaturesVectorSpaceResponse = { size: 0, limit: 50, usage_unknown: false }
let mockConsoleState: Record<string, unknown> = {}
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<UsagePlanInfo>
total?: Partial<UsagePlanInfo>
reset?: Partial<UsageResetInfo>
type BillingOverrides = DeepPartial<GetFeaturesResponse> & {
vectorSpace?: Partial<GetFeaturesVectorSpaceResponse>
}
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<string, unknown> = {},
const setupBilling = (
{ vectorSpace, ...features }: BillingOverrides = {},
education: { enableEducationPlan?: boolean } = {},
appOverrides: Record<string, unknown> = {},
educationStatus: Partial<typeof mockEducationStatus> = {},
) => {
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)
@ -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(<PlanComp loc="test" />)

View File

@ -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({

View File

@ -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,

View File

@ -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(<Billing />, { 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(<AnnotationUsage />, { 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()
})

View File

@ -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<Props> = ({ 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 (
<UsageInfo
className={className}
Icon={MessageFastPlus}
name={t(($) => $['annotatedResponse.quotaTitle'], { ns: 'billing' })}
usage={usage.annotatedResponse}
total={total.annotatedResponse}
usage={annotationQuota.size}
total={parseLimit(annotationQuota.limit)}
/>
)
}

View File

@ -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 (
<div>
<PlanComp loc="billing-page" />
<div className="grid min-h-132 xl:min-h-120">
<React.Suspense
fallback={
<Loading className="rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn" />
}
>
<PlanComp loc="billing-page" />
</React.Suspense>
</div>
{deploymentEdition === 'CLOUD' && isCurrentWorkspaceManager && (
<a
className={cn(

View File

@ -69,31 +69,3 @@ export const ALL_PLANS: Record<CloudPlan, PlanInfo> = {
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,
},
}

View File

@ -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<typeof import('@/context/provider-context')>()
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: () => <button type="button">View Plan</button>,
}))
@ -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' },
})

View File

@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ loc }) => {
return (
<div className="relative rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn">
<div className="p-6 pb-2">
{plan.type === 'sandbox' && <Sandbox />}
{plan.type === 'professional' && <Professional />}
{plan.type === 'team' && <Team />}
{type === 'sandbox' && <Sandbox />}
{type === 'professional' && <Professional />}
{type === 'team' && <Team />}
<div className="mt-1 flex items-center">
<div className="grow">
<div className="mb-1 flex items-center gap-1">
@ -110,41 +111,46 @@ const PlanComp: FC<Props> = ({ loc }) => {
</div>
{/* Plan detail */}
<div className="grid grid-cols-3 content-start gap-1 p-2">
<AppsInfo />
<UsageInfo
Icon={RiApps2Line}
name={t(($) => $['usagePage.buildApps'], { ns: 'billing' })}
usage={features.apps.size}
total={parseLimit(features.apps.limit)}
/>
<UsageInfo
Icon={RiGroupLine}
name={t(($) => $['usagePage.teamMembers'], { ns: 'billing' })}
usage={usage.teamMembers}
total={total.teamMembers}
usage={features.members.size}
total={parseLimit(features.members.limit)}
/>
<UsageInfo
Icon={RiBook2Line}
name={t(($) => $['usagePage.documentsUploadQuota'], { ns: 'billing' })}
usage={usage.documentsUploadQuota}
total={total.documentsUploadQuota}
usage={features.documents_upload_quota.size}
total={parseLimit(features.documents_upload_quota.limit)}
/>
<VectorSpaceInfo />
<UsageInfo
Icon={RiFileEditLine}
name={t(($) => $['usagePage.annotationQuota'], { ns: 'billing' })}
usage={usage.annotatedResponse}
total={total.annotatedResponse}
usage={features.annotation_quota_limit.size}
total={parseLimit(features.annotation_quota_limit.limit)}
/>
<UsageInfo
Icon={TriggerAll}
name={t(($) => $['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}
/>
<UsageInfo
Icon={ApiAggregate}
name={t(($) => $['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)
}

View File

@ -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
}

View File

@ -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<Props> = ({ className }) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const { usage, total } = plan
return (
<UsageInfo
className={className}
Icon={RiApps2Line}
name={t(($) => $['usagePage.buildApps'], { ns: 'billing' })}
usage={usage.buildApps}
total={total.buildApps}
/>
)
}
export default React.memo(AppsInfo)

View File

@ -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<Props> = ({ 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 (
<UsageInfo
@ -30,15 +32,15 @@ const VectorSpaceInfo: FC<Props> = ({ 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}
/>
)
}

View File

@ -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> = {},
): 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)
})
})
})

View File

@ -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),
},
}
}

View File

@ -25,7 +25,9 @@ const VectorSpaceFull: FC = () => {
</div>
<UpgradeBtn loc="knowledge-add-file" />
</div>
<VectorSpaceInfo className="pt-4" />
<React.Suspense fallback={null}>
<VectorSpaceInfo className="pt-4" />
</React.Suspense>
</div>
</GridMask>
)

View File

@ -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<AutoDisabledDocumentsResponse>(() => {}),
}),
),
)
const { wrapper, queryClient } = createConsoleQueryWrapper()
render(<AutoDisabledDocument datasetId="dataset" />, { 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(

View File

@ -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<Props> = ({ 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 (
<StatusWithAction

View File

@ -3,7 +3,6 @@ import type { DocumentListResponse } from '@/models/datasets'
import { QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, screen } from '@testing-library/react'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useProviderContext } from '@/context/provider-context'
import { DataSourceType } from '@/models/datasets'
import { useDocumentList } from '@/service/knowledge/use-document'
import { createAccountProfileQueryClient } from '@/test/console/account-profile'
@ -54,12 +53,6 @@ vi.mock('@/context/dataset-detail', () => ({
}),
}))
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<typeof useProviderContext>)
render(<Documents {...defaultProps} />)
expect(screen.getByTestId('documents-header')).toBeInTheDocument()
})
})
describe('Pagination', () => {

View File

@ -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(<DocumentsHeader {...defaultProps} isFreePlan={false} />)
expect(screen.getByTestId('auto-disabled-document')).toBeInTheDocument()
})
it('should not show AutoDisabledDocument when on free plan', () => {
render(<DocumentsHeader {...defaultProps} isFreePlan={true} />)
expect(screen.queryByTestId('auto-disabled-document')).not.toBeInTheDocument()
})
it('should not show AutoDisabledDocument without document edit permission', () => {
render(<DocumentsHeader {...defaultProps} canEditDocument={false} />)
expect(screen.queryByTestId('auto-disabled-document')).not.toBeInTheDocument()

View File

@ -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<DocumentsHeaderProps> = ({
canManageMetadata = false,
canAddDocument = false,
canEditDocument = false,
isFreePlan,
statusFilterValue,
sortValue,
inputValue,
@ -174,7 +172,7 @@ const DocumentsHeader: FC<DocumentsHeaderProps> = ({
{/* Right: Actions */}
<div className="flex h-8! items-center justify-center gap-2">
{!isFreePlan && canEditDocument && <AutoDisabledDocument datasetId={datasetId} />}
{canEditDocument && <AutoDisabledDocument datasetId={datasetId} />}
{canEditDocument && <IndexFailed datasetId={datasetId} />}
{!embeddingAvailable && (
<StatusWithAction

View File

@ -7,7 +7,6 @@ import { useCallback } from 'react'
import Loading from '@/app/components/base/loading'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { useProviderContext } from '@/context/provider-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { DataSourceType } from '@/models/datasets'
import { useRouter } from '@/next/navigation'
@ -35,8 +34,6 @@ const FORCED_POLLING_STATUSES = new Set(['queuing', 'indexing', 'paused'])
const Documents: FC<IDocumentsProps> = ({ 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<IDocumentsProps> = ({ datasetId }) => {
canManageMetadata={datasetACLCapabilities.canEdit}
canAddDocument={datasetACLCapabilities.canUse}
canEditDocument={datasetACLCapabilities.canEdit}
isFreePlan={isFreePlan}
statusFilterValue={statusFilterValue}
sortValue={sortValue}
inputValue={inputValue}

View File

@ -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<CreateAppModalProps['onConfirm']>[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 })

View File

@ -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<typeof import('@/context/provider-context')>()
return {
...actual,
useProviderContext: vi.fn(),
}
})
vi.mock('@/context/modal-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/modal-context')>()
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')

View File

@ -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 (
<DropdownMenuItem
className="h-10 justify-between py-1 pr-2 pl-1"
@ -167,7 +176,7 @@ function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProp
<ComplianceDocActionVisual
isCurrentPlanCanDownload={isCurrentPlanCanDownload}
isPending={isPending}
tooltipText={upgradeTooltip[plan.type]}
tooltipText={plan ? upgradeTooltip[plan] : ''}
downloadText={t(($) => $['operation.download'], { ns: 'common' })}
upgradeText={t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
/>

View File

@ -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 }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
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(<ModelListItem model={mockModel} provider={mockProvider} isConfigurable />, {
wrapper: createWrapper('COMMUNITY'),
})
expect(screen.getByRole('button', { name: 'modify load balancing' })).toBeInTheDocument()
})
it('should render model item with icon and name', () => {
render(<ModelListItem model={mockModel} provider={mockProvider} isConfigurable={false} />, {
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(<ModelListItem model={mockModel} provider={mockProvider} isConfigurable={false} />, {

View File

@ -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 = ({
</Badge>
)}
{canConfigureModels &&
(modelLoadBalancingEnabled || plan.type === 'sandbox') &&
(deploymentEdition !== 'CLOUD' || modelLoadBalancingEnabled || plan === 'sandbox') &&
!model.deprecated &&
[ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status) && (
<ConfigModel

View File

@ -657,7 +657,6 @@ describe('MainNav', () => {
}
;(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, {

View File

@ -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' } })

View File

@ -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<typeof import('@/context/i18n')>('@/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?: {

View File

@ -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',

View File

@ -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 (
<ProviderContext.Provider
value={{
@ -85,13 +54,10 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro
isAPIKeySet: !!textGenerationModelList?.data?.some(
(model) => model.status === ModelStatusEnum.active,
),
supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [],
plan,
enableSkill,
enableReplaceWebAppLogo,
modelLoadBalancingEnabled,
enableEducationPlan,
webappCopyrightEnabled,
isAllowTransferWorkspace,
isAllowPublishAsCustomKnowledgePipelineTemplate,
humanInputEmailDeliveryEnabled,

View File

@ -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<void>
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,

View File

@ -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<CodeBasedExtension>({
queryKey: commonQueryKeys.codeBasedExtensions(module),