mirror of
https://github.com/langgenius/dify.git
synced 2026-08-31 01:36:38 +08:00
refactor(api): move license expiry notice flag onto license detail (#40128)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -18937,6 +18937,7 @@ Enum class for large language model mode.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| expired_at | string | | Yes |
|
||||
| license_expiry_notice_enabled | boolean | | Yes |
|
||||
| seats | [LicenseLimitationModel](#licenselimitationmodel) | | Yes |
|
||||
| status | [LicenseStatus](#licensestatus) | | Yes |
|
||||
| workspaces | [LicenseLimitationModel](#licenselimitationmodel) | | Yes |
|
||||
@@ -22287,7 +22288,6 @@ 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,7 +1566,6 @@ 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 |
|
||||
|
||||
@@ -89,6 +89,7 @@ class LicenseModel(LicenseStatusModel):
|
||||
expired_at: str = ""
|
||||
workspaces: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0)
|
||||
seats: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0)
|
||||
license_expiry_notice_enabled: bool = False
|
||||
|
||||
|
||||
class BrandingModel(FeatureResponseModel):
|
||||
@@ -197,4 +198,3 @@ 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
|
||||
|
||||
@@ -151,9 +151,12 @@ class FeatureService:
|
||||
Non-enterprise deployments have no license, so an unconstrained default
|
||||
(unlimited seats/workspaces) is returned.
|
||||
"""
|
||||
if not dify_config.ENTERPRISE_ENABLED:
|
||||
return feature_entities.LicenseModel()
|
||||
return cls._build_license(EnterpriseService.get_info())
|
||||
if dify_config.ENTERPRISE_ENABLED:
|
||||
license_model = cls._build_license(EnterpriseService.get_info())
|
||||
license_model.license_expiry_notice_enabled = dify_config.ENABLE_LICENSE_EXPIRY_NOTICE
|
||||
else:
|
||||
license_model = feature_entities.LicenseModel()
|
||||
return license_model
|
||||
|
||||
@staticmethod
|
||||
def is_explore_banner_enabled() -> bool:
|
||||
@@ -173,7 +176,6 @@ 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]:
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
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.entities.feature_entities import LicenseModel, LicenseStatus
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
_ENTERPRISE_INFO = {"License": {"status": LicenseStatus.EXPIRING, "expiredAt": "2026-12-31"}}
|
||||
|
||||
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
|
||||
def test_license_model_defaults_license_expiry_notice_disabled() -> None:
|
||||
"""Without a license there is no expiry to announce, so the notice is off unless enabled explicitly."""
|
||||
assert LicenseModel().license_expiry_notice_enabled is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [True, False])
|
||||
def test_get_system_features_reads_enable_license_expiry_notice(monkeypatch: pytest.MonkeyPatch, enabled: bool) -> None:
|
||||
def test_get_license_non_enterprise_ignores_expiry_notice_config(
|
||||
monkeypatch: pytest.MonkeyPatch, enabled: bool
|
||||
) -> None:
|
||||
"""Non-enterprise deployments have no license, so the env toggle never turns the notice on."""
|
||||
monkeypatch.setattr(feature_service_module.dify_config, "ENABLE_LICENSE_EXPIRY_NOTICE", enabled)
|
||||
monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", False)
|
||||
|
||||
result = FeatureService.get_system_features()
|
||||
result = FeatureService.get_license()
|
||||
|
||||
assert result.enable_license_expiry_notice is enabled
|
||||
assert result.license_expiry_notice_enabled is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [True, False])
|
||||
def test_get_license_enterprise_reads_license_expiry_notice_enabled(
|
||||
monkeypatch: pytest.MonkeyPatch, enabled: bool
|
||||
) -> None:
|
||||
"""The enterprise-sourced license carries the env-resolved notice flag alongside its real status."""
|
||||
monkeypatch.setattr(feature_service_module.dify_config, "ENABLE_LICENSE_EXPIRY_NOTICE", enabled)
|
||||
monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
feature_service_module.EnterpriseService,
|
||||
"get_info",
|
||||
staticmethod(lambda: _ENTERPRISE_INFO),
|
||||
)
|
||||
|
||||
result = FeatureService.get_license()
|
||||
|
||||
assert result.status == LicenseStatus.EXPIRING
|
||||
assert result.expired_at == "2026-12-31"
|
||||
assert result.license_expiry_notice_enabled is enabled
|
||||
|
||||
@@ -15,7 +15,6 @@ 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
|
||||
@@ -32,6 +31,7 @@ export type SystemFeatureModel = {
|
||||
|
||||
export type LicenseModel = {
|
||||
expired_at: string
|
||||
license_expiry_notice_enabled: boolean
|
||||
seats: LicenseLimitationModel
|
||||
status: LicenseStatus
|
||||
workspaces: LicenseLimitationModel
|
||||
|
||||
@@ -48,6 +48,7 @@ export const zLicenseStatus = z.enum(['active', 'expired', 'expiring', 'inactive
|
||||
*/
|
||||
export const zLicenseModel = z.object({
|
||||
expired_at: z.string().default(''),
|
||||
license_expiry_notice_enabled: z.boolean().default(false),
|
||||
seats: zLicenseLimitationModel.default({
|
||||
enabled: false,
|
||||
limit: 0,
|
||||
@@ -127,7 +128,6 @@ 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,7 +520,6 @@ 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,7 +788,6 @@ 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,4 +1,3 @@
|
||||
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'
|
||||
@@ -10,10 +9,7 @@ import {
|
||||
} from '@/test/console/query-data'
|
||||
import LicenseBadge from '../index'
|
||||
|
||||
const renderLicenseBadge = (
|
||||
license?: Parameters<typeof seedSystemFeaturesLicense>[1],
|
||||
systemFeatures?: ConsoleQueryTestOptions['systemFeatures'],
|
||||
) => {
|
||||
const renderLicenseBadge = (license?: Parameters<typeof seedSystemFeaturesLicense>[1]) => {
|
||||
const queryClient = createConsoleQueryClient()
|
||||
if (license) seedSystemFeaturesLicense(queryClient, license)
|
||||
else {
|
||||
@@ -22,7 +18,7 @@ const renderLicenseBadge = (
|
||||
queryFn: () => new Promise(() => {}),
|
||||
})
|
||||
}
|
||||
return renderWithConsoleQuery(<LicenseBadge />, { queryClient, systemFeatures })
|
||||
return renderWithConsoleQuery(<LicenseBadge />, { queryClient })
|
||||
}
|
||||
|
||||
describe('LicenseBadge', () => {
|
||||
@@ -54,40 +50,53 @@ describe('LicenseBadge', () => {
|
||||
|
||||
it('should render singular expiring message when license expires in 0 days', () => {
|
||||
const expiredAt = dayjs().add(2, 'hours').toISOString()
|
||||
renderLicenseBadge({ status: zLicenseStatus.enum.expiring, expired_at: expiredAt })
|
||||
renderLicenseBadge({
|
||||
status: zLicenseStatus.enum.expiring,
|
||||
expired_at: expiredAt,
|
||||
license_expiry_notice_enabled: true,
|
||||
})
|
||||
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()
|
||||
renderLicenseBadge({ status: zLicenseStatus.enum.expiring, expired_at: tomorrow })
|
||||
renderLicenseBadge({
|
||||
status: zLicenseStatus.enum.expiring,
|
||||
expired_at: tomorrow,
|
||||
license_expiry_notice_enabled: true,
|
||||
})
|
||||
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()
|
||||
renderLicenseBadge({ status: zLicenseStatus.enum.expiring, expired_at: fiveDaysLater })
|
||||
renderLicenseBadge({
|
||||
status: zLicenseStatus.enum.expiring,
|
||||
expired_at: fiveDaysLater,
|
||||
license_expiry_notice_enabled: true,
|
||||
})
|
||||
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 },
|
||||
)
|
||||
renderLicenseBadge({
|
||||
status: zLicenseStatus.enum.expiring,
|
||||
expired_at: fiveDaysLater,
|
||||
license_expiry_notice_enabled: 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 },
|
||||
)
|
||||
renderLicenseBadge({
|
||||
status: zLicenseStatus.enum.active,
|
||||
license_expiry_notice_enabled: false,
|
||||
})
|
||||
expect(screen.getByText('Enterprise')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { zLicenseStatus } from '@dify/contracts/api/console/system-features/zod.gen'
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useQuery } 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 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 (isExpiring && expiryNoticeEnabled) {
|
||||
if (isExpiring && license.license_expiry_notice_enabled) {
|
||||
const count = dayjs(license.expired_at).diff(dayjs(), 'days')
|
||||
return (
|
||||
<PremiumBadge color="orange" className="select-none">
|
||||
|
||||
@@ -57,11 +57,11 @@ 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 = {
|
||||
status: zLicenseStatus.enum.none,
|
||||
license_expiry_notice_enabled: false,
|
||||
expired_at: '',
|
||||
workspaces: {
|
||||
enabled: false,
|
||||
|
||||
Reference in New Issue
Block a user