From d3684f1542eb72142df5ec651633284bd2b62485 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 12 May 2026 19:28:46 +0800 Subject: [PATCH] hide for ce --- api/services/feature_service.py | 2 + .../services/test_feature_service.py | 4 ++ .../(commonLayout)/role-route-guard.spec.tsx | 57 ++++++++++++------- web/app/(commonLayout)/role-route-guard.tsx | 12 +++- .../header/__tests__/index.spec.tsx | 25 ++++++++ web/app/components/header/index.tsx | 5 +- web/types/feature.ts | 46 ++++++++------- 7 files changed, 106 insertions(+), 45 deletions(-) diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 257c4bea9a..70e582670c 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -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 diff --git a/api/tests/test_containers_integration_tests/services/test_feature_service.py b/api/tests/test_containers_integration_tests/services/test_feature_service.py index a678e37b41..f4adc2ebfd 100644 --- a/api/tests/test_containers_integration_tests/services/test_feature_service.py +++ b/api/tests/test_containers_integration_tests/services/test_feature_service.py @@ -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 diff --git a/web/app/(commonLayout)/role-route-guard.spec.tsx b/web/app/(commonLayout)/role-route-guard.spec.tsx index ef409393b0..896dfb066a 100644 --- a/web/app/(commonLayout)/role-route-guard.spec.tsx +++ b/web/app/(commonLayout)/role-route-guard.spec.tsx @@ -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 = {}) => { }) } +const renderRoleRouteGuard = (systemFeatures: { enable_app_deploy?: boolean } = {}) => + renderWithSystemFeatures( + ( + +
content
+
+ ), + { systemFeatures }, + ) + describe('RoleRouteGuard', () => { beforeEach(() => { vi.clearAllMocks() @@ -46,11 +57,7 @@ describe('RoleRouteGuard', () => { isLoadingCurrentWorkspace: true, }) - render(( - -
content
-
- )) + renderRoleRouteGuard() expect(screen.getByRole('status')).toBeInTheDocument() expect(screen.queryByText('content')).not.toBeInTheDocument() @@ -62,11 +69,7 @@ describe('RoleRouteGuard', () => { isCurrentWorkspaceDatasetOperator: true, }) - render(( - -
content
-
- )) + renderRoleRouteGuard() expect(screen.queryByText('content')).not.toBeInTheDocument() await waitFor(() => { @@ -80,11 +83,7 @@ describe('RoleRouteGuard', () => { isCurrentWorkspaceDatasetOperator: true, }) - render(( - -
content
-
- )) + renderRoleRouteGuard() expect(screen.getByText('content')).toBeInTheDocument() expect(mockReplace).not.toHaveBeenCalled() @@ -96,14 +95,30 @@ describe('RoleRouteGuard', () => { isLoadingCurrentWorkspace: true, }) - render(( - -
content
-
- )) + 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() + }) }) diff --git a/web/app/(commonLayout)/role-route-guard.tsx b/web/app/(commonLayout)/role-route-guard.tsx index 483dfef095..58b7e7caac 100644 --- a/web/app/(commonLayout)/role-route-guard.tsx +++ b/web/app/(commonLayout)/role-route-guard.tsx @@ -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) diff --git a/web/app/components/header/__tests__/index.spec.tsx b/web/app/components/header/__tests__/index.spec.tsx index 699e3c20e5..7e55f1bb0b 100644 --- a/web/app/components/header/__tests__/index.spec.tsx +++ b/web/app/components/header/__tests__/index.spec.tsx @@ -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 }) => (