mirror of
https://github.com/langgenius/dify.git
synced 2026-09-01 15:09:21 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
+32
-10
@@ -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()
|
||||
})
|
||||
})
|
||||
+11
-5
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user