Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29c7469206 | ||
|
|
19858061e6 | ||
|
|
0cc2d69177 | ||
|
|
f6bdd9c673 | ||
|
|
795febe737 | ||
|
|
fa4e4ac34a | ||
|
|
9476581992 | ||
|
|
06ad009c48 | ||
|
|
bb41da013b | ||
|
|
997993196b | ||
|
|
dda75e4192 | ||
|
|
410e827263 | ||
|
|
c457182f4d | ||
|
|
d2596222d7 | ||
|
|
df17f15bcb | ||
|
|
0a777fe7ff | ||
|
|
258a9fbfd4 | ||
|
|
6e21df1bb1 | ||
|
|
64b7cfce24 | ||
|
|
447a5b1076 | ||
|
|
eaa41ff318 |
@@ -1497,6 +1497,11 @@ class LoginConfig(BaseSettings):
|
|||||||
|
|
||||||
|
|
||||||
class AccountConfig(BaseSettings):
|
class AccountConfig(BaseSettings):
|
||||||
|
ENABLE_CHANGE_EMAIL: bool = Field(
|
||||||
|
description="whether users can change their email address",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
ACCOUNT_DELETION_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
|
ACCOUNT_DELETION_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
|
||||||
description="Duration in minutes for which a account deletion token remains valid",
|
description="Duration in minutes for which a account deletion token remains valid",
|
||||||
default=5,
|
default=5,
|
||||||
|
|||||||
@@ -17318,6 +17318,12 @@ Default model entity.
|
|||||||
| tool_name | string | | Yes |
|
| tool_name | string | | Yes |
|
||||||
| type | string | | Yes |
|
| type | string | | Yes |
|
||||||
|
|
||||||
|
#### DeploymentEdition
|
||||||
|
|
||||||
|
| Name | Type | Description | Required |
|
||||||
|
| ---- | ---- | ----------- | -------- |
|
||||||
|
| DeploymentEdition | string | | |
|
||||||
|
|
||||||
#### DismissNotificationPayload
|
#### DismissNotificationPayload
|
||||||
|
|
||||||
| Name | Type | Description | Required |
|
| Name | Type | Description | Required |
|
||||||
@@ -22026,6 +22032,7 @@ Model class for provider system configuration response.
|
|||||||
| Name | Type | Description | Required |
|
| Name | Type | Description | Required |
|
||||||
| ---- | ---- | ----------- | -------- |
|
| ---- | ---- | ----------- | -------- |
|
||||||
| branding | [BrandingModel](#brandingmodel) | | Yes |
|
| branding | [BrandingModel](#brandingmodel) | | Yes |
|
||||||
|
| deployment_edition | [DeploymentEdition](#deploymentedition) | | Yes |
|
||||||
| enable_app_deploy | boolean | | Yes |
|
| enable_app_deploy | boolean | | Yes |
|
||||||
| enable_change_email | boolean, <br>**Default:** true | | Yes |
|
| enable_change_email | boolean, <br>**Default:** true | | Yes |
|
||||||
| enable_collaboration_mode | boolean, <br>**Default:** true | | Yes |
|
| enable_collaboration_mode | boolean, <br>**Default:** true | | Yes |
|
||||||
|
|||||||
@@ -1058,6 +1058,12 @@ Button styles for user actions.
|
|||||||
| auto_generate | boolean | Automatically generate the conversation name. When `true`, the `name` field is ignored. | No |
|
| auto_generate | boolean | Automatically generate the conversation name. When `true`, the `name` field is ignored. | No |
|
||||||
| name | string | Conversation name. Required when `auto_generate` is `false`. | No |
|
| name | string | Conversation name. Required when `auto_generate` is `false`. | No |
|
||||||
|
|
||||||
|
#### DeploymentEdition
|
||||||
|
|
||||||
|
| Name | Type | Description | Required |
|
||||||
|
| ---- | ---- | ----------- | -------- |
|
||||||
|
| DeploymentEdition | string | | |
|
||||||
|
|
||||||
#### EmailCodeLoginSendPayload
|
#### EmailCodeLoginSendPayload
|
||||||
|
|
||||||
| Name | Type | Description | Required |
|
| Name | Type | Description | Required |
|
||||||
@@ -1567,6 +1573,7 @@ Default configuration for form inputs.
|
|||||||
| Name | Type | Description | Required |
|
| Name | Type | Description | Required |
|
||||||
| ---- | ---- | ----------- | -------- |
|
| ---- | ---- | ----------- | -------- |
|
||||||
| branding | [BrandingModel](#brandingmodel) | | Yes |
|
| branding | [BrandingModel](#brandingmodel) | | Yes |
|
||||||
|
| deployment_edition | [DeploymentEdition](#deploymentedition) | | Yes |
|
||||||
| enable_app_deploy | boolean | | Yes |
|
| enable_app_deploy | boolean | | Yes |
|
||||||
| enable_change_email | boolean, <br>**Default:** true | | Yes |
|
| enable_change_email | boolean, <br>**Default:** true | | Yes |
|
||||||
| enable_collaboration_mode | boolean, <br>**Default:** true | | Yes |
|
| enable_collaboration_mode | boolean, <br>**Default:** true | | Yes |
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ class LicenseStatus(StrEnum):
|
|||||||
LOST = "lost"
|
LOST = "lost"
|
||||||
|
|
||||||
|
|
||||||
|
class DeploymentEdition(StrEnum):
|
||||||
|
COMMUNITY = "COMMUNITY"
|
||||||
|
ENTERPRISE = "ENTERPRISE"
|
||||||
|
CLOUD = "CLOUD"
|
||||||
|
|
||||||
|
|
||||||
class LicenseModel(FeatureResponseModel):
|
class LicenseModel(FeatureResponseModel):
|
||||||
status: LicenseStatus = LicenseStatus.NONE
|
status: LicenseStatus = LicenseStatus.NONE
|
||||||
expired_at: str = ""
|
expired_at: str = ""
|
||||||
@@ -162,6 +168,7 @@ class PluginManagerModel(FeatureResponseModel):
|
|||||||
|
|
||||||
|
|
||||||
class SystemFeatureModel(FeatureResponseModel):
|
class SystemFeatureModel(FeatureResponseModel):
|
||||||
|
deployment_edition: DeploymentEdition
|
||||||
enable_app_deploy: bool = False
|
enable_app_deploy: bool = False
|
||||||
sso_enforced_for_signin: bool = False
|
sso_enforced_for_signin: bool = False
|
||||||
sso_enforced_for_signin_protocol: str = ""
|
sso_enforced_for_signin_protocol: str = ""
|
||||||
@@ -252,7 +259,7 @@ class FeatureService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_system_features(cls, is_authenticated: bool = False) -> SystemFeatureModel:
|
def get_system_features(cls, is_authenticated: bool = False) -> SystemFeatureModel:
|
||||||
system_features = SystemFeatureModel()
|
system_features = SystemFeatureModel(deployment_edition=cls._resolve_deployment_edition())
|
||||||
system_features.rbac_enabled = dify_config.RBAC_ENABLED
|
system_features.rbac_enabled = dify_config.RBAC_ENABLED
|
||||||
|
|
||||||
cls._fulfill_system_params_from_env(system_features)
|
cls._fulfill_system_params_from_env(system_features)
|
||||||
@@ -272,6 +279,14 @@ class FeatureService:
|
|||||||
|
|
||||||
return system_features
|
return system_features
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _resolve_deployment_edition(cls) -> DeploymentEdition:
|
||||||
|
if dify_config.EDITION == "CLOUD":
|
||||||
|
return DeploymentEdition.CLOUD
|
||||||
|
if dify_config.ENTERPRISE_ENABLED:
|
||||||
|
return DeploymentEdition.ENTERPRISE
|
||||||
|
return DeploymentEdition.COMMUNITY
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_app_dsl_version(cls) -> str:
|
def get_app_dsl_version(cls) -> str:
|
||||||
return CURRENT_APP_DSL_VERSION
|
return CURRENT_APP_DSL_VERSION
|
||||||
@@ -285,6 +300,7 @@ class FeatureService:
|
|||||||
system_features.is_allow_register = dify_config.ALLOW_REGISTER
|
system_features.is_allow_register = dify_config.ALLOW_REGISTER
|
||||||
system_features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE
|
system_features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE
|
||||||
system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != ""
|
system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != ""
|
||||||
|
system_features.enable_change_email = dify_config.ENABLE_CHANGE_EMAIL
|
||||||
system_features.enable_trial_app = dify_config.ENABLE_TRIAL_APP
|
system_features.enable_trial_app = dify_config.ENABLE_TRIAL_APP
|
||||||
system_features.enable_explore_banner = dify_config.ENABLE_EXPLORE_BANNER
|
system_features.enable_explore_banner = dify_config.ENABLE_EXPLORE_BANNER
|
||||||
system_features.enable_learn_app = dify_config.ENABLE_LEARN_APP
|
system_features.enable_learn_app = dify_config.ENABLE_LEARN_APP
|
||||||
|
|||||||
@@ -414,6 +414,7 @@ class TestFeatureService:
|
|||||||
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
|
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
|
||||||
mock_config.ENABLE_SOCIAL_OAUTH_LOGIN = False
|
mock_config.ENABLE_SOCIAL_OAUTH_LOGIN = False
|
||||||
mock_config.ENABLE_COLLABORATION_MODE = False
|
mock_config.ENABLE_COLLABORATION_MODE = False
|
||||||
|
mock_config.ENABLE_CHANGE_EMAIL = True
|
||||||
mock_config.ALLOW_REGISTER = True
|
mock_config.ALLOW_REGISTER = True
|
||||||
mock_config.ALLOW_CREATE_WORKSPACE = True
|
mock_config.ALLOW_CREATE_WORKSPACE = True
|
||||||
mock_config.MAIL_TYPE = "smtp"
|
mock_config.MAIL_TYPE = "smtp"
|
||||||
@@ -616,6 +617,7 @@ class TestFeatureService:
|
|||||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||||
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
|
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
|
||||||
mock_config.ENABLE_SOCIAL_OAUTH_LOGIN = True
|
mock_config.ENABLE_SOCIAL_OAUTH_LOGIN = True
|
||||||
|
mock_config.ENABLE_CHANGE_EMAIL = True
|
||||||
mock_config.ALLOW_REGISTER = False
|
mock_config.ALLOW_REGISTER = False
|
||||||
mock_config.ALLOW_CREATE_WORKSPACE = False
|
mock_config.ALLOW_CREATE_WORKSPACE = False
|
||||||
mock_config.MAIL_TYPE = None
|
mock_config.MAIL_TYPE = None
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from models.engine import db
|
|||||||
from models.model import App, AppMode
|
from models.model import App, AppMode
|
||||||
from services.app_dsl_service import ImportStatus
|
from services.app_dsl_service import ImportStatus
|
||||||
from services.entities.dsl_entities import CheckDependenciesResult
|
from services.entities.dsl_entities import CheckDependenciesResult
|
||||||
from services.feature_service import SystemFeatureModel, WebAppAuthModel
|
from services.feature_service import DeploymentEdition, SystemFeatureModel, WebAppAuthModel
|
||||||
|
|
||||||
|
|
||||||
def _unwrap(func):
|
def _unwrap(func):
|
||||||
@@ -47,7 +47,10 @@ class _Result:
|
|||||||
|
|
||||||
|
|
||||||
def _install_features(monkeypatch: pytest.MonkeyPatch, enabled: bool) -> None:
|
def _install_features(monkeypatch: pytest.MonkeyPatch, enabled: bool) -> None:
|
||||||
features = SystemFeatureModel(webapp_auth=WebAppAuthModel(enabled=enabled))
|
features = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
webapp_auth=WebAppAuthModel(enabled=enabled),
|
||||||
|
)
|
||||||
monkeypatch.setattr(app_import_module.FeatureService, "get_system_features", lambda: features)
|
monkeypatch.setattr(app_import_module.FeatureService, "get_system_features", lambda: features)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from controllers.console.auth.email_register import (
|
|||||||
EmailRegisterResetApi,
|
EmailRegisterResetApi,
|
||||||
EmailRegisterSendEmailApi,
|
EmailRegisterSendEmailApi,
|
||||||
)
|
)
|
||||||
from services.feature_service import SystemFeatureModel
|
from services.feature_service import DeploymentEdition, SystemFeatureModel
|
||||||
|
|
||||||
|
|
||||||
class TestEmailRegisterSendEmailApi:
|
class TestEmailRegisterSendEmailApi:
|
||||||
@@ -34,7 +34,11 @@ class TestEmailRegisterSendEmailApi:
|
|||||||
mock_account = MagicMock()
|
mock_account = MagicMock()
|
||||||
mock_get_account.return_value = mock_account
|
mock_get_account.return_value = mock_account
|
||||||
|
|
||||||
feature_flags = SystemFeatureModel(enable_email_password_login=True, is_allow_register=True)
|
feature_flags = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_email_password_login=True,
|
||||||
|
is_allow_register=True,
|
||||||
|
)
|
||||||
with (
|
with (
|
||||||
patch("controllers.console.auth.email_register.dify_config.BILLING_ENABLED", True),
|
patch("controllers.console.auth.email_register.dify_config.BILLING_ENABLED", True),
|
||||||
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
||||||
@@ -75,7 +79,11 @@ class TestEmailRegisterCheckApi:
|
|||||||
mock_get_data.return_value = {"email": "User@Example.com", "code": "4321"}
|
mock_get_data.return_value = {"email": "User@Example.com", "code": "4321"}
|
||||||
mock_generate_token.return_value = (None, "new-token")
|
mock_generate_token.return_value = (None, "new-token")
|
||||||
|
|
||||||
feature_flags = SystemFeatureModel(enable_email_password_login=True, is_allow_register=True)
|
feature_flags = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_email_password_login=True,
|
||||||
|
is_allow_register=True,
|
||||||
|
)
|
||||||
with (
|
with (
|
||||||
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
||||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
||||||
@@ -123,7 +131,11 @@ class TestEmailRegisterResetApi:
|
|||||||
mock_login.return_value = token_pair
|
mock_login.return_value = token_pair
|
||||||
mock_get_account.return_value = None
|
mock_get_account.return_value = None
|
||||||
|
|
||||||
feature_flags = SystemFeatureModel(enable_email_password_login=True, is_allow_register=True)
|
feature_flags = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_email_password_login=True,
|
||||||
|
is_allow_register=True,
|
||||||
|
)
|
||||||
with (
|
with (
|
||||||
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
||||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
||||||
@@ -171,7 +183,11 @@ class TestEmailRegisterResetApi:
|
|||||||
mock_login.return_value = token_pair
|
mock_login.return_value = token_pair
|
||||||
mock_get_account.return_value = None
|
mock_get_account.return_value = None
|
||||||
|
|
||||||
feature_flags = SystemFeatureModel(enable_email_password_login=True, is_allow_register=True)
|
feature_flags = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_email_password_login=True,
|
||||||
|
is_allow_register=True,
|
||||||
|
)
|
||||||
with (
|
with (
|
||||||
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
||||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
||||||
@@ -224,7 +240,11 @@ class TestEmailRegisterResetApi:
|
|||||||
mock_login.return_value = token_pair
|
mock_login.return_value = token_pair
|
||||||
mock_get_account.return_value = None
|
mock_get_account.return_value = None
|
||||||
|
|
||||||
feature_flags = SystemFeatureModel(enable_email_password_login=True, is_allow_register=True)
|
feature_flags = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_email_password_login=True,
|
||||||
|
is_allow_register=True,
|
||||||
|
)
|
||||||
with (
|
with (
|
||||||
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
||||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from controllers.console.auth.forgot_password import (
|
|||||||
)
|
)
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from models.engine import db
|
from models.engine import db
|
||||||
from services.feature_service import SystemFeatureModel
|
from services.feature_service import DeploymentEdition, SystemFeatureModel
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -46,8 +46,15 @@ class TestForgotPasswordSendEmailApi:
|
|||||||
mock_get_account.return_value = mock_account
|
mock_get_account.return_value = mock_account
|
||||||
mock_send_email.return_value = "token-123"
|
mock_send_email.return_value = "token-123"
|
||||||
|
|
||||||
wraps_features = SystemFeatureModel(enable_email_password_login=True, is_allow_register=True)
|
wraps_features = SystemFeatureModel(
|
||||||
controller_features = SystemFeatureModel(is_allow_register=True)
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_email_password_login=True,
|
||||||
|
is_allow_register=True,
|
||||||
|
)
|
||||||
|
controller_features = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
is_allow_register=True,
|
||||||
|
)
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"controllers.console.auth.forgot_password.FeatureService.get_system_features",
|
"controllers.console.auth.forgot_password.FeatureService.get_system_features",
|
||||||
@@ -95,7 +102,10 @@ class TestForgotPasswordCheckApi:
|
|||||||
mock_get_data.return_value = {"email": "Admin@Example.com", "code": "4321"}
|
mock_get_data.return_value = {"email": "Admin@Example.com", "code": "4321"}
|
||||||
mock_generate_token.return_value = (None, "new-token")
|
mock_generate_token.return_value = (None, "new-token")
|
||||||
|
|
||||||
wraps_features = SystemFeatureModel(enable_email_password_login=True)
|
wraps_features = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_email_password_login=True,
|
||||||
|
)
|
||||||
with (
|
with (
|
||||||
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
||||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=wraps_features),
|
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=wraps_features),
|
||||||
@@ -138,7 +148,10 @@ class TestForgotPasswordResetApi:
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
mock_get_account.return_value = account
|
mock_get_account.return_value = account
|
||||||
|
|
||||||
wraps_features = SystemFeatureModel(enable_email_password_login=True)
|
wraps_features = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_email_password_login=True,
|
||||||
|
)
|
||||||
with (
|
with (
|
||||||
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"),
|
||||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=wraps_features),
|
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=wraps_features),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from controllers.console.auth.forgot_password import (
|
|||||||
)
|
)
|
||||||
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
|
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
|
||||||
from models.account import Account, Tenant, TenantAccountJoin
|
from models.account import Account, Tenant, TenantAccountJoin
|
||||||
from services.feature_service import SystemFeatureModel
|
from services.feature_service import DeploymentEdition, SystemFeatureModel
|
||||||
|
|
||||||
SQLITE_MODELS = (Account, Tenant, TenantAccountJoin)
|
SQLITE_MODELS = (Account, Tenant, TenantAccountJoin)
|
||||||
|
|
||||||
@@ -48,7 +48,10 @@ def enable_password_login_wrappers(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||||||
monkeypatch.setattr("controllers.console.wraps.dify_config.EDITION", "CLOUD")
|
monkeypatch.setattr("controllers.console.wraps.dify_config.EDITION", "CLOUD")
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"controllers.console.wraps.FeatureService.get_system_features",
|
"controllers.console.wraps.FeatureService.get_system_features",
|
||||||
lambda: SystemFeatureModel(enable_email_password_login=True),
|
lambda: SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_email_password_login=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from inspect import unwrap
|
|||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from models import Account
|
from models import Account
|
||||||
from services.feature_service import FeatureModel, LimitationModel, SystemFeatureModel
|
from services.feature_service import DeploymentEdition, FeatureModel, LimitationModel, SystemFeatureModel
|
||||||
|
|
||||||
|
|
||||||
def make_account() -> Account:
|
def make_account() -> Account:
|
||||||
@@ -94,7 +94,11 @@ class TestSystemFeatureApi:
|
|||||||
"controllers.console.feature.current_account_with_tenant_optional",
|
"controllers.console.feature.current_account_with_tenant_optional",
|
||||||
return_value=(account, "tenant-123"),
|
return_value=(account, "tenant-123"),
|
||||||
)
|
)
|
||||||
system_features = SystemFeatureModel(is_allow_register=True, enable_learn_app=True)
|
system_features = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
is_allow_register=True,
|
||||||
|
enable_learn_app=True,
|
||||||
|
)
|
||||||
get_system_features = mocker.patch(
|
get_system_features = mocker.patch(
|
||||||
"controllers.console.feature.FeatureService.get_system_features",
|
"controllers.console.feature.FeatureService.get_system_features",
|
||||||
return_value=system_features,
|
return_value=system_features,
|
||||||
@@ -119,7 +123,10 @@ class TestSystemFeatureApi:
|
|||||||
"controllers.console.feature.current_account_with_tenant_optional",
|
"controllers.console.feature.current_account_with_tenant_optional",
|
||||||
return_value=(None, None),
|
return_value=(None, None),
|
||||||
)
|
)
|
||||||
system_features = SystemFeatureModel(is_allow_register=False)
|
system_features = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
is_allow_register=False,
|
||||||
|
)
|
||||||
get_system_features = mocker.patch(
|
get_system_features = mocker.patch(
|
||||||
"controllers.console.feature.FeatureService.get_system_features",
|
"controllers.console.feature.FeatureService.get_system_features",
|
||||||
return_value=system_features,
|
return_value=system_features,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from controllers.web.forgot_password import (
|
|||||||
)
|
)
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from models.engine import db
|
from models.engine import db
|
||||||
from services.feature_service import SystemFeatureModel
|
from services.feature_service import DeploymentEdition, SystemFeatureModel
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -32,7 +32,10 @@ def database_app() -> Iterator[Flask]:
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _patch_wraps():
|
def _patch_wraps():
|
||||||
wraps_features = SystemFeatureModel(enable_email_password_login=True)
|
wraps_features = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_email_password_login=True,
|
||||||
|
)
|
||||||
with (
|
with (
|
||||||
patch("controllers.console.wraps.db") as mock_db,
|
patch("controllers.console.wraps.db") as mock_db,
|
||||||
patch("controllers.console.wraps.dify_config.ENTERPRISE_ENABLED", True),
|
patch("controllers.console.wraps.dify_config.ENTERPRISE_ENABLED", True),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from models.engine import db
|
|||||||
from models.provider import Provider, ProviderCredential, TenantPreferredModelProvider
|
from models.provider import Provider, ProviderCredential, TenantPreferredModelProvider
|
||||||
from services.errors.plugin import PluginInstallationForbiddenError
|
from services.errors.plugin import PluginInstallationForbiddenError
|
||||||
from services.feature_service import (
|
from services.feature_service import (
|
||||||
|
DeploymentEdition,
|
||||||
PluginInstallationPermissionModel,
|
PluginInstallationPermissionModel,
|
||||||
PluginInstallationScope,
|
PluginInstallationScope,
|
||||||
SystemFeatureModel,
|
SystemFeatureModel,
|
||||||
@@ -35,10 +36,11 @@ def _make_features(
|
|||||||
scope: PluginInstallationScope = PluginInstallationScope.ALL,
|
scope: PluginInstallationScope = PluginInstallationScope.ALL,
|
||||||
) -> SystemFeatureModel:
|
) -> SystemFeatureModel:
|
||||||
return SystemFeatureModel(
|
return SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
plugin_installation_permission=PluginInstallationPermissionModel(
|
plugin_installation_permission=PluginInstallationPermissionModel(
|
||||||
restrict_to_marketplace_only=restrict_to_marketplace,
|
restrict_to_marketplace_only=restrict_to_marketplace,
|
||||||
plugin_installation_scope=scope,
|
plugin_installation_scope=scope,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("enabled", [False, True])
|
||||||
|
def test_get_system_features_reads_enable_change_email(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
enabled: bool,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr("services.feature_service.dify_config.ENABLE_CHANGE_EMAIL", enabled)
|
||||||
|
|
||||||
|
result = FeatureService.get_system_features()
|
||||||
|
|
||||||
|
assert result.enable_change_email is enabled
|
||||||
|
|
||||||
|
|
||||||
|
def test_enterprise_disables_change_email(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr("services.feature_service.dify_config.ENABLE_CHANGE_EMAIL", True)
|
||||||
|
monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True)
|
||||||
|
monkeypatch.setattr("services.feature_service.FeatureService._fulfill_params_from_enterprise", lambda *_: None)
|
||||||
|
|
||||||
|
result = FeatureService.get_system_features()
|
||||||
|
|
||||||
|
assert result.enable_change_email is False
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from services.feature_service import DeploymentEdition, FeatureService, SystemFeatureModel
|
||||||
|
|
||||||
|
|
||||||
|
def test_system_feature_model_requires_deployment_edition() -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
SystemFeatureModel.model_validate({})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("edition", "enterprise_enabled", "expected"),
|
||||||
|
[
|
||||||
|
("SELF_HOSTED", False, DeploymentEdition.COMMUNITY),
|
||||||
|
("SELF_HOSTED", True, DeploymentEdition.ENTERPRISE),
|
||||||
|
("CLOUD", False, DeploymentEdition.CLOUD),
|
||||||
|
("CLOUD", True, DeploymentEdition.CLOUD),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_system_features_resolves_deployment_edition(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
edition: str,
|
||||||
|
enterprise_enabled: bool,
|
||||||
|
expected: DeploymentEdition,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr("services.feature_service.dify_config.EDITION", edition)
|
||||||
|
monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", enterprise_enabled)
|
||||||
|
monkeypatch.setattr("services.feature_service.FeatureService._fulfill_params_from_enterprise", lambda *_: None)
|
||||||
|
|
||||||
|
result = FeatureService.get_system_features()
|
||||||
|
|
||||||
|
assert result.deployment_edition is expected
|
||||||
|
assert result.model_dump(mode="json")["deployment_edition"] == expected.value
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from services import feature_service as feature_service_module
|
from services import feature_service as feature_service_module
|
||||||
from services.feature_service import FeatureService, SystemFeatureModel
|
from services.feature_service import DeploymentEdition, FeatureService, SystemFeatureModel
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -29,7 +29,7 @@ def test_fulfill_params_from_enterprise_enable_app_deploy(
|
|||||||
staticmethod(lambda: enterprise_info),
|
staticmethod(lambda: enterprise_info),
|
||||||
)
|
)
|
||||||
|
|
||||||
features = SystemFeatureModel()
|
features = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY)
|
||||||
features.enable_app_deploy = initial
|
features.enable_app_deploy = initial
|
||||||
|
|
||||||
FeatureService._fulfill_params_from_enterprise(features)
|
FeatureService._fulfill_params_from_enterprise(features)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from services.feature_service import FeatureService, SystemFeatureModel
|
from services.feature_service import DeploymentEdition, FeatureService, SystemFeatureModel
|
||||||
|
|
||||||
|
|
||||||
def test_system_feature_model_disables_knowledge_fs_by_default() -> None:
|
def test_system_feature_model_disables_knowledge_fs_by_default() -> None:
|
||||||
assert SystemFeatureModel().knowledge_fs_enabled is False
|
assert SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY).knowledge_fs_enabled is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("enabled", [False, True])
|
@pytest.mark.parametrize("enabled", [False, True])
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from services import feature_service as feature_service_module
|
from services import feature_service as feature_service_module
|
||||||
from services.feature_service import FeatureService, SystemFeatureModel
|
from services.feature_service import DeploymentEdition, FeatureService, SystemFeatureModel
|
||||||
|
|
||||||
|
|
||||||
def test_system_feature_model_defaults_enable_learn_app():
|
def test_system_feature_model_defaults_enable_learn_app():
|
||||||
assert SystemFeatureModel().enable_learn_app is True
|
system_features = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY)
|
||||||
assert SystemFeatureModel().enable_step_by_step_tour is False
|
|
||||||
|
assert system_features.enable_learn_app is True
|
||||||
|
assert system_features.enable_step_by_step_tour is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("enabled", [True, False])
|
@pytest.mark.parametrize("enabled", [True, False])
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from services import feature_service as feature_service_module
|
from services import feature_service as feature_service_module
|
||||||
from services.feature_service import FeatureService, SystemFeatureModel
|
from services.feature_service import DeploymentEdition, FeatureService, SystemFeatureModel
|
||||||
|
|
||||||
_ENTERPRISE_INFO = {"License": {"licensedSeats": {"enabled": True, "limit": 3, "used": 1}}}
|
_ENTERPRISE_INFO = {"License": {"licensedSeats": {"enabled": True, "limit": 3, "used": 1}}}
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ def test_fulfill_params_from_enterprise_parses_licensed_seats(monkeypatch: pytes
|
|||||||
staticmethod(lambda: _ENTERPRISE_INFO),
|
staticmethod(lambda: _ENTERPRISE_INFO),
|
||||||
)
|
)
|
||||||
|
|
||||||
features = SystemFeatureModel()
|
features = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY)
|
||||||
FeatureService._fulfill_params_from_enterprise(features, is_authenticated=True)
|
FeatureService._fulfill_params_from_enterprise(features, is_authenticated=True)
|
||||||
|
|
||||||
assert features.license.seats.enabled is True
|
assert features.license.seats.enabled is True
|
||||||
@@ -30,7 +30,7 @@ def test_fulfill_params_from_enterprise_withholds_seats_when_unauthenticated(mon
|
|||||||
staticmethod(lambda: _ENTERPRISE_INFO),
|
staticmethod(lambda: _ENTERPRISE_INFO),
|
||||||
)
|
)
|
||||||
|
|
||||||
features = SystemFeatureModel()
|
features = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY)
|
||||||
FeatureService._fulfill_params_from_enterprise(features, is_authenticated=False)
|
FeatureService._fulfill_params_from_enterprise(features, is_authenticated=False)
|
||||||
|
|
||||||
assert features.license.seats.enabled is False
|
assert features.license.seats.enabled is False
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from services.feature_service import FeatureService, SystemFeatureModel
|
from services.feature_service import DeploymentEdition, FeatureService, SystemFeatureModel
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -18,7 +18,7 @@ def test_fulfill_system_params_from_env_sets_allow_public_access(
|
|||||||
):
|
):
|
||||||
monkeypatch.setattr("services.feature_service.dify_config.WEBAPP_PUBLIC_ACCESS_ENABLED", env_value)
|
monkeypatch.setattr("services.feature_service.dify_config.WEBAPP_PUBLIC_ACCESS_ENABLED", env_value)
|
||||||
|
|
||||||
system_features = SystemFeatureModel()
|
system_features = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY)
|
||||||
FeatureService._fulfill_system_params_from_env(system_features)
|
FeatureService._fulfill_system_params_from_env(system_features)
|
||||||
|
|
||||||
assert system_features.webapp_auth.allow_public_access is expected
|
assert system_features.webapp_auth.allow_public_access is expected
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from models.model import AccountTrialAppRecord, App, AppMode, TrialApp
|
from models.model import AccountTrialAppRecord, App, AppMode, TrialApp
|
||||||
from services import recommended_app_service as service_module
|
from services import recommended_app_service as service_module
|
||||||
from services.feature_service import SystemFeatureModel
|
from services.feature_service import DeploymentEdition, SystemFeatureModel
|
||||||
from services.recommended_app_service import RecommendedAppService
|
from services.recommended_app_service import RecommendedAppService
|
||||||
|
|
||||||
pytestmark = pytest.mark.parametrize(
|
pytestmark = pytest.mark.parametrize(
|
||||||
@@ -298,7 +298,10 @@ class TestRecommendedAppServiceGetDetail:
|
|||||||
sqlite_session: Session,
|
sqlite_session: Session,
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = "remote"
|
mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = "remote"
|
||||||
mock_feature_service.get_system_features.return_value = SystemFeatureModel(enable_trial_app=False)
|
mock_feature_service.get_system_features.return_value = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_trial_app=False,
|
||||||
|
)
|
||||||
cases: list[tuple[str, RecommendedAppPayload]] = [
|
cases: list[tuple[str, RecommendedAppPayload]] = [
|
||||||
(
|
(
|
||||||
"complex-app",
|
"complex-app",
|
||||||
@@ -337,7 +340,10 @@ class TestRecommendedAppServiceGetDetail:
|
|||||||
mock_feature_service: MagicMock,
|
mock_feature_service: MagicMock,
|
||||||
sqlite_session: Session,
|
sqlite_session: Session,
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_feature_service.get_system_features.return_value = SystemFeatureModel(enable_trial_app=False)
|
mock_feature_service.get_system_features.return_value = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_trial_app=False,
|
||||||
|
)
|
||||||
for mode in ["remote", "builtin", "db"]:
|
for mode in ["remote", "builtin", "db"]:
|
||||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = mode
|
mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = mode
|
||||||
detail = _app_detail(app_id="test-app", name=f"App from {mode}")
|
detail = _app_detail(app_id="test-app", name=f"App from {mode}")
|
||||||
@@ -367,7 +373,10 @@ class TestRecommendedAppServiceGetLearnDifyApps:
|
|||||||
sqlite_session: Session,
|
sqlite_session: Session,
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = "remote"
|
mock_config.HOSTED_FETCH_APP_TEMPLATES_MODE = "remote"
|
||||||
mock_feature_service.get_system_features.return_value = SystemFeatureModel(enable_trial_app=False)
|
mock_feature_service.get_system_features.return_value = SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_trial_app=False,
|
||||||
|
)
|
||||||
expected_app = RecommendedAppPayload(app_id="app-1", category="Workflow")
|
expected_app = RecommendedAppPayload(app_id="app-1", category="Workflow")
|
||||||
mock_instance = MagicMock()
|
mock_instance = MagicMock()
|
||||||
mock_instance.get_learn_dify_apps.return_value = {
|
mock_instance.get_learn_dify_apps.return_value = {
|
||||||
@@ -402,7 +411,12 @@ class TestRecommendedAppServiceGetLearnDifyApps:
|
|||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
service_module.FeatureService,
|
service_module.FeatureService,
|
||||||
"get_system_features",
|
"get_system_features",
|
||||||
MagicMock(return_value=SystemFeatureModel(enable_trial_app=True)),
|
MagicMock(
|
||||||
|
return_value=SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_trial_app=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
can_trial_mock = MagicMock(return_value=True)
|
can_trial_mock = MagicMock(return_value=True)
|
||||||
monkeypatch.setattr(RecommendedAppService, "_can_trial_app", can_trial_mock)
|
monkeypatch.setattr(RecommendedAppService, "_can_trial_app", can_trial_mock)
|
||||||
@@ -425,7 +439,12 @@ class TestRecommendedAppServiceTrialFeatures:
|
|||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
service_module.FeatureService,
|
service_module.FeatureService,
|
||||||
"get_system_features",
|
"get_system_features",
|
||||||
MagicMock(return_value=SystemFeatureModel(enable_trial_app=False)),
|
MagicMock(
|
||||||
|
return_value=SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_trial_app=False,
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = RecommendedAppService.get_recommended_apps_and_categories("en-US", session=sqlite_session)
|
result = RecommendedAppService.get_recommended_apps_and_categories("en-US", session=sqlite_session)
|
||||||
@@ -456,7 +475,12 @@ class TestRecommendedAppServiceTrialFeatures:
|
|||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
service_module.FeatureService,
|
service_module.FeatureService,
|
||||||
"get_system_features",
|
"get_system_features",
|
||||||
MagicMock(return_value=SystemFeatureModel(enable_trial_app=True)),
|
MagicMock(
|
||||||
|
return_value=SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_trial_app=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = RecommendedAppService.get_recommended_apps_and_categories("ja-JP", session=sqlite_session)
|
result = RecommendedAppService.get_recommended_apps_and_categories("ja-JP", session=sqlite_session)
|
||||||
@@ -492,7 +516,12 @@ class TestRecommendedAppServiceTrialFeatures:
|
|||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
service_module.FeatureService,
|
service_module.FeatureService,
|
||||||
"get_system_features",
|
"get_system_features",
|
||||||
MagicMock(return_value=SystemFeatureModel(enable_trial_app=True)),
|
MagicMock(
|
||||||
|
return_value=SystemFeatureModel(
|
||||||
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
|
enable_trial_app=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = RecommendedAppService.get_recommend_app_detail(app_id, session=sqlite_session)
|
result = RecommendedAppService.get_recommend_app_detail(app_id, session=sqlite_session)
|
||||||
|
|||||||
@@ -16,11 +16,21 @@ CHECK_UPDATE_URL=https://updates.dify.ai
|
|||||||
OPENAI_API_BASE=https://api.openai.com/v1
|
OPENAI_API_BASE=https://api.openai.com/v1
|
||||||
MIGRATION_ENABLED=true
|
MIGRATION_ENABLED=true
|
||||||
FILES_ACCESS_TIMEOUT=300
|
FILES_ACCESS_TIMEOUT=300
|
||||||
|
# System Features
|
||||||
|
MARKETPLACE_ENABLED=true
|
||||||
|
ENABLE_EMAIL_CODE_LOGIN=false
|
||||||
|
ENABLE_EMAIL_PASSWORD_LOGIN=true
|
||||||
|
ENABLE_SOCIAL_OAUTH_LOGIN=false
|
||||||
# Remove `collaboration` from COMPOSE_PROFILES to stop the dedicated websocket service.
|
# Remove `collaboration` from COMPOSE_PROFILES to stop the dedicated websocket service.
|
||||||
ENABLE_COLLABORATION_MODE=true
|
ENABLE_COLLABORATION_MODE=true
|
||||||
|
ALLOW_REGISTER=false
|
||||||
# Learn app feature toggle
|
ALLOW_CREATE_WORKSPACE=false
|
||||||
|
ENABLE_CHANGE_EMAIL=true
|
||||||
|
ENABLE_TRIAL_APP=false
|
||||||
|
ENABLE_EXPLORE_BANNER=false
|
||||||
ENABLE_LEARN_APP=true
|
ENABLE_LEARN_APP=true
|
||||||
|
ENABLE_STEP_BY_STEP_TOUR=false
|
||||||
|
RBAC_ENABLED=false
|
||||||
CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1
|
CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1
|
||||||
CELERY_TASK_ANNOTATIONS=null
|
CELERY_TASK_ANNOTATIONS=null
|
||||||
AZURE_BLOB_ACCOUNT_URL=https://<your_account_name>.blob.core.windows.net
|
AZURE_BLOB_ACCOUNT_URL=https://<your_account_name>.blob.core.windows.net
|
||||||
@@ -90,6 +100,8 @@ WORKFLOW_LOG_CLEANUP_SPECIFIC_WORKFLOW_IDS=
|
|||||||
EXPOSE_PLUGIN_DEBUGGING_HOST=localhost
|
EXPOSE_PLUGIN_DEBUGGING_HOST=localhost
|
||||||
EXPOSE_PLUGIN_DEBUGGING_PORT=5003
|
EXPOSE_PLUGIN_DEBUGGING_PORT=5003
|
||||||
DEPLOY_ENV=PRODUCTION
|
DEPLOY_ENV=PRODUCTION
|
||||||
|
EDITION=SELF_HOSTED
|
||||||
|
ENTERPRISE_ENABLED=false
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS=30
|
REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||||
APP_DEFAULT_ACTIVE_REQUESTS=0
|
APP_DEFAULT_ACTIVE_REQUESTS=0
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ TEXT_GENERATION_TIMEOUT_MS=60000
|
|||||||
ALLOW_INLINE_STYLES=false
|
ALLOW_INLINE_STYLES=false
|
||||||
ALLOW_UNSAFE_DATA_SCHEME=false
|
ALLOW_UNSAFE_DATA_SCHEME=false
|
||||||
MAX_TREE_DEPTH=50
|
MAX_TREE_DEPTH=50
|
||||||
MARKETPLACE_ENABLED=true
|
|
||||||
MARKETPLACE_API_URL=https://marketplace.dify.ai
|
MARKETPLACE_API_URL=https://marketplace.dify.ai
|
||||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
||||||
ALLOW_EMBED=false
|
ALLOW_EMBED=false
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type ClientOptions = {
|
|||||||
|
|
||||||
export type SystemFeatureModel = {
|
export type SystemFeatureModel = {
|
||||||
branding: BrandingModel
|
branding: BrandingModel
|
||||||
|
deployment_edition: DeploymentEdition
|
||||||
enable_app_deploy: boolean
|
enable_app_deploy: boolean
|
||||||
enable_change_email: boolean
|
enable_change_email: boolean
|
||||||
enable_collaboration_mode: boolean
|
enable_collaboration_mode: boolean
|
||||||
@@ -40,6 +41,8 @@ export type BrandingModel = {
|
|||||||
workspace_logo: string
|
workspace_logo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeploymentEdition = 'CLOUD' | 'COMMUNITY' | 'ENTERPRISE'
|
||||||
|
|
||||||
export type LicenseModel = {
|
export type LicenseModel = {
|
||||||
expired_at: string
|
expired_at: string
|
||||||
seats: LicenseLimitationModel
|
seats: LicenseLimitationModel
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export const zBrandingModel = z.object({
|
|||||||
workspace_logo: z.string().default(''),
|
workspace_logo: z.string().default(''),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DeploymentEdition
|
||||||
|
*/
|
||||||
|
export const zDeploymentEdition = z.enum(['CLOUD', 'COMMUNITY', 'ENTERPRISE'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PluginManagerModel
|
* PluginManagerModel
|
||||||
*/
|
*/
|
||||||
@@ -104,6 +109,7 @@ export const zSystemFeatureModel = z.object({
|
|||||||
login_page_logo: '',
|
login_page_logo: '',
|
||||||
workspace_logo: '',
|
workspace_logo: '',
|
||||||
}),
|
}),
|
||||||
|
deployment_edition: zDeploymentEdition,
|
||||||
enable_app_deploy: z.boolean().default(false),
|
enable_app_deploy: z.boolean().default(false),
|
||||||
enable_change_email: z.boolean().default(true),
|
enable_change_email: z.boolean().default(true),
|
||||||
enable_collaboration_mode: z.boolean().default(true),
|
enable_collaboration_mode: z.boolean().default(true),
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ export type ConversationRenamePayload = (
|
|||||||
name?: string | null
|
name?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeploymentEdition = 'CLOUD' | 'COMMUNITY' | 'ENTERPRISE'
|
||||||
|
|
||||||
export type EmailCodeLoginSendPayload = {
|
export type EmailCodeLoginSendPayload = {
|
||||||
email: string
|
email: string
|
||||||
language?: string | null
|
language?: string | null
|
||||||
@@ -510,6 +512,7 @@ export type SuggestedQuestionsResponse = {
|
|||||||
|
|
||||||
export type SystemFeatureModel = {
|
export type SystemFeatureModel = {
|
||||||
branding: BrandingModel
|
branding: BrandingModel
|
||||||
|
deployment_edition: DeploymentEdition
|
||||||
enable_app_deploy: boolean
|
enable_app_deploy: boolean
|
||||||
enable_change_email: boolean
|
enable_change_email: boolean
|
||||||
enable_collaboration_mode: boolean
|
enable_collaboration_mode: boolean
|
||||||
|
|||||||
@@ -132,6 +132,11 @@ export const zConversationRenamePayload = z.intersection(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DeploymentEdition
|
||||||
|
*/
|
||||||
|
export const zDeploymentEdition = z.enum(['CLOUD', 'COMMUNITY', 'ENTERPRISE'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EmailCodeLoginSendPayload
|
* EmailCodeLoginSendPayload
|
||||||
*/
|
*/
|
||||||
@@ -782,6 +787,7 @@ export const zSystemFeatureModel = z.object({
|
|||||||
login_page_logo: '',
|
login_page_logo: '',
|
||||||
workspace_logo: '',
|
workspace_logo: '',
|
||||||
}),
|
}),
|
||||||
|
deployment_edition: zDeploymentEdition,
|
||||||
enable_app_deploy: z.boolean().default(false),
|
enable_app_deploy: z.boolean().default(false),
|
||||||
enable_change_email: z.boolean().default(true),
|
enable_change_email: z.boolean().default(true),
|
||||||
enable_collaboration_mode: z.boolean().default(true),
|
enable_collaboration_mode: z.boolean().default(true),
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# For production release, change this to PRODUCTION
|
# For production release, change this to PRODUCTION
|
||||||
NEXT_PUBLIC_DEPLOY_ENV=DEVELOPMENT
|
NEXT_PUBLIC_DEPLOY_ENV=DEVELOPMENT
|
||||||
# The deployment edition, SELF_HOSTED
|
|
||||||
NEXT_PUBLIC_EDITION=SELF_HOSTED
|
|
||||||
# Whether a self-hosted deployment runs Enterprise Edition
|
|
||||||
NEXT_PUBLIC_ENTERPRISE_ENABLED=false
|
|
||||||
# The base path for the application
|
# The base path for the application
|
||||||
NEXT_PUBLIC_BASE_PATH=
|
NEXT_PUBLIC_BASE_PATH=
|
||||||
# Server-only console API origin for server-side requests.
|
# Server-only console API origin for server-side requests.
|
||||||
@@ -114,20 +110,3 @@ NEXT_PUBLIC_WEB_PREFIX=
|
|||||||
|
|
||||||
# number of concurrency
|
# number of concurrency
|
||||||
NEXT_PUBLIC_BATCH_CONCURRENCY=5
|
NEXT_PUBLIC_BATCH_CONCURRENCY=5
|
||||||
|
|
||||||
# Cloud system-features frontend defaults.
|
|
||||||
# These values are only used when NEXT_PUBLIC_EDITION=CLOUD (IS_CLOUD_EDITION).
|
|
||||||
NEXT_PUBLIC_ENABLE_MARKETPLACE=true
|
|
||||||
NEXT_PUBLIC_ENABLE_EMAIL_CODE_LOGIN=true
|
|
||||||
NEXT_PUBLIC_ENABLE_EMAIL_PASSWORD_LOGIN=false
|
|
||||||
NEXT_PUBLIC_ENABLE_SOCIAL_OAUTH_LOGIN=true
|
|
||||||
NEXT_PUBLIC_ENABLE_COLLABORATION_MODE=false
|
|
||||||
NEXT_PUBLIC_ALLOW_REGISTER=true
|
|
||||||
NEXT_PUBLIC_ALLOW_CREATE_WORKSPACE=true
|
|
||||||
NEXT_PUBLIC_IS_EMAIL_SETUP=true
|
|
||||||
NEXT_PUBLIC_ENABLE_CHANGE_EMAIL=true
|
|
||||||
NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED=true
|
|
||||||
NEXT_PUBLIC_ENABLE_TRIAL_APP=true
|
|
||||||
NEXT_PUBLIC_ENABLE_EXPLORE_BANNER=true
|
|
||||||
NEXT_PUBLIC_RBAC_ENABLED=false
|
|
||||||
NEXT_PUBLIC_KNOWLEDGE_FS_ENABLED=false
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ RUN pnpm build && pnpm build:vinext
|
|||||||
FROM base AS production
|
FROM base AS production
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV EDITION=SELF_HOSTED
|
|
||||||
ENV DEPLOY_ENV=PRODUCTION
|
ENV DEPLOY_ENV=PRODUCTION
|
||||||
ENV CONSOLE_API_URL=http://127.0.0.1:5001
|
ENV CONSOLE_API_URL=http://127.0.0.1:5001
|
||||||
ENV APP_API_URL=http://127.0.0.1:5001
|
ENV APP_API_URL=http://127.0.0.1:5001
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { RenderOptions } from '@testing-library/react'
|
||||||
|
import type { ReactElement } from 'react'
|
||||||
import type { UsagePlanInfo, UsageResetInfo } from '@/app/components/billing/type'
|
import type { UsagePlanInfo, UsageResetInfo } from '@/app/components/billing/type'
|
||||||
import { screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
@@ -14,21 +16,21 @@ import TriggerEventsLimitModal from '@/app/components/billing/trigger-events-lim
|
|||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||||
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
|
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
|
||||||
import { render } from '@/test/console/render'
|
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||||
|
import { render as renderWithConsoleState } from '@/test/console/render'
|
||||||
|
|
||||||
|
const render = (ui: ReactElement, options: RenderOptions = {}) => {
|
||||||
|
const { wrapper } = createConsoleQueryWrapper({
|
||||||
|
systemFeatures: { deployment_edition: 'CLOUD' },
|
||||||
|
})
|
||||||
|
return renderWithConsoleState(ui, { ...options, wrapper })
|
||||||
|
}
|
||||||
|
|
||||||
let mockProviderCtx: Record<string, unknown> = {}
|
let mockProviderCtx: Record<string, unknown> = {}
|
||||||
let mockConsoleState: Record<string, unknown> = {}
|
let mockConsoleState: Record<string, unknown> = {}
|
||||||
const mockSetShowPricingModal = vi.fn()
|
const mockSetShowPricingModal = vi.fn()
|
||||||
const mockSetShowAccountSettingModal = vi.fn()
|
const mockSetShowAccountSettingModal = vi.fn()
|
||||||
|
|
||||||
vi.mock('@/config', async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import('@/config')>()
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
IS_CLOUD_EDITION: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: () => mockProviderCtx,
|
useProviderContext: () => mockProviderCtx,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { RenderOptions } from '@testing-library/react'
|
||||||
|
import type { ReactElement } from 'react'
|
||||||
/**
|
/**
|
||||||
* Integration test: Education Verification Flow
|
* Integration test: Education Verification Flow
|
||||||
*
|
*
|
||||||
@@ -14,7 +16,15 @@ import * as React from 'react'
|
|||||||
import { defaultPlan } from '@/app/components/billing/config'
|
import { defaultPlan } from '@/app/components/billing/config'
|
||||||
import PlanComp from '@/app/components/billing/plan'
|
import PlanComp from '@/app/components/billing/plan'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { render } from '@/test/console/render'
|
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||||
|
import { render as renderWithConsoleState } from '@/test/console/render'
|
||||||
|
|
||||||
|
const render = (ui: ReactElement, options: RenderOptions = {}) => {
|
||||||
|
const { wrapper } = createConsoleQueryWrapper({
|
||||||
|
systemFeatures: { deployment_edition: 'CLOUD' },
|
||||||
|
})
|
||||||
|
return renderWithConsoleState(ui, { ...options, wrapper })
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Mock state ──────────────────────────────────────────────────────────────
|
// ─── Mock state ──────────────────────────────────────────────────────────────
|
||||||
let mockProviderCtx: Record<string, unknown> = {}
|
let mockProviderCtx: Record<string, unknown> = {}
|
||||||
@@ -25,14 +35,6 @@ const mockRouterPush = vi.fn()
|
|||||||
const mockMutateAsync = vi.fn()
|
const mockMutateAsync = vi.fn()
|
||||||
const mockSetEducationVerifying = vi.hoisted(() => vi.fn())
|
const mockSetEducationVerifying = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
vi.mock('@/config', async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import('@/config')>()
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
IS_CLOUD_EDITION: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ─── Context mocks ───────────────────────────────────────────────────────────
|
// ─── Context mocks ───────────────────────────────────────────────────────────
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: () => mockProviderCtx,
|
useProviderContext: () => mockProviderCtx,
|
||||||
|
|||||||
@@ -7,11 +7,15 @@
|
|||||||
* Covers URL param reading, cookie persistence, API bind on mount,
|
* Covers URL param reading, cookie persistence, API bind on mount,
|
||||||
* cookie cleanup after successful bind, and error handling for 400 status.
|
* cookie cleanup after successful bind, and error handling for 400 status.
|
||||||
*/
|
*/
|
||||||
import { act, cleanup, render, renderHook, waitFor } from '@testing-library/react'
|
import { act, cleanup, renderHook, waitFor } from '@testing-library/react'
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import usePSInfo from '@/app/components/billing/partner-stack/use-ps-info'
|
import usePSInfo from '@/app/components/billing/partner-stack/use-ps-info'
|
||||||
import { PARTNER_STACK_CONFIG } from '@/config'
|
import { PARTNER_STACK_CONFIG } from '@/config'
|
||||||
|
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||||
|
|
||||||
|
const render = (ui: React.ReactElement) =>
|
||||||
|
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } })
|
||||||
|
|
||||||
// ─── Mock state ──────────────────────────────────────────────────────────────
|
// ─── Mock state ──────────────────────────────────────────────────────────────
|
||||||
let mockSearchParams = new URLSearchParams()
|
let mockSearchParams = new URLSearchParams()
|
||||||
@@ -39,7 +43,6 @@ vi.mock('@/config', async (importOriginal) => {
|
|||||||
const actual = await importOriginal<Record<string, unknown>>()
|
const actual = await importOriginal<Record<string, unknown>>()
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
IS_CLOUD_EDITION: true,
|
|
||||||
PARTNER_STACK_CONFIG: {
|
PARTNER_STACK_CONFIG: {
|
||||||
cookieName: 'partner_stack_info',
|
cookieName: 'partner_stack_info',
|
||||||
saveCookieDays: 90,
|
saveCookieDays: 90,
|
||||||
@@ -288,7 +291,7 @@ describe('Partner Stack Flow', () => {
|
|||||||
|
|
||||||
// ─── 4. PartnerStack Component Mount ────────────────────────────────────
|
// ─── 4. PartnerStack Component Mount ────────────────────────────────────
|
||||||
describe('PartnerStack component mount behavior', () => {
|
describe('PartnerStack component mount behavior', () => {
|
||||||
it('should call saveOrUpdate and bind on mount when IS_CLOUD_EDITION is true', async () => {
|
it('should call saveOrUpdate and bind on mount', async () => {
|
||||||
mockSearchParams = new URLSearchParams({
|
mockSearchParams = new URLSearchParams({
|
||||||
ps_partner_key: 'mount-partner',
|
ps_partner_key: 'mount-partner',
|
||||||
ps_xid: 'mount-click',
|
ps_xid: 'mount-click',
|
||||||
@@ -299,19 +302,14 @@ describe('Partner Stack Flow', () => {
|
|||||||
|
|
||||||
render(<PartnerStack />)
|
render(<PartnerStack />)
|
||||||
|
|
||||||
// The component calls saveOrUpdate and bind in useEffect
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// Bind should have been called
|
|
||||||
expect(mockMutateAsync).toHaveBeenCalledWith({
|
expect(mockMutateAsync).toHaveBeenCalledWith({
|
||||||
partnerKey: 'mount-partner',
|
partnerKey: 'mount-partner',
|
||||||
clickId: 'mount-click',
|
clickId: 'mount-click',
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// Cookie should have been saved (saveOrUpdate was called before bind)
|
|
||||||
// After bind succeeds, cookie is removed
|
|
||||||
expect(Cookies.get(PARTNER_STACK_CONFIG.cookieName)).toBeUndefined()
|
expect(Cookies.get(PARTNER_STACK_CONFIG.cookieName)).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('should render nothing (return null)', async () => {
|
it('should render nothing (return null)', async () => {
|
||||||
const { default: PartnerStack } = await import('@/app/components/billing/partner-stack')
|
const { default: PartnerStack } = await import('@/app/components/billing/partner-stack')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
describe('env runtime transport', () => {
|
describe('env runtime transport', () => {
|
||||||
const originalAgentV2Env = process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
const originalAgentV2Env = process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||||
const originalRbacEnv = process.env.NEXT_PUBLIC_RBAC_ENABLED
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
@@ -8,17 +7,12 @@ describe('env runtime transport', () => {
|
|||||||
vi.doUnmock('../utils/client')
|
vi.doUnmock('../utils/client')
|
||||||
document.body.removeAttribute('data-enable-agent-v2')
|
document.body.removeAttribute('data-enable-agent-v2')
|
||||||
document.body.removeAttribute('data-enable-agent-v-2')
|
document.body.removeAttribute('data-enable-agent-v-2')
|
||||||
document.body.removeAttribute('data-rbac-enabled')
|
|
||||||
delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||||
delete process.env.NEXT_PUBLIC_RBAC_ENABLED
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
if (originalAgentV2Env === undefined) delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
if (originalAgentV2Env === undefined) delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||||
else process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = originalAgentV2Env
|
else process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = originalAgentV2Env
|
||||||
|
|
||||||
if (originalRbacEnv === undefined) delete process.env.NEXT_PUBLIC_RBAC_ENABLED
|
|
||||||
else process.env.NEXT_PUBLIC_RBAC_ENABLED = originalRbacEnv
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should read NEXT_PUBLIC_ENABLE_AGENT_V2 from the browser runtime dataset key', async () => {
|
it('should read NEXT_PUBLIC_ENABLE_AGENT_V2 from the browser runtime dataset key', async () => {
|
||||||
@@ -29,14 +23,6 @@ describe('env runtime transport', () => {
|
|||||||
expect(env.NEXT_PUBLIC_ENABLE_AGENT_V2).toBe(true)
|
expect(env.NEXT_PUBLIC_ENABLE_AGENT_V2).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should read NEXT_PUBLIC_RBAC_ENABLED from the browser runtime dataset key', async () => {
|
|
||||||
document.body.setAttribute('data-rbac-enabled', 'true')
|
|
||||||
|
|
||||||
const { env } = await import('../env')
|
|
||||||
|
|
||||||
expect(env.NEXT_PUBLIC_RBAC_ENABLED).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should emit the Agent v2 runtime dataset attribute from getDatasetMap on the server', async () => {
|
it('should emit the Agent v2 runtime dataset attribute from getDatasetMap on the server', async () => {
|
||||||
process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = 'true'
|
process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = 'true'
|
||||||
|
|
||||||
@@ -51,18 +37,4 @@ describe('env runtime transport', () => {
|
|||||||
expect(datasetMap['data-enable-agent-v2']).toBe(true)
|
expect(datasetMap['data-enable-agent-v2']).toBe(true)
|
||||||
expect(datasetMap['data-enable-agent-v-2']).toBeUndefined()
|
expect(datasetMap['data-enable-agent-v-2']).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should emit the RBAC runtime dataset attribute from getDatasetMap on the server', async () => {
|
|
||||||
process.env.NEXT_PUBLIC_RBAC_ENABLED = 'true'
|
|
||||||
|
|
||||||
vi.doMock('../utils/client', () => ({
|
|
||||||
isClient: false,
|
|
||||||
isServer: true,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const { getDatasetMap } = await import('../env')
|
|
||||||
const datasetMap = getDatasetMap()
|
|
||||||
|
|
||||||
expect(datasetMap['data-rbac-enabled']).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react'
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createSystemFeaturesFixture } from '@/test/console/system-features'
|
||||||
import { ConsoleBootstrapGate } from '../console-bootstrap-gate'
|
import { ConsoleBootstrapGate } from '../console-bootstrap-gate'
|
||||||
|
|
||||||
const profileQueryKey = ['console', 'account', 'profile', 'get']
|
const profileQueryKey = ['console', 'account', 'profile', 'get']
|
||||||
@@ -26,7 +27,9 @@ const createDeferred = <T,>(): Deferred<T> => {
|
|||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
profileQuery: undefined as Deferred<{ id: string }> | undefined,
|
profileQuery: undefined as Deferred<{ id: string }> | undefined,
|
||||||
systemFeaturesQuery: undefined as Deferred<{ branding: { enabled: boolean } }> | undefined,
|
systemFeaturesQuery: undefined as
|
||||||
|
| Deferred<ReturnType<typeof createSystemFeaturesFixture>>
|
||||||
|
| undefined,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/features/account-profile/client', () => ({
|
vi.mock('@/features/account-profile/client', () => ({
|
||||||
@@ -36,12 +39,24 @@ vi.mock('@/features/account-profile/client', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/features/system-features/client', () => ({
|
vi.mock('@/service/client', async (importOriginal) => {
|
||||||
systemFeaturesQueryOptions: () => ({
|
const actual = await importOriginal<typeof import('@/service/client')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
consoleQuery: {
|
||||||
|
...actual.consoleQuery,
|
||||||
|
systemFeatures: {
|
||||||
|
get: {
|
||||||
|
...actual.consoleQuery.systemFeatures.get,
|
||||||
|
queryOptions: () => ({
|
||||||
queryKey: systemFeaturesQueryKey,
|
queryKey: systemFeaturesQueryKey,
|
||||||
queryFn: () => mocks.systemFeaturesQuery!.promise,
|
queryFn: () => mocks.systemFeaturesQuery!.promise,
|
||||||
}),
|
}),
|
||||||
}))
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function createQueryClient() {
|
function createQueryClient() {
|
||||||
return new QueryClient({
|
return new QueryClient({
|
||||||
@@ -78,7 +93,7 @@ describe('ConsoleBootstrapGate', () => {
|
|||||||
expect(screen.queryByText('Console shell')).not.toBeInTheDocument()
|
expect(screen.queryByText('Console shell')).not.toBeInTheDocument()
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
mocks.systemFeaturesQuery!.resolve({ branding: { enabled: false } })
|
mocks.systemFeaturesQuery!.resolve(createSystemFeaturesFixture())
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(await screen.findByText('Console shell')).toBeInTheDocument()
|
expect(await screen.findByText('Console shell')).toBeInTheDocument()
|
||||||
@@ -87,11 +102,9 @@ describe('ConsoleBootstrapGate', () => {
|
|||||||
it('keeps atom consumers mounted when a cached profile background refetch fails', async () => {
|
it('keeps atom consumers mounted when a cached profile background refetch fails', async () => {
|
||||||
const queryClient = createQueryClient()
|
const queryClient = createQueryClient()
|
||||||
queryClient.setQueryData(profileQueryKey, { id: 'user-1' }, { updatedAt: 1 })
|
queryClient.setQueryData(profileQueryKey, { id: 'user-1' }, { updatedAt: 1 })
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(systemFeaturesQueryKey, createSystemFeaturesFixture(), {
|
||||||
systemFeaturesQueryKey,
|
updatedAt: 1,
|
||||||
{ branding: { enabled: false } },
|
})
|
||||||
{ updatedAt: 1 },
|
|
||||||
)
|
|
||||||
|
|
||||||
renderGate(<div>Console shell</div>, queryClient)
|
renderGate(<div>Console shell</div>, queryClient)
|
||||||
|
|
||||||
@@ -102,7 +115,7 @@ describe('ConsoleBootstrapGate', () => {
|
|||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
mocks.profileQuery!.reject(new Error('profile refetch failed'))
|
mocks.profileQuery!.reject(new Error('profile refetch failed'))
|
||||||
mocks.systemFeaturesQuery!.resolve({ branding: { enabled: false } })
|
mocks.systemFeaturesQuery!.resolve(createSystemFeaturesFixture())
|
||||||
})
|
})
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import type { DehydratedState } from '@tanstack/react-query'
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
queryClient: undefined as QueryClient | undefined,
|
rootQueryClient: undefined as QueryClient | undefined,
|
||||||
profileQueryFn: vi.fn(),
|
profileQueryFn: vi.fn(),
|
||||||
systemFeaturesQueryFn: vi.fn(),
|
systemFeaturesQueryFn: vi.fn(),
|
||||||
workspaceQueryFn: vi.fn(),
|
workspaceQueryFn: vi.fn(),
|
||||||
@@ -18,9 +19,14 @@ const mocks = vi.hoisted(() => ({
|
|||||||
basePath: '',
|
basePath: '',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/query-client-server', () => ({
|
vi.mock('@/context/query-client-server', async (importOriginal) => {
|
||||||
getQueryClientServer: () => mocks.queryClient,
|
const actual = await importOriginal<typeof import('@/context/query-client-server')>()
|
||||||
}))
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getQueryClientServer: () => mocks.rootQueryClient,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/next/headers', () => ({
|
vi.mock('@/next/headers', () => ({
|
||||||
headers: () => mocks.headers(),
|
headers: () => mocks.headers(),
|
||||||
@@ -44,6 +50,14 @@ vi.mock('@/features/account-profile/server', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/features/system-features/server', () => ({
|
||||||
|
serverSystemFeaturesQueryOptions: () => ({
|
||||||
|
queryKey: ['console', 'system-features'],
|
||||||
|
queryFn: mocks.systemFeaturesQueryFn,
|
||||||
|
retry: false,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/service/server', () => ({
|
vi.mock('@/service/server', () => ({
|
||||||
getServerConsoleClientContext: () => mocks.getServerConsoleClientContext(),
|
getServerConsoleClientContext: () => mocks.getServerConsoleClientContext(),
|
||||||
resolveServerConsoleApiUrl: (...args: unknown[]) => mocks.resolveServerConsoleApiUrl(...args),
|
resolveServerConsoleApiUrl: (...args: unknown[]) => mocks.resolveServerConsoleApiUrl(...args),
|
||||||
@@ -58,19 +72,11 @@ vi.mock('@/service/server', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/features/system-features/server', () => ({
|
|
||||||
serverSystemFeaturesQueryOptions: () => ({
|
|
||||||
queryKey: ['console', 'system-features'],
|
|
||||||
queryFn: mocks.systemFeaturesQueryFn,
|
|
||||||
retry: false,
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('CommonLayoutHydrationBoundary', () => {
|
describe('CommonLayoutHydrationBoundary', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mocks.basePath = ''
|
mocks.basePath = ''
|
||||||
mocks.queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
mocks.rootQueryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||||
mocks.headers.mockResolvedValue(
|
mocks.headers.mockResolvedValue(
|
||||||
new Headers({
|
new Headers({
|
||||||
'x-dify-pathname': '/apps',
|
'x-dify-pathname': '/apps',
|
||||||
@@ -107,7 +113,7 @@ describe('CommonLayoutHydrationBoundary', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should prefetch common layout queries and render children', async () => {
|
it('should prefetch common layout queries without requesting System Features', async () => {
|
||||||
const { CommonLayoutHydrationBoundary } = await import('../hydration-boundary')
|
const { CommonLayoutHydrationBoundary } = await import('../hydration-boundary')
|
||||||
|
|
||||||
const element = await CommonLayoutHydrationBoundary({
|
const element = await CommonLayoutHydrationBoundary({
|
||||||
@@ -121,7 +127,7 @@ describe('CommonLayoutHydrationBoundary', () => {
|
|||||||
)
|
)
|
||||||
expect(screen.getByText('Common shell')).toBeInTheDocument()
|
expect(screen.getByText('Common shell')).toBeInTheDocument()
|
||||||
expect(mocks.profileQueryFn).toHaveBeenCalledTimes(1)
|
expect(mocks.profileQueryFn).toHaveBeenCalledTimes(1)
|
||||||
expect(mocks.systemFeaturesQueryFn).toHaveBeenCalledTimes(1)
|
expect(mocks.systemFeaturesQueryFn).not.toHaveBeenCalled()
|
||||||
expect(mocks.getServerConsoleClientContext).toHaveBeenCalledTimes(1)
|
expect(mocks.getServerConsoleClientContext).toHaveBeenCalledTimes(1)
|
||||||
expect(mocks.workspaceQueryOptions).toHaveBeenCalledWith({
|
expect(mocks.workspaceQueryOptions).toHaveBeenCalledWith({
|
||||||
context: {
|
context: {
|
||||||
@@ -133,6 +139,25 @@ describe('CommonLayoutHydrationBoundary', () => {
|
|||||||
expect(mocks.workspaceQueryFn).toHaveBeenCalledTimes(1)
|
expect(mocks.workspaceQueryFn).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should dehydrate only Common-owned queries', async () => {
|
||||||
|
mocks.rootQueryClient?.setQueryData(['console', 'system-features'], {
|
||||||
|
deployment_edition: 'CLOUD',
|
||||||
|
})
|
||||||
|
const { CommonLayoutHydrationBoundary } = await import('../hydration-boundary')
|
||||||
|
|
||||||
|
const element = await CommonLayoutHydrationBoundary({ children: null })
|
||||||
|
const state = (element as ReactElement<{ state: DehydratedState }>).props.state
|
||||||
|
const queryKeys = state.queries.map((query) => query.queryKey)
|
||||||
|
|
||||||
|
expect(queryKeys).toHaveLength(2)
|
||||||
|
expect(queryKeys).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
['common', 'user-profile'],
|
||||||
|
['console', 'workspaces', 'current', 'post'],
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should redirect unauthorized users to the refresh route with the current path', async () => {
|
it('should redirect unauthorized users to the refresh route with the current path', async () => {
|
||||||
mocks.basePath = '/workflow'
|
mocks.basePath = '/workflow'
|
||||||
mocks.profileQueryFn.mockRejectedValue(
|
mocks.profileQueryFn.mockRejectedValue(
|
||||||
|
|||||||
+5
-2
@@ -1,5 +1,6 @@
|
|||||||
|
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||||
import { screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
import { renderWithAccountProfile as render } from '@/test/console/account-profile'
|
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||||
import { AppACLPermission } from '@/utils/permission'
|
import { AppACLPermission } from '@/utils/permission'
|
||||||
import ChartView from '../chart-view'
|
import ChartView from '../chart-view'
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ const testState = vi.hoisted(() => ({
|
|||||||
currentUserId: 'user-1',
|
currentUserId: 'user-1',
|
||||||
workspacePermissionKeys: [] as string[],
|
workspacePermissionKeys: [] as string[],
|
||||||
chartRenderSpy: vi.fn(),
|
chartRenderSpy: vi.fn(),
|
||||||
|
conversationPeriodSpy: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/workspace-state', async () => {
|
vi.mock('@/context/workspace-state', async () => {
|
||||||
@@ -46,8 +48,9 @@ vi.mock('@/app/components/app/overview/app-chart', () => ({
|
|||||||
testState.chartRenderSpy('avg-user-interactions')
|
testState.chartRenderSpy('avg-user-interactions')
|
||||||
return <div>avg user interactions chart</div>
|
return <div>avg user interactions chart</div>
|
||||||
},
|
},
|
||||||
ConversationsChart: () => {
|
ConversationsChart: ({ period }: { period: PeriodParams }) => {
|
||||||
testState.chartRenderSpy('conversations')
|
testState.chartRenderSpy('conversations')
|
||||||
|
testState.conversationPeriodSpy(period)
|
||||||
return <div>conversations chart</div>
|
return <div>conversations chart</div>
|
||||||
},
|
},
|
||||||
CostChart: () => {
|
CostChart: () => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen'
|
||||||
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
@@ -23,10 +25,10 @@ import {
|
|||||||
WorkflowMessagesChart,
|
WorkflowMessagesChart,
|
||||||
} from '@/app/components/app/overview/app-chart'
|
} from '@/app/components/app/overview/app-chart'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
|
||||||
import { userProfileIdAtom } from '@/context/account-state'
|
import { userProfileIdAtom } from '@/context/account-state'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { getAppACLCapabilities } from '@/utils/permission'
|
import { getAppACLCapabilities } from '@/utils/permission'
|
||||||
import LongTimeRangePicker from './long-time-range-picker'
|
import LongTimeRangePicker from './long-time-range-picker'
|
||||||
import TimeRangePicker from './time-range-picker'
|
import TimeRangePicker from './time-range-picker'
|
||||||
@@ -51,7 +53,28 @@ type IChartViewProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartViewContent
|
||||||
|
appId={appId}
|
||||||
|
headerRight={headerRight}
|
||||||
|
deploymentEdition={deploymentEdition}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartViewContent({
|
||||||
|
appId,
|
||||||
|
headerRight,
|
||||||
|
deploymentEdition,
|
||||||
|
}: IChartViewProps & { deploymentEdition: DeploymentEdition }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const isCloudEdition = deploymentEdition === 'CLOUD'
|
||||||
|
const isNonCloudEdition = deploymentEdition === 'COMMUNITY' || deploymentEdition === 'ENTERPRISE'
|
||||||
const docLink = useDocLink()
|
const docLink = useDocLink()
|
||||||
const appDetail = useAppStore((state) => state.appDetail)
|
const appDetail = useAppStore((state) => state.appDetail)
|
||||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||||
@@ -67,8 +90,8 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
|||||||
)
|
)
|
||||||
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
|
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
|
||||||
const isWorkflow = appDetail?.mode === 'workflow'
|
const isWorkflow = appDetail?.mode === 'workflow'
|
||||||
const [period, setPeriod] = useState<PeriodParams>(
|
const [period, setPeriod] = useState<PeriodParams>(() =>
|
||||||
IS_CLOUD_EDITION
|
isCloudEdition
|
||||||
? {
|
? {
|
||||||
name: t(($) => $['filter.period.today'], { ns: 'appLog' }),
|
name: t(($) => $['filter.period.today'], { ns: 'appLog' }),
|
||||||
query: {
|
query: {
|
||||||
@@ -112,13 +135,14 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex h-10 items-center justify-between pr-10 pl-6">
|
<div className="mt-1 flex h-10 items-center justify-between pr-10 pl-6">
|
||||||
{IS_CLOUD_EDITION ? (
|
{isCloudEdition && (
|
||||||
<TimeRangePicker
|
<TimeRangePicker
|
||||||
ranges={TIME_PERIOD_MAPPING}
|
ranges={TIME_PERIOD_MAPPING}
|
||||||
onSelect={setPeriod}
|
onSelect={setPeriod}
|
||||||
queryDateFormat={queryDateFormat}
|
queryDateFormat={queryDateFormat}
|
||||||
/>
|
/>
|
||||||
) : (
|
)}
|
||||||
|
{isNonCloudEdition && (
|
||||||
<LongTimeRangePicker
|
<LongTimeRangePicker
|
||||||
periodMapping={LONG_TIME_PERIOD_MAPPING}
|
periodMapping={LONG_TIME_PERIOD_MAPPING}
|
||||||
onSelect={setPeriod}
|
onSelect={setPeriod}
|
||||||
|
|||||||
-8
@@ -43,14 +43,6 @@ vi.mock('@/context/permission-state', async () => {
|
|||||||
workspacePermissionKeys: [],
|
workspacePermissionKeys: [],
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
vi.mock('@/context/system-features-state', async () => {
|
|
||||||
const { createSystemFeaturesStateModuleMock } = await import('@/test/console/state-fixture')
|
|
||||||
|
|
||||||
return createSystemFeaturesStateModuleMock(() => ({
|
|
||||||
datasetRbacEnabled: mockIsRbacEnabled,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock('@/context/event-emitter', () => ({
|
vi.mock('@/context/event-emitter', () => ({
|
||||||
useEventEmitterContextContext: () => ({
|
useEventEmitterContextContext: () => ({
|
||||||
eventEmitter: undefined,
|
eventEmitter: undefined,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { DataSet } from '@/models/datasets'
|
import type { DataSet } from '@/models/datasets'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
@@ -13,8 +14,8 @@ import {
|
|||||||
workspacePermissionKeysAtom,
|
workspacePermissionKeysAtom,
|
||||||
workspacePermissionKeysLoadingAtom,
|
workspacePermissionKeysLoadingAtom,
|
||||||
} from '@/context/permission-state'
|
} from '@/context/permission-state'
|
||||||
import { datasetRbacEnabledAtom } from '@/context/system-features-state'
|
|
||||||
import { currentWorkspaceLoadingAtom } from '@/context/workspace-state'
|
import { currentWorkspaceLoadingAtom } from '@/context/workspace-state'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { usePathname, useRouter } from '@/next/navigation'
|
import { usePathname, useRouter } from '@/next/navigation'
|
||||||
import { useDatasetDetail } from '@/service/knowledge/use-dataset'
|
import { useDatasetDetail } from '@/service/knowledge/use-dataset'
|
||||||
@@ -60,7 +61,10 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
|
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
|
||||||
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
|
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
|
||||||
const isRbacEnabled = useAtomValue(datasetRbacEnabledAtom)
|
const { data: isRbacEnabled } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ rbac_enabled }) => rbac_enabled,
|
||||||
|
})
|
||||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
|
import { useQueryErrorResetBoundary } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FullScreenLoading } from '@/app/components/full-screen-loading'
|
import { FullScreenLoading } from '@/app/components/full-screen-loading'
|
||||||
import { isLegacyBase401 } from '@/features/account-profile/client'
|
import { isLegacyBase401 } from '@/features/account-profile/client'
|
||||||
@@ -12,6 +13,7 @@ type Props = Readonly<{
|
|||||||
|
|
||||||
export default function CommonLayoutError({ error, unstable_retry }: Props) {
|
export default function CommonLayoutError({ error, unstable_retry }: Props) {
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
|
const { reset } = useQueryErrorResetBoundary()
|
||||||
|
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
@@ -26,7 +28,14 @@ export default function CommonLayoutError({ error, unstable_retry }: Props) {
|
|||||||
<div className="system-sm-regular text-text-tertiary">
|
<div className="system-sm-regular text-text-tertiary">
|
||||||
{t(($) => $['errorBoundary.message'])}
|
{t(($) => $['errorBoundary.message'])}
|
||||||
</div>
|
</div>
|
||||||
<Button size="small" variant="secondary" onClick={() => unstable_retry()}>
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
reset()
|
||||||
|
unstable_retry()
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t(($) => $['errorBoundary.tryAgain'])}
|
{t(($) => $['errorBoundary.tryAgain'])}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
|
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
|
||||||
import { getQueryClientServer } from '@/context/query-client-server'
|
import { makeQueryClient } from '@/context/query-client-server'
|
||||||
import { serverUserProfileQueryOptions } from '@/features/account-profile/server'
|
import { serverUserProfileQueryOptions } from '@/features/account-profile/server'
|
||||||
import { serverSystemFeaturesQueryOptions } from '@/features/system-features/server'
|
|
||||||
import { headers } from '@/next/headers'
|
import { headers } from '@/next/headers'
|
||||||
import { redirect } from '@/next/navigation'
|
import { redirect } from '@/next/navigation'
|
||||||
import {
|
import {
|
||||||
@@ -56,7 +55,7 @@ const handleProfileError = async (error: unknown) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function CommonLayoutHydrationBoundary({ children }: { children: ReactNode }) {
|
export async function CommonLayoutHydrationBoundary({ children }: { children: ReactNode }) {
|
||||||
const queryClient = getQueryClientServer()
|
const queryClient = makeQueryClient()
|
||||||
const accountProfileUrl = resolveServerConsoleApiUrl(ACCOUNT_PROFILE_PATH)
|
const accountProfileUrl = resolveServerConsoleApiUrl(ACCOUNT_PROFILE_PATH)
|
||||||
|
|
||||||
if (accountProfileUrl) {
|
if (accountProfileUrl) {
|
||||||
@@ -65,7 +64,6 @@ export async function CommonLayoutHydrationBoundary({ children }: { children: Re
|
|||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.fetchQuery(serverUserProfileQueryOptions()),
|
queryClient.fetchQuery(serverUserProfileQueryOptions()),
|
||||||
queryClient.prefetchQuery(serverSystemFeaturesQueryOptions()),
|
|
||||||
queryClient.prefetchQuery(
|
queryClient.prefetchQuery(
|
||||||
serverConsoleQuery.workspaces.current.post.queryOptions({
|
serverConsoleQuery.workspaces.current.post.queryOptions({
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import * as React from 'react'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
|
||||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { LicenseStatus } from '@/features/system-features/constants'
|
import { LicenseStatus } from '@/features/system-features/constants'
|
||||||
import Link from '@/next/link'
|
import Link from '@/next/link'
|
||||||
@@ -19,6 +18,9 @@ const NormalForm = () => {
|
|||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
const isNonCloudEdition =
|
||||||
|
systemFeatures.deployment_edition === 'COMMUNITY' ||
|
||||||
|
systemFeatures.deployment_edition === 'ENTERPRISE'
|
||||||
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
|
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
|
||||||
const [showORLine, setShowORLine] = useState(false)
|
const [showORLine, setShowORLine] = useState(false)
|
||||||
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
|
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
|
||||||
@@ -236,7 +238,7 @@ const NormalForm = () => {
|
|||||||
{t(($) => $.pp, { ns: 'login' })}
|
{t(($) => $.pp, { ns: 'login' })}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{IS_CE_EDITION && (
|
{isNonCloudEdition && (
|
||||||
<div className="w-hull mt-2 block system-xs-regular text-text-tertiary">
|
<div className="w-hull mt-2 block system-xs-regular text-text-tertiary">
|
||||||
{t(($) => $.goToInit, { ns: 'login' })}
|
{t(($) => $.goToInit, { ns: 'login' })}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { QueryErrorResetBoundary } from '@tanstack/react-query'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import CommonLayoutError from '@/app/(commonLayout)/error'
|
||||||
|
import AppError from '@/app/error'
|
||||||
|
|
||||||
|
describe('route error recovery', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: 'root error',
|
||||||
|
renderError: (retry: () => void) => <AppError error={new Error('failed')} reset={retry} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'common layout error',
|
||||||
|
renderError: (retry: () => void) => (
|
||||||
|
<CommonLayoutError error={new Error('failed')} unstable_retry={retry} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
])('resets failed queries before retrying the $name', async ({ renderError }) => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const retry = vi.fn()
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
|
||||||
|
render(
|
||||||
|
<QueryErrorResetBoundary>
|
||||||
|
{({ isReset }) => renderError(() => retry(isReset()))}
|
||||||
|
</QueryErrorResetBoundary>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'common.errorBoundary.tryAgain' }))
|
||||||
|
|
||||||
|
expect(retry).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { QueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
let queryClient: QueryClient
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
getSystemFeatures: vi.fn(),
|
||||||
|
getCloudAnalyticsBoundaryState: vi.fn(() => ({ enabled: false })),
|
||||||
|
requestHeaders: new Headers(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/query-client-server', () => ({
|
||||||
|
getQueryClientServer: () => queryClient,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/env', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('@/env')>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getDatasetMap: () => ({}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/features/system-features/server', () => ({
|
||||||
|
serverSystemFeaturesQueryOptions: () => ({
|
||||||
|
queryKey: ['console', 'system-features'],
|
||||||
|
queryFn: mocks.getSystemFeatures,
|
||||||
|
retry: false,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/i18n-config/server', () => ({
|
||||||
|
getLocaleOnServer: async () => 'en-US',
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/next/headers', () => ({
|
||||||
|
headers: async () => mocks.requestHeaders,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/base/analytics-consent/cloud-analytics-state', () => ({
|
||||||
|
getCloudAnalyticsBoundaryState: mocks.getCloudAnalyticsBoundaryState,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('Root layout System Features bootstrap', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with the resolved deployment edition', async () => {
|
||||||
|
mocks.getSystemFeatures.mockResolvedValue({ deployment_edition: 'CLOUD' })
|
||||||
|
const { default: RootLayout } = await import('../layout')
|
||||||
|
|
||||||
|
await expect(RootLayout({ children: <div>App</div> })).resolves.toBeDefined()
|
||||||
|
|
||||||
|
expect(mocks.getSystemFeatures).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mocks.getCloudAnalyticsBoundaryState).toHaveBeenCalledWith(mocks.requestHeaders, 'CLOUD')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('propagates System Features failures without rendering a fallback', async () => {
|
||||||
|
const error = new Error('system features unavailable')
|
||||||
|
mocks.getSystemFeatures.mockRejectedValue(error)
|
||||||
|
const { default: RootLayout } = await import('../layout')
|
||||||
|
|
||||||
|
await expect(RootLayout({ children: <div>App</div> })).rejects.toBe(error)
|
||||||
|
|
||||||
|
expect(mocks.getCloudAnalyticsBoundaryState).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -12,7 +12,7 @@ import AppIcon from '@/app/components/base/app-icon'
|
|||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||||
import Collapse from '@/app/components/header/account-setting/collapse'
|
import Collapse from '@/app/components/header/account-setting/collapse'
|
||||||
import { IS_CE_EDITION, validPassword } from '@/config'
|
import { validPassword } from '@/config'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
@@ -243,7 +243,7 @@ export default function AccountPage() {
|
|||||||
wrapperClassName="mt-2"
|
wrapperClassName="mt-2"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!IS_CE_EDITION && (
|
{systemFeatures.deployment_edition === 'CLOUD' && (
|
||||||
<Button
|
<Button
|
||||||
className="mt-2 text-components-button-destructive-secondary-text"
|
className="mt-2 text-components-button-destructive-secondary-text"
|
||||||
onClick={() => setShowDeleteAccountModal(true)}
|
onClick={() => setShowDeleteAccountModal(true)}
|
||||||
|
|||||||
@@ -4,16 +4,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
basePath: '',
|
basePath: '',
|
||||||
isCloudEdition: false,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/config', () => ({
|
vi.mock('@/config', () => ({
|
||||||
API_PREFIX: 'http://localhost:5001/console/api',
|
API_PREFIX: 'http://localhost:5001/console/api',
|
||||||
CSRF_COOKIE_NAME: () => 'csrf_token',
|
CSRF_COOKIE_NAME: () => 'csrf_token',
|
||||||
CSRF_HEADER_NAME: 'X-CSRF-Token',
|
CSRF_HEADER_NAME: 'X-CSRF-Token',
|
||||||
get IS_CLOUD_EDITION() {
|
|
||||||
return mocks.isCloudEdition
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('server-only', () => ({}))
|
vi.mock('server-only', () => ({}))
|
||||||
@@ -52,7 +48,6 @@ describe('auth refresh route', () => {
|
|||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
vi.unstubAllGlobals()
|
vi.unstubAllGlobals()
|
||||||
mocks.basePath = ''
|
mocks.basePath = ''
|
||||||
mocks.isCloudEdition = false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should refresh cookies and redirect back to the requested path', async () => {
|
it('should refresh cookies and redirect back to the requested path', async () => {
|
||||||
@@ -244,8 +239,7 @@ describe('auth refresh route', () => {
|
|||||||
expect(response.headers.get('location')).toBe('/signin?redirect_url=%2F')
|
expect(response.headers.get('location')).toBe('/signin?redirect_url=%2F')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should keep a Cloud staging fallback on the current deployment after refresh', async () => {
|
it('should keep a staging fallback on the current deployment after refresh', async () => {
|
||||||
mocks.isCloudEdition = true
|
|
||||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 200 })))
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 200 })))
|
||||||
const { GET } = await import('../route')
|
const { GET } = await import('../route')
|
||||||
|
|
||||||
@@ -260,8 +254,7 @@ describe('auth refresh route', () => {
|
|||||||
expect(response.headers.get('location')).toBe('/')
|
expect(response.headers.get('location')).toBe('/')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should carry the current Cloud deployment fallback through signin when refresh fails', async () => {
|
it('should carry the current deployment fallback through signin when refresh fails', async () => {
|
||||||
mocks.isCloudEdition = true
|
|
||||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 401 })))
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 401 })))
|
||||||
const { GET } = await import('../route')
|
const { GET } = await import('../route')
|
||||||
|
|
||||||
@@ -277,7 +270,6 @@ describe('auth refresh route', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should use the current deployment home when a trusted target loops back to auth refresh', async () => {
|
it('should use the current deployment home when a trusted target loops back to auth refresh', async () => {
|
||||||
mocks.isCloudEdition = true
|
|
||||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 200 })))
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 200 })))
|
||||||
const { GET } = await import('../route')
|
const { GET } = await import('../route')
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,10 @@ import { setAnalyticsConsent } from '@/app/components/base/analytics-consent/con
|
|||||||
import { useSearchParams } from '@/next/navigation'
|
import { useSearchParams } from '@/next/navigation'
|
||||||
import ExternalAttributionRecorder from '../external-attribution-recorder'
|
import ExternalAttributionRecorder from '../external-attribution-recorder'
|
||||||
|
|
||||||
const mockConfig = vi.hoisted(() => ({ IS_CLOUD_EDITION: true }))
|
|
||||||
const { mockRememberCreateAppExternalAttribution } = vi.hoisted(() => ({
|
const { mockRememberCreateAppExternalAttribution } = vi.hoisted(() => ({
|
||||||
mockRememberCreateAppExternalAttribution: vi.fn(),
|
mockRememberCreateAppExternalAttribution: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/config', () => ({
|
|
||||||
get IS_CLOUD_EDITION() {
|
|
||||||
return mockConfig.IS_CLOUD_EDITION
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/next/navigation', () => ({
|
vi.mock('@/next/navigation', () => ({
|
||||||
useSearchParams: vi.fn(),
|
useSearchParams: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@@ -43,7 +36,6 @@ describe('ExternalAttributionRecorder', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
Cookies.remove('utm_info')
|
Cookies.remove('utm_info')
|
||||||
mockConfig.IS_CLOUD_EDITION = true
|
|
||||||
setAnalyticsConsent('granted')
|
setAnalyticsConsent('granted')
|
||||||
setSearchParams()
|
setSearchParams()
|
||||||
})
|
})
|
||||||
@@ -150,14 +142,4 @@ describe('ExternalAttributionRecorder', () => {
|
|||||||
})
|
})
|
||||||
expect(mockRememberCreateAppExternalAttribution).toHaveBeenCalledTimes(1)
|
expect(mockRememberCreateAppExternalAttribution).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('is a no-op outside the cloud edition', () => {
|
|
||||||
mockConfig.IS_CLOUD_EDITION = false
|
|
||||||
setSearchParams('utm_source=dify_blog&slug=get-started-with-dify')
|
|
||||||
|
|
||||||
render(<ExternalAttributionRecorder />)
|
|
||||||
|
|
||||||
expect(getUtmInfoCookie()).toBeNull()
|
|
||||||
expect(mockRememberCreateAppExternalAttribution).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,7 +35,15 @@ vi.mock('@/service/access-control', () => ({
|
|||||||
|
|
||||||
vi.mock('@/service/client', () => ({
|
vi.mock('@/service/client', () => ({
|
||||||
consoleQuery: {
|
consoleQuery: {
|
||||||
systemFeatures: { get: { queryKey: () => ['system-features'] } },
|
systemFeatures: {
|
||||||
|
get: {
|
||||||
|
queryKey: () => ['system-features'],
|
||||||
|
queryOptions: (options: Record<string, unknown> = {}) => ({
|
||||||
|
queryKey: ['system-features'],
|
||||||
|
...options,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
enterprise: {
|
enterprise: {
|
||||||
webAppAuth: {
|
webAppAuth: {
|
||||||
updateWebAppWhitelistSubjects: {
|
updateWebAppWhitelistSubjects: {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* oxlint-disable typescript/no-explicit-any */
|
/* oxlint-disable typescript/no-explicit-any */
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
|
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import {
|
import {
|
||||||
AccessModeDisplay,
|
AccessModeDisplay,
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import type { IConfigVarProps } from '../index'
|
|||||||
import type { ExternalDataTool } from '@/models/common'
|
import type { ExternalDataTool } from '@/models/common'
|
||||||
import type { PromptVariable } from '@/models/debug'
|
import type { PromptVariable } from '@/models/debug'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import DebugConfigurationContext from '@/context/debug-configuration'
|
import DebugConfigurationContext from '@/context/debug-configuration'
|
||||||
|
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from '../index'
|
import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from '../index'
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,10 +1,11 @@
|
|||||||
import type { InputVar } from '@/app/components/workflow/types'
|
import type { InputVar } from '@/app/components/workflow/types'
|
||||||
import type { App, AppSSO } from '@/types/app'
|
import type { App, AppSSO } from '@/types/app'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useStore } from '@/app/components/app/store'
|
import { useStore } from '@/app/components/app/store'
|
||||||
import { InputVarType } from '@/app/components/workflow/types'
|
import { InputVarType } from '@/app/components/workflow/types'
|
||||||
|
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import ConfigModal from '../index'
|
import ConfigModal from '../index'
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,6 @@ import { IndexingType } from '@/app/components/datasets/create/step-two'
|
|||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { defaultSystemFeatures } from '@/features/system-features/config'
|
|
||||||
import {
|
import {
|
||||||
ChunkingMode,
|
ChunkingMode,
|
||||||
DatasetPermission,
|
DatasetPermission,
|
||||||
@@ -18,6 +17,7 @@ import {
|
|||||||
import { updateDatasetSetting } from '@/service/datasets'
|
import { updateDatasetSetting } from '@/service/datasets'
|
||||||
import { useMembers } from '@/service/use-common'
|
import { useMembers } from '@/service/use-common'
|
||||||
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||||
|
import { createSystemFeaturesFixture } from '@/test/console/system-features'
|
||||||
import { RETRIEVE_METHOD } from '@/types/app'
|
import { RETRIEVE_METHOD } from '@/types/app'
|
||||||
import { DatasetACLPermission } from '@/utils/permission'
|
import { DatasetACLPermission } from '@/utils/permission'
|
||||||
import SettingsModal from '../index'
|
import SettingsModal from '../index'
|
||||||
@@ -210,7 +210,7 @@ const renderWithProviders = (dataset: DataSet) => {
|
|||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: { queries: { retry: false } },
|
defaultOptions: { queries: { retry: false } },
|
||||||
})
|
})
|
||||||
queryClient.setQueryData(systemFeaturesQueryOptions().queryKey, defaultSystemFeatures)
|
queryClient.setQueryData(systemFeaturesQueryOptions().queryKey, createSystemFeaturesFixture())
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
import type { App } from '@/models/explore'
|
import type { App } from '@/models/explore'
|
||||||
import type { AppIconType } from '@/types/app'
|
import type { AppIconType } from '@/types/app'
|
||||||
import { screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { trackEvent } from '@/app/components/base/amplitude'
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
import AppListContext from '@/context/app-list-context'
|
import AppListContext from '@/context/app-list-context'
|
||||||
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import AppCard from '../index'
|
import AppCard from '../index'
|
||||||
|
|
||||||
vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: vi.fn() }))
|
vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: vi.fn() }))
|
||||||
|
|
||||||
const mockConfig = vi.hoisted(() => ({ isCloudEdition: true }))
|
const render = (ui: ReactElement) =>
|
||||||
vi.mock('@/config', async (importOriginal) => ({
|
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } })
|
||||||
...(await importOriginal<typeof import('@/config')>()),
|
|
||||||
get IS_CLOUD_EDITION() {
|
|
||||||
return mockConfig.isCloudEdition
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const app: App = {
|
const app: App = {
|
||||||
can_trial: true,
|
can_trial: true,
|
||||||
@@ -47,7 +43,6 @@ const app: App = {
|
|||||||
|
|
||||||
describe('AppCard', () => {
|
describe('AppCard', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockConfig.isCloudEdition = true
|
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { PlusIcon } from '@heroicons/react/20/solid'
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { RiInformation2Line } from '@remixicon/react'
|
import { RiInformation2Line } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useContextSelector } from 'use-context-selector'
|
import { useContextSelector } from 'use-context-selector'
|
||||||
import { trackEvent } from '@/app/components/base/amplitude'
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
|
||||||
import AppListContext from '@/context/app-list-context'
|
import AppListContext from '@/context/app-list-context'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { AppTypeIcon, AppTypeLabel } from '../../type-selector'
|
import { AppTypeIcon, AppTypeLabel } from '../../type-selector'
|
||||||
|
|
||||||
type AppCardProps = {
|
type AppCardProps = {
|
||||||
@@ -21,8 +22,12 @@ type AppCardProps = {
|
|||||||
|
|
||||||
const AppCard = ({ app, canCreate, onCreate }: AppCardProps) => {
|
const AppCard = ({ app, canCreate, onCreate }: AppCardProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
const { app: appBasicInfo } = app
|
const { app: appBasicInfo } = app
|
||||||
const canViewApp = IS_CLOUD_EDITION
|
const canViewApp = deploymentEdition === 'CLOUD'
|
||||||
const setShowTryAppPanel = useContextSelector(AppListContext, (ctx) => ctx.setShowTryAppPanel)
|
const setShowTryAppPanel = useContextSelector(AppListContext, (ctx) => ctx.setShowTryAppPanel)
|
||||||
const handleShowTryAppPanel = useCallback(() => {
|
const handleShowTryAppPanel = useCallback(() => {
|
||||||
trackEvent('preview_template', {
|
trackEvent('preview_template', {
|
||||||
|
|||||||
@@ -3,27 +3,26 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import InSiteMessageNotification from '../notification'
|
import InSiteMessageNotification from '../notification'
|
||||||
|
|
||||||
const { mockConfig, mockNotification, mockNotificationDismiss } = vi.hoisted(() => ({
|
const { mockEdition, mockNotification, mockNotificationDismiss } = vi.hoisted(() => ({
|
||||||
mockConfig: {
|
mockEdition: {
|
||||||
isCloudEdition: true,
|
value: 'CLOUD' as 'COMMUNITY' | 'ENTERPRISE' | 'CLOUD' | null,
|
||||||
},
|
},
|
||||||
mockNotification: vi.fn(),
|
mockNotification: vi.fn(),
|
||||||
mockNotificationDismiss: vi.fn(),
|
mockNotificationDismiss: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock(import('@/config'), async (importOriginal) => {
|
|
||||||
const actual = await importOriginal()
|
|
||||||
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
get IS_CLOUD_EDITION() {
|
|
||||||
return mockConfig.isCloudEdition
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock('@/service/client', () => ({
|
vi.mock('@/service/client', () => ({
|
||||||
consoleQuery: {
|
consoleQuery: {
|
||||||
|
systemFeatures: {
|
||||||
|
get: {
|
||||||
|
queryKey: () => ['console', 'systemFeatures', 'get'],
|
||||||
|
queryOptions: (options?: Record<string, unknown>) => ({
|
||||||
|
queryKey: ['console', 'systemFeatures', 'get'],
|
||||||
|
queryFn: () => new Promise(() => {}),
|
||||||
|
...options,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
notification: {
|
notification: {
|
||||||
get: {
|
get: {
|
||||||
queryOptions: (options?: Record<string, unknown>) => ({
|
queryOptions: (options?: Record<string, unknown>) => ({
|
||||||
@@ -56,6 +55,9 @@ const createWrapper = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
queryClient.setQueryData(['console', 'systemFeatures', 'get'], {
|
||||||
|
deployment_edition: mockEdition.value,
|
||||||
|
})
|
||||||
|
|
||||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
@@ -67,7 +69,7 @@ const createWrapper = () => {
|
|||||||
describe('InSiteMessageNotification', () => {
|
describe('InSiteMessageNotification', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockConfig.isCloudEdition = true
|
mockEdition.value = 'CLOUD'
|
||||||
vi.stubGlobal('open', vi.fn())
|
vi.stubGlobal('open', vi.fn())
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ describe('InSiteMessageNotification', () => {
|
|||||||
// Validate query gating and empty state rendering.
|
// Validate query gating and empty state rendering.
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('should render null and skip query when not cloud edition', async () => {
|
it('should render null and skip query when not cloud edition', async () => {
|
||||||
mockConfig.isCloudEdition = false
|
mockEdition.value = 'COMMUNITY'
|
||||||
const Wrapper = createWrapper()
|
const Wrapper = createWrapper()
|
||||||
const { container } = render(<InSiteMessageNotification />, { wrapper: Wrapper })
|
const { container } = render(<InSiteMessageNotification />, { wrapper: Wrapper })
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { InSiteMessageActionItem } from './index'
|
import type { InSiteMessageActionItem } from './index'
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { consoleQuery } from '@/service/client'
|
import { consoleQuery } from '@/service/client'
|
||||||
import InSiteMessage from './index'
|
import InSiteMessage from './index'
|
||||||
|
|
||||||
@@ -56,20 +56,25 @@ function parseNotificationBody(body: string): NotificationBodyPayload | null {
|
|||||||
|
|
||||||
function InSiteMessageNotification() {
|
function InSiteMessageNotification() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
|
const isCloudEdition = deploymentEdition === 'CLOUD'
|
||||||
const dismissNotificationMutation = useMutation(
|
const dismissNotificationMutation = useMutation(
|
||||||
consoleQuery.notification.dismiss.post.mutationOptions(),
|
consoleQuery.notification.dismiss.post.mutationOptions(),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { data } = useQuery(
|
const { data } = useQuery(
|
||||||
consoleQuery.notification.get.queryOptions({
|
consoleQuery.notification.get.queryOptions({
|
||||||
enabled: IS_CLOUD_EDITION,
|
enabled: isCloudEdition,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const notification = data?.notifications?.[0]
|
const notification = data?.notifications?.[0]
|
||||||
const parsedBody = notification ? parseNotificationBody(notification.body) : null
|
const parsedBody = notification ? parseNotificationBody(notification.body) : null
|
||||||
|
|
||||||
if (!IS_CLOUD_EDITION || !notification || !notification.notification_id) return null
|
if (!isCloudEdition || !notification || !notification.notification_id) return null
|
||||||
|
|
||||||
const notificationId = notification.notification_id
|
const notificationId = notification.notification_id
|
||||||
const fallbackActions: InSiteMessageActionItem[] = [
|
const fallbackActions: InSiteMessageActionItem[] = [
|
||||||
|
|||||||
@@ -5,17 +5,10 @@ import { Plan } from '@/app/components/billing/type'
|
|||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { useModalContextSelector } from '@/context/modal-context'
|
import { useModalContextSelector } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||||
import { render } from '@/test/console/render'
|
import { render } from '@/test/console/render'
|
||||||
import { ArchivedLogsNotice } from '../archived-logs-notice'
|
import { ArchivedLogsNotice } from '../archived-logs-notice'
|
||||||
|
|
||||||
vi.mock('@/config', async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import('@/config')>()
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
IS_CLOUD_EDITION: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock('@/context/workspace-state', async () => {
|
vi.mock('@/context/workspace-state', async () => {
|
||||||
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
|
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
|
||||||
|
|
||||||
@@ -57,6 +50,12 @@ function mockProviderPlan(planType: Plan) {
|
|||||||
|
|
||||||
describe('ArchivedLogsNotice', () => {
|
describe('ArchivedLogsNotice', () => {
|
||||||
const setShowAccountSettingModal = vi.fn()
|
const setShowAccountSettingModal = vi.fn()
|
||||||
|
const renderNotice = () => {
|
||||||
|
const { wrapper } = createConsoleQueryWrapper({
|
||||||
|
systemFeatures: { deployment_edition: 'CLOUD' },
|
||||||
|
})
|
||||||
|
return render(<ArchivedLogsNotice />, { wrapper })
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
@@ -69,7 +68,7 @@ describe('ArchivedLogsNotice', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show notice for paid workspace managers', () => {
|
it('should show notice for paid workspace managers', () => {
|
||||||
render(<ArchivedLogsNotice />)
|
renderNotice()
|
||||||
|
|
||||||
expect(screen.getByText('appLog.archives.notice.description')).toBeInTheDocument()
|
expect(screen.getByText('appLog.archives.notice.description')).toBeInTheDocument()
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'appLog.archives.notice.action' }))
|
fireEvent.click(screen.getByRole('button', { name: 'appLog.archives.notice.action' }))
|
||||||
@@ -81,7 +80,7 @@ describe('ArchivedLogsNotice', () => {
|
|||||||
it('should not show notice for sandbox workspaces', () => {
|
it('should not show notice for sandbox workspaces', () => {
|
||||||
mockProviderPlan(Plan.sandbox)
|
mockProviderPlan(Plan.sandbox)
|
||||||
|
|
||||||
render(<ArchivedLogsNotice />)
|
renderNotice()
|
||||||
|
|
||||||
expect(screen.queryByText('appLog.archives.notice.description')).not.toBeInTheDocument()
|
expect(screen.queryByText('appLog.archives.notice.description')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
|
||||||
import { useModalContextSelector } from '@/context/modal-context'
|
import { useModalContextSelector } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state'
|
import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
|
|
||||||
export function ArchivedLogsNotice() {
|
export function ArchivedLogsNotice() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom)
|
const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom)
|
||||||
const { enableBilling, plan } = useProviderContext()
|
const { enableBilling, plan } = useProviderContext()
|
||||||
const setShowAccountSettingModal = useModalContextSelector(
|
const setShowAccountSettingModal = useModalContextSelector(
|
||||||
@@ -18,7 +23,7 @@ export function ArchivedLogsNotice() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!IS_CLOUD_EDITION ||
|
deploymentEdition !== 'CLOUD' ||
|
||||||
!isCurrentWorkspaceManager ||
|
!isCurrentWorkspaceManager ||
|
||||||
!enableBilling ||
|
!enableBilling ||
|
||||||
plan.type === Plan.sandbox
|
plan.type === Plan.sandbox
|
||||||
|
|||||||
@@ -313,6 +313,7 @@ describe('app-card-utils', () => {
|
|||||||
expect(snippet).toContain('name: "Alice"')
|
expect(snippet).toContain('name: "Alice"')
|
||||||
expect(snippet).toContain('count: "5"')
|
expect(snippet).toContain('count: "5"')
|
||||||
expect(snippet).toContain('background-color: #FF0000')
|
expect(snippet).toContain('background-color: #FF0000')
|
||||||
|
expect(snippet).toContain(`baseUrl: 'https://example.com${basePath}'`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should generate an embedded script snippet with empty inputs comment', () => {
|
it('should generate an embedded script snippet with empty inputs comment', () => {
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import {
|
|||||||
interactions,
|
interactions,
|
||||||
mockUseModalContext,
|
mockUseModalContext,
|
||||||
scenarios,
|
scenarios,
|
||||||
|
setDeploymentEdition,
|
||||||
} from './test-utils'
|
} from './test-utils'
|
||||||
|
|
||||||
vi.mock('@/config', () => ({ IS_CE_EDITION: false }))
|
|
||||||
|
|
||||||
afterEach(cleanup)
|
afterEach(cleanup)
|
||||||
|
|
||||||
describe('APIKeyInfoPanel - Cloud Edition', () => {
|
describe('APIKeyInfoPanel - Cloud Edition', () => {
|
||||||
@@ -16,6 +15,7 @@ describe('APIKeyInfoPanel - Cloud Edition', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
clearAllMocks()
|
clearAllMocks()
|
||||||
|
setDeploymentEdition('CLOUD')
|
||||||
mockUseModalContext.mockReturnValue({
|
mockUseModalContext.mockReturnValue({
|
||||||
...defaultModalContext,
|
...defaultModalContext,
|
||||||
setShowAccountSettingModal,
|
setShowAccountSettingModal,
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import {
|
|||||||
interactions,
|
interactions,
|
||||||
mockUseModalContext,
|
mockUseModalContext,
|
||||||
scenarios,
|
scenarios,
|
||||||
|
setDeploymentEdition,
|
||||||
textKeys,
|
textKeys,
|
||||||
} from './test-utils'
|
} from './test-utils'
|
||||||
|
|
||||||
vi.mock('@/config', () => ({ IS_CE_EDITION: true }))
|
|
||||||
|
|
||||||
afterEach(cleanup)
|
afterEach(cleanup)
|
||||||
|
|
||||||
describe('APIKeyInfoPanel - Community Edition', () => {
|
describe('APIKeyInfoPanel - Community Edition', () => {
|
||||||
@@ -17,6 +16,7 @@ describe('APIKeyInfoPanel - Community Edition', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
clearAllMocks()
|
clearAllMocks()
|
||||||
|
setDeploymentEdition('COMMUNITY')
|
||||||
mockUseModalContext.mockReturnValue({
|
mockUseModalContext.mockReturnValue({
|
||||||
...defaultModalContext,
|
...defaultModalContext,
|
||||||
setShowAccountSettingModal,
|
setShowAccountSettingModal,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen'
|
||||||
import type { RenderOptions } from '@testing-library/react'
|
import type { RenderOptions } from '@testing-library/react'
|
||||||
import type { Mock, MockedFunction } from 'vitest'
|
import type { Mock, MockedFunction } from 'vitest'
|
||||||
import type { ModalContextState } from '@/context/modal-context'
|
import type { ModalContextState } from '@/context/modal-context'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { noop } from 'es-toolkit/function'
|
import { noop } from 'es-toolkit/function'
|
||||||
import { defaultPlan } from '@/app/components/billing/config'
|
import { defaultPlan } from '@/app/components/billing/config'
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
useModalContextSelector as actualUseModalContextSelector,
|
useModalContextSelector as actualUseModalContextSelector,
|
||||||
} from '@/context/modal-context'
|
} from '@/context/modal-context'
|
||||||
import { useProviderContext as actualUseProviderContext } from '@/context/provider-context'
|
import { useProviderContext as actualUseProviderContext } from '@/context/provider-context'
|
||||||
|
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||||
import APIKeyInfoPanel from '../index'
|
import APIKeyInfoPanel from '../index'
|
||||||
|
|
||||||
const { mockRouterPush } = vi.hoisted(() => ({
|
const { mockRouterPush } = vi.hoisted(() => ({
|
||||||
@@ -102,6 +104,7 @@ type APIKeyInfoPanelRenderOptions = {
|
|||||||
} & Omit<RenderOptions, 'wrapper'>
|
} & Omit<RenderOptions, 'wrapper'>
|
||||||
|
|
||||||
const mainButtonName = /appOverview\.apiKeyInfo\.setAPIBtn/
|
const mainButtonName = /appOverview\.apiKeyInfo\.setAPIBtn/
|
||||||
|
let deploymentEdition: DeploymentEdition = 'COMMUNITY'
|
||||||
|
|
||||||
// Setup function to configure mocks
|
// Setup function to configure mocks
|
||||||
function setupMocks(overrides: MockOverrides = {}) {
|
function setupMocks(overrides: MockOverrides = {}) {
|
||||||
@@ -129,7 +132,10 @@ function renderAPIKeyInfoPanel(options: APIKeyInfoPanelRenderOptions = {}) {
|
|||||||
|
|
||||||
setupMocks(mockOverrides)
|
setupMocks(mockOverrides)
|
||||||
|
|
||||||
return render(<APIKeyInfoPanel />, renderOptions)
|
return renderWithConsoleQuery(<APIKeyInfoPanel />, {
|
||||||
|
...renderOptions,
|
||||||
|
systemFeatures: { deployment_edition: deploymentEdition },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for common test scenarios
|
// Helper functions for common test scenarios
|
||||||
@@ -200,5 +206,9 @@ export function clearAllMocks() {
|
|||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setDeploymentEdition(value: DeploymentEdition) {
|
||||||
|
deploymentEdition = value
|
||||||
|
}
|
||||||
|
|
||||||
// Export mock functions for external access
|
// Export mock functions for external access
|
||||||
export { defaultModalContext, mockUseModalContext }
|
export { defaultModalContext, mockUseModalContext }
|
||||||
|
|||||||
@@ -3,17 +3,22 @@ import type { FC } from 'react'
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
|
|
||||||
const APIKeyInfoPanel: FC = () => {
|
const APIKeyInfoPanel: FC = () => {
|
||||||
const isCloud = !IS_CE_EDITION
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
|
const isCloud = deploymentEdition === 'CLOUD'
|
||||||
|
|
||||||
const { isAPIKeySet } = useProviderContext()
|
const { isAPIKeySet } = useProviderContext()
|
||||||
const openIntegrationsSetting = useIntegrationsSetting()
|
const openIntegrationsSetting = useIntegrationsSetting()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { InputVar } from '@/app/components/workflow/types'
|
|||||||
import type { AppDetailResponse } from '@/models/app'
|
import type { AppDetailResponse } from '@/models/app'
|
||||||
import type { AppSSO } from '@/types/app'
|
import type { AppSSO } from '@/types/app'
|
||||||
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
|
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { basePath } from '@/utils/var'
|
import { basePath } from '@/utils/var'
|
||||||
@@ -177,20 +176,16 @@ export const getEmbeddedScriptSnippet = ({
|
|||||||
primaryColor: string
|
primaryColor: string
|
||||||
isTestEnv?: boolean
|
isTestEnv?: boolean
|
||||||
inputValues: Record<string, WorkflowLaunchInputValue>
|
inputValues: Record<string, WorkflowLaunchInputValue>
|
||||||
}) =>
|
}) => {
|
||||||
`<script>
|
return `<script>
|
||||||
window.difyChatbotConfig = {
|
window.difyChatbotConfig = {
|
||||||
token: '${token}'${
|
token: '${token}'${
|
||||||
isTestEnv
|
isTestEnv
|
||||||
? `,
|
? `,
|
||||||
isDev: true`
|
isDev: true`
|
||||||
: ''
|
: ''
|
||||||
}${
|
},
|
||||||
IS_CE_EDITION
|
baseUrl: '${url}${basePath}'${
|
||||||
? `,
|
|
||||||
baseUrl: '${url}${basePath}'`
|
|
||||||
: ''
|
|
||||||
}${
|
|
||||||
webAppRoute !== 'chatbot'
|
webAppRoute !== 'chatbot'
|
||||||
? `,
|
? `,
|
||||||
routeSegment: '${webAppRoute}'`
|
routeSegment: '${webAppRoute}'`
|
||||||
@@ -221,6 +216,7 @@ export const getEmbeddedScriptSnippet = ({
|
|||||||
height: 40rem !important;
|
height: 40rem !important;
|
||||||
}
|
}
|
||||||
</style>`
|
</style>`
|
||||||
|
}
|
||||||
|
|
||||||
export const getChromePluginContent = (iframeUrl: string) => `ChatBot URL: ${iframeUrl}`
|
export const getChromePluginContent = (iframeUrl: string) => `ChatBot URL: ${iframeUrl}`
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,10 @@ vi.mock('@/service/client', () => ({
|
|||||||
systemFeatures: {
|
systemFeatures: {
|
||||||
get: {
|
get: {
|
||||||
queryKey: () => ['console', 'systemFeatures', 'get'],
|
queryKey: () => ['console', 'systemFeatures', 'get'],
|
||||||
|
queryOptions: (options: Record<string, unknown> = {}) => ({
|
||||||
|
queryKey: ['console', 'systemFeatures', 'get'],
|
||||||
|
...options,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
|
|
||||||
const mockConfig = vi.hoisted(() => ({
|
const mockConfig = vi.hoisted(() => ({
|
||||||
AMPLITUDE_API_KEY: 'test-api-key',
|
AMPLITUDE_API_KEY: 'test-api-key',
|
||||||
IS_CLOUD_EDITION: true,
|
|
||||||
}))
|
}))
|
||||||
const mockConsent = vi.hoisted(() => ({
|
const mockConsent = vi.hoisted(() => ({
|
||||||
value: 'granted' as 'unknown' | 'denied' | 'granted',
|
value: 'granted' as 'unknown' | 'denied' | 'granted',
|
||||||
@@ -17,12 +16,6 @@ vi.mock('@/config', () => ({
|
|||||||
get AMPLITUDE_API_KEY() {
|
get AMPLITUDE_API_KEY() {
|
||||||
return mockConfig.AMPLITUDE_API_KEY
|
return mockConfig.AMPLITUDE_API_KEY
|
||||||
},
|
},
|
||||||
get IS_CLOUD_EDITION() {
|
|
||||||
return mockConfig.IS_CLOUD_EDITION
|
|
||||||
},
|
|
||||||
get isAmplitudeEnabled() {
|
|
||||||
return mockConfig.IS_CLOUD_EDITION && !!mockConfig.AMPLITUDE_API_KEY
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@amplitude/analytics-browser', () => ({
|
vi.mock('@amplitude/analytics-browser', () => ({
|
||||||
@@ -44,7 +37,6 @@ describe('AmplitudeProvider', () => {
|
|||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockConfig.AMPLITUDE_API_KEY = 'test-api-key'
|
mockConfig.AMPLITUDE_API_KEY = 'test-api-key'
|
||||||
mockConfig.IS_CLOUD_EDITION = true
|
|
||||||
mockConsent.value = 'granted'
|
mockConsent.value = 'granted'
|
||||||
;({ AmplitudeProvider } = await import('../AmplitudeProvider'))
|
;({ AmplitudeProvider } = await import('../AmplitudeProvider'))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
|
|
||||||
const mockConfig = vi.hoisted(() => ({
|
const mockConfig = vi.hoisted(() => ({
|
||||||
AMPLITUDE_API_KEY: 'test-api-key',
|
AMPLITUDE_API_KEY: 'test-api-key',
|
||||||
IS_CLOUD_EDITION: true,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
let ensureAmplitudeInitialized: typeof import('../init').ensureAmplitudeInitialized
|
let ensureAmplitudeInitialized: typeof import('../init').ensureAmplitudeInitialized
|
||||||
@@ -13,12 +12,6 @@ vi.mock('@/config', () => ({
|
|||||||
get AMPLITUDE_API_KEY() {
|
get AMPLITUDE_API_KEY() {
|
||||||
return mockConfig.AMPLITUDE_API_KEY
|
return mockConfig.AMPLITUDE_API_KEY
|
||||||
},
|
},
|
||||||
get IS_CLOUD_EDITION() {
|
|
||||||
return mockConfig.IS_CLOUD_EDITION
|
|
||||||
},
|
|
||||||
get isAmplitudeEnabled() {
|
|
||||||
return mockConfig.IS_CLOUD_EDITION && !!mockConfig.AMPLITUDE_API_KEY
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@amplitude/analytics-browser', () => ({
|
vi.mock('@amplitude/analytics-browser', () => ({
|
||||||
@@ -36,7 +29,6 @@ describe('amplitude init helper', () => {
|
|||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockConfig.AMPLITUDE_API_KEY = 'test-api-key'
|
mockConfig.AMPLITUDE_API_KEY = 'test-api-key'
|
||||||
mockConfig.IS_CLOUD_EDITION = true
|
|
||||||
;({ ensureAmplitudeInitialized } = await import('../init'))
|
;({ ensureAmplitudeInitialized } = await import('../init'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { flushEvents, resetUser, setUserId, setUserProperties, trackEvent } from
|
|||||||
|
|
||||||
const mockState = vi.hoisted(() => ({
|
const mockState = vi.hoisted(() => ({
|
||||||
consent: 'granted' as 'unknown' | 'denied' | 'granted',
|
consent: 'granted' as 'unknown' | 'denied' | 'granted',
|
||||||
enabled: true,
|
|
||||||
initialized: true,
|
initialized: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -24,12 +23,6 @@ const MockIdentify = vi.hoisted(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
vi.mock('@/config', () => ({
|
|
||||||
get isAmplitudeEnabled() {
|
|
||||||
return mockState.enabled
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/base/analytics-consent/consent-store', () => ({
|
vi.mock('@/app/components/base/analytics-consent/consent-store', () => ({
|
||||||
getAnalyticsConsent: () => mockState.consent,
|
getAnalyticsConsent: () => mockState.consent,
|
||||||
}))
|
}))
|
||||||
@@ -51,12 +44,11 @@ describe('amplitude utils', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockState.consent = 'granted'
|
mockState.consent = 'granted'
|
||||||
mockState.enabled = true
|
|
||||||
mockState.initialized = true
|
mockState.initialized = true
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('trackEvent', () => {
|
describe('trackEvent', () => {
|
||||||
it('should call amplitude.track and return its result when amplitude is enabled', () => {
|
it('should call amplitude.track and return its result when the consented SDK is initialized', () => {
|
||||||
const trackResult = { promise: Promise.resolve({}) }
|
const trackResult = { promise: Promise.resolve({}) }
|
||||||
mockTrack.mockReturnValue(trackResult)
|
mockTrack.mockReturnValue(trackResult)
|
||||||
|
|
||||||
@@ -67,8 +59,8 @@ describe('amplitude utils', () => {
|
|||||||
expect(result).toBe(trackResult)
|
expect(result).toBe(trackResult)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not call amplitude.track when amplitude is disabled', () => {
|
it('should not call amplitude.track before the SDK initializes', () => {
|
||||||
mockState.enabled = false
|
mockState.initialized = false
|
||||||
|
|
||||||
trackEvent('dataset_created', { source: 'wizard' })
|
trackEvent('dataset_created', { source: 'wizard' })
|
||||||
|
|
||||||
@@ -93,7 +85,7 @@ describe('amplitude utils', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('flushEvents', () => {
|
describe('flushEvents', () => {
|
||||||
it('should call amplitude.flush and return its result when amplitude is enabled', () => {
|
it('should call amplitude.flush and return its result when the consented SDK is initialized', () => {
|
||||||
const flushResult = { promise: Promise.resolve() }
|
const flushResult = { promise: Promise.resolve() }
|
||||||
mockFlush.mockReturnValue(flushResult)
|
mockFlush.mockReturnValue(flushResult)
|
||||||
|
|
||||||
@@ -103,8 +95,8 @@ describe('amplitude utils', () => {
|
|||||||
expect(result).toBe(flushResult)
|
expect(result).toBe(flushResult)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not call amplitude.flush when amplitude is disabled', () => {
|
it('should not call amplitude.flush before the SDK initializes', () => {
|
||||||
mockState.enabled = false
|
mockState.initialized = false
|
||||||
|
|
||||||
flushEvents()
|
flushEvents()
|
||||||
|
|
||||||
@@ -121,15 +113,15 @@ describe('amplitude utils', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('setUserId', () => {
|
describe('setUserId', () => {
|
||||||
it('should call amplitude.setUserId when amplitude is enabled', () => {
|
it('should call amplitude.setUserId when the consented SDK is initialized', () => {
|
||||||
setUserId('user-123')
|
setUserId('user-123')
|
||||||
|
|
||||||
expect(mockSetUserId).toHaveBeenCalledTimes(1)
|
expect(mockSetUserId).toHaveBeenCalledTimes(1)
|
||||||
expect(mockSetUserId).toHaveBeenCalledWith('user-123')
|
expect(mockSetUserId).toHaveBeenCalledWith('user-123')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not call amplitude.setUserId when amplitude is disabled', () => {
|
it('should not call amplitude.setUserId before the SDK initializes', () => {
|
||||||
mockState.enabled = false
|
mockState.initialized = false
|
||||||
|
|
||||||
setUserId('user-123')
|
setUserId('user-123')
|
||||||
|
|
||||||
@@ -146,7 +138,7 @@ describe('amplitude utils', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('setUserProperties', () => {
|
describe('setUserProperties', () => {
|
||||||
it('should build identify event and call amplitude.identify when amplitude is enabled', () => {
|
it('should build an identify event when the consented SDK is initialized', () => {
|
||||||
const properties = {
|
const properties = {
|
||||||
role: 'owner',
|
role: 'owner',
|
||||||
seats: 3,
|
seats: 3,
|
||||||
@@ -165,8 +157,8 @@ describe('amplitude utils', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not call amplitude.identify when amplitude is disabled', () => {
|
it('should not call amplitude.identify before the SDK initializes', () => {
|
||||||
mockState.enabled = false
|
mockState.initialized = false
|
||||||
|
|
||||||
setUserProperties({ role: 'owner' })
|
setUserProperties({ role: 'owner' })
|
||||||
|
|
||||||
@@ -183,14 +175,14 @@ describe('amplitude utils', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('resetUser', () => {
|
describe('resetUser', () => {
|
||||||
it('should call amplitude.reset when amplitude is enabled', () => {
|
it('should call amplitude.reset when the consented SDK is initialized', () => {
|
||||||
resetUser()
|
resetUser()
|
||||||
|
|
||||||
expect(mockReset).toHaveBeenCalledTimes(1)
|
expect(mockReset).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not call amplitude.reset when amplitude is disabled', () => {
|
it('should not call amplitude.reset before the SDK initializes', () => {
|
||||||
mockState.enabled = false
|
mockState.initialized = false
|
||||||
|
|
||||||
resetUser()
|
resetUser()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as amplitude from '@amplitude/analytics-browser'
|
import * as amplitude from '@amplitude/analytics-browser'
|
||||||
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
|
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
|
||||||
import { AMPLITUDE_API_KEY, isAmplitudeEnabled } from '@/config'
|
import { AMPLITUDE_API_KEY } from '@/config'
|
||||||
|
|
||||||
export type AmplitudeInitializationOptions = {
|
export type AmplitudeInitializationOptions = {
|
||||||
sessionReplaySampleRate?: number
|
sessionReplaySampleRate?: number
|
||||||
@@ -61,7 +61,7 @@ const createPageNameEnrichmentPlugin = (): amplitude.Types.EnrichmentPlugin => {
|
|||||||
export const ensureAmplitudeInitialized = ({
|
export const ensureAmplitudeInitialized = ({
|
||||||
sessionReplaySampleRate = 0.5,
|
sessionReplaySampleRate = 0.5,
|
||||||
}: AmplitudeInitializationOptions = {}) => {
|
}: AmplitudeInitializationOptions = {}) => {
|
||||||
if (!isAmplitudeEnabled || isAmplitudeInitialized) return
|
if (!AMPLITUDE_API_KEY || isAmplitudeInitialized) return
|
||||||
|
|
||||||
isAmplitudeInitialized = true
|
isAmplitudeInitialized = true
|
||||||
|
|
||||||
@@ -90,6 +90,6 @@ export const ensureAmplitudeInitialized = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const setAmplitudeOptOut = (optOut: boolean) => {
|
export const setAmplitudeOptOut = (optOut: boolean) => {
|
||||||
if (!isAmplitudeEnabled || !isAmplitudeInitialized) return
|
if (!AMPLITUDE_API_KEY || !isAmplitudeInitialized) return
|
||||||
amplitude.setOptOut(optOut)
|
amplitude.setOptOut(optOut)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import * as amplitude from '@amplitude/analytics-browser'
|
import * as amplitude from '@amplitude/analytics-browser'
|
||||||
import { getAnalyticsConsent } from '@/app/components/base/analytics-consent/consent-store'
|
import { getAnalyticsConsent } from '@/app/components/base/analytics-consent/consent-store'
|
||||||
import { isAmplitudeEnabled } from '@/config'
|
|
||||||
import { getIsAmplitudeInitialized } from './init'
|
import { getIsAmplitudeInitialized } from './init'
|
||||||
|
|
||||||
const canUseAmplitude = () =>
|
const canUseAmplitude = () => getAnalyticsConsent() === 'granted' && getIsAmplitudeInitialized()
|
||||||
isAmplitudeEnabled && getAnalyticsConsent() === 'granted' && getIsAmplitudeInitialized()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track custom event
|
* Track custom event
|
||||||
|
|||||||
+14
-7
@@ -3,7 +3,6 @@ import { render } from '@testing-library/react'
|
|||||||
|
|
||||||
type ConfigState = {
|
type ConfigState = {
|
||||||
cookieYesSiteKey: string
|
cookieYesSiteKey: string
|
||||||
isCloudEdition: boolean
|
|
||||||
isProd: boolean
|
isProd: boolean
|
||||||
webPrefix: string | undefined
|
webPrefix: string | undefined
|
||||||
}
|
}
|
||||||
@@ -11,7 +10,6 @@ type ConfigState = {
|
|||||||
const { configState, mockHeadersGet } = vi.hoisted(() => ({
|
const { configState, mockHeadersGet } = vi.hoisted(() => ({
|
||||||
configState: {
|
configState: {
|
||||||
cookieYesSiteKey: 'site-key',
|
cookieYesSiteKey: 'site-key',
|
||||||
isCloudEdition: true,
|
|
||||||
isProd: true,
|
isProd: true,
|
||||||
webPrefix: 'https://cloud.dify.ai',
|
webPrefix: 'https://cloud.dify.ai',
|
||||||
} as ConfigState,
|
} as ConfigState,
|
||||||
@@ -22,9 +20,6 @@ vi.mock('@/config', () => ({
|
|||||||
get COOKIEYES_SITE_KEY() {
|
get COOKIEYES_SITE_KEY() {
|
||||||
return configState.cookieYesSiteKey
|
return configState.cookieYesSiteKey
|
||||||
},
|
},
|
||||||
get IS_CLOUD_EDITION() {
|
|
||||||
return configState.isCloudEdition
|
|
||||||
},
|
|
||||||
get IS_PROD() {
|
get IS_PROD() {
|
||||||
return configState.isProd
|
return configState.isProd
|
||||||
},
|
},
|
||||||
@@ -62,7 +57,7 @@ async function renderBoundary() {
|
|||||||
import('../cloud-analytics-boundary'),
|
import('../cloud-analytics-boundary'),
|
||||||
import('../cloud-analytics-state'),
|
import('../cloud-analytics-state'),
|
||||||
])
|
])
|
||||||
const state = getCloudAnalyticsBoundaryState({ get: mockHeadersGet })
|
const state = getCloudAnalyticsBoundaryState({ get: mockHeadersGet }, 'CLOUD')
|
||||||
const view = render(<CloudAnalyticsBoundary {...state} />)
|
const view = render(<CloudAnalyticsBoundary {...state} />)
|
||||||
return { ...view, state }
|
return { ...view, state }
|
||||||
}
|
}
|
||||||
@@ -72,7 +67,6 @@ describe('CloudAnalyticsBoundary', () => {
|
|||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
configState.cookieYesSiteKey = 'site-key'
|
configState.cookieYesSiteKey = 'site-key'
|
||||||
configState.isCloudEdition = true
|
|
||||||
configState.isProd = true
|
configState.isProd = true
|
||||||
configState.webPrefix = 'https://cloud.dify.ai'
|
configState.webPrefix = 'https://cloud.dify.ai'
|
||||||
mockHeadersGet.mockImplementation((name: string) => {
|
mockHeadersGet.mockImplementation((name: string) => {
|
||||||
@@ -143,4 +137,17 @@ describe('CloudAnalyticsBoundary', () => {
|
|||||||
expect(state.enabled).toBe(false)
|
expect(state.enabled).toBe(false)
|
||||||
expect(container.querySelector('script')).toBeNull()
|
expect(container.querySelector('script')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.each(['COMMUNITY', 'ENTERPRISE', null] as const)(
|
||||||
|
'disables analytics when deployment edition is %s',
|
||||||
|
async (deploymentEdition) => {
|
||||||
|
const { CloudAnalyticsBoundary } = await import('../cloud-analytics-boundary')
|
||||||
|
const { getCloudAnalyticsBoundaryState } = await import('../cloud-analytics-state')
|
||||||
|
const state = getCloudAnalyticsBoundaryState({ get: mockHeadersGet }, deploymentEdition)
|
||||||
|
const { container } = render(<CloudAnalyticsBoundary {...state} />)
|
||||||
|
|
||||||
|
expect(state.enabled).toBe(false)
|
||||||
|
expect(container.querySelector('script')).toBeNull()
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ vi.mock('@/app/components/base/amplitude', () => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/external-attribution-recorder', () => ({
|
||||||
|
default: () => <span data-testid="external-attribution-recorder" />,
|
||||||
|
}))
|
||||||
|
|
||||||
describe('CloudAnalyticsRuntime', () => {
|
describe('CloudAnalyticsRuntime', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockState.pathname = '/signin'
|
mockState.pathname = '/signin'
|
||||||
@@ -26,6 +30,7 @@ describe('CloudAnalyticsRuntime', () => {
|
|||||||
const { rerender } = render(<CloudAnalyticsRuntime />)
|
const { rerender } = render(<CloudAnalyticsRuntime />)
|
||||||
|
|
||||||
expect(screen.getByTestId('cookieyes-consent-bridge')).toBeInTheDocument()
|
expect(screen.getByTestId('cookieyes-consent-bridge')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('external-attribution-recorder')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('amplitude-provider')).toHaveAttribute('data-active', 'true')
|
expect(screen.getByTestId('amplitude-provider')).toHaveAttribute('data-active', 'true')
|
||||||
|
|
||||||
mockState.pathname = '/integrations'
|
mockState.pathname = '/integrations'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||||
|
import ExternalAttributionRecorder from '@/app/components/external-attribution-recorder'
|
||||||
import { usePathname } from '@/next/navigation'
|
import { usePathname } from '@/next/navigation'
|
||||||
import { CookieYesConsentBridge } from './cookieyes-consent-bridge'
|
import { CookieYesConsentBridge } from './cookieyes-consent-bridge'
|
||||||
import { isCloudAnalyticsPath } from './request-boundary'
|
import { isCloudAnalyticsPath } from './request-boundary'
|
||||||
@@ -12,6 +13,7 @@ export function CloudAnalyticsRuntime() {
|
|||||||
<>
|
<>
|
||||||
<CookieYesConsentBridge />
|
<CookieYesConsentBridge />
|
||||||
<AmplitudeProvider active={isCloudAnalyticsPath(pathname)} />
|
<AmplitudeProvider active={isCloudAnalyticsPath(pathname)} />
|
||||||
|
<ExternalAttributionRecorder />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { COOKIEYES_SITE_KEY, IS_CLOUD_EDITION, IS_PROD, WEB_PREFIX } from '@/config'
|
import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen'
|
||||||
|
import { COOKIEYES_SITE_KEY, IS_PROD, WEB_PREFIX } from '@/config'
|
||||||
import { isCloudAnalyticsRequest } from './request-boundary'
|
import { isCloudAnalyticsRequest } from './request-boundary'
|
||||||
|
|
||||||
const CURRENT_PATHNAME_HEADER = 'x-dify-pathname'
|
const CURRENT_PATHNAME_HEADER = 'x-dify-pathname'
|
||||||
@@ -15,12 +16,13 @@ export type CloudAnalyticsBoundaryState = {
|
|||||||
|
|
||||||
export function getCloudAnalyticsBoundaryState(
|
export function getCloudAnalyticsBoundaryState(
|
||||||
requestHeaders: RequestHeaders,
|
requestHeaders: RequestHeaders,
|
||||||
|
deploymentEdition: DeploymentEdition | null,
|
||||||
): CloudAnalyticsBoundaryState {
|
): CloudAnalyticsBoundaryState {
|
||||||
const pathname = requestHeaders.get(CURRENT_PATHNAME_HEADER) || '/'
|
const pathname = requestHeaders.get(CURRENT_PATHNAME_HEADER) || '/'
|
||||||
const requestHost = requestHeaders.get('x-forwarded-host') || requestHeaders.get('host')
|
const requestHost = requestHeaders.get('x-forwarded-host') || requestHeaders.get('host')
|
||||||
const enabled = isCloudAnalyticsRequest({
|
const enabled = isCloudAnalyticsRequest({
|
||||||
cookieYesSiteKey: COOKIEYES_SITE_KEY,
|
cookieYesSiteKey: COOKIEYES_SITE_KEY,
|
||||||
isCloudEdition: IS_CLOUD_EDITION,
|
isCloudEdition: deploymentEdition === 'CLOUD',
|
||||||
isProd: IS_PROD,
|
isProd: IS_PROD,
|
||||||
pathname,
|
pathname,
|
||||||
requestHost,
|
requestHost,
|
||||||
|
|||||||
+3
-2
@@ -1,7 +1,8 @@
|
|||||||
import type { ModerationConfig } from '@/models/debug'
|
import type { ModerationConfig } from '@/models/debug'
|
||||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
import { act, fireEvent, screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import * as i18n from 'react-i18next'
|
import * as i18n from 'react-i18next'
|
||||||
|
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||||
import { withSelectorKey } from '@/test/i18n-mock'
|
import { withSelectorKey } from '@/test/i18n-mock'
|
||||||
import ModerationSettingModal from '../moderation-setting-modal'
|
import ModerationSettingModal from '../moderation-setting-modal'
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ const defaultData: ModerationConfig = {
|
|||||||
|
|
||||||
describe('ModerationSettingModal', () => {
|
describe('ModerationSettingModal', () => {
|
||||||
const onSave = vi.fn()
|
const onSave = vi.fn()
|
||||||
const renderModal = async (ui: React.ReactNode) => {
|
const renderModal = async (ui: React.ReactElement) => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(ui)
|
render(ui)
|
||||||
await Promise.resolve()
|
await Promise.resolve()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
import Zendesk from '../index'
|
import Zendesk from '../index'
|
||||||
|
|
||||||
// Shared state for mocks
|
// Shared state for mocks
|
||||||
let mockIsCeEdition = false
|
let mockDeploymentEdition: 'COMMUNITY' | 'ENTERPRISE' | 'CLOUD' | null = 'CLOUD'
|
||||||
let mockZendeskWidgetKey: string | undefined = 'test-key'
|
let mockZendeskWidgetKey: string | undefined = 'test-key'
|
||||||
let mockIsProd = false
|
let mockIsProd = false
|
||||||
let mockNonce: string | null = 'test-nonce'
|
let mockNonce: string | null = 'test-nonce'
|
||||||
@@ -18,11 +18,7 @@ vi.mock('react', async (importOriginal) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mock config
|
|
||||||
vi.mock('@/config', () => ({
|
vi.mock('@/config', () => ({
|
||||||
get IS_CE_EDITION() {
|
|
||||||
return mockIsCeEdition
|
|
||||||
},
|
|
||||||
get ZENDESK_WIDGET_KEY() {
|
get ZENDESK_WIDGET_KEY() {
|
||||||
return mockZendeskWidgetKey
|
return mockZendeskWidgetKey
|
||||||
},
|
},
|
||||||
@@ -31,6 +27,16 @@ vi.mock('@/config', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/query-client-server', () => ({
|
||||||
|
getQueryClientServer: () => ({
|
||||||
|
ensureQueryData: vi.fn(async () => ({ deployment_edition: mockDeploymentEdition })),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/features/system-features/server', () => ({
|
||||||
|
serverSystemFeaturesQueryOptions: vi.fn(() => ({})),
|
||||||
|
}))
|
||||||
|
|
||||||
// Mock next/headers
|
// Mock next/headers
|
||||||
vi.mock('@/next/headers', () => ({
|
vi.mock('@/next/headers', () => ({
|
||||||
headers: vi.fn(() => ({
|
headers: vi.fn(() => ({
|
||||||
@@ -61,7 +67,7 @@ vi.mock('@/next/script', () => ({
|
|||||||
describe('Zendesk', () => {
|
describe('Zendesk', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockIsCeEdition = false
|
mockDeploymentEdition = 'CLOUD'
|
||||||
mockZendeskWidgetKey = 'test-key'
|
mockZendeskWidgetKey = 'test-key'
|
||||||
mockIsProd = false
|
mockIsProd = false
|
||||||
mockNonce = 'test-nonce'
|
mockNonce = 'test-nonce'
|
||||||
@@ -73,11 +79,14 @@ describe('Zendesk', () => {
|
|||||||
return await Component()
|
return await Component()
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should render nothing when IS_CE_EDITION is true', async () => {
|
it.each(['COMMUNITY', 'ENTERPRISE', null] as const)(
|
||||||
mockIsCeEdition = true
|
'should render nothing when deployment edition is %s',
|
||||||
|
async (deploymentEdition) => {
|
||||||
|
mockDeploymentEdition = deploymentEdition
|
||||||
const result = await renderZendesk()
|
const result = await renderZendesk()
|
||||||
expect(result).toBeNull()
|
expect(result).toBeNull()
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
it('should render nothing when ZENDESK_WIDGET_KEY is missing', async () => {
|
it('should render nothing when ZENDESK_WIDGET_KEY is missing', async () => {
|
||||||
mockZendeskWidgetKey = undefined
|
mockZendeskWidgetKey = undefined
|
||||||
|
|||||||
@@ -1,32 +1,27 @@
|
|||||||
|
import { openZendeskWindow, setZendeskConversationFields } from '../utils'
|
||||||
|
|
||||||
describe('zendesk/utils', () => {
|
describe('zendesk/utils', () => {
|
||||||
// Create mock for window.zE
|
|
||||||
const mockZE = vi.fn()
|
const mockZE = vi.fn()
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetModules()
|
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
// Set up window.zE mock before each test
|
|
||||||
window.zE = mockZE
|
window.zE = mockZE
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers()
|
vi.useRealTimers()
|
||||||
// Clean up window.zE after each test
|
|
||||||
window.zE = mockZE
|
window.zE = mockZE
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('setZendeskConversationFields', () => {
|
describe('setZendeskConversationFields', () => {
|
||||||
it('should call window.zE with correct arguments when not CE edition and zE exists', async () => {
|
it('sets conversation fields in Cloud when zE exists', () => {
|
||||||
vi.doMock('@/config', () => ({ IS_CE_EDITION: false }))
|
|
||||||
const { setZendeskConversationFields } = await import('../utils')
|
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
{ id: 'field1', value: 'value1' },
|
{ id: 'field1', value: 'value1' },
|
||||||
{ id: 'field2', value: 'value2' },
|
{ id: 'field2', value: 'value2' },
|
||||||
]
|
]
|
||||||
const callback = vi.fn()
|
const callback = vi.fn()
|
||||||
|
|
||||||
setZendeskConversationFields(fields, callback)
|
setZendeskConversationFields(fields, 'CLOUD', callback)
|
||||||
|
|
||||||
expect(window.zE).toHaveBeenCalledWith(
|
expect(window.zE).toHaveBeenCalledWith(
|
||||||
'messenger:set',
|
'messenger:set',
|
||||||
@@ -36,24 +31,19 @@ describe('zendesk/utils', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not call window.zE when IS_CE_EDITION is true', async () => {
|
it.each(['COMMUNITY', 'ENTERPRISE', null] as const)(
|
||||||
vi.doMock('@/config', () => ({ IS_CE_EDITION: true }))
|
'does not set fields when deployment edition is %s',
|
||||||
const { setZendeskConversationFields } = await import('../utils')
|
(deploymentEdition) => {
|
||||||
|
setZendeskConversationFields([{ id: 'field1', value: 'value1' }], deploymentEdition)
|
||||||
const fields = [{ id: 'field1', value: 'value1' }]
|
|
||||||
|
|
||||||
setZendeskConversationFields(fields)
|
|
||||||
|
|
||||||
expect(window.zE).not.toHaveBeenCalled()
|
expect(window.zE).not.toHaveBeenCalled()
|
||||||
})
|
},
|
||||||
|
)
|
||||||
it('should work without callback', async () => {
|
|
||||||
vi.doMock('@/config', () => ({ IS_CE_EDITION: false }))
|
|
||||||
const { setZendeskConversationFields } = await import('../utils')
|
|
||||||
|
|
||||||
|
it('works without a callback', () => {
|
||||||
const fields = [{ id: 'field1', value: 'value1' }]
|
const fields = [{ id: 'field1', value: 'value1' }]
|
||||||
|
|
||||||
setZendeskConversationFields(fields)
|
setZendeskConversationFields(fields, 'CLOUD')
|
||||||
|
|
||||||
expect(window.zE).toHaveBeenCalledWith(
|
expect(window.zE).toHaveBeenCalledWith(
|
||||||
'messenger:set',
|
'messenger:set',
|
||||||
@@ -65,38 +55,32 @@ describe('zendesk/utils', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('openZendeskWindow', () => {
|
describe('openZendeskWindow', () => {
|
||||||
it('should show and open messenger when zE exists', async () => {
|
it('shows and opens the messenger in Cloud when zE exists', () => {
|
||||||
vi.doMock('@/config', () => ({ IS_CE_EDITION: false }))
|
openZendeskWindow('CLOUD')
|
||||||
const { openZendeskWindow } = await import('../utils')
|
|
||||||
|
|
||||||
openZendeskWindow()
|
|
||||||
|
|
||||||
expect(window.zE).toHaveBeenCalledWith('messenger', 'show')
|
expect(window.zE).toHaveBeenCalledWith('messenger', 'show')
|
||||||
expect(window.zE).toHaveBeenCalledWith('messenger', 'open')
|
expect(window.zE).toHaveBeenCalledWith('messenger', 'open')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should retry opening until zE is ready', async () => {
|
it('retries opening until zE is ready', () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
vi.doMock('@/config', () => ({ IS_CE_EDITION: false }))
|
|
||||||
const { openZendeskWindow } = await import('../utils')
|
|
||||||
|
|
||||||
window.zE = undefined
|
window.zE = undefined
|
||||||
openZendeskWindow({ interval: 10, retries: 2 })
|
|
||||||
|
openZendeskWindow('CLOUD', { interval: 10, retries: 2 })
|
||||||
window.zE = mockZE
|
window.zE = mockZE
|
||||||
vi.advanceTimersByTime(10)
|
vi.advanceTimersByTime(10)
|
||||||
|
|
||||||
expect(window.zE).toHaveBeenCalledWith('messenger', 'show')
|
expect(window.zE).toHaveBeenCalledWith('messenger', 'show')
|
||||||
expect(window.zE).toHaveBeenCalledWith('messenger', 'open')
|
expect(window.zE).toHaveBeenCalledWith('messenger', 'open')
|
||||||
vi.useRealTimers()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not call window.zE when IS_CE_EDITION is true', async () => {
|
it.each(['COMMUNITY', 'ENTERPRISE', null] as const)(
|
||||||
vi.doMock('@/config', () => ({ IS_CE_EDITION: true }))
|
'does not open when deployment edition is %s',
|
||||||
const { openZendeskWindow } = await import('../utils')
|
(deploymentEdition) => {
|
||||||
|
openZendeskWindow(deploymentEdition)
|
||||||
openZendeskWindow()
|
|
||||||
|
|
||||||
expect(window.zE).not.toHaveBeenCalled()
|
expect(window.zE).not.toHaveBeenCalled()
|
||||||
})
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { IS_CE_EDITION, IS_PROD, ZENDESK_WIDGET_KEY } from '@/config'
|
import { IS_PROD, ZENDESK_WIDGET_KEY } from '@/config'
|
||||||
|
import { getQueryClientServer } from '@/context/query-client-server'
|
||||||
|
import { serverSystemFeaturesQueryOptions } from '@/features/system-features/server'
|
||||||
import { headers } from '@/next/headers'
|
import { headers } from '@/next/headers'
|
||||||
import Script from '@/next/script'
|
import Script from '@/next/script'
|
||||||
|
|
||||||
const Zendesk = async () => {
|
const Zendesk = async () => {
|
||||||
if (IS_CE_EDITION || !ZENDESK_WIDGET_KEY) return null
|
if (!ZENDESK_WIDGET_KEY) return null
|
||||||
|
|
||||||
|
const systemFeatures = await getQueryClientServer().ensureQueryData(
|
||||||
|
serverSystemFeaturesQueryOptions(),
|
||||||
|
)
|
||||||
|
if (systemFeatures.deployment_edition !== 'CLOUD') return null
|
||||||
|
|
||||||
const nonce = IS_PROD ? ((await headers()).get('x-nonce') ?? '') : ''
|
const nonce = IS_PROD ? ((await headers()).get('x-nonce') ?? '') : ''
|
||||||
/* v8 ignore next -- `nonce` is always a string (`''` or header value), so nullish fallback is unreachable in runtime. @preserve */
|
/* v8 ignore next -- `nonce` is always a string (`''` or header value), so nullish fallback is unreachable in runtime. @preserve */
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IS_CE_EDITION } from '@/config'
|
import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen'
|
||||||
|
|
||||||
type ConversationField = {
|
type ConversationField = {
|
||||||
id: string
|
id: string
|
||||||
@@ -19,9 +19,10 @@ declare global {
|
|||||||
|
|
||||||
export const setZendeskConversationFields = (
|
export const setZendeskConversationFields = (
|
||||||
fields: ConversationField[],
|
fields: ConversationField[],
|
||||||
|
deploymentEdition: DeploymentEdition | null,
|
||||||
callback?: () => unknown,
|
callback?: () => unknown,
|
||||||
) => {
|
) => {
|
||||||
if (!IS_CE_EDITION && window.zE)
|
if (deploymentEdition === 'CLOUD' && window.zE)
|
||||||
window.zE('messenger:set', 'conversationFields', fields, callback)
|
window.zE('messenger:set', 'conversationFields', fields, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,25 +31,25 @@ type OpenZendeskWindowOptions = {
|
|||||||
retries?: number
|
retries?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const openZendeskWindowOnce = () => {
|
const openZendeskWindowOnce = (deploymentEdition: DeploymentEdition | null) => {
|
||||||
if (IS_CE_EDITION || !window.zE) return false
|
if (deploymentEdition !== 'CLOUD' || !window.zE) return false
|
||||||
|
|
||||||
window.zE('messenger', 'show')
|
window.zE('messenger', 'show')
|
||||||
window.zE('messenger', 'open')
|
window.zE('messenger', 'open')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openZendeskWindow = ({
|
export const openZendeskWindow = (
|
||||||
interval = 100,
|
deploymentEdition: DeploymentEdition | null,
|
||||||
retries = 20,
|
{ interval = 100, retries = 20 }: OpenZendeskWindowOptions = {},
|
||||||
}: OpenZendeskWindowOptions = {}) => {
|
) => {
|
||||||
if (IS_CE_EDITION) return
|
if (deploymentEdition !== 'CLOUD') return
|
||||||
|
|
||||||
if (openZendeskWindowOnce()) return
|
if (openZendeskWindowOnce(deploymentEdition)) return
|
||||||
|
|
||||||
let attempts = 0
|
let attempts = 0
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
attempts += 1
|
attempts += 1
|
||||||
if (openZendeskWindowOnce() || attempts >= retries) window.clearInterval(timer)
|
if (openZendeskWindowOnce(deploymentEdition) || attempts >= retries) window.clearInterval(timer)
|
||||||
}, interval)
|
}, interval)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
|
||||||
import usePSInfo from './use-ps-info'
|
import usePSInfo from './use-ps-info'
|
||||||
|
|
||||||
const PartnerStackCookieRecorder = () => {
|
const PartnerStackCookieRecorder = () => {
|
||||||
const { saveOrUpdate } = usePSInfo()
|
const { saveOrUpdate } = usePSInfo()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!IS_CLOUD_EDITION) return
|
|
||||||
saveOrUpdate()
|
saveOrUpdate()
|
||||||
}, [])
|
}, [saveOrUpdate])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import usePSInfo from './use-ps-info'
|
import usePSInfo from './use-ps-info'
|
||||||
|
|
||||||
const PartnerStack: FC = () => {
|
const PartnerStack: FC = () => {
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
|
const isCloudEdition = deploymentEdition === 'CLOUD'
|
||||||
const { saveOrUpdate, bind } = usePSInfo()
|
const { saveOrUpdate, bind } = usePSInfo()
|
||||||
|
const hasProcessedRef = React.useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!IS_CLOUD_EDITION) return
|
if (!isCloudEdition || hasProcessedRef.current) return
|
||||||
|
hasProcessedRef.current = true
|
||||||
// Save PartnerStack info in cookie first. Because if user hasn't logged in, redirecting to login page would cause lose the partnerStack info in URL.
|
// Save PartnerStack info in cookie first. Because if user hasn't logged in, redirecting to login page would cause lose the partnerStack info in URL.
|
||||||
saveOrUpdate()
|
saveOrUpdate()
|
||||||
// bind PartnerStack info after user logged in
|
// bind PartnerStack info after user logged in
|
||||||
bind()
|
bind()
|
||||||
}, [])
|
}, [bind, isCloudEdition, saveOrUpdate])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { RiBook2Line, RiFileEditLine, RiGroupLine } from '@remixicon/react'
|
import { RiBook2Line, RiFileEditLine, RiGroupLine } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useUnmountedRef } from 'ahooks'
|
import { useUnmountedRef } from 'ahooks'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
@@ -11,11 +12,11 @@ import { ApiAggregate, TriggerAll } from '@/app/components/base/icons/src/vender
|
|||||||
import UsageInfo from '@/app/components/billing/usage-info'
|
import UsageInfo from '@/app/components/billing/usage-info'
|
||||||
import { useSetEducationVerifying } from '@/app/education-apply/storage'
|
import { useSetEducationVerifying } from '@/app/education-apply/storage'
|
||||||
import VerifyStateModal from '@/app/education-apply/verify-state-modal'
|
import VerifyStateModal from '@/app/education-apply/verify-state-modal'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
|
||||||
import { userProfileEmailAtom } from '@/context/account-state'
|
import { userProfileEmailAtom } from '@/context/account-state'
|
||||||
import { useModalContextSelector } from '@/context/modal-context'
|
import { useModalContextSelector } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state'
|
import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { usePathname, useRouter } from '@/next/navigation'
|
import { usePathname, useRouter } from '@/next/navigation'
|
||||||
import { useEducationVerify } from '@/service/use-education'
|
import { useEducationVerify } from '@/service/use-education'
|
||||||
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
||||||
@@ -34,6 +35,11 @@ type Props = Readonly<{
|
|||||||
|
|
||||||
const PlanComp: FC<Props> = ({ loc }) => {
|
const PlanComp: FC<Props> = ({ loc }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
|
const isCloudEdition = deploymentEdition === 'CLOUD'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const path = usePathname()
|
const path = usePathname()
|
||||||
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
||||||
@@ -97,16 +103,14 @@ const PlanComp: FC<Props> = ({ loc }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
{IS_CLOUD_EDITION &&
|
{isCloudEdition && enableEducationPlan && (!isEducationAccount || isAboutToExpire) && (
|
||||||
enableEducationPlan &&
|
|
||||||
(!isEducationAccount || isAboutToExpire) && (
|
|
||||||
<Button variant="ghost" onClick={handleVerify} disabled={isPending}>
|
<Button variant="ghost" onClick={handleVerify} disabled={isPending}>
|
||||||
<span className="mr-1 i-ri-graduation-cap-line size-4" />
|
<span className="mr-1 i-ri-graduation-cap-line size-4" />
|
||||||
{t(($) => $.toVerified, { ns: 'education' })}
|
{t(($) => $.toVerified, { ns: 'education' })}
|
||||||
{isPending && <Loading className="ml-1 animate-spin-slow" />}
|
{isPending && <Loading className="ml-1 animate-spin-slow" />}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{IS_CLOUD_EDITION &&
|
{isCloudEdition &&
|
||||||
enableEducationPlan &&
|
enableEducationPlan &&
|
||||||
isEducationAccount &&
|
isEducationAccount &&
|
||||||
type === Plan.sandbox &&
|
type === Plan.sandbox &&
|
||||||
@@ -121,7 +125,7 @@ const PlanComp: FC<Props> = ({ loc }) => {
|
|||||||
{isEducationDiscountLoading && <Loading className="ml-1 animate-spin-slow" />}
|
{isEducationDiscountLoading && <Loading className="ml-1 animate-spin-slow" />}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{IS_CLOUD_EDITION && !isEnterprisePlan && (
|
{isCloudEdition && !isEnterprisePlan && (
|
||||||
<UpgradeBtn className="shrink-0" isPlain={type === Plan.team} isShort loc={loc} />
|
<UpgradeBtn className="shrink-0" isPlain={type === Plan.team} isShort loc={loc} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
import type { CSSProperties, FC } from 'react'
|
import type { CSSProperties, FC } from 'react'
|
||||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
|
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { PremiumBadgeButton } from '../../base/premium-badge'
|
import { PremiumBadgeButton } from '../../base/premium-badge'
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
@@ -37,9 +38,13 @@ const UpgradeBtn: FC<Props> = ({
|
|||||||
labelKey,
|
labelKey,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
const { setShowPricingModal } = useModalContext()
|
const { setShowPricingModal } = useModalContext()
|
||||||
|
|
||||||
if (!IS_CLOUD_EDITION) return null
|
if (deploymentEdition !== 'CLOUD') return null
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (_onClick) _onClick()
|
if (_onClick) _onClick()
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ vi.mock('@/config', async (importOriginal) => {
|
|||||||
const actual = await importOriginal<typeof import('@/config')>()
|
const actual = await importOriginal<typeof import('@/config')>()
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
IS_CLOUD_EDITION: true,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const render = (ui: ReactElement) =>
|
const render = (ui: ReactElement) =>
|
||||||
renderWithConsoleQuery(ui, {
|
renderWithConsoleQuery(ui, {
|
||||||
systemFeatures: {
|
systemFeatures: {
|
||||||
|
deployment_edition: 'CLOUD',
|
||||||
branding: {
|
branding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
workspace_logo: 'https://example.com/workspace-logo.png',
|
workspace_logo: 'https://example.com/workspace-logo.png',
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { contactSalesUrl } from '@/app/components/billing/config'
|
import { contactSalesUrl } from '@/app/components/billing/config'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import CustomWebAppBrand from '../custom-web-app-brand'
|
import CustomWebAppBrand from '../custom-web-app-brand'
|
||||||
|
|
||||||
const CustomPage = () => {
|
const CustomPage = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
const { plan, enableBilling } = useProviderContext()
|
const { plan, enableBilling } = useProviderContext()
|
||||||
const { setShowPricingModal } = useModalContext()
|
const { setShowPricingModal } = useModalContext()
|
||||||
const showBillingTip = IS_CLOUD_EDITION && enableBilling && plan.type === Plan.sandbox
|
const showBillingTip =
|
||||||
|
deploymentEdition === 'CLOUD' && enableBilling && plan.type === Plan.sandbox
|
||||||
const showContact = enableBilling && (plan.type === Plan.professional || plan.type === Plan.team)
|
const showContact = enableBilling && (plan.type === Plan.professional || plan.type === Plan.team)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -97,17 +97,6 @@ vi.mock('@/context/permission-state', async () => {
|
|||||||
|
|
||||||
return createPermissionStateModuleMock(() => mockConsoleState)
|
return createPermissionStateModuleMock(() => mockConsoleState)
|
||||||
})
|
})
|
||||||
vi.mock('@/context/system-features-state', async () => {
|
|
||||||
const { createSystemFeaturesStateModuleMock } = await import('@/test/console/state-fixture')
|
|
||||||
|
|
||||||
return createSystemFeaturesStateModuleMock(() => ({
|
|
||||||
...(() => mockConsoleState)(),
|
|
||||||
datasetRbacEnabled: (() => ({
|
|
||||||
isRbacEnabled: mockIsRbacEnabled,
|
|
||||||
}))().isRbacEnabled,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock('@/app/components/access-rules-editor', () => ({
|
vi.mock('@/app/components/access-rules-editor', () => ({
|
||||||
default: (props: AccessRulesEditorProps) => {
|
default: (props: AccessRulesEditorProps) => {
|
||||||
mockAccessRulesEditor.props = props
|
mockAccessRulesEditor.props = props
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import type { ResourceOpenScope } from '@/models/access-control'
|
import type { ResourceOpenScope } from '@/models/access-control'
|
||||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { useCallback, useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -11,7 +12,7 @@ import { userProfileIdAtom } from '@/context/account-state'
|
|||||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||||
import { datasetRbacEnabledAtom } from '@/context/system-features-state'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { getAccessControlTemplateLanguage } from '@/i18n-config/language'
|
import { getAccessControlTemplateLanguage } from '@/i18n-config/language'
|
||||||
import {
|
import {
|
||||||
useDatasetAccessRules,
|
useDatasetAccessRules,
|
||||||
@@ -33,7 +34,10 @@ const DatasetAccessConfigPage = ({ datasetId }: DatasetAccessConfigPageProps) =>
|
|||||||
const dataset = useDatasetDetailContextWithSelector((state) => state.dataset)
|
const dataset = useDatasetDetailContextWithSelector((state) => state.dataset)
|
||||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
const isRbacEnabled = useAtomValue(datasetRbacEnabledAtom)
|
const { data: isRbacEnabled } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ rbac_enabled }) => rbac_enabled,
|
||||||
|
})
|
||||||
const canAccessConfig = getDatasetACLCapabilities(dataset?.permission_keys, {
|
const canAccessConfig = getDatasetACLCapabilities(dataset?.permission_keys, {
|
||||||
currentUserId,
|
currentUserId,
|
||||||
resourceMaintainer: dataset?.maintainer,
|
resourceMaintainer: dataset?.maintainer,
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||||
import { PROGRESS_NOT_STARTED } from '../constants'
|
import { PROGRESS_NOT_STARTED } from '../constants'
|
||||||
import FileUploader from '../index'
|
import FileUploader from '../index'
|
||||||
|
|
||||||
|
const render = (ui: React.ReactElement) =>
|
||||||
|
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } })
|
||||||
|
|
||||||
const mockNotify = vi.fn()
|
const mockNotify = vi.fn()
|
||||||
vi.mock('use-context-selector', async () => {
|
vi.mock('use-context-selector', async () => {
|
||||||
const actual =
|
const actual =
|
||||||
@@ -31,10 +35,6 @@ vi.mock('@/i18n-config/language', () => ({
|
|||||||
LanguagesSupported: ['en-US', 'zh-Hans'],
|
LanguagesSupported: ['en-US', 'zh-Hans'],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/config', () => ({
|
|
||||||
IS_CE_EDITION: false,
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/base/file-uploader/utils', () => ({
|
vi.mock('@/app/components/base/file-uploader/utils', () => ({
|
||||||
getFileUploadErrorMessage: () => 'Upload error',
|
getFileUploadErrorMessage: () => 'Upload error',
|
||||||
}))
|
}))
|
||||||
@@ -44,7 +44,8 @@ vi.mock('@/hooks/use-theme', () => ({
|
|||||||
default: () => ({ theme: 'light' }),
|
default: () => ({ theme: 'light' }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/types/app', () => ({
|
vi.mock('@/types/app', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<typeof import('@/types/app')>()),
|
||||||
Theme: { dark: 'dark', light: 'light' },
|
Theme: { dark: 'dark', light: 'light' },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
+7
-9
@@ -1,7 +1,8 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import type { CustomFile, FileItem } from '@/models/datasets'
|
import type { CustomFile, FileItem } from '@/models/datasets'
|
||||||
import { act, render, renderHook, waitFor } from '@testing-library/react'
|
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createConsoleQueryWrapper, renderWithConsoleQuery } from '@/test/console/query-data'
|
||||||
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../../constants'
|
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../../constants'
|
||||||
// Import after mocks
|
// Import after mocks
|
||||||
import { useFileUpload } from '../use-file-upload'
|
import { useFileUpload } from '../use-file-upload'
|
||||||
@@ -40,18 +41,15 @@ vi.mock('@/i18n-config/language', () => ({
|
|||||||
LanguagesSupported: ['en-US', 'zh-Hans'],
|
LanguagesSupported: ['en-US', 'zh-Hans'],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/config', () => ({
|
|
||||||
IS_CE_EDITION: false,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock file upload error message
|
// Mock file upload error message
|
||||||
vi.mock('@/app/components/base/file-uploader/utils', () => ({
|
vi.mock('@/app/components/base/file-uploader/utils', () => ({
|
||||||
getFileUploadErrorMessage: (_e: unknown, defaultMsg: string) => defaultMsg,
|
getFileUploadErrorMessage: (_e: unknown, defaultMsg: string) => defaultMsg,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const createWrapper = () => {
|
const createWrapper = () =>
|
||||||
return ({ children }: { children: ReactNode }) => <>{children}</>
|
createConsoleQueryWrapper({ systemFeatures: { deployment_edition: 'CLOUD' } }).wrapper
|
||||||
}
|
const render = (ui: ReactElement) =>
|
||||||
|
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } })
|
||||||
|
|
||||||
describe('useFileUpload', () => {
|
describe('useFileUpload', () => {
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
import type { RefObject } from 'react'
|
import type { RefObject } from 'react'
|
||||||
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
|
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { LanguagesSupported } from '@/i18n-config/language'
|
import { LanguagesSupported } from '@/i18n-config/language'
|
||||||
import { upload } from '@/service/base'
|
import { upload } from '@/service/base'
|
||||||
import { useFileSupportTypes, useFileUploadConfig } from '@/service/use-common'
|
import { useFileSupportTypes, useFileUploadConfig } from '@/service/use-common'
|
||||||
@@ -69,6 +70,11 @@ export const useFileUpload = ({
|
|||||||
allowedExtensions,
|
allowedExtensions,
|
||||||
}: UseFileUploadOptions): UseFileUploadReturn => {
|
}: UseFileUploadOptions): UseFileUploadReturn => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
|
const isCloudEdition = deploymentEdition === 'CLOUD'
|
||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
|
|
||||||
const [dragging, setDragging] = useState(false)
|
const [dragging, setDragging] = useState(false)
|
||||||
@@ -219,7 +225,7 @@ export const useFileUpload = ({
|
|||||||
const filesCountLimit = fileUploadConfig.file_upload_limit
|
const filesCountLimit = fileUploadConfig.file_upload_limit
|
||||||
if (!files.length) return false
|
if (!files.length) return false
|
||||||
|
|
||||||
if (files.length + fileList.length > filesCountLimit && !IS_CE_EDITION) {
|
if (files.length + fileList.length > filesCountLimit && isCloudEdition) {
|
||||||
toast.error(
|
toast.error(
|
||||||
t(($) => $['stepOne.uploader.validation.filesNumber'], {
|
t(($) => $['stepOne.uploader.validation.filesNumber'], {
|
||||||
ns: 'datasetCreation',
|
ns: 'datasetCreation',
|
||||||
@@ -239,7 +245,7 @@ export const useFileUpload = ({
|
|||||||
fileListRef.current = newFiles
|
fileListRef.current = newFiles
|
||||||
uploadMultipleFiles(preparedFiles)
|
uploadMultipleFiles(preparedFiles)
|
||||||
},
|
},
|
||||||
[prepareFileList, uploadMultipleFiles, t, fileList, fileUploadConfig],
|
[prepareFileList, uploadMultipleFiles, t, fileList, fileUploadConfig, isCloudEdition],
|
||||||
)
|
)
|
||||||
|
|
||||||
const traverseFileEntry = useCallback(
|
const traverseFileEntry = useCallback(
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
|
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
|
||||||
import type { NotionPage } from '@/models/common'
|
import type { NotionPage } from '@/models/common'
|
||||||
import type { CrawlOptions, CrawlResultItem, DataSet, FileItem } from '@/models/datasets'
|
import type { CrawlOptions, CrawlResultItem, DataSet, FileItem } from '@/models/datasets'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { DataSourceType } from '@/models/datasets'
|
import { DataSourceType } from '@/models/datasets'
|
||||||
|
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||||
import StepOne from '../index'
|
import StepOne from '../index'
|
||||||
|
|
||||||
|
const render = (ui: React.ReactElement) =>
|
||||||
|
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } })
|
||||||
|
|
||||||
// Mock config for website crawl features
|
// Mock config for website crawl features
|
||||||
vi.mock('@/config', () => ({
|
vi.mock('@/config', async (importOriginal) => ({
|
||||||
IS_CLOUD_EDITION: false,
|
...(await importOriginal<typeof import('@/config')>()),
|
||||||
ENABLE_WEBSITE_FIRECRAWL: true,
|
ENABLE_WEBSITE_FIRECRAWL: true,
|
||||||
ENABLE_WEBSITE_JINAREADER: false,
|
ENABLE_WEBSITE_JINAREADER: false,
|
||||||
ENABLE_WEBSITE_WATERCRAWL: false,
|
ENABLE_WEBSITE_WATERCRAWL: false,
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||||
import UpgradeCard from '../upgrade-card'
|
import UpgradeCard from '../upgrade-card'
|
||||||
|
|
||||||
const mockSetShowPricingModal = vi.fn()
|
const mockSetShowPricingModal = vi.fn()
|
||||||
|
|
||||||
vi.mock('@/config', async (importOriginal) => ({
|
const render = (ui: React.ReactElement) =>
|
||||||
...(await importOriginal<typeof import('@/config')>()),
|
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } })
|
||||||
IS_CLOUD_EDITION: true,
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/context/modal-context', () => ({
|
vi.mock('@/context/modal-context', () => ({
|
||||||
useModalContext: () => ({ setShowPricingModal: mockSetShowPricingModal }),
|
useModalContext: () => ({ setShowPricingModal: mockSetShowPricingModal }),
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
import type { NotionPage } from '@/models/common'
|
import type { NotionPage } from '@/models/common'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||||
import PreviewPanel from '../preview-panel'
|
import PreviewPanel from '../preview-panel'
|
||||||
|
|
||||||
vi.mock('../../../file-preview', () => ({
|
vi.mock('../../../file-preview', () => ({
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
|
|
||||||
const UpgradeCard: FC = () => {
|
const UpgradeCard: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
const { setShowPricingModal } = useModalContext()
|
const { setShowPricingModal } = useModalContext()
|
||||||
|
|
||||||
const handleUpgrade = useCallback(() => {
|
const handleUpgrade = useCallback(() => {
|
||||||
setShowPricingModal()
|
setShowPricingModal()
|
||||||
}, [setShowPricingModal])
|
}, [setShowPricingModal])
|
||||||
|
|
||||||
if (!IS_CLOUD_EDITION) return null
|
if (deploymentEdition !== 'CLOUD') return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg py-3 pr-3.5 pl-4 shadow-xs backdrop-blur-[5px]">
|
<div className="flex items-center justify-between rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg py-3 pr-3.5 pl-4 shadow-xs backdrop-blur-[5px]">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
Rules,
|
Rules,
|
||||||
} from '@/models/datasets'
|
} from '@/models/datasets'
|
||||||
import type { RetrievalConfig } from '@/types/app'
|
import type { RetrievalConfig } from '@/types/app'
|
||||||
import { act, cleanup, fireEvent, render, renderHook, screen } from '@testing-library/react'
|
import { act, cleanup, fireEvent, renderHook, screen } from '@testing-library/react'
|
||||||
import {
|
import {
|
||||||
ConfigurationMethodEnum,
|
ConfigurationMethodEnum,
|
||||||
ModelStatusEnum,
|
ModelStatusEnum,
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { ChunkingMode, DataSourceType, ProcessMode } from '@/models/datasets'
|
import { ChunkingMode, DataSourceType, ProcessMode } from '@/models/datasets'
|
||||||
import { expectLoadingButton } from '@/test/button'
|
import { expectLoadingButton } from '@/test/button'
|
||||||
|
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||||
import { RETRIEVE_METHOD } from '@/types/app'
|
import { RETRIEVE_METHOD } from '@/types/app'
|
||||||
import { PreviewPanel } from '../components/preview-panel'
|
import { PreviewPanel } from '../components/preview-panel'
|
||||||
import { StepTwoFooter } from '../components/step-two-footer'
|
import { StepTwoFooter } from '../components/step-two-footer'
|
||||||
@@ -196,12 +197,6 @@ vi.mock('@/app/components/base/amplitude', () => ({
|
|||||||
trackEvent: vi.fn(),
|
trackEvent: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Enable IS_CE_EDITION to show QA checkbox in tests
|
|
||||||
vi.mock('@/config', async () => {
|
|
||||||
const actual = await vi.importActual('@/config')
|
|
||||||
return { ...actual, IS_CE_EDITION: true }
|
|
||||||
})
|
|
||||||
|
|
||||||
// Mock PreviewDocumentPicker to allow testing handlePickerChange
|
// Mock PreviewDocumentPicker to allow testing handlePickerChange
|
||||||
vi.mock('@/app/components/datasets/common/document-picker/preview-document-picker', () => ({
|
vi.mock('@/app/components/datasets/common/document-picker/preview-document-picker', () => ({
|
||||||
/* oxlint-disable typescript/no-explicit-any */
|
/* oxlint-disable typescript/no-explicit-any */
|
||||||
|
|||||||
+4
-5
@@ -1,7 +1,8 @@
|
|||||||
import type { PreProcessingRule } from '@/models/datasets'
|
import type { PreProcessingRule } from '@/models/datasets'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { ChunkingMode } from '@/models/datasets'
|
import { ChunkingMode } from '@/models/datasets'
|
||||||
|
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||||
import { GeneralChunkingOptions } from '../general-chunking-options'
|
import { GeneralChunkingOptions } from '../general-chunking-options'
|
||||||
|
|
||||||
vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
|
vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
|
||||||
@@ -21,11 +22,9 @@ vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/config', () => ({
|
|
||||||
IS_CE_EDITION: true,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const ns = 'datasetCreation'
|
const ns = 'datasetCreation'
|
||||||
|
const render = (ui: React.ReactElement) =>
|
||||||
|
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'COMMUNITY' } })
|
||||||
|
|
||||||
const createRules = (): PreProcessingRule[] => [
|
const createRules = (): PreProcessingRule[] => [
|
||||||
{ id: 'remove_extra_spaces', enabled: true },
|
{ id: 'remove_extra_spaces', enabled: true },
|
||||||
|
|||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import type { RetrievalConfig } from '@/types/app'
|
import type { RetrievalConfig } from '@/types/app'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { ChunkingMode } from '@/models/datasets'
|
import { ChunkingMode } from '@/models/datasets'
|
||||||
|
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||||
import { IndexingType } from '../../hooks'
|
import { IndexingType } from '../../hooks'
|
||||||
import { IndexingModeSection } from '../indexing-mode-section'
|
import { IndexingModeSection } from '../indexing-mode-section'
|
||||||
|
|
||||||
|
|||||||
+4
-5
@@ -1,8 +1,9 @@
|
|||||||
import type { ParentChildConfig } from '../../hooks'
|
import type { ParentChildConfig } from '../../hooks'
|
||||||
import type { PreProcessingRule } from '@/models/datasets'
|
import type { PreProcessingRule } from '@/models/datasets'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { ChunkingMode } from '@/models/datasets'
|
import { ChunkingMode } from '@/models/datasets'
|
||||||
|
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||||
import { ParentChildOptions } from '../parent-child-options'
|
import { ParentChildOptions } from '../parent-child-options'
|
||||||
|
|
||||||
vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
|
vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
|
||||||
@@ -22,11 +23,9 @@ vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/config', () => ({
|
|
||||||
IS_CE_EDITION: true,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const ns = 'datasetCreation'
|
const ns = 'datasetCreation'
|
||||||
|
const render = (ui: React.ReactElement) =>
|
||||||
|
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'COMMUNITY' } })
|
||||||
|
|
||||||
const createRules = (): PreProcessingRule[] => [
|
const createRules = (): PreProcessingRule[] => [
|
||||||
{ id: 'remove_extra_spaces', enabled: true },
|
{ id: 'remove_extra_spaces', enabled: true },
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import type {
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||||
import { RiAlertFill, RiSearchEyeLine } from '@remixicon/react'
|
import { RiAlertFill, RiSearchEyeLine } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import { Infotip } from '@/app/components/base/infotip'
|
import { Infotip } from '@/app/components/base/infotip'
|
||||||
import SummaryIndexSetting from '@/app/components/datasets/settings/summary-index-setting'
|
import SummaryIndexSetting from '@/app/components/datasets/settings/summary-index-setting'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { ChunkingMode } from '@/models/datasets'
|
import { ChunkingMode } from '@/models/datasets'
|
||||||
import SettingCog from '../../assets/setting-gear-mod.svg'
|
import SettingCog from '../../assets/setting-gear-mod.svg'
|
||||||
import s from '../index.module.css'
|
import s from '../index.module.css'
|
||||||
@@ -82,6 +83,11 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
|
|||||||
onSummaryIndexSettingChange,
|
onSummaryIndexSettingChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
|
const isNonCloudEdition = deploymentEdition === 'COMMUNITY' || deploymentEdition === 'ENTERPRISE'
|
||||||
|
|
||||||
const getRuleName = (key: string): string => {
|
const getRuleName = (key: string): string => {
|
||||||
const ruleNameMap: Record<string, string> = {
|
const ruleNameMap: Record<string, string> = {
|
||||||
@@ -150,7 +156,7 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
{showSummaryIndexSetting && IS_CE_EDITION && (
|
{showSummaryIndexSetting && isNonCloudEdition && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<SummaryIndexSetting
|
<SummaryIndexSetting
|
||||||
entry="create-document"
|
entry="create-document"
|
||||||
@@ -159,7 +165,7 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{IS_CE_EDITION && (
|
{isNonCloudEdition && (
|
||||||
<>
|
<>
|
||||||
<Divider type="horizontal" className="my-4 bg-divider-subtle" />
|
<Divider type="horizontal" className="my-4 bg-divider-subtle" />
|
||||||
<div className="flex items-center py-0.5">
|
<div className="flex items-center py-0.5">
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import { Button } from '@langgenius/dify-ui/button'
|
|||||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||||
import { RadioGroup } from '@langgenius/dify-ui/radio'
|
import { RadioGroup } from '@langgenius/dify-ui/radio'
|
||||||
import { RiSearchEyeLine } from '@remixicon/react'
|
import { RiSearchEyeLine } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import { ParentChildChunk } from '@/app/components/base/icons/src/vender/knowledge'
|
import { ParentChildChunk } from '@/app/components/base/icons/src/vender/knowledge'
|
||||||
import RadioCard from '@/app/components/base/radio-card'
|
import RadioCard from '@/app/components/base/radio-card'
|
||||||
import SummaryIndexSetting from '@/app/components/datasets/settings/summary-index-setting'
|
import SummaryIndexSetting from '@/app/components/datasets/settings/summary-index-setting'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { ChunkingMode } from '@/models/datasets'
|
import { ChunkingMode } from '@/models/datasets'
|
||||||
import FileList from '../../assets/file-list-3-fill.svg'
|
import FileList from '../../assets/file-list-3-fill.svg'
|
||||||
import Note from '../../assets/note-mod.svg'
|
import Note from '../../assets/note-mod.svg'
|
||||||
@@ -78,6 +79,11 @@ export const ParentChildOptions: FC<ParentChildOptionsProps> = ({
|
|||||||
showSummaryIndexSetting,
|
showSummaryIndexSetting,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { data: deploymentEdition } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: ({ deployment_edition }) => deployment_edition,
|
||||||
|
})
|
||||||
|
const isNonCloudEdition = deploymentEdition === 'COMMUNITY' || deploymentEdition === 'ENTERPRISE'
|
||||||
|
|
||||||
const getRuleName = (key: string): string => {
|
const getRuleName = (key: string): string => {
|
||||||
const ruleNameMap: Record<string, string> = {
|
const ruleNameMap: Record<string, string> = {
|
||||||
@@ -202,7 +208,7 @@ export const ParentChildOptions: FC<ParentChildOptionsProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
{showSummaryIndexSetting && IS_CE_EDITION && (
|
{showSummaryIndexSetting && isNonCloudEdition && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<SummaryIndexSetting
|
<SummaryIndexSetting
|
||||||
entry="create-document"
|
entry="create-document"
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
import type { SortType } from '@/service/datasets'
|
import type { SortType } from '@/service/datasets'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { DataSourceType } from '@/models/datasets'
|
import { DataSourceType } from '@/models/datasets'
|
||||||
|
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||||
import DocumentsHeader from '../documents-header'
|
import DocumentsHeader from '../documents-header'
|
||||||
|
|
||||||
// Mock the context hooks
|
// Mock the context hooks
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user