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