mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
hide for ce
This commit is contained in:
parent
86e88ef6c8
commit
d3684f1542
@ -159,6 +159,7 @@ class PluginManagerModel(BaseModel):
|
||||
|
||||
class SystemFeatureModel(BaseModel):
|
||||
app_dsl_version: str = ""
|
||||
enable_app_deploy: bool = False
|
||||
sso_enforced_for_signin: bool = False
|
||||
sso_enforced_for_signin_protocol: str = ""
|
||||
enable_marketplace: bool = False
|
||||
@ -233,6 +234,7 @@ class FeatureService:
|
||||
cls._fulfill_system_params_from_env(system_features)
|
||||
|
||||
if dify_config.ENTERPRISE_ENABLED:
|
||||
system_features.enable_app_deploy = True
|
||||
system_features.branding.enabled = True
|
||||
system_features.webapp_auth.enabled = True
|
||||
system_features.enable_change_email = False
|
||||
|
||||
@ -291,6 +291,7 @@ class TestFeatureService:
|
||||
assert isinstance(result, SystemFeatureModel)
|
||||
|
||||
# Verify enterprise features
|
||||
assert result.enable_app_deploy is True
|
||||
assert result.branding.enabled is True
|
||||
assert result.webapp_auth.enabled is True
|
||||
assert result.enable_change_email is False
|
||||
@ -377,6 +378,7 @@ class TestFeatureService:
|
||||
# Ensure that data required for frontend rendering remains accessible.
|
||||
|
||||
# Branding should match the mock data
|
||||
assert result.enable_app_deploy is True
|
||||
assert result.branding.enabled is True
|
||||
assert result.branding.application_title == "Test Enterprise"
|
||||
assert result.branding.login_page_logo == "https://example.com/logo.png"
|
||||
@ -424,6 +426,7 @@ class TestFeatureService:
|
||||
assert isinstance(result, SystemFeatureModel)
|
||||
|
||||
# Verify basic configuration
|
||||
assert result.enable_app_deploy is False
|
||||
assert result.branding.enabled is False
|
||||
assert result.webapp_auth.enabled is False
|
||||
assert result.enable_change_email is True
|
||||
@ -625,6 +628,7 @@ class TestFeatureService:
|
||||
assert isinstance(result, SystemFeatureModel)
|
||||
|
||||
# Verify enterprise features are disabled
|
||||
assert result.enable_app_deploy is False
|
||||
assert result.branding.enabled is False
|
||||
assert result.webapp_auth.enabled is False
|
||||
assert result.enable_change_email is True
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||
import RoleRouteGuard from './role-route-guard'
|
||||
|
||||
const mockReplace = vi.fn()
|
||||
@ -34,6 +35,16 @@ const setAppContext = (overrides: Partial<AppContextMock> = {}) => {
|
||||
})
|
||||
}
|
||||
|
||||
const renderRoleRouteGuard = (systemFeatures: { enable_app_deploy?: boolean } = {}) =>
|
||||
renderWithSystemFeatures(
|
||||
(
|
||||
<RoleRouteGuard>
|
||||
<div>content</div>
|
||||
</RoleRouteGuard>
|
||||
),
|
||||
{ systemFeatures },
|
||||
)
|
||||
|
||||
describe('RoleRouteGuard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -46,11 +57,7 @@ describe('RoleRouteGuard', () => {
|
||||
isLoadingCurrentWorkspace: true,
|
||||
})
|
||||
|
||||
render((
|
||||
<RoleRouteGuard>
|
||||
<div>content</div>
|
||||
</RoleRouteGuard>
|
||||
))
|
||||
renderRoleRouteGuard()
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
expect(screen.queryByText('content')).not.toBeInTheDocument()
|
||||
@ -62,11 +69,7 @@ describe('RoleRouteGuard', () => {
|
||||
isCurrentWorkspaceDatasetOperator: true,
|
||||
})
|
||||
|
||||
render((
|
||||
<RoleRouteGuard>
|
||||
<div>content</div>
|
||||
</RoleRouteGuard>
|
||||
))
|
||||
renderRoleRouteGuard()
|
||||
|
||||
expect(screen.queryByText('content')).not.toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
@ -80,11 +83,7 @@ describe('RoleRouteGuard', () => {
|
||||
isCurrentWorkspaceDatasetOperator: true,
|
||||
})
|
||||
|
||||
render((
|
||||
<RoleRouteGuard>
|
||||
<div>content</div>
|
||||
</RoleRouteGuard>
|
||||
))
|
||||
renderRoleRouteGuard()
|
||||
|
||||
expect(screen.getByText('content')).toBeInTheDocument()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
@ -96,14 +95,30 @@ describe('RoleRouteGuard', () => {
|
||||
isLoadingCurrentWorkspace: true,
|
||||
})
|
||||
|
||||
render((
|
||||
<RoleRouteGuard>
|
||||
<div>content</div>
|
||||
</RoleRouteGuard>
|
||||
))
|
||||
renderRoleRouteGuard()
|
||||
|
||||
expect(screen.getByText('content')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should redirect deployments routes when app deploy is disabled', async () => {
|
||||
mockPathname = '/deployments'
|
||||
|
||||
renderRoleRouteGuard({ enable_app_deploy: false })
|
||||
|
||||
expect(screen.queryByText('content')).not.toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/apps')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow deployments routes when app deploy is enabled', () => {
|
||||
mockPathname = '/deployments/app-1/overview'
|
||||
|
||||
renderRoleRouteGuard({ enable_app_deploy: true })
|
||||
|
||||
expect(screen.getByText('content')).toBeInTheDocument()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useEffect } from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
|
||||
const datasetOperatorRedirectRoutes = ['/apps', '/app', '/explore', '/tools'] as const
|
||||
|
||||
@ -12,15 +14,19 @@ const isPathUnderRoute = (pathname: string, route: string) => pathname === route
|
||||
|
||||
export default function RoleRouteGuard({ children }: { children: ReactNode }) {
|
||||
const { isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const shouldGuardRoute = datasetOperatorRedirectRoutes.some(route => isPathUnderRoute(pathname, route))
|
||||
const shouldRedirect = shouldGuardRoute && !isLoadingCurrentWorkspace && isCurrentWorkspaceDatasetOperator
|
||||
const shouldRedirectDatasetOperator = shouldGuardRoute && !isLoadingCurrentWorkspace && isCurrentWorkspaceDatasetOperator
|
||||
const shouldRedirectAppDeploy = isPathUnderRoute(pathname, '/deployments') && !systemFeatures.enable_app_deploy
|
||||
const shouldRedirect = shouldRedirectDatasetOperator || shouldRedirectAppDeploy
|
||||
const redirectPath = shouldRedirectAppDeploy ? '/apps' : '/datasets'
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldRedirect)
|
||||
router.replace('/datasets')
|
||||
}, [shouldRedirect, router])
|
||||
router.replace(redirectPath)
|
||||
}, [redirectPath, shouldRedirect, router])
|
||||
|
||||
// Block rendering only for guarded routes to avoid permission flicker.
|
||||
if (shouldGuardRoute && isLoadingCurrentWorkspace)
|
||||
|
||||
@ -44,6 +44,10 @@ vi.mock('@/app/components/header/tools-nav', () => ({
|
||||
default: createMockComponent('tools-nav'),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/deployments/nav', () => ({
|
||||
DeploymentsNav: createMockComponent('deployments-nav'),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/plan-badge', () => ({
|
||||
PlanBadge: ({ onClick, plan }: { onClick?: () => void, plan?: string }) => (
|
||||
<button data-testid="plan-badge" onClick={onClick} data-plan={plan} />
|
||||
@ -66,6 +70,7 @@ let mockPlanType = 'sandbox'
|
||||
let mockBrandingEnabled = false
|
||||
let mockBrandingTitle: string | null = null
|
||||
let mockBrandingLogo: string | null = null
|
||||
let mockEnableAppDeploy = false
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
|
||||
@ -103,6 +108,7 @@ const renderHeader = (ui: ReactElement = <Header />) =>
|
||||
application_title: mockBrandingTitle ?? '',
|
||||
workspace_logo: mockBrandingLogo ?? '',
|
||||
},
|
||||
enable_app_deploy: mockEnableAppDeploy,
|
||||
},
|
||||
})
|
||||
|
||||
@ -117,6 +123,7 @@ describe('Header', () => {
|
||||
mockBrandingEnabled = false
|
||||
mockBrandingTitle = null
|
||||
mockBrandingLogo = null
|
||||
mockEnableAppDeploy = false
|
||||
})
|
||||
|
||||
it('should render header with main nav components', () => {
|
||||
@ -214,6 +221,24 @@ describe('Header', () => {
|
||||
expect(screen.getByTestId('app-nav')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide deployments nav when app deploy is disabled', () => {
|
||||
mockIsWorkspaceEditor = true
|
||||
mockEnableAppDeploy = false
|
||||
|
||||
renderHeader()
|
||||
|
||||
expect(screen.queryByTestId('deployments-nav')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show deployments nav for editors when app deploy is enabled', () => {
|
||||
mockIsWorkspaceEditor = true
|
||||
mockEnableAppDeploy = true
|
||||
|
||||
renderHeader()
|
||||
|
||||
expect(screen.getByTestId('deployments-nav')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide dataset nav when neither editor nor dataset operator', () => {
|
||||
mockIsWorkspaceEditor = false
|
||||
mockIsDatasetOperator = false
|
||||
|
||||
@ -37,6 +37,7 @@ export function Header() {
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const isFreePlan = plan.type === Plan.sandbox
|
||||
const isBrandingEnabled = systemFeatures.branding.enabled
|
||||
const canUseAppDeploy = isCurrentWorkspaceEditor && systemFeatures.enable_app_deploy
|
||||
|
||||
function handlePlanClick() {
|
||||
if (isFreePlan)
|
||||
@ -86,7 +87,7 @@ export function Header() {
|
||||
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
|
||||
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
|
||||
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
|
||||
{isCurrentWorkspaceEditor && <DeploymentsNav />}
|
||||
{canUseAppDeploy && <DeploymentsNav />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -107,7 +108,7 @@ export function Header() {
|
||||
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
|
||||
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
|
||||
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
|
||||
{isCurrentWorkspaceEditor && <DeploymentsNav />}
|
||||
{canUseAppDeploy && <DeploymentsNav />}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 items-center justify-end gap-2 pr-3 pl-2 min-[1280px]:pl-3">
|
||||
<EnvNav />
|
||||
|
||||
@ -1,26 +1,32 @@
|
||||
import type { ModelProviderQuotaGetPaid } from './model-provider'
|
||||
|
||||
export enum SSOProtocol {
|
||||
SAML = 'saml',
|
||||
OIDC = 'oidc',
|
||||
OAuth2 = 'oauth2',
|
||||
}
|
||||
export const SSOProtocol = {
|
||||
SAML: 'saml',
|
||||
OIDC: 'oidc',
|
||||
OAuth2: 'oauth2',
|
||||
} as const
|
||||
|
||||
export enum LicenseStatus {
|
||||
NONE = 'none',
|
||||
INACTIVE = 'inactive',
|
||||
ACTIVE = 'active',
|
||||
EXPIRING = 'expiring',
|
||||
EXPIRED = 'expired',
|
||||
LOST = 'lost',
|
||||
}
|
||||
export type SSOProtocol = typeof SSOProtocol[keyof typeof SSOProtocol]
|
||||
|
||||
export enum InstallationScope {
|
||||
ALL = 'all',
|
||||
NONE = 'none',
|
||||
OFFICIAL_ONLY = 'official_only',
|
||||
OFFICIAL_AND_PARTNER = 'official_and_specific_partners',
|
||||
}
|
||||
export const LicenseStatus = {
|
||||
NONE: 'none',
|
||||
INACTIVE: 'inactive',
|
||||
ACTIVE: 'active',
|
||||
EXPIRING: 'expiring',
|
||||
EXPIRED: 'expired',
|
||||
LOST: 'lost',
|
||||
} as const
|
||||
|
||||
export type LicenseStatus = typeof LicenseStatus[keyof typeof LicenseStatus]
|
||||
|
||||
export const InstallationScope = {
|
||||
ALL: 'all',
|
||||
NONE: 'none',
|
||||
OFFICIAL_ONLY: 'official_only',
|
||||
OFFICIAL_AND_PARTNER: 'official_and_specific_partners',
|
||||
} as const
|
||||
|
||||
export type InstallationScope = typeof InstallationScope[keyof typeof InstallationScope]
|
||||
|
||||
type License = {
|
||||
status: LicenseStatus
|
||||
@ -29,6 +35,7 @@ type License = {
|
||||
|
||||
export type SystemFeatures = {
|
||||
app_dsl_version: string
|
||||
enable_app_deploy: boolean
|
||||
trial_models: ModelProviderQuotaGetPaid[]
|
||||
plugin_installation_permission: {
|
||||
plugin_installation_scope: InstallationScope
|
||||
@ -71,6 +78,7 @@ export type SystemFeatures = {
|
||||
|
||||
export const defaultSystemFeatures: SystemFeatures = {
|
||||
app_dsl_version: '',
|
||||
enable_app_deploy: false,
|
||||
trial_models: [],
|
||||
plugin_installation_permission: {
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user