mirror of
https://github.com/langgenius/dify.git
synced 2026-05-25 11:27:19 +08:00
Compare commits
4 Commits
fix/tag-tr
...
fix/member
| Author | SHA1 | Date | |
|---|---|---|---|
| ae70d97861 | |||
| 639e12a306 | |||
| 3f0da07318 | |||
| 6ed98e180d |
@ -4,6 +4,7 @@ from uuid import UUID
|
||||
from flask import abort, request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, TypeAdapter
|
||||
from sqlalchemy import func, select
|
||||
|
||||
import services
|
||||
from configs import dify_config
|
||||
@ -22,15 +23,15 @@ from controllers.console.auth.error import (
|
||||
from controllers.console.error import EmailSendIpLimitError, WorkspaceMembersLimitExceeded
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
is_allow_transfer_owner,
|
||||
setup_required,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from fields.member_fields import AccountWithRole, AccountWithRoleList
|
||||
from libs.helper import extract_remote_ip
|
||||
from libs.login import current_account_with_tenant, login_required
|
||||
from models.account import Account, TenantAccountRole
|
||||
from models.account import Account, TenantAccountJoin, TenantAccountRole
|
||||
from services.account_service import AccountService, RegisterService, TenantService
|
||||
from services.errors.account import AccountAlreadyInTenantError
|
||||
from services.feature_service import FeatureService
|
||||
@ -79,6 +80,54 @@ def _is_role_enabled(role: TenantAccountRole | str, tenant_id: str) -> bool:
|
||||
return FeatureService.get_features(tenant_id=tenant_id).dataset_operator_enabled
|
||||
|
||||
|
||||
def _normalize_invitee_emails(emails: list[str]) -> list[str]:
|
||||
return list(dict.fromkeys(email.lower() for email in emails))
|
||||
|
||||
|
||||
def _count_new_member_invites(tenant_id: str, emails: list[str]) -> int:
|
||||
new_member_count = 0
|
||||
for email in emails:
|
||||
account = AccountService.get_account_by_email_with_case_fallback(email)
|
||||
if not account:
|
||||
new_member_count += 1
|
||||
continue
|
||||
|
||||
exists = db.session.scalar(
|
||||
select(TenantAccountJoin.id)
|
||||
.where(TenantAccountJoin.tenant_id == tenant_id, TenantAccountJoin.account_id == account.id)
|
||||
.limit(1)
|
||||
)
|
||||
if not exists:
|
||||
new_member_count += 1
|
||||
|
||||
return new_member_count
|
||||
|
||||
|
||||
def _count_current_members(tenant_id: str) -> int:
|
||||
return (
|
||||
db.session.scalar(select(func.count(TenantAccountJoin.id)).where(TenantAccountJoin.tenant_id == tenant_id)) or 0
|
||||
)
|
||||
|
||||
|
||||
def _check_member_invite_limits(tenant_id: str, new_member_count: int) -> None:
|
||||
if new_member_count <= 0:
|
||||
return
|
||||
|
||||
features = FeatureService.get_features(tenant_id=tenant_id)
|
||||
|
||||
if dify_config.ENTERPRISE_ENABLED:
|
||||
workspace_members = features.workspace_members
|
||||
if workspace_members.enabled is True and not workspace_members.is_available(new_member_count):
|
||||
raise WorkspaceMembersLimitExceeded()
|
||||
return
|
||||
|
||||
if dify_config.BILLING_ENABLED and features.billing.enabled is True:
|
||||
members = features.members
|
||||
current_member_count = _count_current_members(tenant_id)
|
||||
if 0 < members.limit < current_member_count + new_member_count:
|
||||
raise WorkspaceMembersLimitExceeded()
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/members")
|
||||
class MemberListApi(Resource):
|
||||
"""List all members of current tenant."""
|
||||
@ -105,12 +154,11 @@ class MemberInviteEmailApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("members")
|
||||
def post(self):
|
||||
payload = console_ns.payload or {}
|
||||
args = MemberInvitePayload.model_validate(payload)
|
||||
|
||||
invitee_emails = args.emails
|
||||
invitee_emails = _normalize_invitee_emails(args.emails)
|
||||
invitee_role = args.role
|
||||
interface_language = args.language
|
||||
if not TenantAccountRole.is_non_owner_role(invitee_role):
|
||||
@ -130,37 +178,36 @@ class MemberInviteEmailApi(Resource):
|
||||
invitation_results = []
|
||||
console_web_url = dify_config.CONSOLE_WEB_URL
|
||||
|
||||
workspace_members = FeatureService.get_features(tenant_id=inviter.current_tenant.id).workspace_members
|
||||
tenant_id = inviter.current_tenant.id
|
||||
with redis_client.lock(f"workspace_member_invite:{tenant_id}", timeout=60):
|
||||
new_member_count = _count_new_member_invites(tenant_id, invitee_emails)
|
||||
_check_member_invite_limits(tenant_id, new_member_count)
|
||||
|
||||
if not workspace_members.is_available(len(invitee_emails)):
|
||||
raise WorkspaceMembersLimitExceeded()
|
||||
|
||||
for invitee_email in invitee_emails:
|
||||
normalized_invitee_email = invitee_email.lower()
|
||||
try:
|
||||
if not inviter.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
token = RegisterService.invite_new_member(
|
||||
tenant=inviter.current_tenant,
|
||||
email=invitee_email,
|
||||
language=interface_language,
|
||||
role=invitee_role,
|
||||
inviter=inviter,
|
||||
)
|
||||
encoded_invitee_email = parse.quote(normalized_invitee_email)
|
||||
invitation_results.append(
|
||||
{
|
||||
"status": "success",
|
||||
"email": normalized_invitee_email,
|
||||
"url": f"{console_web_url}/activate?email={encoded_invitee_email}&token={token}",
|
||||
}
|
||||
)
|
||||
except AccountAlreadyInTenantError:
|
||||
invitation_results.append(
|
||||
{"status": "success", "email": normalized_invitee_email, "url": f"{console_web_url}/signin"}
|
||||
)
|
||||
except Exception as e:
|
||||
invitation_results.append({"status": "failed", "email": normalized_invitee_email, "message": str(e)})
|
||||
for invitee_email in invitee_emails:
|
||||
try:
|
||||
if not inviter.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
token = RegisterService.invite_new_member(
|
||||
tenant=inviter.current_tenant,
|
||||
email=invitee_email,
|
||||
language=interface_language,
|
||||
role=invitee_role,
|
||||
inviter=inviter,
|
||||
)
|
||||
encoded_invitee_email = parse.quote(invitee_email)
|
||||
invitation_results.append(
|
||||
{
|
||||
"status": "success",
|
||||
"email": invitee_email,
|
||||
"url": f"{console_web_url}/activate?email={encoded_invitee_email}&token={token}",
|
||||
}
|
||||
)
|
||||
except AccountAlreadyInTenantError:
|
||||
invitation_results.append(
|
||||
{"status": "success", "email": invitee_email, "url": f"{console_web_url}/signin"}
|
||||
)
|
||||
except Exception as e:
|
||||
invitation_results.append({"status": "failed", "email": invitee_email, "message": str(e)})
|
||||
|
||||
return {
|
||||
"result": "success",
|
||||
|
||||
@ -162,9 +162,9 @@ class DatasetVectorSettingResponse(ResponseModel):
|
||||
|
||||
|
||||
class DatasetWeightedScoreResponse(ResponseModel):
|
||||
weight_type: str | None
|
||||
keyword_setting: DatasetKeywordSettingResponse | None
|
||||
vector_setting: DatasetVectorSettingResponse | None
|
||||
weight_type: str | None = None
|
||||
keyword_setting: DatasetKeywordSettingResponse | None = None
|
||||
vector_setting: DatasetVectorSettingResponse | None = None
|
||||
|
||||
|
||||
class DatasetRetrievalModelResponse(ResponseModel):
|
||||
|
||||
@ -12085,9 +12085,9 @@ Condition detail
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| keyword_setting | [DatasetKeywordSettingResponse](#datasetkeywordsettingresponse) | | Yes |
|
||||
| vector_setting | [DatasetVectorSettingResponse](#datasetvectorsettingresponse) | | Yes |
|
||||
| weight_type | string | | Yes |
|
||||
| keyword_setting | [DatasetKeywordSettingResponse](#datasetkeywordsettingresponse) | | No |
|
||||
| vector_setting | [DatasetVectorSettingResponse](#datasetvectorsettingresponse) | | No |
|
||||
| weight_type | string | | No |
|
||||
|
||||
#### DatasourceCredentialDeletePayload
|
||||
|
||||
|
||||
@ -2574,9 +2574,9 @@ Condition detail
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| keyword_setting | [DatasetKeywordSettingResponse](#datasetkeywordsettingresponse) | | Yes |
|
||||
| vector_setting | [DatasetVectorSettingResponse](#datasetvectorsettingresponse) | | Yes |
|
||||
| weight_type | string | | Yes |
|
||||
| keyword_setting | [DatasetKeywordSettingResponse](#datasetkeywordsettingresponse) | | No |
|
||||
| vector_setting | [DatasetVectorSettingResponse](#datasetvectorsettingresponse) | | No |
|
||||
| weight_type | string | | No |
|
||||
|
||||
#### DatasourceNodeRunPayload
|
||||
|
||||
|
||||
@ -245,6 +245,55 @@ class TestDatasetList:
|
||||
|
||||
assert status == 200
|
||||
|
||||
def test_get_allows_legacy_weighted_score_without_weight_type(self, app: Flask):
|
||||
api = DatasetListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
current_user = self._mock_user()
|
||||
datasets = [
|
||||
make_dataset(
|
||||
retrieval_model={
|
||||
"search_method": "hybrid_search",
|
||||
"reranking_enable": True,
|
||||
"reranking_mode": "weighted_score",
|
||||
"reranking_model": None,
|
||||
"weights": {
|
||||
"vector_setting": {
|
||||
"vector_weight": 0.7,
|
||||
"embedding_model_name": "text-embedding",
|
||||
"embedding_provider_name": "openai",
|
||||
},
|
||||
"keyword_setting": {"keyword_weight": 0.3},
|
||||
},
|
||||
"top_k": 3,
|
||||
"score_threshold_enabled": False,
|
||||
"score_threshold": 0.0,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
with app.test_request_context("/datasets"):
|
||||
with (
|
||||
patch(
|
||||
"controllers.console.datasets.datasets.current_account_with_tenant",
|
||||
return_value=(current_user, "tenant-1"),
|
||||
),
|
||||
patch.object(
|
||||
DatasetService,
|
||||
"get_datasets",
|
||||
return_value=(datasets, 1),
|
||||
),
|
||||
patch.object(
|
||||
ProviderManager,
|
||||
"get_configurations",
|
||||
return_value=MagicMock(get_models=lambda **_: []),
|
||||
),
|
||||
):
|
||||
resp, status = method(api)
|
||||
|
||||
assert status == 200
|
||||
assert resp["data"][0]["retrieval_model_dict"]["weights"]["weight_type"] is None
|
||||
|
||||
def test_embedding_available_false(self, app: Flask):
|
||||
api = DatasetListApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from contextlib import nullcontext
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
@ -18,7 +19,7 @@ def app():
|
||||
|
||||
def _build_feature_flags():
|
||||
placeholder_quota = SimpleNamespace(limit=0, size=0)
|
||||
workspace_members = SimpleNamespace(is_available=lambda count: True)
|
||||
workspace_members = SimpleNamespace(enabled=False, is_available=lambda count: True)
|
||||
return SimpleNamespace(
|
||||
billing=SimpleNamespace(enabled=False),
|
||||
workspace_members=workspace_members,
|
||||
@ -31,6 +32,11 @@ def _build_feature_flags():
|
||||
|
||||
|
||||
class TestMemberInviteEmailApi:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_member_invite_lock(self):
|
||||
with patch("controllers.console.workspace.members.redis_client.lock", return_value=nullcontext()):
|
||||
yield
|
||||
|
||||
@patch("controllers.console.workspace.members.FeatureService.get_features")
|
||||
@patch("controllers.console.workspace.members.RegisterService.invite_new_member")
|
||||
@patch("controllers.console.workspace.members.current_account_with_tenant")
|
||||
@ -52,7 +58,12 @@ class TestMemberInviteEmailApi:
|
||||
inviter = SimpleNamespace(email="Owner@Example.com", current_tenant=tenant, status="active")
|
||||
mock_current_account.return_value = (inviter, tenant.id)
|
||||
|
||||
with patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "https://console.example.com"):
|
||||
with (
|
||||
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "https://console.example.com"),
|
||||
patch("controllers.console.workspace.members._count_new_member_invites", return_value=1),
|
||||
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
|
||||
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/workspaces/current/members/invite-email",
|
||||
method="POST",
|
||||
@ -70,7 +81,7 @@ class TestMemberInviteEmailApi:
|
||||
assert mock_invite_member.call_count == 1
|
||||
call_args = mock_invite_member.call_args
|
||||
assert call_args.kwargs["tenant"] == tenant
|
||||
assert call_args.kwargs["email"] == "User@Example.com"
|
||||
assert call_args.kwargs["email"] == "user@example.com"
|
||||
assert call_args.kwargs["language"] == "en-US"
|
||||
assert call_args.kwargs["role"] == TenantAccountRole.EDITOR
|
||||
assert call_args.kwargs["inviter"] == inviter
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from contextlib import nullcontext
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@ -75,6 +76,11 @@ class TestMemberListApi:
|
||||
|
||||
|
||||
class TestMemberInviteEmailApi:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_member_invite_lock(self):
|
||||
with patch("controllers.console.workspace.members.redis_client.lock", return_value=nullcontext()):
|
||||
yield
|
||||
|
||||
def test_invite_success(self, app: Flask):
|
||||
api = MemberInviteEmailApi()
|
||||
method = unwrap(api.post)
|
||||
@ -82,6 +88,8 @@ class TestMemberInviteEmailApi:
|
||||
tenant = MagicMock(id="t1")
|
||||
user = MagicMock(current_tenant=tenant)
|
||||
features = MagicMock()
|
||||
features.billing.enabled = False
|
||||
features.workspace_members.enabled = False
|
||||
features.workspace_members.is_available.return_value = True
|
||||
|
||||
payload = {
|
||||
@ -94,8 +102,11 @@ class TestMemberInviteEmailApi:
|
||||
app.test_request_context("/", json=payload),
|
||||
patch("controllers.console.workspace.members.current_account_with_tenant", return_value=(user, "t1")),
|
||||
patch("controllers.console.workspace.members.FeatureService.get_features", return_value=features),
|
||||
patch("controllers.console.workspace.members._count_new_member_invites", return_value=1),
|
||||
patch("controllers.console.workspace.members.RegisterService.invite_new_member", return_value="token"),
|
||||
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
|
||||
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
|
||||
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
||||
):
|
||||
result, status = method(api)
|
||||
|
||||
@ -109,6 +120,8 @@ class TestMemberInviteEmailApi:
|
||||
tenant = MagicMock(id="t1")
|
||||
user = MagicMock(current_tenant=tenant)
|
||||
features = MagicMock()
|
||||
features.billing.enabled = False
|
||||
features.workspace_members.enabled = True
|
||||
features.workspace_members.is_available.return_value = False
|
||||
|
||||
payload = {
|
||||
@ -120,6 +133,38 @@ class TestMemberInviteEmailApi:
|
||||
app.test_request_context("/", json=payload),
|
||||
patch("controllers.console.workspace.members.current_account_with_tenant", return_value=(user, "t1")),
|
||||
patch("controllers.console.workspace.members.FeatureService.get_features", return_value=features),
|
||||
patch("controllers.console.workspace.members._count_new_member_invites", return_value=1),
|
||||
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", True),
|
||||
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
||||
):
|
||||
with pytest.raises(WorkspaceMembersLimitExceeded):
|
||||
method(api)
|
||||
|
||||
def test_invite_billing_limit_exceeded(self, app: Flask):
|
||||
api = MemberInviteEmailApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
tenant = MagicMock(id="t1")
|
||||
user = MagicMock(current_tenant=tenant)
|
||||
features = MagicMock()
|
||||
features.billing.enabled = True
|
||||
features.members.size = 9
|
||||
features.members.limit = 10
|
||||
features.workspace_members.enabled = False
|
||||
|
||||
payload = {
|
||||
"emails": ["a@test.com", "b@test.com"],
|
||||
"role": "normal",
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", json=payload),
|
||||
patch("controllers.console.workspace.members.current_account_with_tenant", return_value=(user, "t1")),
|
||||
patch("controllers.console.workspace.members.FeatureService.get_features", return_value=features),
|
||||
patch("controllers.console.workspace.members._count_new_member_invites", return_value=2),
|
||||
patch("controllers.console.workspace.members._count_current_members", return_value=9),
|
||||
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
|
||||
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", True),
|
||||
):
|
||||
with pytest.raises(WorkspaceMembersLimitExceeded):
|
||||
method(api)
|
||||
@ -131,6 +176,8 @@ class TestMemberInviteEmailApi:
|
||||
tenant = MagicMock(id="t1")
|
||||
user = MagicMock(current_tenant=tenant)
|
||||
features = MagicMock()
|
||||
features.billing.enabled = False
|
||||
features.workspace_members.enabled = False
|
||||
features.workspace_members.is_available.return_value = True
|
||||
|
||||
payload = {
|
||||
@ -142,11 +189,14 @@ class TestMemberInviteEmailApi:
|
||||
app.test_request_context("/", json=payload),
|
||||
patch("controllers.console.workspace.members.current_account_with_tenant", return_value=(user, "t1")),
|
||||
patch("controllers.console.workspace.members.FeatureService.get_features", return_value=features),
|
||||
patch("controllers.console.workspace.members._count_new_member_invites", return_value=0),
|
||||
patch(
|
||||
"controllers.console.workspace.members.RegisterService.invite_new_member",
|
||||
side_effect=AccountAlreadyInTenantError(),
|
||||
),
|
||||
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
|
||||
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
|
||||
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
||||
):
|
||||
result, status = method(api)
|
||||
|
||||
@ -174,6 +224,8 @@ class TestMemberInviteEmailApi:
|
||||
tenant = MagicMock(id="t1")
|
||||
user = MagicMock(current_tenant=tenant)
|
||||
features = MagicMock()
|
||||
features.billing.enabled = False
|
||||
features.workspace_members.enabled = False
|
||||
features.workspace_members.is_available.return_value = True
|
||||
|
||||
payload = {
|
||||
@ -185,11 +237,14 @@ class TestMemberInviteEmailApi:
|
||||
app.test_request_context("/", json=payload),
|
||||
patch("controllers.console.workspace.members.current_account_with_tenant", return_value=(user, "t1")),
|
||||
patch("controllers.console.workspace.members.FeatureService.get_features", return_value=features),
|
||||
patch("controllers.console.workspace.members._count_new_member_invites", return_value=1),
|
||||
patch(
|
||||
"controllers.console.workspace.members.RegisterService.invite_new_member",
|
||||
side_effect=Exception("boom"),
|
||||
),
|
||||
patch("controllers.console.workspace.members.dify_config.CONSOLE_WEB_URL", "http://x"),
|
||||
patch("controllers.console.workspace.members.dify_config.ENTERPRISE_ENABLED", False),
|
||||
patch("controllers.console.workspace.members.dify_config.BILLING_ENABLED", False),
|
||||
):
|
||||
result, _ = method(api)
|
||||
|
||||
|
||||
@ -709,9 +709,9 @@ export type DatasetRerankingModelResponse = {
|
||||
}
|
||||
|
||||
export type DatasetWeightedScoreResponse = {
|
||||
keyword_setting: DatasetKeywordSettingResponse
|
||||
vector_setting: DatasetVectorSettingResponse
|
||||
weight_type: string | null
|
||||
keyword_setting?: DatasetKeywordSettingResponse
|
||||
vector_setting?: DatasetVectorSettingResponse
|
||||
weight_type?: string | null
|
||||
}
|
||||
|
||||
export type DatasetRerankingModel = {
|
||||
|
||||
@ -670,9 +670,9 @@ export const zDatasetVectorSettingResponse = z.object({
|
||||
* DatasetWeightedScoreResponse
|
||||
*/
|
||||
export const zDatasetWeightedScoreResponse = z.object({
|
||||
keyword_setting: zDatasetKeywordSettingResponse,
|
||||
vector_setting: zDatasetVectorSettingResponse,
|
||||
weight_type: z.string().nullable(),
|
||||
keyword_setting: zDatasetKeywordSettingResponse.optional(),
|
||||
vector_setting: zDatasetVectorSettingResponse.optional(),
|
||||
weight_type: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@ -377,9 +377,9 @@ export type DatasetVectorSettingResponse = {
|
||||
}
|
||||
|
||||
export type DatasetWeightedScoreResponse = {
|
||||
keyword_setting: DatasetKeywordSettingResponse
|
||||
vector_setting: DatasetVectorSettingResponse
|
||||
weight_type: string | null
|
||||
keyword_setting?: DatasetKeywordSettingResponse
|
||||
vector_setting?: DatasetVectorSettingResponse
|
||||
weight_type?: string | null
|
||||
}
|
||||
|
||||
export type DatasourceNodeRunPayload = {
|
||||
|
||||
@ -354,9 +354,9 @@ export const zDatasetVectorSettingResponse = z.object({
|
||||
* DatasetWeightedScoreResponse
|
||||
*/
|
||||
export const zDatasetWeightedScoreResponse = z.object({
|
||||
keyword_setting: zDatasetKeywordSettingResponse,
|
||||
vector_setting: zDatasetVectorSettingResponse,
|
||||
weight_type: z.string().nullable(),
|
||||
keyword_setting: zDatasetKeywordSettingResponse.optional(),
|
||||
vector_setting: zDatasetVectorSettingResponse.optional(),
|
||||
weight_type: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@ -91,6 +91,7 @@ export type CurrentPlanInfoBackend = {
|
||||
}
|
||||
webapp_copyright_enabled: boolean
|
||||
workspace_members: {
|
||||
enabled?: boolean
|
||||
size: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
@ -28,6 +28,37 @@ type ProviderContextProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
type MemberInviteLimit = {
|
||||
size: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
const unlimitedMemberInviteLimit: MemberInviteLimit = {
|
||||
size: 0,
|
||||
limit: 0,
|
||||
}
|
||||
|
||||
const resolveMemberInviteLimit = (data: Awaited<ReturnType<typeof fetchCurrentPlanInfo>>): MemberInviteLimit => {
|
||||
if (!data)
|
||||
return unlimitedMemberInviteLimit
|
||||
|
||||
if (data.workspace_members?.enabled) {
|
||||
return {
|
||||
size: data.workspace_members.size,
|
||||
limit: data.workspace_members.limit,
|
||||
}
|
||||
}
|
||||
|
||||
if (data.billing?.enabled && data.members?.limit > 0) {
|
||||
return {
|
||||
size: data.members.size,
|
||||
limit: data.members.limit,
|
||||
}
|
||||
}
|
||||
|
||||
return unlimitedMemberInviteLimit
|
||||
}
|
||||
|
||||
export const ProviderContextProvider = ({
|
||||
children,
|
||||
}: ProviderContextProviderProps) => {
|
||||
@ -87,8 +118,7 @@ export const ProviderContextProvider = ({
|
||||
setDatasetOperatorEnabled(true)
|
||||
if (data.webapp_copyright_enabled)
|
||||
setWebappCopyrightEnabled(true)
|
||||
if (data.workspace_members)
|
||||
setLicenseLimit({ workspace_members: data.workspace_members })
|
||||
setLicenseLimit({ workspace_members: resolveMemberInviteLimit(data) })
|
||||
if (data.is_allow_transfer_workspace)
|
||||
setIsAllowTransferWorkspace(data.is_allow_transfer_workspace)
|
||||
if (data.knowledge_pipeline?.publish_enabled)
|
||||
|
||||
@ -21,15 +21,6 @@ describe('Trigger', () => {
|
||||
expect(screen.getByText('Backend')).toBeInTheDocument()
|
||||
expect(screen.queryByText('common.tag.addTag')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should preserve tag label casing for mixed-case tags', () => {
|
||||
render(<TagTrigger tags={['Prod', 'prod']} />)
|
||||
|
||||
expect(screen.getByText('Prod')).toHaveClass('system-2xs-medium')
|
||||
expect(screen.getByText('Prod')).not.toHaveClass('system-2xs-medium-uppercase')
|
||||
expect(screen.getByText('prod')).toHaveClass('system-2xs-medium')
|
||||
expect(screen.getByText('prod')).not.toHaveClass('system-2xs-medium-uppercase')
|
||||
})
|
||||
})
|
||||
|
||||
// Prop-driven rendering updates.
|
||||
|
||||
@ -34,7 +34,7 @@ export const TagTrigger = ({
|
||||
className="flex max-w-30 min-w-0 shrink-0 items-center gap-x-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1.25 py-0.75"
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-price-tag-3-line size-3 shrink-0 text-text-quaternary" />
|
||||
<div className="truncate system-2xs-medium text-text-tertiary">
|
||||
<div className="truncate system-2xs-medium-uppercase text-text-tertiary">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,15 +1,29 @@
|
||||
import type { CurrentPlanInfoBackend, SubscriptionUrlsBackend } from '@/app/components/billing/type'
|
||||
import { get } from './base'
|
||||
|
||||
type CurrentPlanInfoResponse = Omit<CurrentPlanInfoBackend, 'workspace_members'> & {
|
||||
workspace_members: Omit<CurrentPlanInfoBackend['workspace_members'], 'size'> & {
|
||||
size: number | null
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchCurrentPlanInfo = async (): Promise<CurrentPlanInfoBackend> => {
|
||||
const data = await get<CurrentPlanInfoResponse>('/features')
|
||||
|
||||
return {
|
||||
...data,
|
||||
workspace_members: {
|
||||
...data.workspace_members,
|
||||
size: data.workspace_members.size ?? 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export type CurrentPlanVectorSpaceBackend = {
|
||||
size: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
export const fetchCurrentPlanInfo = () => {
|
||||
return get<CurrentPlanInfoBackend>('/features')
|
||||
}
|
||||
|
||||
export const fetchCurrentPlanVectorSpace = () => {
|
||||
return get<CurrentPlanVectorSpaceBackend>('/features/vector-space')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user