feat: make the console license expiry badge optional (#39972)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Xiyuan Chen
2026-08-06 19:28:58 -07:00
committed by GitHub
parent 73197e4354
commit 4b2c5ac4ae
16 changed files with 85 additions and 17 deletions
+3
View File
@@ -39,6 +39,9 @@ ENABLE_COLLABORATION_MODE=true
# Learn app feature toggle
ENABLE_LEARN_APP=true
# Show the license expiry countdown badge in the console (enterprise only)
ENABLE_LICENSE_EXPIRY_NOTICE=true
# Access token expiration time in minutes
ACCESS_TOKEN_EXPIRE_MINUTES=60
+6
View File
@@ -25,6 +25,12 @@ class EnterpriseFeatureConfig(BaseSettings):
default=False,
)
ENABLE_LICENSE_EXPIRY_NOTICE: bool = Field(
description="Show the license expiry countdown badge in the console when the license is expiring. "
"Disable to hide the badge; license status and all enforcement remain unaffected.",
default=True,
)
ENTERPRISE_REQUEST_TIMEOUT: int = Field(
ge=1, description="Maximum timeout in seconds for enterprise requests", default=5
)
+1
View File
@@ -22276,6 +22276,7 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
| enable_email_password_login | boolean, <br>**Default:** true | | Yes |
| enable_explore_banner | boolean | | Yes |
| enable_learn_app | boolean, <br>**Default:** true | | Yes |
| enable_license_expiry_notice | boolean, <br>**Default:** true | | Yes |
| enable_marketplace | boolean | | Yes |
| enable_social_oauth_login | boolean | | Yes |
| enable_step_by_step_tour | boolean | | Yes |
+1
View File
@@ -1566,6 +1566,7 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
| enable_email_password_login | boolean, <br>**Default:** true | | Yes |
| enable_explore_banner | boolean | | Yes |
| enable_learn_app | boolean, <br>**Default:** true | | Yes |
| enable_license_expiry_notice | boolean, <br>**Default:** true | | Yes |
| enable_marketplace | boolean | | Yes |
| enable_social_oauth_login | boolean | | Yes |
| enable_step_by_step_tour | boolean | | Yes |
@@ -197,3 +197,4 @@ class SystemFeatureModel(FeatureResponseModel):
enable_step_by_step_tour: bool = False
rbac_enabled: bool = False
knowledge_fs_enabled: bool = False
enable_license_expiry_notice: bool = True
+1
View File
@@ -173,6 +173,7 @@ class FeatureService:
system_features.webapp_auth.allow_public_access = dify_config.WEBAPP_PUBLIC_ACCESS_ENABLED
system_features.enable_step_by_step_tour = dify_config.ENABLE_STEP_BY_STEP_TOUR
system_features.knowledge_fs_enabled = dify_config.KNOWLEDGE_FS_ENABLED
system_features.enable_license_expiry_notice = dify_config.ENABLE_LICENSE_EXPIRY_NOTICE
@classmethod
def _fulfill_trial_models_from_env(cls) -> list[str]:
@@ -0,0 +1,21 @@
import pytest
from enums.deployment_edition import DeploymentEdition
from services import feature_service as feature_service_module
from services.entities.feature_entities import SystemFeatureModel
from services.feature_service import FeatureService
def test_system_feature_model_defaults_enable_license_expiry_notice() -> None:
system_features = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY)
assert system_features.enable_license_expiry_notice is True
@pytest.mark.parametrize("enabled", [True, False])
def test_get_system_features_reads_enable_license_expiry_notice(monkeypatch: pytest.MonkeyPatch, enabled: bool) -> None:
monkeypatch.setattr(feature_service_module.dify_config, "ENABLE_LICENSE_EXPIRY_NOTICE", enabled)
result = FeatureService.get_system_features()
assert result.enable_license_expiry_notice is enabled
@@ -31,6 +31,7 @@ ENABLE_EXPLORE_BANNER=false
ENABLE_LEARN_APP=true
ENABLE_STEP_BY_STEP_TOUR=false
RBAC_ENABLED=false
ENABLE_LICENSE_EXPIRY_NOTICE=true
CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1
CELERY_TASK_ANNOTATIONS=null
AZURE_BLOB_ACCOUNT_URL=https://<your_account_name>.blob.core.windows.net
@@ -15,6 +15,7 @@ export type SystemFeatureModel = {
enable_email_password_login: boolean
enable_explore_banner: boolean
enable_learn_app: boolean
enable_license_expiry_notice: boolean
enable_marketplace: boolean
enable_social_oauth_login: boolean
enable_step_by_step_tour: boolean
@@ -127,6 +127,7 @@ export const zSystemFeatureModel = z.object({
enable_email_password_login: z.boolean().default(true),
enable_explore_banner: z.boolean().default(false),
enable_learn_app: z.boolean().default(true),
enable_license_expiry_notice: z.boolean().default(true),
enable_marketplace: z.boolean().default(false),
enable_social_oauth_login: z.boolean().default(false),
enable_step_by_step_tour: z.boolean().default(false),
@@ -520,6 +520,7 @@ export type SystemFeatureModel = {
enable_email_password_login: boolean
enable_explore_banner: boolean
enable_learn_app: boolean
enable_license_expiry_notice: boolean
enable_marketplace: boolean
enable_social_oauth_login: boolean
enable_step_by_step_tour: boolean
@@ -788,6 +788,7 @@ export const zSystemFeatureModel = z.object({
enable_email_password_login: z.boolean().default(true),
enable_explore_banner: z.boolean().default(false),
enable_learn_app: z.boolean().default(true),
enable_license_expiry_notice: z.boolean().default(true),
enable_marketplace: z.boolean().default(false),
enable_social_oauth_login: z.boolean().default(false),
enable_step_by_step_tour: z.boolean().default(false),
@@ -1,3 +1,4 @@
import type { ConsoleQueryTestOptions } from '@/test/console/query-data'
import { zLicenseStatus } from '@dify/contracts/api/console/system-features/zod.gen'
import { screen } from '@testing-library/react'
import dayjs from 'dayjs'
@@ -7,9 +8,12 @@ import {
renderWithConsoleQuery,
seedSystemFeaturesLicense,
} from '@/test/console/query-data'
import LicenseNav from '../index'
import LicenseBadge from '../index'
const renderLicenseNav = (license?: Parameters<typeof seedSystemFeaturesLicense>[1]) => {
const renderLicenseBadge = (
license?: Parameters<typeof seedSystemFeaturesLicense>[1],
systemFeatures?: ConsoleQueryTestOptions['systemFeatures'],
) => {
const queryClient = createConsoleQueryClient()
if (license) seedSystemFeaturesLicense(queryClient, license)
else {
@@ -18,10 +22,10 @@ const renderLicenseNav = (license?: Parameters<typeof seedSystemFeaturesLicense>
queryFn: () => new Promise(() => {}),
})
}
return renderWithConsoleQuery(<LicenseNav />, { queryClient })
return renderWithConsoleQuery(<LicenseBadge />, { queryClient, systemFeatures })
}
describe('LicenseNav', () => {
describe('LicenseBadge', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
@@ -34,38 +38,56 @@ describe('LicenseNav', () => {
})
it('should render nothing while license detail is loading', () => {
const { container } = renderLicenseNav()
const { container } = renderLicenseBadge()
expect(container).toBeEmptyDOMElement()
})
it('should render nothing when license status is NONE', () => {
const { container } = renderLicenseNav({})
const { container } = renderLicenseBadge({})
expect(container).toBeEmptyDOMElement()
})
it('should render Enterprise badge when license status is ACTIVE', () => {
renderLicenseNav({ status: zLicenseStatus.enum.active })
renderLicenseBadge({ status: zLicenseStatus.enum.active })
expect(screen.getByText('Enterprise')).toBeInTheDocument()
})
it('should render singular expiring message when license expires in 0 days', () => {
const expiredAt = dayjs().add(2, 'hours').toISOString()
renderLicenseNav({ status: zLicenseStatus.enum.expiring, expired_at: expiredAt })
renderLicenseBadge({ status: zLicenseStatus.enum.expiring, expired_at: expiredAt })
expect(screen.getByText(/license\.expiring/)).toBeInTheDocument()
expect(screen.getByText(/count":0/)).toBeInTheDocument()
})
it('should render singular expiring message when license expires in 1 day', () => {
const tomorrow = dayjs().add(1, 'day').add(1, 'hour').toISOString()
renderLicenseNav({ status: zLicenseStatus.enum.expiring, expired_at: tomorrow })
renderLicenseBadge({ status: zLicenseStatus.enum.expiring, expired_at: tomorrow })
expect(screen.getByText(/license\.expiring/)).toBeInTheDocument()
expect(screen.getByText(/count":1/)).toBeInTheDocument()
})
it('should render plural expiring message when license expires in 5 days', () => {
const fiveDaysLater = dayjs().add(5, 'day').add(1, 'hour').toISOString()
renderLicenseNav({ status: zLicenseStatus.enum.expiring, expired_at: fiveDaysLater })
renderLicenseBadge({ status: zLicenseStatus.enum.expiring, expired_at: fiveDaysLater })
expect(screen.getByText(/license\.expiring_plural/)).toBeInTheDocument()
expect(screen.getByText(/count":5/)).toBeInTheDocument()
})
it('should fall back to the Enterprise badge when the expiry notice is disabled', () => {
const fiveDaysLater = dayjs().add(5, 'day').add(1, 'hour').toISOString()
renderLicenseBadge(
{ status: zLicenseStatus.enum.expiring, expired_at: fiveDaysLater },
{ enable_license_expiry_notice: false },
)
expect(screen.queryByText(/license\.expiring/)).not.toBeInTheDocument()
expect(screen.getByText('Enterprise')).toBeInTheDocument()
})
it('should keep rendering the Enterprise badge for an active license when the expiry notice is disabled', () => {
renderLicenseBadge(
{ status: zLicenseStatus.enum.active },
{ enable_license_expiry_notice: false },
)
expect(screen.getByText('Enterprise')).toBeInTheDocument()
})
})
@@ -1,17 +1,23 @@
'use client'
import { zLicenseStatus } from '@dify/contracts/api/console/system-features/zod.gen'
import { useQuery } from '@tanstack/react-query'
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { consoleQuery } from '@/service/client'
import PremiumBadge from '../../base/premium-badge'
function LicenseNav() {
function LicenseBadge() {
const { t } = useTranslation()
const { data: license } = useQuery(consoleQuery.systemFeatures.license.get.queryOptions())
const { data: expiryNoticeEnabled } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
select: ({ enable_license_expiry_notice }) => enable_license_expiry_notice,
})
const isExpiring = license?.status === zLicenseStatus.enum.expiring
if (license?.status === zLicenseStatus.enum.expiring) {
if (isExpiring && expiryNoticeEnabled) {
const count = dayjs(license.expired_at).diff(dayjs(), 'days')
return (
<PremiumBadge color="orange" className="select-none">
@@ -34,7 +40,7 @@ function LicenseNav() {
</PremiumBadge>
)
}
if (license?.status === zLicenseStatus.enum.active) {
if (license?.status === zLicenseStatus.enum.active || isExpiring) {
return (
<PremiumBadge color="indigo" className="select-none">
<span className="px-1 system-xs-medium">Enterprise</span>
@@ -44,4 +50,4 @@ function LicenseNav() {
return null
}
export default LicenseNav
export default LicenseBadge
@@ -16,7 +16,7 @@ import {
settingsQueryParamName,
settingsQueryParser,
} from '@/app/components/header/account-setting/query-params'
import LicenseNav from '@/app/components/header/license-env'
import LicenseBadge from '@/app/components/header/license-badge'
import { buildIntegrationPath } from '@/app/components/integrations/routes'
import { useModalContext } from '@/context/modal-context'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
@@ -287,7 +287,7 @@ export function WorkspaceCard() {
const renderWorkspaceStatus = () => {
if (deploymentEdition === 'CLOUD')
return workspacePlan ? <WorkspacePlanBadge plan={workspacePlan} /> : null
if (deploymentEdition === 'ENTERPRISE') return <LicenseNav />
if (deploymentEdition === 'ENTERPRISE') return <LicenseBadge />
return null
}
+1
View File
@@ -57,6 +57,7 @@ const baseSystemFeatures = {
enable_learn_app: true,
enable_step_by_step_tour: false,
knowledge_fs_enabled: false,
enable_license_expiry_notice: true,
} satisfies GetSystemFeaturesResponse
const baseSystemFeaturesLicense = {