diff --git a/web/app/components/app/log/__tests__/index.spec.tsx b/web/app/components/app/log/__tests__/index.spec.tsx index 827b8056d41..9d6901e3020 100644 --- a/web/app/components/app/log/__tests__/index.spec.tsx +++ b/web/app/components/app/log/__tests__/index.spec.tsx @@ -33,6 +33,20 @@ vi.mock('@/next/navigation', () => ({ }), })) +vi.mock('@/config', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + IS_CLOUD_EDITION: true, + } +}) + +vi.mock('@/context/app-context', () => ({ + useAppContext: () => ({ + isCurrentWorkspaceManager: true, + }), +})) + vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => `https://docs.example.com${path}`, })) diff --git a/web/app/components/app/log/archived-logs-notice.tsx b/web/app/components/app/log/archived-logs-notice.tsx index 2e8e29ee7cf..41b84d14bcd 100644 --- a/web/app/components/app/log/archived-logs-notice.tsx +++ b/web/app/components/app/log/archived-logs-notice.tsx @@ -2,12 +2,18 @@ import { useTranslation } from 'react-i18next' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' +import { IS_CLOUD_EDITION } from '@/config' +import { useAppContext } from '@/context/app-context' import { useModalContextSelector } from '@/context/modal-context' export function ArchivedLogsNotice() { const { t } = useTranslation() + const { isCurrentWorkspaceManager } = useAppContext() const setShowAccountSettingModal = useModalContextSelector(state => state.setShowAccountSettingModal) + if (!IS_CLOUD_EDITION || !isCurrentWorkspaceManager) + return null + return (
diff --git a/web/app/components/header/account-setting/__tests__/index.spec.tsx b/web/app/components/header/account-setting/__tests__/index.spec.tsx index 7efce325296..0957cd29c56 100644 --- a/web/app/components/header/account-setting/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/index.spec.tsx @@ -10,10 +10,23 @@ import { ACCOUNT_SETTING_TAB } from '../constants' import AccountSetting from '../index' const mockResetModelProviderListExpanded = vi.fn() +const mockConfig = vi.hoisted(() => ({ + IS_CLOUD_EDITION: true, +})) const mockAppContextState = vi.hoisted(() => ({ current: null as unknown, })) +vi.mock('@/config', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + get IS_CLOUD_EDITION() { + return mockConfig.IS_CLOUD_EDITION + }, + } +}) + vi.mock('@/context/provider-context', async (importOriginal) => { const actual = await importOriginal() return { @@ -220,6 +233,7 @@ describe('AccountSetting', () => { beforeEach(() => { vi.clearAllMocks() + mockConfig.IS_CLOUD_EDITION = true vi.mocked(useProviderContext).mockReturnValue({ ...baseProviderContextValue, enableBilling: true, @@ -505,6 +519,54 @@ describe('AccountSetting', () => { expect(screen.queryByRole('button', { name: 'common.settings.billing' })).not.toBeInTheDocument() }) + it('should hide workflow log archives outside cloud edition', () => { + // Arrange + mockConfig.IS_CLOUD_EDITION = false + + // Act + renderAccountSetting() + + // Assert + expect(screen.queryByRole('button', { name: 'appLog.archives.title' })).not.toBeInTheDocument() + }) + + it('should hide workflow log archives from custom RBAC roles that are not owner or admin', () => { + // Arrange + const contextWithRoleManagePermissionButNotManager = { + ...baseAppContextValue, + currentWorkspace: { + ...baseAppContextValue.currentWorkspace, + role: 'normal' as const, + }, + isCurrentWorkspaceManager: false, + isCurrentWorkspaceOwner: false, + workspacePermissionKeys: [ + ...baseAppContextValue.workspacePermissionKeys, + 'workspace.role.manage', + ], + } + vi.mocked(useAppContext).mockReturnValue(contextWithRoleManagePermissionButNotManager) + mockAppContextState.current = contextWithRoleManagePermissionButNotManager + + // Act + renderAccountSetting() + + // Assert + expect(screen.queryByRole('button', { name: 'appLog.archives.title' })).not.toBeInTheDocument() + }) + + it('should not render workflow log archives page outside cloud edition', () => { + // Arrange + mockConfig.IS_CLOUD_EDITION = false + + // Act + renderAccountSetting({ initialTab: ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES }) + + // Assert + expect(screen.queryByTestId('workflow-log-archives-page')).not.toBeInTheDocument() + expect(screen.getAllByText('common.settings.members').length).toBeGreaterThan(0) + }) + it('should not render billing page when active billing tab lacks billing view permission', () => { // Arrange const contextWithoutBillingViewPermission = { diff --git a/web/app/components/header/account-setting/index.tsx b/web/app/components/header/account-setting/index.tsx index 67678811f79..157cd42f0c4 100644 --- a/web/app/components/header/account-setting/index.tsx +++ b/web/app/components/header/account-setting/index.tsx @@ -12,6 +12,7 @@ import { ACCOUNT_SETTING_TAB, } from '@/app/components/header/account-setting/constants' import MenuDialog from '@/app/components/header/account-setting/menu-dialog' +import { IS_CLOUD_EDITION } from '@/config' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' @@ -59,7 +60,7 @@ export default function AccountSetting({ const isRbacEnabled = systemFeatures.rbac_enabled const canManageWorkspaceRoles = isRbacEnabled && hasPermission(workspacePermissionKeys, 'workspace.role.manage') const canViewBilling = enableBilling && hasPermission(workspacePermissionKeys, BillingPermission.View) - const canViewWorkflowLogArchives = isCurrentWorkspaceManager || canManageWorkspaceRoles + const canViewWorkflowLogArchives = IS_CLOUD_EDITION && isCurrentWorkspaceManager // Keep legacy `language` deep links opening Preferences during the tab rename migration. const normalizedActiveTab = activeTab === ACCOUNT_SETTING_TAB.LANGUAGE ? ACCOUNT_SETTING_TAB.PREFERENCES : activeTab const activeMenu = (() => {