mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
chore: improve current workspace summary (#40121)
Co-authored-by: Jingyi-Dify <jingyi.qi@dify.ai>
This commit is contained in:
1 parent
4b2c5ac4ae
commit
baba36d47d
48 files changed
+978
-776
No files matched your search
@@ -1,6 +1,12 @@
|
||||
from libs.exception import BaseHTTPException
|
||||
|
||||
|
||||
class CurrentWorkspaceArchivedError(BaseHTTPException):
|
||||
error_code = "current_workspace_archived"
|
||||
description = "The current workspace has been archived."
|
||||
code = 409
|
||||
|
||||
|
||||
class RepeatPasswordNotMatchError(BaseHTTPException):
|
||||
error_code = "repeat_password_not_match"
|
||||
description = "New password and repeat password does not match."
|
||||
|
||||
@@ -7,9 +7,10 @@ from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import NotFound, Unauthorized
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
import services
|
||||
from configs import dify_config
|
||||
from controllers.common.errors import (
|
||||
FilenameNotExistsError,
|
||||
FileTooLargeError,
|
||||
@@ -28,6 +29,7 @@ from controllers.console import console_ns
|
||||
from controllers.console.admin import admin_required
|
||||
from controllers.console.error import AccountNotLinkTenantError
|
||||
from controllers.console.flask_admission import console_account_admission
|
||||
from controllers.console.workspace.error import CurrentWorkspaceArchivedError
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
@@ -43,7 +45,7 @@ from libs.helper import dump_response, to_timestamp
|
||||
from libs.login import login_required
|
||||
from libs.pagination import paginate_query
|
||||
from machinery.context import RequestContext
|
||||
from models.account import Account, Tenant, TenantCustomConfigDict, TenantStatus
|
||||
from models.account import Account, Tenant, TenantAccountRole, TenantCustomConfigDict, TenantStatus
|
||||
from services.account_service import TenantService
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.file_service import FileService
|
||||
@@ -105,6 +107,14 @@ class TenantInfoResponse(ResponseModel):
|
||||
return to_timestamp(value)
|
||||
|
||||
|
||||
class CurrentWorkspaceSummaryResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
role: TenantAccountRole
|
||||
plan: str | None
|
||||
credits: int | None = Field(description="Remaining credits in the effective pool; -1 means unlimited.")
|
||||
|
||||
|
||||
class TenantListItemResponse(ResponseModel):
|
||||
id: str
|
||||
name: str | None = None
|
||||
@@ -201,6 +211,7 @@ register_schema_models(
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
CurrentWorkspaceSummaryResponse,
|
||||
TenantInfoResponse,
|
||||
TenantListItemResponse,
|
||||
TenantListResponse,
|
||||
@@ -245,35 +256,31 @@ class WorkspaceListApi(Resource):
|
||||
).model_dump(mode="json"), HTTPStatus.OK
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current", endpoint="workspaces_current")
|
||||
@console_ns.route("/info", endpoint="info") # Deprecated
|
||||
class TenantApi(Resource):
|
||||
@console_ns.route("/workspaces/current/summary")
|
||||
class CurrentWorkspaceSummaryApi(Resource):
|
||||
@console_ns.response(
|
||||
HTTPStatus.OK,
|
||||
"Success",
|
||||
console_ns.models[CurrentWorkspaceSummaryResponse.__name__],
|
||||
)
|
||||
@console_ns.response(HTTPStatus.CONFLICT, "Current workspace is archived")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[TenantInfoResponse.__name__])
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account):
|
||||
if request.path == "/info":
|
||||
logger.warning("Deprecated URL /info was used.")
|
||||
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_user: Account):
|
||||
tenant = current_user.current_tenant
|
||||
if not tenant:
|
||||
raise ValueError("No current tenant")
|
||||
|
||||
if tenant.status == TenantStatus.ARCHIVE:
|
||||
tenants = TenantService.get_join_tenants(current_user, session=session)
|
||||
# if there is any tenant, switch to the first one
|
||||
if len(tenants) > 0:
|
||||
TenantService.switch_tenant(current_user, tenants[0].id, session=session)
|
||||
tenant = tenants[0]
|
||||
# else, raise Unauthorized
|
||||
else:
|
||||
raise Unauthorized("workspace is archived")
|
||||
raise CurrentWorkspaceArchivedError()
|
||||
|
||||
return (
|
||||
dump_response(TenantInfoResponse, WorkspaceService.get_tenant_info(tenant, session=session)),
|
||||
dump_response(
|
||||
CurrentWorkspaceSummaryResponse,
|
||||
WorkspaceService.get_current_workspace_summary(tenant, current_user.id, session=session),
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
@@ -308,6 +315,31 @@ class SwitchWorkspaceApi(Resource):
|
||||
|
||||
@console_ns.route("/workspaces/custom-config")
|
||||
class CustomConfigWorkspaceApi(Resource):
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspaceCustomConfigResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_tenant_id: str):
|
||||
tenant = TenantService.get_tenant_by_id(current_tenant_id, session=session)
|
||||
if tenant is None:
|
||||
raise NotFound()
|
||||
|
||||
custom_config = tenant.custom_config_dict
|
||||
replace_webapp_logo = (
|
||||
f"{dify_config.FILES_URL}/files/workspaces/{tenant.id}/webapp-logo"
|
||||
if custom_config.get("replace_webapp_logo")
|
||||
else None
|
||||
)
|
||||
return dump_response(
|
||||
WorkspaceCustomConfigResponse,
|
||||
{
|
||||
"remove_webapp_brand": custom_config.get("remove_webapp_brand", False),
|
||||
"replace_webapp_logo": replace_webapp_logo,
|
||||
},
|
||||
)
|
||||
|
||||
@console_ns.expect(console_ns.models[WorkspaceCustomConfigPayload.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspaceTenantResultResponse.__name__])
|
||||
@setup_required
|
||||
|
||||
@@ -7037,13 +7037,6 @@ Request body:
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [ConsoleHumanInputFormSubmitResponse](#consolehumaninputformsubmitresponse)<br> |
|
||||
|
||||
### [POST] /info
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [TenantInfoResponse](#tenantinforesponse)<br> |
|
||||
|
||||
### [GET] /installed-apps
|
||||
#### Parameters
|
||||
|
||||
@@ -10069,13 +10062,6 @@ Returns information about why and where the workflow is paused.
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [TenantListResponse](#tenantlistresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [TenantInfoResponse](#tenantinforesponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/agent-provider/{provider_name}
|
||||
Get specific agent provider details
|
||||
|
||||
@@ -12031,6 +12017,14 @@ Returns permission flags that control workspace features like member invitations
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceAccessMatrix](#workspaceaccessmatrix)<br> |
|
||||
|
||||
### [GET] /workspaces/current/summary
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [CurrentWorkspaceSummaryResponse](#currentworkspacesummaryresponse)<br> |
|
||||
| 409 | Current workspace is archived | |
|
||||
|
||||
### [GET] /workspaces/current/tool-labels
|
||||
#### Responses
|
||||
|
||||
@@ -12838,6 +12832,13 @@ Returns permission flags that control workspace features like member invitations
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Trigger providers retrieved successfully | **application/json**: [TriggerProviderListResponse](#triggerproviderlistresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/custom-config
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceCustomConfigResponse](#workspacecustomconfigresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/custom-config
|
||||
#### Request Body
|
||||
|
||||
@@ -16619,6 +16620,16 @@ Model class for credential form schema.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| CredentialType | string | | |
|
||||
|
||||
#### CurrentWorkspaceSummaryResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| credits | integer | Remaining credits in the effective pool; -1 means unlimited. | Yes |
|
||||
| id | string | | Yes |
|
||||
| name | string | | Yes |
|
||||
| plan | string | | Yes |
|
||||
| role | [TenantAccountRole](#tenantaccountrole) | | Yes |
|
||||
|
||||
#### CustomConfigurationResponse
|
||||
|
||||
Model class for provider custom configuration response.
|
||||
|
||||
@@ -325,26 +325,41 @@ class AccountService:
|
||||
if account.status == AccountStatus.BANNED:
|
||||
raise Unauthorized("Account is banned.")
|
||||
|
||||
current_tenant = session.scalar(
|
||||
current_tenant_join = session.scalar(
|
||||
select(TenantAccountJoin)
|
||||
.where(TenantAccountJoin.account_id == account.id, TenantAccountJoin.current == True)
|
||||
.limit(1)
|
||||
)
|
||||
if current_tenant:
|
||||
account.set_tenant_id_with_session(current_tenant.tenant_id, session=session)
|
||||
else:
|
||||
available_ta = session.scalar(
|
||||
if current_tenant_join is not None:
|
||||
account.set_tenant_id_with_session(current_tenant_join.tenant_id, session=session)
|
||||
|
||||
has_valid_current_tenant = (
|
||||
current_tenant_join is not None
|
||||
and account.current_tenant is not None
|
||||
and account.current_tenant.status == TenantStatus.NORMAL
|
||||
)
|
||||
if not has_valid_current_tenant:
|
||||
if current_tenant_join is not None:
|
||||
current_tenant_join.current = False
|
||||
|
||||
available_tenant_join = session.scalar(
|
||||
select(TenantAccountJoin)
|
||||
.where(TenantAccountJoin.account_id == account.id)
|
||||
.join(Tenant, TenantAccountJoin.tenant_id == Tenant.id)
|
||||
.where(
|
||||
TenantAccountJoin.account_id == account.id,
|
||||
Tenant.status == TenantStatus.NORMAL,
|
||||
)
|
||||
.order_by(TenantAccountJoin.id.asc())
|
||||
.limit(1)
|
||||
)
|
||||
if not available_ta:
|
||||
if available_tenant_join is None:
|
||||
if current_tenant_join is not None:
|
||||
session.commit()
|
||||
return None
|
||||
|
||||
account.set_tenant_id_with_session(available_ta.tenant_id, session=session)
|
||||
available_ta.current = True
|
||||
available_ta.last_opened_at = naive_utc_now()
|
||||
account.set_tenant_id_with_session(available_tenant_join.tenant_id, session=session)
|
||||
available_tenant_join.current = True
|
||||
available_tenant_join.last_opened_at = naive_utc_now()
|
||||
session.commit()
|
||||
|
||||
AccountService._refresh_account_last_active(account, session)
|
||||
|
||||
@@ -96,6 +96,25 @@ class WorkspaceService:
|
||||
next_credit_reset_date=billing_info.get("next_credit_reset_date"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_current_workspace_summary(cls, tenant: Tenant, account_id: str, *, session: Session) -> dict[str, object]:
|
||||
tenant_account_join = session.scalar(
|
||||
select(TenantAccountJoin)
|
||||
.where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account_id)
|
||||
.limit(1)
|
||||
)
|
||||
assert tenant_account_join is not None, "TenantAccountJoin not found"
|
||||
|
||||
effective_pool = cls.get_effective_credit_pool(tenant.id, session=session)
|
||||
|
||||
return {
|
||||
"id": tenant.id,
|
||||
"name": tenant.name,
|
||||
"role": tenant_account_join.role,
|
||||
"plan": effective_pool.plan,
|
||||
"credits": effective_pool.remaining_credits,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_tenant_info(cls, tenant: Tenant, session: Session):
|
||||
if not tenant:
|
||||
|
||||
@@ -5,14 +5,14 @@ from http import HTTPStatus
|
||||
from inspect import unwrap
|
||||
from io import BytesIO
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from sqlalchemy import Engine, event
|
||||
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.exceptions import Unauthorized
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
import services
|
||||
from controllers.common.errors import (
|
||||
@@ -22,11 +22,13 @@ from controllers.common.errors import (
|
||||
TooManyFilesError,
|
||||
UnsupportedFileTypeError,
|
||||
)
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.error import AccountNotLinkTenantError
|
||||
from controllers.console.workspace.error import CurrentWorkspaceArchivedError
|
||||
from controllers.console.workspace.workspace import (
|
||||
CurrentWorkspaceSummaryApi,
|
||||
CustomConfigWorkspaceApi,
|
||||
SwitchWorkspaceApi,
|
||||
TenantApi,
|
||||
TenantInfoResponse,
|
||||
TenantListApi,
|
||||
WebappLogoWorkspaceApi,
|
||||
@@ -334,66 +336,59 @@ class TestWorkspaceListApi:
|
||||
assert result["has_more"] is True
|
||||
|
||||
|
||||
class TestTenantApi:
|
||||
def test_post_active_tenant(self, app: Flask):
|
||||
api = TenantApi()
|
||||
method = unwrap(api.post)
|
||||
def test_legacy_current_workspace_routes_are_not_registered():
|
||||
urls = {url for _resource, resource_urls, _route_doc, _kwargs in console_ns.resources for url in resource_urls}
|
||||
|
||||
assert "/workspaces/current" not in urls
|
||||
assert "/info" not in urls
|
||||
|
||||
|
||||
class TestCurrentWorkspaceSummaryApi:
|
||||
def test_get_summary(self, app: Flask):
|
||||
api = CurrentWorkspaceSummaryApi()
|
||||
method = unwrap(api.get)
|
||||
tenant = make_tenant()
|
||||
user = make_account_with_tenant(tenant)
|
||||
session = MagicMock()
|
||||
summary = {
|
||||
"id": tenant.id,
|
||||
"name": tenant.name,
|
||||
"role": "owner",
|
||||
"plan": CloudPlan.SANDBOX,
|
||||
"credits": 180,
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/workspaces/current"),
|
||||
app.test_request_context("/workspaces/current/summary"),
|
||||
patch(
|
||||
"controllers.console.workspace.workspace.WorkspaceService.get_tenant_info", return_value={"id": "t1"}
|
||||
),
|
||||
"controllers.console.workspace.workspace.WorkspaceService.get_current_workspace_summary",
|
||||
return_value=summary,
|
||||
) as get_summary,
|
||||
):
|
||||
result, status = method(api, MagicMock(), user)
|
||||
result, status = method(api, session, user)
|
||||
|
||||
assert status == HTTPStatus.OK
|
||||
assert result["id"] == "t1"
|
||||
assert result == {
|
||||
"id": tenant.id,
|
||||
"name": tenant.name,
|
||||
"role": "owner",
|
||||
"plan": "sandbox",
|
||||
"credits": 180,
|
||||
}
|
||||
get_summary.assert_called_once_with(tenant, user.id, session=session)
|
||||
|
||||
def test_post_archived_with_switch(self, app: Flask):
|
||||
api = TenantApi()
|
||||
method = unwrap(api.post)
|
||||
archived = make_tenant(status=TenantStatus.ARCHIVE)
|
||||
new_tenant = make_tenant("new")
|
||||
user = make_account_with_tenant(archived)
|
||||
with (
|
||||
app.test_request_context("/workspaces/current"),
|
||||
patch("controllers.console.workspace.workspace.TenantService.get_join_tenants", return_value=[new_tenant]),
|
||||
patch("controllers.console.workspace.workspace.TenantService.switch_tenant") as switch_tenant,
|
||||
patch(
|
||||
"controllers.console.workspace.workspace.WorkspaceService.get_tenant_info", return_value={"id": "new"}
|
||||
),
|
||||
):
|
||||
result, status = method(api, MagicMock(), user)
|
||||
assert result["id"] == "new"
|
||||
switch_tenant.assert_called_once_with(user, new_tenant.id, session=ANY)
|
||||
def test_get_archived_tenant_returns_conflict(self, app: Flask):
|
||||
api = CurrentWorkspaceSummaryApi()
|
||||
method = unwrap(api.get)
|
||||
tenant = make_tenant(status=TenantStatus.ARCHIVE)
|
||||
user = make_account_with_tenant(tenant)
|
||||
|
||||
def test_post_archived_no_tenant(self, app: Flask):
|
||||
api = TenantApi()
|
||||
method = unwrap(api.post)
|
||||
user = make_account_with_tenant(make_tenant(status=TenantStatus.ARCHIVE))
|
||||
with (
|
||||
app.test_request_context("/workspaces/current"),
|
||||
patch("controllers.console.workspace.workspace.TenantService.get_join_tenants", return_value=[]),
|
||||
):
|
||||
with pytest.raises(Unauthorized):
|
||||
with app.test_request_context("/workspaces/current/summary"):
|
||||
with pytest.raises(CurrentWorkspaceArchivedError) as exc_info:
|
||||
method(api, MagicMock(), user)
|
||||
|
||||
def test_post_info_path(self, app: Flask, caplog: pytest.LogCaptureFixture):
|
||||
api = TenantApi()
|
||||
method = unwrap(api.post)
|
||||
tenant = make_tenant()
|
||||
user = make_account_with_tenant(tenant)
|
||||
with (
|
||||
app.test_request_context("/info"),
|
||||
caplog.at_level(logging.WARNING, logger="controllers.console.workspace.workspace"),
|
||||
patch(
|
||||
"controllers.console.workspace.workspace.WorkspaceService.get_tenant_info", return_value={"id": "t1"}
|
||||
),
|
||||
):
|
||||
result, status = method(api, MagicMock(), user)
|
||||
assert "Deprecated URL /info was used." in caplog.messages
|
||||
assert status == HTTPStatus.OK
|
||||
assert exc_info.value.code == HTTPStatus.CONFLICT
|
||||
assert exc_info.value.error_code == "current_workspace_archived"
|
||||
|
||||
|
||||
class TestTenantInfoResponse:
|
||||
@@ -467,6 +462,43 @@ class TestSwitchWorkspaceApi:
|
||||
|
||||
|
||||
class TestCustomConfigWorkspaceApi:
|
||||
def test_get_workspace_not_found(self, app: Flask, workspace_session: scoped_session[Session]):
|
||||
api = CustomConfigWorkspaceApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
with app.test_request_context("/workspaces/custom-config"), pytest.raises(NotFound):
|
||||
method(api, workspace_session, "missing")
|
||||
|
||||
def test_get_defaults(self, app: Flask, workspace_session: scoped_session[Session]):
|
||||
api = CustomConfigWorkspaceApi()
|
||||
method = unwrap(api.get)
|
||||
tenant = make_tenant(custom_config={})
|
||||
workspace_session.add(tenant)
|
||||
workspace_session.commit()
|
||||
|
||||
with app.test_request_context("/workspaces/custom-config"):
|
||||
result = method(api, workspace_session, tenant.id)
|
||||
|
||||
assert result == {"remove_webapp_brand": False, "replace_webapp_logo": None}
|
||||
|
||||
def test_get_configured_brand(self, app: Flask, workspace_session: scoped_session[Session]):
|
||||
api = CustomConfigWorkspaceApi()
|
||||
method = unwrap(api.get)
|
||||
tenant = make_tenant(custom_config={"remove_webapp_brand": True, "replace_webapp_logo": "logo-file-id"})
|
||||
workspace_session.add(tenant)
|
||||
workspace_session.commit()
|
||||
|
||||
with (
|
||||
app.test_request_context("/workspaces/custom-config"),
|
||||
patch("controllers.console.workspace.workspace.dify_config.FILES_URL", "https://files.example.com"),
|
||||
):
|
||||
result = method(api, workspace_session, tenant.id)
|
||||
|
||||
assert result == {
|
||||
"remove_webapp_brand": True,
|
||||
"replace_webapp_logo": f"https://files.example.com/files/workspaces/{tenant.id}/webapp-logo",
|
||||
}
|
||||
|
||||
def test_post_success(self, app: Flask, workspace_session: scoped_session[Session]):
|
||||
api = CustomConfigWorkspaceApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
@@ -456,14 +456,11 @@ class TestAccountService:
|
||||
sqlite_session.add(tenant_join)
|
||||
sqlite_session.commit()
|
||||
|
||||
with (
|
||||
patch.object(Account, "set_tenant_id_with_session") as mock_set_tenant_id,
|
||||
patch.object(AccountService, "_refresh_account_last_active") as mock_refresh_last_active,
|
||||
):
|
||||
with patch.object(AccountService, "_refresh_account_last_active") as mock_refresh_last_active:
|
||||
result = AccountService.load_user(account.id, sqlite_session)
|
||||
|
||||
assert result is account
|
||||
mock_set_tenant_id.assert_called_once_with(tenant.id, session=sqlite_session)
|
||||
assert result.current_tenant_id == tenant.id
|
||||
mock_refresh_last_active.assert_called_once_with(account, sqlite_session)
|
||||
|
||||
def test_load_user_not_found(self, sqlite_session: Session) -> None:
|
||||
@@ -519,6 +516,54 @@ class TestAccountService:
|
||||
assert persisted_tenant_join.current is True
|
||||
assert persisted_tenant_join.last_opened_at == mock_now
|
||||
|
||||
def test_load_user_switches_from_archived_current_tenant(self, sqlite_session: Session) -> None:
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
archived_tenant = Tenant(name="Archived Workspace", status=TenantStatus.ARCHIVE)
|
||||
available_tenant = Tenant(name="Available Workspace")
|
||||
sqlite_session.add_all([account, archived_tenant, available_tenant])
|
||||
sqlite_session.flush()
|
||||
archived_join = TenantAccountJoin(
|
||||
tenant_id=archived_tenant.id,
|
||||
account_id=account.id,
|
||||
role=TenantAccountRole.NORMAL,
|
||||
current=True,
|
||||
)
|
||||
available_join = TenantAccountJoin(
|
||||
tenant_id=available_tenant.id,
|
||||
account_id=account.id,
|
||||
role=TenantAccountRole.NORMAL,
|
||||
current=False,
|
||||
)
|
||||
sqlite_session.add_all([archived_join, available_join])
|
||||
sqlite_session.commit()
|
||||
|
||||
with patch.object(AccountService, "_refresh_account_last_active"):
|
||||
result = AccountService.load_user(account.id, sqlite_session)
|
||||
|
||||
assert result is account
|
||||
assert result.current_tenant_id == available_tenant.id
|
||||
assert archived_join.current is False
|
||||
assert available_join.current is True
|
||||
|
||||
def test_load_user_returns_none_without_normal_tenant(self, sqlite_session: Session) -> None:
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
archived_tenant = Tenant(name="Archived Workspace", status=TenantStatus.ARCHIVE)
|
||||
sqlite_session.add_all([account, archived_tenant])
|
||||
sqlite_session.flush()
|
||||
archived_join = TenantAccountJoin(
|
||||
tenant_id=archived_tenant.id,
|
||||
account_id=account.id,
|
||||
role=TenantAccountRole.NORMAL,
|
||||
current=True,
|
||||
)
|
||||
sqlite_session.add(archived_join)
|
||||
sqlite_session.commit()
|
||||
|
||||
result = AccountService.load_user(account.id, sqlite_session)
|
||||
|
||||
assert result is None
|
||||
assert archived_join.current is False
|
||||
|
||||
def test_load_user_keeps_tenant_accessible_with_expiring_session(self, sqlite_session: Session) -> None:
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
tenant = Tenant(name="Test Workspace")
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from enums.cloud_plan import CloudPlan
|
||||
from models.account import Tenant
|
||||
from services.credit_pool_service import CreditPoolBalance
|
||||
from services.workspace_service import WorkspaceService
|
||||
|
||||
|
||||
def test_get_current_workspace_summary_sandbox_uses_trial_only() -> None:
|
||||
tenant = Tenant(name="Workspace")
|
||||
membership = SimpleNamespace(role="owner")
|
||||
session = MagicMock()
|
||||
session.scalar.return_value = membership
|
||||
trial_pool = CreditPoolBalance(
|
||||
tenant_id=tenant.id,
|
||||
pool_type="trial",
|
||||
quota_limit=200,
|
||||
quota_used=20,
|
||||
)
|
||||
billing_info = {
|
||||
"enabled": True,
|
||||
"subscription": {"plan": CloudPlan.SANDBOX},
|
||||
}
|
||||
config = SimpleNamespace(BILLING_ENABLED=True)
|
||||
|
||||
with (
|
||||
patch("services.workspace_service.dify_config", config),
|
||||
patch("services.workspace_service.BillingService.get_info", return_value=billing_info) as get_info,
|
||||
patch("services.credit_pool_service.CreditPoolService.get_pool", return_value=trial_pool) as get_pool,
|
||||
patch("services.workspace_service.FeatureService.get_features") as get_features,
|
||||
):
|
||||
result = WorkspaceService.get_current_workspace_summary(tenant, "account-1", session=session)
|
||||
|
||||
assert result == {
|
||||
"id": tenant.id,
|
||||
"name": tenant.name,
|
||||
"role": "owner",
|
||||
"plan": CloudPlan.SANDBOX,
|
||||
"credits": 180,
|
||||
}
|
||||
get_info.assert_called_once_with(tenant.id, exclude_vector_space=True)
|
||||
get_pool.assert_called_once_with(tenant_id=tenant.id, pool_type="trial", session=session)
|
||||
get_features.assert_not_called()
|
||||
|
||||
|
||||
def test_get_current_workspace_summary_falls_back_from_exhausted_paid_pool() -> None:
|
||||
tenant = Tenant(name="Workspace")
|
||||
session = MagicMock()
|
||||
session.scalar.return_value = SimpleNamespace(role="admin")
|
||||
paid_pool = CreditPoolBalance(
|
||||
tenant_id=tenant.id,
|
||||
pool_type="paid",
|
||||
quota_limit=500,
|
||||
quota_used=500,
|
||||
)
|
||||
trial_pool = CreditPoolBalance(
|
||||
tenant_id=tenant.id,
|
||||
pool_type="trial",
|
||||
quota_limit=100,
|
||||
quota_used=40,
|
||||
)
|
||||
billing_info = {
|
||||
"enabled": True,
|
||||
"subscription": {"plan": CloudPlan.TEAM},
|
||||
}
|
||||
config = SimpleNamespace(BILLING_ENABLED=True)
|
||||
|
||||
with (
|
||||
patch("services.workspace_service.dify_config", config),
|
||||
patch("services.workspace_service.BillingService.get_info", return_value=billing_info),
|
||||
patch(
|
||||
"services.credit_pool_service.CreditPoolService.get_pool",
|
||||
side_effect=[paid_pool, trial_pool],
|
||||
) as get_pool,
|
||||
):
|
||||
result = WorkspaceService.get_current_workspace_summary(tenant, "account-1", session=session)
|
||||
|
||||
assert result["plan"] == CloudPlan.TEAM
|
||||
assert result["credits"] == 60
|
||||
assert get_pool.call_args_list == [
|
||||
call(tenant_id=tenant.id, pool_type="paid", session=session),
|
||||
call(tenant_id=tenant.id, pool_type="trial", session=session),
|
||||
]
|
||||
|
||||
|
||||
def test_get_current_workspace_summary_billing_disabled_skips_billing_and_credits() -> None:
|
||||
tenant = Tenant(name="Workspace")
|
||||
session = MagicMock()
|
||||
session.scalar.return_value = SimpleNamespace(role="editor")
|
||||
config = SimpleNamespace(BILLING_ENABLED=False)
|
||||
|
||||
with (
|
||||
patch("services.workspace_service.dify_config", config),
|
||||
patch("services.workspace_service.BillingService.get_info") as get_info,
|
||||
patch("services.credit_pool_service.CreditPoolService.get_pool") as get_pool,
|
||||
):
|
||||
result = WorkspaceService.get_current_workspace_summary(tenant, "account-1", session=session)
|
||||
|
||||
assert result == {
|
||||
"id": tenant.id,
|
||||
"name": tenant.name,
|
||||
"role": "editor",
|
||||
"plan": None,
|
||||
"credits": None,
|
||||
}
|
||||
get_info.assert_not_called()
|
||||
get_pool.assert_not_called()
|
||||
@@ -5919,7 +5919,7 @@
|
||||
"count": 1
|
||||
},
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 8
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"web/service/datasets.ts": {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { oc } from '@orpc/contract'
|
||||
import { zPostInfoResponse } from './zod.gen'
|
||||
|
||||
export const post = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postInfo',
|
||||
path: '/info',
|
||||
tags: ['console'],
|
||||
})
|
||||
.output(zPostInfoResponse)
|
||||
|
||||
export const info = {
|
||||
post,
|
||||
}
|
||||
|
||||
export const contract = {
|
||||
info,
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type ClientOptions = {
|
||||
baseUrl: `${string}://${string}/console/api` | (string & {})
|
||||
}
|
||||
|
||||
export type TenantInfoResponse = {
|
||||
created_at?: number | null
|
||||
custom_config?: WorkspaceCustomConfigResponse | null
|
||||
id: string
|
||||
in_trial?: boolean | null
|
||||
name?: string | null
|
||||
next_credit_reset_date?: number | null
|
||||
plan?: string | null
|
||||
role?: string | null
|
||||
status?: string | null
|
||||
trial_credits?: number | null
|
||||
trial_credits_exhausted_at?: number | null
|
||||
trial_credits_used?: number | null
|
||||
trial_end_reason?: string | null
|
||||
}
|
||||
|
||||
export type WorkspaceCustomConfigResponse = {
|
||||
remove_webapp_brand?: boolean | null
|
||||
replace_webapp_logo?: string | null
|
||||
}
|
||||
|
||||
export type PostInfoData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/info'
|
||||
}
|
||||
|
||||
export type PostInfoResponses = {
|
||||
200: TenantInfoResponse
|
||||
}
|
||||
|
||||
export type PostInfoResponse = PostInfoResponses[keyof PostInfoResponses]
|
||||
@@ -1,35 +0,0 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import * as z from 'zod'
|
||||
|
||||
/**
|
||||
* WorkspaceCustomConfigResponse
|
||||
*/
|
||||
export const zWorkspaceCustomConfigResponse = z.object({
|
||||
remove_webapp_brand: z.boolean().nullish(),
|
||||
replace_webapp_logo: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* TenantInfoResponse
|
||||
*/
|
||||
export const zTenantInfoResponse = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
custom_config: zWorkspaceCustomConfigResponse.nullish(),
|
||||
id: z.string(),
|
||||
in_trial: z.boolean().nullish(),
|
||||
name: z.string().nullish(),
|
||||
next_credit_reset_date: z.int().nullish(),
|
||||
plan: z.string().nullish(),
|
||||
role: z.string().nullish(),
|
||||
status: z.string().nullish(),
|
||||
trial_credits: z.int().nullish(),
|
||||
trial_credits_exhausted_at: z.int().nullish(),
|
||||
trial_credits_used: z.int().nullish(),
|
||||
trial_end_reason: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zPostInfoResponse = zTenantInfoResponse
|
||||
@@ -34,7 +34,6 @@ export const contractLoaders = {
|
||||
forgotPassword: () =>
|
||||
import('./forgot-password/orpc.gen').then(({ forgotPassword }) => ({ forgotPassword })),
|
||||
form: () => import('./form/orpc.gen').then(({ form }) => ({ form })),
|
||||
info: () => import('./info/orpc.gen').then(({ info }) => ({ info })),
|
||||
init: () => import('./init/orpc.gen').then(({ init }) => ({ init })),
|
||||
installedApps: () =>
|
||||
import('./installed-apps/orpc.gen').then(({ installedApps }) => ({ installedApps })),
|
||||
|
||||
@@ -23,7 +23,6 @@ import { features } from './features/orpc.gen'
|
||||
import { files } from './files/orpc.gen'
|
||||
import { forgotPassword } from './forgot-password/orpc.gen'
|
||||
import { form } from './form/orpc.gen'
|
||||
import { info } from './info/orpc.gen'
|
||||
import { init } from './init/orpc.gen'
|
||||
import { installedApps } from './installed-apps/orpc.gen'
|
||||
import { instructionGenerate } from './instruction-generate/orpc.gen'
|
||||
@@ -80,7 +79,6 @@ const communityContract = {
|
||||
files,
|
||||
forgotPassword,
|
||||
form,
|
||||
info,
|
||||
init,
|
||||
installedApps,
|
||||
instructionGenerate,
|
||||
|
||||
@@ -151,6 +151,7 @@ import {
|
||||
zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdRoleBindingsPath,
|
||||
zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdRoleBindingsResponse,
|
||||
zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse,
|
||||
zGetWorkspacesCurrentSummaryResponse,
|
||||
zGetWorkspacesCurrentToolLabelsResponse,
|
||||
zGetWorkspacesCurrentToolProviderApiGetQuery,
|
||||
zGetWorkspacesCurrentToolProviderApiGetResponse,
|
||||
@@ -205,6 +206,7 @@ import {
|
||||
zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsOauthAuthorizePath,
|
||||
zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsOauthAuthorizeResponse,
|
||||
zGetWorkspacesCurrentTriggersResponse,
|
||||
zGetWorkspacesCustomConfigResponse,
|
||||
zGetWorkspacesResponse,
|
||||
zPatchWorkspacesCurrentCustomizedSnippetsBySnippetIdBody,
|
||||
zPatchWorkspacesCurrentCustomizedSnippetsBySnippetIdPath,
|
||||
@@ -319,7 +321,6 @@ import {
|
||||
zPostWorkspacesCurrentRbacRolesByRoleIdCopyPath,
|
||||
zPostWorkspacesCurrentRbacRolesByRoleIdCopyResponse,
|
||||
zPostWorkspacesCurrentRbacRolesResponse,
|
||||
zPostWorkspacesCurrentResponse,
|
||||
zPostWorkspacesCurrentToolProviderApiAddBody,
|
||||
zPostWorkspacesCurrentToolProviderApiAddResponse,
|
||||
zPostWorkspacesCurrentToolProviderApiDeleteBody,
|
||||
@@ -3037,6 +3038,20 @@ export const rbac = {
|
||||
}
|
||||
|
||||
export const get62 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
operationId: 'getWorkspacesCurrentSummary',
|
||||
path: '/workspaces/current/summary',
|
||||
tags: ['console'],
|
||||
})
|
||||
.output(zGetWorkspacesCurrentSummaryResponse)
|
||||
|
||||
export const summary2 = {
|
||||
get: get62,
|
||||
}
|
||||
|
||||
export const get63 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3047,7 +3062,7 @@ export const get62 = oc
|
||||
.output(zGetWorkspacesCurrentToolLabelsResponse)
|
||||
|
||||
export const toolLabels = {
|
||||
get: get62,
|
||||
get: get63,
|
||||
}
|
||||
|
||||
export const post48 = oc
|
||||
@@ -3080,7 +3095,7 @@ export const delete13 = {
|
||||
post: post49,
|
||||
}
|
||||
|
||||
export const get63 = oc
|
||||
export const get64 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3091,11 +3106,11 @@ export const get63 = oc
|
||||
.input(z.object({ query: zGetWorkspacesCurrentToolProviderApiGetQuery }))
|
||||
.output(zGetWorkspacesCurrentToolProviderApiGetResponse)
|
||||
|
||||
export const get64 = {
|
||||
get: get63,
|
||||
export const get65 = {
|
||||
get: get64,
|
||||
}
|
||||
|
||||
export const get65 = oc
|
||||
export const get66 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3107,7 +3122,7 @@ export const get65 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderApiRemoteResponse)
|
||||
|
||||
export const remote = {
|
||||
get: get65,
|
||||
get: get66,
|
||||
}
|
||||
|
||||
export const post50 = oc
|
||||
@@ -3144,7 +3159,7 @@ export const test = {
|
||||
pre,
|
||||
}
|
||||
|
||||
export const get66 = oc
|
||||
export const get67 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3156,7 +3171,7 @@ export const get66 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderApiToolsResponse)
|
||||
|
||||
export const tools = {
|
||||
get: get66,
|
||||
get: get67,
|
||||
}
|
||||
|
||||
export const post52 = oc
|
||||
@@ -3177,7 +3192,7 @@ export const update2 = {
|
||||
export const api = {
|
||||
add,
|
||||
delete: delete13,
|
||||
get: get64,
|
||||
get: get65,
|
||||
remote,
|
||||
schema,
|
||||
test,
|
||||
@@ -3205,7 +3220,7 @@ export const add2 = {
|
||||
post: post53,
|
||||
}
|
||||
|
||||
export const get67 = oc
|
||||
export const get68 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3222,10 +3237,10 @@ export const get67 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderCredentialInfoResponse)
|
||||
|
||||
export const info = {
|
||||
get: get67,
|
||||
get: get68,
|
||||
}
|
||||
|
||||
export const get68 = oc
|
||||
export const get69 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3245,7 +3260,7 @@ export const get68 = oc
|
||||
)
|
||||
|
||||
export const byCredentialType = {
|
||||
get: get68,
|
||||
get: get69,
|
||||
}
|
||||
|
||||
export const schema2 = {
|
||||
@@ -3257,7 +3272,7 @@ export const credential = {
|
||||
schema: schema2,
|
||||
}
|
||||
|
||||
export const get69 = oc
|
||||
export const get70 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3274,7 +3289,7 @@ export const get69 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderCredentialsResponse)
|
||||
|
||||
export const credentials3 = {
|
||||
get: get69,
|
||||
get: get70,
|
||||
}
|
||||
|
||||
export const post54 = oc
|
||||
@@ -3317,7 +3332,7 @@ export const delete14 = {
|
||||
post: post55,
|
||||
}
|
||||
|
||||
export const get70 = oc
|
||||
export const get71 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3329,10 +3344,10 @@ export const get70 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderIconResponse)
|
||||
|
||||
export const icon2 = {
|
||||
get: get70,
|
||||
get: get71,
|
||||
}
|
||||
|
||||
export const get71 = oc
|
||||
export const get72 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3344,10 +3359,10 @@ export const get71 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderInfoResponse)
|
||||
|
||||
export const info2 = {
|
||||
get: get71,
|
||||
get: get72,
|
||||
}
|
||||
|
||||
export const get72 = oc
|
||||
export const get73 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3361,7 +3376,7 @@ export const get72 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderOauthClientSchemaResponse)
|
||||
|
||||
export const clientSchema = {
|
||||
get: get72,
|
||||
get: get73,
|
||||
}
|
||||
|
||||
export const delete15 = oc
|
||||
@@ -3379,7 +3394,7 @@ export const delete15 = oc
|
||||
)
|
||||
.output(zDeleteWorkspacesCurrentToolProviderBuiltinByProviderOauthCustomClientResponse)
|
||||
|
||||
export const get73 = oc
|
||||
export const get74 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3410,7 +3425,7 @@ export const post56 = oc
|
||||
|
||||
export const customClient = {
|
||||
delete: delete15,
|
||||
get: get73,
|
||||
get: get74,
|
||||
post: post56,
|
||||
}
|
||||
|
||||
@@ -3419,7 +3434,7 @@ export const oauth = {
|
||||
customClient,
|
||||
}
|
||||
|
||||
export const get74 = oc
|
||||
export const get75 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3431,7 +3446,7 @@ export const get74 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderBuiltinByProviderToolsResponse)
|
||||
|
||||
export const tools2 = {
|
||||
get: get74,
|
||||
get: get75,
|
||||
}
|
||||
|
||||
export const post57 = oc
|
||||
@@ -3486,7 +3501,7 @@ export const auth = {
|
||||
post: post58,
|
||||
}
|
||||
|
||||
export const get75 = oc
|
||||
export const get76 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3498,14 +3513,14 @@ export const get75 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderMcpToolsByProviderIdResponse)
|
||||
|
||||
export const byProviderId = {
|
||||
get: get75,
|
||||
get: get76,
|
||||
}
|
||||
|
||||
export const tools3 = {
|
||||
byProviderId,
|
||||
}
|
||||
|
||||
export const get76 = oc
|
||||
export const get77 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3517,7 +3532,7 @@ export const get76 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderMcpUpdateByProviderIdResponse)
|
||||
|
||||
export const byProviderId2 = {
|
||||
get: get76,
|
||||
get: get77,
|
||||
}
|
||||
|
||||
export const update4 = {
|
||||
@@ -3596,7 +3611,7 @@ export const delete17 = {
|
||||
post: post61,
|
||||
}
|
||||
|
||||
export const get77 = oc
|
||||
export const get78 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3607,11 +3622,11 @@ export const get77 = oc
|
||||
.input(z.object({ query: zGetWorkspacesCurrentToolProviderWorkflowGetQuery.optional() }))
|
||||
.output(zGetWorkspacesCurrentToolProviderWorkflowGetResponse)
|
||||
|
||||
export const get78 = {
|
||||
get: get77,
|
||||
export const get79 = {
|
||||
get: get78,
|
||||
}
|
||||
|
||||
export const get79 = oc
|
||||
export const get80 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3623,7 +3638,7 @@ export const get79 = oc
|
||||
.output(zGetWorkspacesCurrentToolProviderWorkflowToolsResponse)
|
||||
|
||||
export const tools4 = {
|
||||
get: get79,
|
||||
get: get80,
|
||||
}
|
||||
|
||||
export const post62 = oc
|
||||
@@ -3644,7 +3659,7 @@ export const update5 = {
|
||||
export const workflow = {
|
||||
create: create2,
|
||||
delete: delete17,
|
||||
get: get78,
|
||||
get: get79,
|
||||
tools: tools4,
|
||||
update: update5,
|
||||
}
|
||||
@@ -3656,7 +3671,7 @@ export const toolProvider = {
|
||||
workflow,
|
||||
}
|
||||
|
||||
export const get80 = oc
|
||||
export const get81 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3668,10 +3683,10 @@ export const get80 = oc
|
||||
.output(zGetWorkspacesCurrentToolProvidersResponse)
|
||||
|
||||
export const toolProviders = {
|
||||
get: get80,
|
||||
get: get81,
|
||||
}
|
||||
|
||||
export const get81 = oc
|
||||
export const get82 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3682,10 +3697,10 @@ export const get81 = oc
|
||||
.output(zGetWorkspacesCurrentToolsApiResponse)
|
||||
|
||||
export const api2 = {
|
||||
get: get81,
|
||||
get: get82,
|
||||
}
|
||||
|
||||
export const get82 = oc
|
||||
export const get83 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3696,10 +3711,10 @@ export const get82 = oc
|
||||
.output(zGetWorkspacesCurrentToolsBuiltinResponse)
|
||||
|
||||
export const builtin2 = {
|
||||
get: get82,
|
||||
get: get83,
|
||||
}
|
||||
|
||||
export const get83 = oc
|
||||
export const get84 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3710,10 +3725,10 @@ export const get83 = oc
|
||||
.output(zGetWorkspacesCurrentToolsMcpResponse)
|
||||
|
||||
export const mcp2 = {
|
||||
get: get83,
|
||||
get: get84,
|
||||
}
|
||||
|
||||
export const get84 = oc
|
||||
export const get85 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3724,7 +3739,7 @@ export const get84 = oc
|
||||
.output(zGetWorkspacesCurrentToolsWorkflowResponse)
|
||||
|
||||
export const workflow2 = {
|
||||
get: get84,
|
||||
get: get85,
|
||||
}
|
||||
|
||||
export const tools5 = {
|
||||
@@ -3734,7 +3749,7 @@ export const tools5 = {
|
||||
workflow: workflow2,
|
||||
}
|
||||
|
||||
export const get85 = oc
|
||||
export const get86 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3746,13 +3761,13 @@ export const get85 = oc
|
||||
.output(zGetWorkspacesCurrentTriggerProviderByProviderIconResponse)
|
||||
|
||||
export const icon3 = {
|
||||
get: get85,
|
||||
get: get86,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info for a trigger provider
|
||||
*/
|
||||
export const get86 = oc
|
||||
export const get87 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3765,7 +3780,7 @@ export const get86 = oc
|
||||
.output(zGetWorkspacesCurrentTriggerProviderByProviderInfoResponse)
|
||||
|
||||
export const info3 = {
|
||||
get: get86,
|
||||
get: get87,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3786,7 +3801,7 @@ export const delete18 = oc
|
||||
/**
|
||||
* Get OAuth client configuration for a provider
|
||||
*/
|
||||
export const get87 = oc
|
||||
export const get88 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3820,7 +3835,7 @@ export const post63 = oc
|
||||
|
||||
export const client = {
|
||||
delete: delete18,
|
||||
get: get87,
|
||||
get: get88,
|
||||
post: post63,
|
||||
}
|
||||
|
||||
@@ -3887,7 +3902,7 @@ export const create3 = {
|
||||
/**
|
||||
* Get the request logs for a subscription instance for a trigger provider
|
||||
*/
|
||||
export const get88 = oc
|
||||
export const get89 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -3908,7 +3923,7 @@ export const get88 = oc
|
||||
)
|
||||
|
||||
export const bySubscriptionBuilderId2 = {
|
||||
get: get88,
|
||||
get: get89,
|
||||
}
|
||||
|
||||
export const logs = {
|
||||
@@ -3982,7 +3997,7 @@ export const verifyAndUpdate = {
|
||||
/**
|
||||
* Get a subscription instance for a trigger provider
|
||||
*/
|
||||
export const get89 = oc
|
||||
export const get90 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -4003,7 +4018,7 @@ export const get89 = oc
|
||||
)
|
||||
|
||||
export const bySubscriptionBuilderId5 = {
|
||||
get: get89,
|
||||
get: get90,
|
||||
}
|
||||
|
||||
export const builder = {
|
||||
@@ -4018,7 +4033,7 @@ export const builder = {
|
||||
/**
|
||||
* List all trigger subscriptions for the current tenant's provider
|
||||
*/
|
||||
export const get90 = oc
|
||||
export const get91 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -4031,13 +4046,13 @@ export const get90 = oc
|
||||
.output(zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsListResponse)
|
||||
|
||||
export const list4 = {
|
||||
get: get90,
|
||||
get: get91,
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate OAuth authorization flow for a trigger provider
|
||||
*/
|
||||
export const get91 = oc
|
||||
export const get92 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -4054,7 +4069,7 @@ export const get91 = oc
|
||||
.output(zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsOauthAuthorizeResponse)
|
||||
|
||||
export const authorize = {
|
||||
get: get91,
|
||||
get: get92,
|
||||
}
|
||||
|
||||
export const oauth3 = {
|
||||
@@ -4171,7 +4186,7 @@ export const triggerProvider = {
|
||||
/**
|
||||
* List all trigger providers for the current tenant
|
||||
*/
|
||||
export const get92 = oc
|
||||
export const get93 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -4183,21 +4198,10 @@ export const get92 = oc
|
||||
.output(zGetWorkspacesCurrentTriggersResponse)
|
||||
|
||||
export const triggers = {
|
||||
get: get92,
|
||||
get: get93,
|
||||
}
|
||||
|
||||
export const post71 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postWorkspacesCurrent',
|
||||
path: '/workspaces/current',
|
||||
tags: ['console'],
|
||||
})
|
||||
.output(zPostWorkspacesCurrentResponse)
|
||||
|
||||
export const current = {
|
||||
post: post71,
|
||||
agentProvider,
|
||||
agentProviders,
|
||||
customizedSnippets,
|
||||
@@ -4210,6 +4214,7 @@ export const current = {
|
||||
permission,
|
||||
plugin: plugin2,
|
||||
rbac,
|
||||
summary: summary2,
|
||||
toolLabels,
|
||||
toolProvider,
|
||||
toolProviders,
|
||||
@@ -4218,7 +4223,7 @@ export const current = {
|
||||
triggers,
|
||||
}
|
||||
|
||||
export const post72 = oc
|
||||
export const post71 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@@ -4231,14 +4236,24 @@ export const post72 = oc
|
||||
.output(zPostWorkspacesCustomConfigWebappLogoUploadResponse)
|
||||
|
||||
export const upload2 = {
|
||||
post: post72,
|
||||
post: post71,
|
||||
}
|
||||
|
||||
export const webappLogo = {
|
||||
upload: upload2,
|
||||
}
|
||||
|
||||
export const post73 = oc
|
||||
export const get94 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
operationId: 'getWorkspacesCustomConfig',
|
||||
path: '/workspaces/custom-config',
|
||||
tags: ['console'],
|
||||
})
|
||||
.output(zGetWorkspacesCustomConfigResponse)
|
||||
|
||||
export const post72 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@@ -4250,11 +4265,12 @@ export const post73 = oc
|
||||
.output(zPostWorkspacesCustomConfigResponse)
|
||||
|
||||
export const customConfig = {
|
||||
post: post73,
|
||||
get: get94,
|
||||
post: post72,
|
||||
webappLogo,
|
||||
}
|
||||
|
||||
export const post74 = oc
|
||||
export const post73 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@@ -4266,10 +4282,10 @@ export const post74 = oc
|
||||
.output(zPostWorkspacesInfoResponse)
|
||||
|
||||
export const info4 = {
|
||||
post: post74,
|
||||
post: post73,
|
||||
}
|
||||
|
||||
export const post75 = oc
|
||||
export const post74 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@@ -4281,10 +4297,10 @@ export const post75 = oc
|
||||
.output(zPostWorkspacesSwitchResponse)
|
||||
|
||||
export const switch3 = {
|
||||
post: post75,
|
||||
post: post74,
|
||||
}
|
||||
|
||||
export const get93 = oc
|
||||
export const get95 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -4296,7 +4312,7 @@ export const get93 = oc
|
||||
.output(zGetWorkspacesByTenantIdModelProvidersByProviderByIconTypeByLangResponse)
|
||||
|
||||
export const byLang = {
|
||||
get: get93,
|
||||
get: get95,
|
||||
}
|
||||
|
||||
export const byIconType = {
|
||||
@@ -4315,7 +4331,7 @@ export const byTenantId = {
|
||||
modelProviders: modelProviders2,
|
||||
}
|
||||
|
||||
export const get94 = oc
|
||||
export const get96 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@@ -4326,7 +4342,7 @@ export const get94 = oc
|
||||
.output(zGetWorkspacesResponse)
|
||||
|
||||
export const workspaces = {
|
||||
get: get94,
|
||||
get: get96,
|
||||
current,
|
||||
customConfig,
|
||||
info: info4,
|
||||
|
||||
@@ -8,22 +8,6 @@ export type TenantListResponse = {
|
||||
workspaces: Array<TenantListItemResponse>
|
||||
}
|
||||
|
||||
export type TenantInfoResponse = {
|
||||
created_at?: number | null
|
||||
custom_config?: WorkspaceCustomConfigResponse | null
|
||||
id: string
|
||||
in_trial?: boolean | null
|
||||
name?: string | null
|
||||
next_credit_reset_date?: number | null
|
||||
plan?: string | null
|
||||
role?: string | null
|
||||
status?: string | null
|
||||
trial_credits?: number | null
|
||||
trial_credits_exhausted_at?: number | null
|
||||
trial_credits_used?: number | null
|
||||
trial_end_reason?: string | null
|
||||
}
|
||||
|
||||
export type AgentProviderResponse = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
@@ -658,6 +642,14 @@ export type WorkspaceAccessMatrix = {
|
||||
pagination?: Pagination | null
|
||||
}
|
||||
|
||||
export type CurrentWorkspaceSummaryResponse = {
|
||||
credits: number | null
|
||||
id: string
|
||||
name: string
|
||||
plan: string | null
|
||||
role: TenantAccountRole
|
||||
}
|
||||
|
||||
export type ToolLabelListResponse = Array<ToolLabel>
|
||||
|
||||
export type ApiToolProviderAddPayload = {
|
||||
@@ -1023,6 +1015,11 @@ export type TriggerOAuthAuthorizeResponse = {
|
||||
|
||||
export type TriggerProviderListResponse = Array<TriggerProviderApiEntity>
|
||||
|
||||
export type WorkspaceCustomConfigResponse = {
|
||||
remove_webapp_brand?: boolean | null
|
||||
replace_webapp_logo?: string | null
|
||||
}
|
||||
|
||||
export type WorkspaceCustomConfigPayload = {
|
||||
remove_webapp_brand?: boolean | null
|
||||
replace_webapp_logo?: string | null
|
||||
@@ -1060,11 +1057,6 @@ export type TenantListItemResponse = {
|
||||
status?: string | null
|
||||
}
|
||||
|
||||
export type WorkspaceCustomConfigResponse = {
|
||||
remove_webapp_brand?: boolean | null
|
||||
replace_webapp_logo?: string | null
|
||||
}
|
||||
|
||||
export type SnippetListItemResponse = {
|
||||
author_name: string | null
|
||||
created_at: number
|
||||
@@ -1549,6 +1541,8 @@ export type AccessPolicyRole = {
|
||||
role_tag?: string
|
||||
}
|
||||
|
||||
export type TenantAccountRole = 'admin' | 'dataset_operator' | 'editor' | 'normal' | 'owner'
|
||||
|
||||
export type ToolLabel = {
|
||||
icon: string
|
||||
label: I18nObject
|
||||
@@ -1708,6 +1702,22 @@ export type TriggerProviderSubscriptionApiEntity = {
|
||||
workflows_in_use: number
|
||||
}
|
||||
|
||||
export type TenantInfoResponse = {
|
||||
created_at?: number | null
|
||||
custom_config?: WorkspaceCustomConfigResponse | null
|
||||
id: string
|
||||
in_trial?: boolean | null
|
||||
name?: string | null
|
||||
next_credit_reset_date?: number | null
|
||||
plan?: string | null
|
||||
role?: string | null
|
||||
status?: string | null
|
||||
trial_credits?: number | null
|
||||
trial_credits_exhausted_at?: number | null
|
||||
trial_credits_used?: number | null
|
||||
trial_end_reason?: string | null
|
||||
}
|
||||
|
||||
export type PluginDependencyType = 'github' | 'marketplace' | 'package'
|
||||
|
||||
export type Github = {
|
||||
@@ -2489,20 +2499,6 @@ export type GetWorkspacesResponses = {
|
||||
|
||||
export type GetWorkspacesResponse = GetWorkspacesResponses[keyof GetWorkspacesResponses]
|
||||
|
||||
export type PostWorkspacesCurrentData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentResponses = {
|
||||
200: TenantInfoResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentResponse =
|
||||
PostWorkspacesCurrentResponses[keyof PostWorkspacesCurrentResponses]
|
||||
|
||||
export type GetWorkspacesCurrentAgentProviderByProviderNameData = {
|
||||
body?: never
|
||||
path: {
|
||||
@@ -4732,6 +4728,24 @@ export type GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses = {
|
||||
export type GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse =
|
||||
GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses[keyof GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSummaryData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/summary'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSummaryErrors = {
|
||||
409: unknown
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSummaryResponses = {
|
||||
200: CurrentWorkspaceSummaryResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSummaryResponse =
|
||||
GetWorkspacesCurrentSummaryResponses[keyof GetWorkspacesCurrentSummaryResponses]
|
||||
|
||||
export type GetWorkspacesCurrentToolLabelsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@@ -5631,6 +5645,20 @@ export type GetWorkspacesCurrentTriggersResponses = {
|
||||
export type GetWorkspacesCurrentTriggersResponse =
|
||||
GetWorkspacesCurrentTriggersResponses[keyof GetWorkspacesCurrentTriggersResponses]
|
||||
|
||||
export type GetWorkspacesCustomConfigData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/custom-config'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCustomConfigResponses = {
|
||||
200: WorkspaceCustomConfigResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCustomConfigResponse =
|
||||
GetWorkspacesCustomConfigResponses[keyof GetWorkspacesCustomConfigResponses]
|
||||
|
||||
export type PostWorkspacesCustomConfigData = {
|
||||
body: WorkspaceCustomConfigPayload
|
||||
path?: never
|
||||
|
||||
@@ -586,6 +586,14 @@ export const zTriggerProviderErrorResponse = z.object({
|
||||
error: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WorkspaceCustomConfigResponse
|
||||
*/
|
||||
export const zWorkspaceCustomConfigResponse = z.object({
|
||||
remove_webapp_brand: z.boolean().nullish(),
|
||||
replace_webapp_logo: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WorkspaceCustomConfigPayload
|
||||
*/
|
||||
@@ -635,49 +643,6 @@ export const zTenantListResponse = z.object({
|
||||
workspaces: z.array(zTenantListItemResponse),
|
||||
})
|
||||
|
||||
/**
|
||||
* WorkspaceCustomConfigResponse
|
||||
*/
|
||||
export const zWorkspaceCustomConfigResponse = z.object({
|
||||
remove_webapp_brand: z.boolean().nullish(),
|
||||
replace_webapp_logo: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* TenantInfoResponse
|
||||
*/
|
||||
export const zTenantInfoResponse = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
custom_config: zWorkspaceCustomConfigResponse.nullish(),
|
||||
id: z.string(),
|
||||
in_trial: z.boolean().nullish(),
|
||||
name: z.string().nullish(),
|
||||
next_credit_reset_date: z.int().nullish(),
|
||||
plan: z.string().nullish(),
|
||||
role: z.string().nullish(),
|
||||
status: z.string().nullish(),
|
||||
trial_credits: z.int().nullish(),
|
||||
trial_credits_exhausted_at: z.int().nullish(),
|
||||
trial_credits_used: z.int().nullish(),
|
||||
trial_end_reason: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WorkspaceTenantResultResponse
|
||||
*/
|
||||
export const zWorkspaceTenantResultResponse = z.object({
|
||||
result: z.string(),
|
||||
tenant: zTenantInfoResponse,
|
||||
})
|
||||
|
||||
/**
|
||||
* SwitchWorkspaceResponse
|
||||
*/
|
||||
export const zSwitchWorkspaceResponse = z.object({
|
||||
new_tenant: zTenantInfoResponse,
|
||||
result: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* IconInfo
|
||||
*
|
||||
@@ -1266,6 +1231,22 @@ export const zWorkspaceAccessMatrix = z.object({
|
||||
pagination: zPagination.nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* TenantAccountRole
|
||||
*/
|
||||
export const zTenantAccountRole = z.enum(['admin', 'dataset_operator', 'editor', 'normal', 'owner'])
|
||||
|
||||
/**
|
||||
* CurrentWorkspaceSummaryResponse
|
||||
*/
|
||||
export const zCurrentWorkspaceSummaryResponse = z.object({
|
||||
credits: z.int().nullable(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
plan: z.string().nullable(),
|
||||
role: zTenantAccountRole,
|
||||
})
|
||||
|
||||
/**
|
||||
* ToolEmojiIcon
|
||||
*/
|
||||
@@ -1575,6 +1556,41 @@ export const zTriggerProviderSubscriptionListResponse = z.array(
|
||||
zTriggerProviderSubscriptionApiEntity,
|
||||
)
|
||||
|
||||
/**
|
||||
* TenantInfoResponse
|
||||
*/
|
||||
export const zTenantInfoResponse = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
custom_config: zWorkspaceCustomConfigResponse.nullish(),
|
||||
id: z.string(),
|
||||
in_trial: z.boolean().nullish(),
|
||||
name: z.string().nullish(),
|
||||
next_credit_reset_date: z.int().nullish(),
|
||||
plan: z.string().nullish(),
|
||||
role: z.string().nullish(),
|
||||
status: z.string().nullish(),
|
||||
trial_credits: z.int().nullish(),
|
||||
trial_credits_exhausted_at: z.int().nullish(),
|
||||
trial_credits_used: z.int().nullish(),
|
||||
trial_end_reason: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WorkspaceTenantResultResponse
|
||||
*/
|
||||
export const zWorkspaceTenantResultResponse = z.object({
|
||||
result: z.string(),
|
||||
tenant: zTenantInfoResponse,
|
||||
})
|
||||
|
||||
/**
|
||||
* SwitchWorkspaceResponse
|
||||
*/
|
||||
export const zSwitchWorkspaceResponse = z.object({
|
||||
new_tenant: zTenantInfoResponse,
|
||||
result: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginDependencyType
|
||||
*/
|
||||
@@ -3483,11 +3499,6 @@ export const zAccountWithRoleListResponseWritable = z.object({
|
||||
*/
|
||||
export const zGetWorkspacesResponse = zTenantListResponse
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zPostWorkspacesCurrentResponse = zTenantInfoResponse
|
||||
|
||||
export const zGetWorkspacesCurrentAgentProviderByProviderNamePath = z.object({
|
||||
provider_name: z.string(),
|
||||
})
|
||||
@@ -4797,6 +4808,11 @@ export const zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdR
|
||||
*/
|
||||
export const zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse = zWorkspaceAccessMatrix
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSummaryResponse = zCurrentWorkspaceSummaryResponse
|
||||
|
||||
/**
|
||||
* Tool labels retrieved successfully
|
||||
*/
|
||||
@@ -5350,6 +5366,11 @@ export const zPostWorkspacesCurrentTriggerProviderBySubscriptionIdSubscriptionsU
|
||||
*/
|
||||
export const zGetWorkspacesCurrentTriggersResponse = zTriggerProviderListResponse
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zGetWorkspacesCustomConfigResponse = zWorkspaceCustomConfigResponse
|
||||
|
||||
export const zPostWorkspacesCustomConfigBody = zWorkspaceCustomConfigPayload
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,8 +54,10 @@ vi.mock('@/service/server', () => ({
|
||||
serverConsoleQuery: {
|
||||
workspaces: {
|
||||
current: {
|
||||
post: {
|
||||
queryOptions: (...args: unknown[]) => mocks.workspaceQueryOptions(...args),
|
||||
summary: {
|
||||
get: {
|
||||
queryOptions: (...args: unknown[]) => mocks.workspaceQueryOptions(...args),
|
||||
},
|
||||
},
|
||||
rbac: {
|
||||
myPermissions: {
|
||||
@@ -97,7 +99,13 @@ describe('CommonLayoutHydrationBoundary', () => {
|
||||
currentEnv: 'DEVELOPMENT',
|
||||
},
|
||||
})
|
||||
mocks.workspaceQueryFn.mockResolvedValue({ id: 'workspace-id', name: 'Workspace' })
|
||||
mocks.workspaceQueryFn.mockResolvedValue({
|
||||
id: 'workspace-id',
|
||||
name: 'Workspace',
|
||||
role: 'owner',
|
||||
plan: 'sandbox',
|
||||
credits: 200,
|
||||
})
|
||||
mocks.permissionQueryFn.mockResolvedValue({
|
||||
workspace: { permission_keys: ['agent.manage'] },
|
||||
app: { default_permission_keys: [], overrides: [] },
|
||||
@@ -108,7 +116,7 @@ describe('CommonLayoutHydrationBoundary', () => {
|
||||
csrfToken: 'csrf-token',
|
||||
})
|
||||
mocks.workspaceQueryOptions.mockReturnValue({
|
||||
queryKey: ['console', 'workspaces', 'current', 'post'],
|
||||
queryKey: ['console', 'workspaces', 'current', 'summary', 'get'],
|
||||
queryFn: mocks.workspaceQueryFn,
|
||||
retry: false,
|
||||
})
|
||||
@@ -166,7 +174,7 @@ describe('CommonLayoutHydrationBoundary', () => {
|
||||
expect(queryKeys).toEqual(
|
||||
expect.arrayContaining([
|
||||
['common', 'user-profile'],
|
||||
['console', 'workspaces', 'current', 'post'],
|
||||
['console', 'workspaces', 'current', 'summary', 'get'],
|
||||
[['console', 'workspaces', 'current', 'rbac', 'myPermissions', 'get'], { type: 'query' }],
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -65,7 +65,7 @@ export async function CommonLayoutHydrationBoundary({ children }: { children: Re
|
||||
await Promise.all([
|
||||
queryClient.fetchQuery(serverUserProfileQueryOptions()),
|
||||
queryClient.prefetchQuery(
|
||||
serverConsoleQuery.workspaces.current.post.queryOptions({
|
||||
serverConsoleQuery.workspaces.current.summary.get.queryOptions({
|
||||
context,
|
||||
retry: false,
|
||||
}),
|
||||
|
||||
@@ -7,7 +7,8 @@ import { contactSalesUrl, defaultPlan } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import CustomPage from '../index'
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
@@ -17,8 +18,15 @@ vi.mock('@/config', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
const render = (ui: ReactElement) =>
|
||||
renderWithConsoleQuery(ui, {
|
||||
const render = (ui: ReactElement) => {
|
||||
const queryClient = createConsoleQueryClient()
|
||||
queryClient.setQueryData(consoleQuery.workspaces.customConfig.get.queryKey(), {
|
||||
remove_webapp_brand: false,
|
||||
replace_webapp_logo: null,
|
||||
})
|
||||
|
||||
return renderWithConsoleQuery(ui, {
|
||||
queryClient,
|
||||
systemFeatures: {
|
||||
deployment_edition: 'CLOUD',
|
||||
branding: {
|
||||
@@ -27,6 +35,7 @@ const render = (ui: ReactElement) =>
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const { mockToast } = vi.hoisted(() => {
|
||||
const mockToast = Object.assign(vi.fn(), {
|
||||
|
||||
@@ -18,6 +18,7 @@ const createHookState = (
|
||||
uploading: false,
|
||||
webappLogo: 'https://example.com/replace.png',
|
||||
webappBrandRemoved: false,
|
||||
isCustomConfigUnavailable: false,
|
||||
uploadDisabled: false,
|
||||
workspaceLogo: 'https://example.com/workspace-logo.png',
|
||||
isSandbox: false,
|
||||
@@ -106,6 +107,18 @@ describe('CustomWebAppBrand', () => {
|
||||
expect(screen.getByRole('button', { name: 'custom.apply' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable brand edits while the custom config is unavailable', () => {
|
||||
renderComponent({
|
||||
fileId: 'new-logo',
|
||||
isCustomConfigUnavailable: true,
|
||||
uploadDisabled: true,
|
||||
})
|
||||
|
||||
expect(screen.getByRole('switch')).toHaveAttribute('aria-disabled', 'true')
|
||||
expect(screen.getByRole('button', { name: 'custom.change' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'custom.apply' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should default the switch to unchecked when brand removal state is missing', () => {
|
||||
const { container } = renderComponent({
|
||||
webappBrandRemoved: undefined,
|
||||
|
||||
+98
-78
@@ -1,22 +1,30 @@
|
||||
import type { GetSystemFeaturesResponse } from '@dify/contracts/api/console/system-features/types.gen'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import type { ConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { act } from '@testing-library/react'
|
||||
import { act, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { initialLangGeniusVersionInfo, initialWorkspaceInfo } from '@/context/app-context-defaults'
|
||||
import { initialLangGeniusVersionInfo } from '@/context/app-context-defaults'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { updateCurrentWorkspace } from '@/service/common'
|
||||
import { renderHookWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { createConsoleQueryClient, renderHookWithConsoleQuery } from '@/test/console/query-data'
|
||||
import useWebAppBrand from '../use-web-app-brand'
|
||||
|
||||
let currentBrandingOverrides: Partial<GetSystemFeaturesResponse['branding']> = {}
|
||||
const renderHook = <Result, Props = void>(callback: (props: Props) => Result) =>
|
||||
renderHookWithConsoleQuery(callback, {
|
||||
let customConfig = {
|
||||
replace_webapp_logo: 'https://example.com/replace.png',
|
||||
remove_webapp_brand: false,
|
||||
}
|
||||
let seedCustomConfig = true
|
||||
let customConfigQueryPending = false
|
||||
let customConfigQueryError: Error | undefined
|
||||
const renderHook = <Result, Props = void>(callback: (props: Props) => Result) => {
|
||||
const queryClient = createConsoleQueryClient()
|
||||
if (seedCustomConfig) queryClient.setQueryData(['custom-config'], customConfig)
|
||||
|
||||
return renderHookWithConsoleQuery(callback, {
|
||||
systemFeatures: {
|
||||
branding: {
|
||||
enabled: true,
|
||||
@@ -24,7 +32,9 @@ const renderHook = <Result, Props = void>(callback: (props: Props) => Result) =>
|
||||
...currentBrandingOverrides,
|
||||
},
|
||||
},
|
||||
queryClient,
|
||||
})
|
||||
}
|
||||
|
||||
const { mockNotify, mockToast } = vi.hoisted(() => {
|
||||
const mockNotify = vi.fn()
|
||||
@@ -42,14 +52,54 @@ const { mockNotify, mockToast } = vi.hoisted(() => {
|
||||
const consoleStateRef = vi.hoisted(() => ({
|
||||
value: undefined as ConsoleStateFixture | undefined,
|
||||
}))
|
||||
const mockConsoleStateReader = vi.hoisted(() => vi.fn())
|
||||
const mockUpdateCustomConfig = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
vi.mock('@/service/common', () => ({
|
||||
updateCurrentWorkspace: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/service/client', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/client')>()
|
||||
const consoleQuery = new Proxy(actual.consoleQuery, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === 'workspaces') {
|
||||
return new Proxy(target.workspaces, {
|
||||
get(workspacesTarget, workspaceProp, workspaceReceiver) {
|
||||
if (workspaceProp === 'customConfig') {
|
||||
return {
|
||||
get: {
|
||||
key: () => ['custom-config'],
|
||||
queryOptions: () => ({
|
||||
queryKey: ['custom-config'],
|
||||
queryFn: async () => {
|
||||
if (customConfigQueryPending) return new Promise<never>(() => {})
|
||||
if (customConfigQueryError)
|
||||
return Promise.reject(new Error(customConfigQueryError.message))
|
||||
return customConfig
|
||||
},
|
||||
}),
|
||||
},
|
||||
post: {
|
||||
mutationOptions: () => ({
|
||||
mutationFn: (variables: unknown) => mockUpdateCustomConfig(variables),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.get(workspacesTarget, workspaceProp, workspaceReceiver)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return Reflect.get(target, prop, receiver)
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
...actual,
|
||||
consoleQuery,
|
||||
}
|
||||
})
|
||||
vi.mock('@/context/account-state', async () => {
|
||||
const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
|
||||
return createAccountStateModuleMock(() => ({
|
||||
@@ -87,7 +137,6 @@ vi.mock('@/app/components/base/image-uploader/utils', () => ({
|
||||
getImageUploadErrorMessage: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockUpdateCurrentWorkspace = vi.mocked(updateCurrentWorkspace)
|
||||
const mockUseProviderContext = vi.mocked(useProviderContext)
|
||||
const mockImageUpload = vi.mocked(imageUpload)
|
||||
const mockGetImageUploadErrorMessage = vi.mocked(getImageUploadErrorMessage)
|
||||
@@ -118,19 +167,6 @@ const createProviderContext = ({
|
||||
}
|
||||
|
||||
const createConsoleState = (overrides: Partial<ConsoleStateFixture> = {}): ConsoleStateFixture => {
|
||||
const { currentWorkspace: currentWorkspaceOverride, ...restOverrides } = overrides
|
||||
const workspaceOverrides: Partial<ConsoleStateFixture['currentWorkspace']> =
|
||||
currentWorkspaceOverride ?? {}
|
||||
const currentWorkspace = {
|
||||
...initialWorkspaceInfo,
|
||||
...workspaceOverrides,
|
||||
custom_config: {
|
||||
replace_webapp_logo: 'https://example.com/replace.png',
|
||||
remove_webapp_brand: false,
|
||||
...workspaceOverrides.custom_config,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
userProfile: testUserProfile,
|
||||
refreshUserProfile: vi.fn(),
|
||||
@@ -142,15 +178,12 @@ const createConsoleState = (overrides: Partial<ConsoleStateFixture> = {}): Conso
|
||||
refreshCurrentWorkspace: vi.fn(),
|
||||
langGeniusVersionInfo: initialLangGeniusVersionInfo,
|
||||
isLoadingCurrentWorkspace: false,
|
||||
...restOverrides,
|
||||
currentWorkspace,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('useWebAppBrand', () => {
|
||||
let consoleState: ConsoleStateFixture
|
||||
const setConsoleState = (nextValue: ConsoleStateFixture) => {
|
||||
consoleState = nextValue
|
||||
consoleStateRef.value = nextValue
|
||||
}
|
||||
|
||||
@@ -160,8 +193,14 @@ describe('useWebAppBrand', () => {
|
||||
setConsoleState(createConsoleState())
|
||||
currentBrandingOverrides = {}
|
||||
|
||||
mockUpdateCurrentWorkspace.mockResolvedValue(consoleState.currentWorkspace as ICurrentWorkspace)
|
||||
mockConsoleStateReader.mockImplementation(() => consoleState)
|
||||
customConfig = {
|
||||
replace_webapp_logo: 'https://example.com/replace.png',
|
||||
remove_webapp_brand: false,
|
||||
}
|
||||
seedCustomConfig = true
|
||||
customConfigQueryPending = false
|
||||
customConfigQueryError = undefined
|
||||
mockUpdateCustomConfig.mockResolvedValue(customConfig)
|
||||
mockUseProviderContext.mockReturnValue(createProviderContext())
|
||||
mockGetImageUploadErrorMessage.mockReturnValue('upload error')
|
||||
})
|
||||
@@ -213,17 +252,7 @@ describe('useWebAppBrand', () => {
|
||||
planType: Plan.sandbox,
|
||||
}),
|
||||
)
|
||||
setConsoleState(
|
||||
createConsoleState({
|
||||
currentWorkspace: {
|
||||
...initialWorkspaceInfo,
|
||||
custom_config: {
|
||||
replace_webapp_logo: 'https://example.com/replace.png',
|
||||
remove_webapp_brand: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
customConfig = { ...customConfig, remove_webapp_brand: true }
|
||||
|
||||
const { result } = renderHook(() => useWebAppBrand())
|
||||
|
||||
@@ -240,18 +269,35 @@ describe('useWebAppBrand', () => {
|
||||
expect(result.current.workspaceLogo).toBe('')
|
||||
})
|
||||
|
||||
it('should fall back to an empty custom logo when custom config is missing', () => {
|
||||
setConsoleState({
|
||||
...createConsoleState(),
|
||||
currentWorkspace: {
|
||||
...initialWorkspaceInfo,
|
||||
},
|
||||
})
|
||||
it('should fall back to an empty custom logo when the custom config has no logo', () => {
|
||||
customConfig = { replace_webapp_logo: '', remove_webapp_brand: false }
|
||||
|
||||
const { result } = renderHook(() => useWebAppBrand())
|
||||
|
||||
expect(result.current.webappLogo).toBe('')
|
||||
})
|
||||
|
||||
it('should disable brand edits while the custom config is loading', () => {
|
||||
seedCustomConfig = false
|
||||
customConfigQueryPending = true
|
||||
|
||||
const { result } = renderHook(() => useWebAppBrand())
|
||||
|
||||
expect(result.current.isCustomConfigUnavailable).toBe(true)
|
||||
expect(result.current.uploadDisabled).toBe(true)
|
||||
})
|
||||
|
||||
it('should disable brand edits when the custom config request fails', async () => {
|
||||
seedCustomConfig = false
|
||||
customConfigQueryError = new Error('custom config unavailable')
|
||||
|
||||
const { result } = renderHook(() => useWebAppBrand())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isCustomConfigUnavailable).toBe(true)
|
||||
})
|
||||
expect(result.current.uploadDisabled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// State transitions driven by user actions.
|
||||
@@ -348,12 +394,6 @@ describe('useWebAppBrand', () => {
|
||||
})
|
||||
|
||||
it('should persist the selected logo and reset transient state on apply', async () => {
|
||||
const mutateCurrentWorkspace = vi.fn()
|
||||
setConsoleState(
|
||||
createConsoleState({
|
||||
refreshCurrentWorkspace: mutateCurrentWorkspace,
|
||||
}),
|
||||
)
|
||||
mockImageUpload.mockImplementation(({ onSuccessCallback }) => {
|
||||
onSuccessCallback({ id: 'new-logo' })
|
||||
})
|
||||
@@ -373,64 +413,44 @@ describe('useWebAppBrand', () => {
|
||||
await result.current.handleApply()
|
||||
})
|
||||
|
||||
expect(mockUpdateCurrentWorkspace).toHaveBeenCalledWith({
|
||||
url: '/workspaces/custom-config',
|
||||
expect(mockUpdateCustomConfig).toHaveBeenCalledWith({
|
||||
body: {
|
||||
remove_webapp_brand: false,
|
||||
replace_webapp_logo: 'new-logo',
|
||||
},
|
||||
})
|
||||
expect(mutateCurrentWorkspace).toHaveBeenCalledTimes(1)
|
||||
expect(result.current.fileId).toBe('')
|
||||
expect(result.current.imgKey).toBe(previousImgKey + 1)
|
||||
dateNowSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should restore the default branding configuration', async () => {
|
||||
const mutateCurrentWorkspace = vi.fn()
|
||||
setConsoleState(
|
||||
createConsoleState({
|
||||
refreshCurrentWorkspace: mutateCurrentWorkspace,
|
||||
}),
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useWebAppBrand())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRestore()
|
||||
})
|
||||
|
||||
expect(mockUpdateCurrentWorkspace).toHaveBeenCalledWith({
|
||||
url: '/workspaces/custom-config',
|
||||
expect(mockUpdateCustomConfig).toHaveBeenCalledWith({
|
||||
body: {
|
||||
remove_webapp_brand: false,
|
||||
replace_webapp_logo: '',
|
||||
},
|
||||
})
|
||||
expect(mutateCurrentWorkspace).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should persist brand removal changes', async () => {
|
||||
const mutateCurrentWorkspace = vi.fn()
|
||||
setConsoleState(
|
||||
createConsoleState({
|
||||
refreshCurrentWorkspace: mutateCurrentWorkspace,
|
||||
}),
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useWebAppBrand())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSwitch(true)
|
||||
})
|
||||
|
||||
expect(mockUpdateCurrentWorkspace).toHaveBeenCalledWith({
|
||||
url: '/workspaces/custom-config',
|
||||
expect(mockUpdateCustomConfig).toHaveBeenCalledWith({
|
||||
body: {
|
||||
remove_webapp_brand: true,
|
||||
},
|
||||
})
|
||||
expect(mutateCurrentWorkspace).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should clear temporary upload state on cancel', () => {
|
||||
|
||||
@@ -1,46 +1,50 @@
|
||||
import type { WorkspaceCustomConfigPayload } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { currentWorkspaceAtom, refreshCurrentWorkspaceAtom } from '@/context/workspace-state'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { updateCurrentWorkspace } from '@/service/common'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
const MAX_LOGO_FILE_SIZE = 5 * 1024 * 1024
|
||||
const CUSTOM_CONFIG_URL = '/workspaces/custom-config'
|
||||
const WEB_APP_LOGO_UPLOAD_URL = '/workspaces/custom-config/webapp-logo/upload'
|
||||
const useWebAppBrand = () => {
|
||||
const { t } = useTranslation()
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const mutateCurrentWorkspace = useSetAtom(refreshCurrentWorkspaceAtom)
|
||||
const queryClient = useQueryClient()
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const [fileId, setFileId] = useState('')
|
||||
const [imgKey, setImgKey] = useState(() => Date.now())
|
||||
const [uploadProgress, setUploadProgress] = useState(0)
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const customConfigQuery = useQuery(consoleQuery.workspaces.customConfig.get.queryOptions())
|
||||
const { data: customConfig } = customConfigQuery
|
||||
const updateCustomConfigMutation = useMutation(
|
||||
consoleQuery.workspaces.customConfig.post.mutationOptions(),
|
||||
)
|
||||
const isSandbox = enableBilling && plan.type === Plan.sandbox
|
||||
const uploading = uploadProgress > 0 && uploadProgress < 100
|
||||
const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || ''
|
||||
const webappBrandRemoved = currentWorkspace.custom_config?.remove_webapp_brand
|
||||
const webappLogo = customConfig?.replace_webapp_logo || ''
|
||||
const webappBrandRemoved = customConfig?.remove_webapp_brand ?? undefined
|
||||
const canManageCustomBrand = hasPermission(workspacePermissionKeys, 'customization.manage')
|
||||
const uploadDisabled = isSandbox || webappBrandRemoved || !canManageCustomBrand
|
||||
const isCustomConfigUnavailable = customConfigQuery.isPending || customConfigQuery.isError
|
||||
const uploadDisabled =
|
||||
isCustomConfigUnavailable || isSandbox || webappBrandRemoved || !canManageCustomBrand
|
||||
const workspaceLogo = systemFeatures.branding.enabled
|
||||
? systemFeatures.branding.workspace_logo
|
||||
: ''
|
||||
const persistWorkspaceBrand = async (body: Record<string, unknown>) => {
|
||||
await updateCurrentWorkspace({
|
||||
url: CUSTOM_CONFIG_URL,
|
||||
body,
|
||||
const persistWorkspaceBrand = async (body: WorkspaceCustomConfigPayload) => {
|
||||
await updateCustomConfigMutation.mutateAsync({ body })
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.workspaces.customConfig.get.key(),
|
||||
})
|
||||
mutateCurrentWorkspace()
|
||||
}
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
@@ -101,6 +105,7 @@ const useWebAppBrand = () => {
|
||||
uploading,
|
||||
webappLogo,
|
||||
webappBrandRemoved,
|
||||
isCustomConfigUnavailable,
|
||||
uploadDisabled,
|
||||
workspaceLogo,
|
||||
isSandbox,
|
||||
|
||||
@@ -18,6 +18,7 @@ const CustomWebAppBrand = () => {
|
||||
uploading,
|
||||
webappLogo,
|
||||
webappBrandRemoved,
|
||||
isCustomConfigUnavailable,
|
||||
uploadDisabled,
|
||||
workspaceLogo,
|
||||
canManageCustomBrand,
|
||||
@@ -36,7 +37,7 @@ const CustomWebAppBrand = () => {
|
||||
<Switch
|
||||
size="lg"
|
||||
checked={webappBrandRemoved ?? false}
|
||||
disabled={isSandbox || !canManageCustomBrand}
|
||||
disabled={isCustomConfigUnavailable || isSandbox || !canManageCustomBrand}
|
||||
onCheckedChange={handleSwitch}
|
||||
/>
|
||||
</div>
|
||||
@@ -105,7 +106,7 @@ const CustomWebAppBrand = () => {
|
||||
variant="primary"
|
||||
className="mr-2"
|
||||
onClick={handleApply}
|
||||
disabled={webappBrandRemoved || !canManageCustomBrand}
|
||||
disabled={isCustomConfigUnavailable || webappBrandRemoved || !canManageCustomBrand}
|
||||
>
|
||||
{t(($) => $.apply, { ns: 'custom' })}
|
||||
</Button>
|
||||
|
||||
@@ -122,6 +122,11 @@ vi.mock('@/app/components/billing/billing-page', () => ({
|
||||
default: () => <div data-testid="billing-page" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/custom/custom-page', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>custom.custom</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/data-source-page-new', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="data-source-page" />,
|
||||
@@ -151,13 +156,7 @@ const baseConsoleState: ConsoleStateFixture = {
|
||||
id: '1',
|
||||
name: 'Workspace',
|
||||
plan: '',
|
||||
status: '',
|
||||
created_at: 0,
|
||||
role: 'owner',
|
||||
providers: [],
|
||||
trial_credits: 0,
|
||||
trial_credits_used: 0,
|
||||
next_credit_reset_date: 0,
|
||||
},
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceOwner: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { Role } from '@/models/access-control'
|
||||
import type { ICurrentWorkspace, Member } from '@/models/common'
|
||||
import type { Member } from '@/models/common'
|
||||
import type { ConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
@@ -227,7 +227,7 @@ describe('MembersPage', () => {
|
||||
|
||||
setConsoleState({
|
||||
userProfile: { email: 'owner@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'owner' } as ICurrentWorkspace,
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'owner' },
|
||||
isCurrentWorkspaceOwner: true,
|
||||
isCurrentWorkspaceManager: true,
|
||||
workspacePermissionKeys: ['workspace.member.manage'],
|
||||
@@ -346,7 +346,7 @@ describe('MembersPage', () => {
|
||||
it('should hide manager controls for non-owner non-manager users', () => {
|
||||
setConsoleState({
|
||||
userProfile: { email: 'admin@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' },
|
||||
isCurrentWorkspaceOwner: false,
|
||||
isCurrentWorkspaceManager: false,
|
||||
} as unknown as ConsoleStateFixture)
|
||||
@@ -459,7 +459,7 @@ describe('MembersPage', () => {
|
||||
it('should show invite button when user is manager but not owner', () => {
|
||||
setConsoleState({
|
||||
userProfile: { email: 'admin@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' },
|
||||
isCurrentWorkspaceOwner: false,
|
||||
isCurrentWorkspaceManager: true,
|
||||
workspacePermissionKeys: ['workspace.member.manage'],
|
||||
@@ -474,7 +474,7 @@ describe('MembersPage', () => {
|
||||
it('should allow admins to operate other non-owner members only', () => {
|
||||
setConsoleState({
|
||||
userProfile: { email: 'admin@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' },
|
||||
isCurrentWorkspaceOwner: false,
|
||||
isCurrentWorkspaceManager: true,
|
||||
workspacePermissionKeys: ['workspace.member.manage'],
|
||||
@@ -580,7 +580,7 @@ describe('MembersPage', () => {
|
||||
it('should render role badge names from account roles', () => {
|
||||
setConsoleState({
|
||||
userProfile: { email: 'admin@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' },
|
||||
isCurrentWorkspaceOwner: false,
|
||||
isCurrentWorkspaceManager: false,
|
||||
} as unknown as ConsoleStateFixture)
|
||||
@@ -651,7 +651,7 @@ describe('MembersPage', () => {
|
||||
const user = userEvent.setup()
|
||||
setConsoleState({
|
||||
userProfile: { email: 'admin@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' },
|
||||
isCurrentWorkspaceOwner: false,
|
||||
isCurrentWorkspaceManager: true,
|
||||
workspacePermissionKeys: ['workspace.member.manage'],
|
||||
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import type { ConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
@@ -39,7 +38,7 @@ describe('InviteButton', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockConsoleStateReader.mockReturnValue({
|
||||
currentWorkspace: { id: 'workspace-id' } as ICurrentWorkspace,
|
||||
currentWorkspace: { id: 'workspace-id' },
|
||||
} as unknown as ConsoleStateFixture)
|
||||
})
|
||||
|
||||
|
||||
+6
-4
@@ -1,4 +1,3 @@
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import type { ConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
@@ -43,7 +42,7 @@ describe('EditWorkspaceModal', () => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
const consoleState = {
|
||||
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
||||
currentWorkspace: { name: 'Test Workspace' },
|
||||
isCurrentWorkspaceOwner: true,
|
||||
} as unknown as ConsoleStateFixture
|
||||
mockConsoleState.current = consoleState
|
||||
@@ -93,7 +92,10 @@ describe('EditWorkspaceModal', () => {
|
||||
assign: mockAssign,
|
||||
origin: 'http://localhost',
|
||||
})
|
||||
vi.mocked(updateWorkspaceInfo).mockResolvedValue({} as ICurrentWorkspace)
|
||||
vi.mocked(updateWorkspaceInfo).mockResolvedValue({
|
||||
result: 'success',
|
||||
tenant: { id: 'workspace-id' },
|
||||
})
|
||||
|
||||
renderModal()
|
||||
|
||||
@@ -170,7 +172,7 @@ describe('EditWorkspaceModal', () => {
|
||||
|
||||
it('should disable confirm button for non-owners', async () => {
|
||||
mockConsoleStateReader.mockReturnValue({
|
||||
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
||||
currentWorkspace: { name: 'Test Workspace' },
|
||||
isCurrentWorkspaceOwner: false,
|
||||
} as unknown as ConsoleStateFixture)
|
||||
|
||||
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import type { ConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
@@ -53,7 +52,7 @@ describe('TransferOwnershipModal', () => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
const consoleState = {
|
||||
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
||||
currentWorkspace: { name: 'Test Workspace' },
|
||||
userProfile: { email: 'owner@example.com', id: 'owner-id' },
|
||||
} as unknown as ConsoleStateFixture
|
||||
mockConsoleState.current = consoleState
|
||||
|
||||
@@ -3,12 +3,15 @@ import type {
|
||||
StepByStepTourStatePatchPayload,
|
||||
StepByStepTourStateResponse,
|
||||
} from '@dify/contracts/api/console/onboarding/types.gen'
|
||||
import type {
|
||||
GetWorkspacesCurrentSummaryResponse,
|
||||
TenantListItemResponse,
|
||||
} from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Mock } from 'vitest'
|
||||
import type { StepByStepTourSessionState } from '@/app/components/step-by-step-tour/types'
|
||||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import type { ICurrentWorkspace, IWorkspace } from '@/models/common'
|
||||
import type { ConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
@@ -238,7 +241,7 @@ vi.mock('react-i18next', async () => {
|
||||
|
||||
vi.mock('@/service/client', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/client')>()
|
||||
const currentWorkspaceQueryKey = ['console', 'workspaces', 'current', 'post'] as const
|
||||
const currentWorkspaceQueryKey = ['console', 'workspaces', 'current', 'summary', 'get'] as const
|
||||
const currentPermissionsQueryKey = [
|
||||
['console', 'workspaces', 'current', 'rbac', 'myPermissions', 'get'],
|
||||
{ type: 'query' },
|
||||
@@ -249,14 +252,16 @@ vi.mock('@/service/client', async (importOriginal) => {
|
||||
if (prop === 'workspaces') {
|
||||
return {
|
||||
current: {
|
||||
post: {
|
||||
key: () => currentWorkspaceQueryKey,
|
||||
queryKey: () => currentWorkspaceQueryKey,
|
||||
queryOptions: (options?: object) => ({
|
||||
queryKey: currentWorkspaceQueryKey,
|
||||
queryFn: () => new Promise(() => {}),
|
||||
...options,
|
||||
}),
|
||||
summary: {
|
||||
get: {
|
||||
key: () => currentWorkspaceQueryKey,
|
||||
queryKey: () => currentWorkspaceQueryKey,
|
||||
queryOptions: (options?: object) => ({
|
||||
queryKey: currentWorkspaceQueryKey,
|
||||
queryFn: () => new Promise(() => {}),
|
||||
...options,
|
||||
}),
|
||||
},
|
||||
},
|
||||
rbac: {
|
||||
myPermissions: {
|
||||
@@ -392,7 +397,7 @@ let mockPathname = '/apps'
|
||||
let mockInstalledApps: InstalledAppResponse[] = []
|
||||
let mockInstalledAppsPending = false
|
||||
let mockInstalledAppsHasNextPage = false
|
||||
let mockWorkspaces: IWorkspace[] = []
|
||||
let mockWorkspaces: TenantListItemResponse[] = []
|
||||
|
||||
const ownerWorkspacePermissionKeys = [
|
||||
'workspace.member.manage',
|
||||
@@ -450,13 +455,8 @@ const consoleState: ConsoleStateFixture = {
|
||||
id: 'workspace-1',
|
||||
name: 'Solar Studio',
|
||||
plan: Plan.team,
|
||||
status: 'normal',
|
||||
created_at: 0,
|
||||
credits: 7500,
|
||||
role: 'owner',
|
||||
providers: [],
|
||||
trial_credits: 10000,
|
||||
trial_credits_used: 2500,
|
||||
next_credit_reset_date: 0,
|
||||
},
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceOwner: true,
|
||||
@@ -497,8 +497,8 @@ const renderMainNav = (
|
||||
const currentConsoleState = mockConsoleState.current ?? consoleState
|
||||
mockConsoleState.current = currentConsoleState
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.workspaces.current.post.queryKey(),
|
||||
currentConsoleState.currentWorkspace as ICurrentWorkspace,
|
||||
consoleQuery.workspaces.current.summary.get.queryKey(),
|
||||
currentConsoleState.currentWorkspace as GetWorkspacesCurrentSummaryResponse,
|
||||
)
|
||||
queryClient.setQueryData(userProfileQueryOptions().queryKey, {
|
||||
profile: {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type {
|
||||
GetWorkspacesCurrentSummaryResponse,
|
||||
TenantListItemResponse,
|
||||
} from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import type { IWorkspace } from '@/models/common'
|
||||
import { zLicenseStatus } from '@dify/contracts/api/console/system-features/zod.gen'
|
||||
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
@@ -25,7 +27,7 @@ const {
|
||||
} = vi.hoisted(() => ({
|
||||
mockFetchWorkspaces: vi.fn(),
|
||||
mockSwitchWorkspace: vi.fn(),
|
||||
mockCurrentWorkspaceQueryKey: ['console', 'workspaces', 'current', 'post'] as const,
|
||||
mockCurrentWorkspaceQueryKey: ['console', 'workspaces', 'current', 'summary', 'get'] as const,
|
||||
mockWorkspacesQueryKey: ['console', 'workspaces', 'get'] as const,
|
||||
}))
|
||||
const mockConsoleState = vi.hoisted(() => ({
|
||||
@@ -58,14 +60,16 @@ vi.mock('@/service/client', async (importOriginal) => {
|
||||
if (prop === 'workspaces') {
|
||||
return {
|
||||
current: {
|
||||
post: {
|
||||
key: () => mockCurrentWorkspaceQueryKey,
|
||||
queryKey: () => mockCurrentWorkspaceQueryKey,
|
||||
queryOptions: (options?: object) => ({
|
||||
queryKey: mockCurrentWorkspaceQueryKey,
|
||||
queryFn: () => new Promise(() => {}),
|
||||
...options,
|
||||
}),
|
||||
summary: {
|
||||
get: {
|
||||
key: () => mockCurrentWorkspaceQueryKey,
|
||||
queryKey: () => mockCurrentWorkspaceQueryKey,
|
||||
queryOptions: (options?: object) => ({
|
||||
queryKey: mockCurrentWorkspaceQueryKey,
|
||||
queryFn: () => new Promise(() => {}),
|
||||
...options,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
get: {
|
||||
@@ -96,17 +100,12 @@ vi.mock('@/service/client', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
const currentWorkspaceValue: PostWorkspacesCurrentResponse = {
|
||||
const currentWorkspaceValue: GetWorkspacesCurrentSummaryResponse = {
|
||||
id: 'workspace-1',
|
||||
name: 'Solar Studio',
|
||||
plan: Plan.sandbox,
|
||||
status: 'normal',
|
||||
created_at: 0,
|
||||
role: 'owner',
|
||||
trial_credits: 10000,
|
||||
trial_credits_used: 2500,
|
||||
trial_credits_exhausted_at: 0,
|
||||
next_credit_reset_date: 0,
|
||||
credits: 7500,
|
||||
}
|
||||
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
@@ -115,11 +114,11 @@ vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
let mockCurrentWorkspace: PostWorkspacesCurrentResponse | undefined = currentWorkspaceValue
|
||||
let mockWorkspaces: IWorkspace[] = []
|
||||
let mockCurrentWorkspace: GetWorkspacesCurrentSummaryResponse | undefined = currentWorkspaceValue
|
||||
let mockWorkspaces: TenantListItemResponse[] = []
|
||||
|
||||
const mockCurrentWorkspaceQuery = (
|
||||
data: PostWorkspacesCurrentResponse | undefined = currentWorkspaceValue,
|
||||
data: GetWorkspacesCurrentSummaryResponse | undefined = currentWorkspaceValue,
|
||||
isPending = false,
|
||||
) => {
|
||||
mockCurrentWorkspace = isPending ? undefined : data
|
||||
@@ -134,7 +133,10 @@ const renderWorkspaceCard = (options?: RenderWorkspaceCardOptions) => {
|
||||
const { seedWorkspaces = true, systemFeaturesLicense, ...renderOptions } = options ?? {}
|
||||
const queryClient = createConsoleQueryClient()
|
||||
if (mockCurrentWorkspace)
|
||||
queryClient.setQueryData(consoleQuery.workspaces.current.post.queryKey(), mockCurrentWorkspace)
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.workspaces.current.summary.get.queryKey(),
|
||||
mockCurrentWorkspace,
|
||||
)
|
||||
if (seedWorkspaces)
|
||||
queryClient.setQueryData(consoleQuery.workspaces.get.queryKey(), { workspaces: mockWorkspaces })
|
||||
if (systemFeaturesLicense) seedSystemFeaturesLicense(queryClient, systemFeaturesLicense)
|
||||
@@ -209,6 +211,24 @@ describe('WorkspaceCard', () => {
|
||||
).toHaveAttribute('href', '/integrations/model-provider')
|
||||
})
|
||||
|
||||
it('renders unlimited credits from the summary contract', () => {
|
||||
mockCurrentWorkspaceQuery({ ...currentWorkspaceValue, credits: -1 })
|
||||
|
||||
renderWorkspaceCard({ systemFeatures: { deployment_edition: 'CLOUD' } })
|
||||
|
||||
expect(screen.getByText('common.license.unlimited')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides the credits link when the summary has no effective credits', () => {
|
||||
mockCurrentWorkspaceQuery({ ...currentWorkspaceValue, credits: null })
|
||||
|
||||
renderWorkspaceCard({ systemFeatures: { deployment_edition: 'CLOUD' } })
|
||||
|
||||
expect(
|
||||
screen.queryByRole('link', { name: /common\.mainNav\.workspace\.credits/ }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders a stable skeleton while the current workspace is loading', () => {
|
||||
mockCurrentWorkspaceQuery(undefined, true)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
@@ -25,7 +25,7 @@ import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { formatCredits, getRemainingCredits } from '../utils'
|
||||
import { formatCredits } from '../utils'
|
||||
import { WorkspaceMenuItemContent } from './workspace-menu-content'
|
||||
import WorkspacePlanBadge from './workspace-plan-badge'
|
||||
import { WorkspaceSwitcher } from './workspace-switcher'
|
||||
@@ -73,13 +73,13 @@ function WorkspaceCardSkeleton({
|
||||
)
|
||||
}
|
||||
|
||||
function WorkspaceCreditsLabel({ credits, unit }: { credits: string; unit: string }) {
|
||||
const label = `${credits} ${unit}`
|
||||
function WorkspaceCreditsLabel({ credits, unit }: { credits: string; unit?: string }) {
|
||||
const label = [credits, unit].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<span className="flex min-w-0 flex-1 items-baseline gap-0.5" title={label}>
|
||||
<span className="shrink-0 system-xs-medium">{credits}</span>
|
||||
<span className="min-w-0 truncate system-xs-regular">{unit}</span>
|
||||
{unit && <span className="min-w-0 truncate system-xs-regular">{unit}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -97,7 +97,7 @@ function WorkspaceCardTrigger({
|
||||
}: {
|
||||
name: string
|
||||
status: ReactNode
|
||||
credits: number
|
||||
credits: number | null
|
||||
showCloudBilling: boolean
|
||||
showPlanAction: boolean
|
||||
planActionLabel: string
|
||||
@@ -107,7 +107,12 @@ function WorkspaceCardTrigger({
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const creditsUnit = t(($) => $['mainNav.workspace.creditsUnit'], { ns: 'common' })
|
||||
const formattedCredits = formatCredits(credits)
|
||||
const isUnlimited = credits === -1
|
||||
const formattedCredits = isUnlimited
|
||||
? t(($) => $['license.unlimited'], { ns: 'common' })
|
||||
: credits === null
|
||||
? ''
|
||||
: formatCredits(credits)
|
||||
const showStatus = status !== undefined && status !== null
|
||||
|
||||
return (
|
||||
@@ -142,14 +147,23 @@ function WorkspaceCardTrigger({
|
||||
</PopoverTrigger>
|
||||
{showCloudBilling && (
|
||||
<div className="flex items-center justify-center gap-1.5 border-t border-divider-subtle py-2 pr-2.5 pl-2">
|
||||
<Link
|
||||
href={creditsHref}
|
||||
className="flex min-w-0 flex-1 items-center gap-0.5 px-1 text-left text-text-tertiary transition-colors hover:text-text-secondary focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid focus-visible:outline-hidden"
|
||||
aria-label={t(($) => $['mainNav.workspace.credits'], { ns: 'common', count: credits })}
|
||||
>
|
||||
<span className="i-custom-vender-main-nav-credits h-3 w-3 shrink-0" aria-hidden />
|
||||
<WorkspaceCreditsLabel credits={formattedCredits} unit={creditsUnit} />
|
||||
</Link>
|
||||
{credits !== null && (
|
||||
<Link
|
||||
href={creditsHref}
|
||||
className="flex min-w-0 flex-1 items-center gap-0.5 px-1 text-left text-text-tertiary transition-colors hover:text-text-secondary focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid focus-visible:outline-hidden"
|
||||
aria-label={
|
||||
isUnlimited
|
||||
? formattedCredits
|
||||
: t(($) => $['mainNav.workspace.credits'], { ns: 'common', count: credits })
|
||||
}
|
||||
>
|
||||
<span className="i-custom-vender-main-nav-credits h-3 w-3 shrink-0" aria-hidden />
|
||||
<WorkspaceCreditsLabel
|
||||
credits={formattedCredits}
|
||||
unit={isUnlimited ? undefined : creditsUnit}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
{showPlanAction && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -228,15 +242,15 @@ function WorkspaceMenuHeader({
|
||||
}
|
||||
|
||||
type CurrentWorkspaceCardSource = Pick<
|
||||
PostWorkspacesCurrentResponse,
|
||||
'id' | 'name' | 'plan' | 'trial_credits' | 'trial_credits_used'
|
||||
GetWorkspacesCurrentSummaryResponse,
|
||||
'id' | 'name' | 'plan' | 'credits'
|
||||
>
|
||||
|
||||
const selectCurrentWorkspaceCardData = (workspace: CurrentWorkspaceCardSource) => ({
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
plan: workspace.plan,
|
||||
credits: getRemainingCredits(workspace.trial_credits ?? 0, workspace.trial_credits_used ?? 0),
|
||||
credits: workspace.credits,
|
||||
})
|
||||
|
||||
export function WorkspaceCard() {
|
||||
@@ -247,7 +261,7 @@ export function WorkspaceCard() {
|
||||
select: ({ deployment_edition }) => deployment_edition,
|
||||
})
|
||||
const currentWorkspaceQuery = useQuery(
|
||||
consoleQuery.workspaces.current.post.queryOptions({
|
||||
consoleQuery.workspaces.current.summary.get.queryOptions({
|
||||
select: selectCurrentWorkspaceCardData,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
export const getRemainingCredits = (total: number, used: number) => Math.max(total - used, 0)
|
||||
|
||||
export const formatCredits = (value: number) => new Intl.NumberFormat().format(value)
|
||||
@@ -2,8 +2,8 @@ import type {
|
||||
StepByStepTourStatePatchPayload,
|
||||
StepByStepTourStateResponse,
|
||||
} from '@dify/contracts/api/console/onboarding/types.gen'
|
||||
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { StepByStepTourSessionState, StepByStepTourTaskId } from '../types'
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import type { ConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
@@ -33,8 +33,6 @@ type StepByStepTourFixtureState = StepByStepTourSessionState & {
|
||||
updatedAt?: string | null
|
||||
}
|
||||
|
||||
type WorkspaceRole = ICurrentWorkspace['role']
|
||||
|
||||
const mockRouterPush = vi.fn()
|
||||
const mockTrackEvent = vi.hoisted(() => vi.fn())
|
||||
let mockPathname = '/apps'
|
||||
@@ -54,7 +52,7 @@ const mockIsCurrentWorkspaceManager = vi.hoisted(() => ({
|
||||
value: true,
|
||||
}))
|
||||
const mockCurrentWorkspaceRole = vi.hoisted(() => ({
|
||||
value: 'owner' as WorkspaceRole,
|
||||
value: 'owner' as GetWorkspacesCurrentSummaryResponse['role'],
|
||||
}))
|
||||
const mockEnableLearnApp = vi.hoisted(() => ({
|
||||
value: true,
|
||||
@@ -404,13 +402,7 @@ function getMockAppContextState() {
|
||||
id: 'workspace-1',
|
||||
name: 'Solar Studio',
|
||||
plan: Plan.sandbox,
|
||||
status: 'normal',
|
||||
role: mockCurrentWorkspaceRole.value,
|
||||
created_at: 0,
|
||||
providers: [],
|
||||
trial_credits: 0,
|
||||
trial_credits_used: 0,
|
||||
next_credit_reset_date: 0,
|
||||
},
|
||||
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager.value,
|
||||
workspacePermissionKeys: mockWorkspacePermissionKeys.value,
|
||||
|
||||
@@ -53,8 +53,10 @@ export default function DevicePage() {
|
||||
refetchOnMount: false,
|
||||
})
|
||||
const account = userResp?.profile
|
||||
const { data: currentWorkspace } = useQuery({
|
||||
...consoleQuery.workspaces.current.post.queryOptions(),
|
||||
const { data: currentWorkspaceName } = useQuery({
|
||||
...consoleQuery.workspaces.current.summary.get.queryOptions({
|
||||
select: (workspace) => workspace.name,
|
||||
}),
|
||||
enabled: !!account && !profileErr,
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
@@ -175,7 +177,7 @@ export default function DevicePage() {
|
||||
accountEmail={account?.email}
|
||||
accountName={account?.name}
|
||||
accountAvatarUrl={account?.avatar_url ?? null}
|
||||
defaultWorkspace={currentWorkspace?.name ?? undefined}
|
||||
defaultWorkspace={currentWorkspaceName ?? undefined}
|
||||
onApproved={() => setView({ kind: 'success' })}
|
||||
onDenied={() => setView({ kind: 'error_expired' })}
|
||||
onError={(e) => setErrMsg(e)}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import type { TenantListItemResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type {
|
||||
GetWorkspacesCurrentSummaryResponse,
|
||||
TenantListItemResponse,
|
||||
} from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Plan as PlanType } from '@/app/components/billing/type'
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import { Select, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
@@ -12,7 +14,7 @@ import { PlanBadge } from '@/app/components/header/plan-badge'
|
||||
|
||||
type AppliedEducationContentProps = {
|
||||
workspaces: TenantListItemResponse[]
|
||||
currentWorkspace: ICurrentWorkspace
|
||||
currentWorkspace: GetWorkspacesCurrentSummaryResponse
|
||||
plan: PlanType
|
||||
action: ReactNode
|
||||
isSwitchingWorkspace: boolean
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Plan as PlanType } from '@/app/components/billing/type'
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
@@ -292,7 +292,7 @@ const EducationApplyAgeContent = () => {
|
||||
}
|
||||
|
||||
type AppliedEducationWorkspaceBlockProps = {
|
||||
currentWorkspace: ICurrentWorkspace
|
||||
currentWorkspace: GetWorkspacesCurrentSummaryResponse
|
||||
plan: PlanType
|
||||
action: ReactNode
|
||||
isSwitchingWorkspace: boolean
|
||||
|
||||
@@ -14,7 +14,7 @@ import { setZendeskConversationFields } from '@/app/components/base/zendesk/util
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import { createSystemFeaturesFixture } from '@/test/console/system-features'
|
||||
import { refreshUserProfileAtom, userProfileAtom } from '../account-state'
|
||||
import { initialWorkspaceInfo } from '../app-context-defaults'
|
||||
import { initialWorkspaceSummary } from '../app-context-defaults'
|
||||
import {
|
||||
datasetDefaultPermissionKeysAtom,
|
||||
refreshWorkspacePermissionKeysAfterMutationDenialAtom,
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
isCurrentWorkspaceEditorAtom,
|
||||
isCurrentWorkspaceManagerAtom,
|
||||
isCurrentWorkspaceOwnerAtom,
|
||||
refreshCurrentWorkspaceAtom,
|
||||
} from '../workspace-state'
|
||||
|
||||
const mockGetRequest = vi.hoisted(() => vi.fn())
|
||||
@@ -43,13 +42,8 @@ const mockCurrentWorkspaceResponse = vi.hoisted(() => ({
|
||||
id: 'workspace-1',
|
||||
name: 'Workspace',
|
||||
plan: 'sandbox',
|
||||
status: 'normal',
|
||||
created_at: 1704067200,
|
||||
credits: 200,
|
||||
role: 'editor',
|
||||
trial_credits: 200,
|
||||
trial_credits_used: 0,
|
||||
next_credit_reset_date: 1706745600,
|
||||
custom_config: {},
|
||||
}))
|
||||
const mockCurrentWorkspaceQueryState = vi.hoisted(() => ({
|
||||
data: mockCurrentWorkspaceResponse as typeof mockCurrentWorkspaceResponse | undefined,
|
||||
@@ -143,19 +137,21 @@ vi.mock('@/service/client', () => ({
|
||||
},
|
||||
workspaces: {
|
||||
current: {
|
||||
post: {
|
||||
key: () => ['current-workspace'],
|
||||
queryOptions: (options: {
|
||||
select?: (workspace?: typeof mockCurrentWorkspaceResponse) => unknown
|
||||
}) => ({
|
||||
queryKey: ['current-workspace'],
|
||||
queryFn: async () => {
|
||||
if (mockCurrentWorkspaceQueryState.isPending) return new Promise(() => {})
|
||||
summary: {
|
||||
get: {
|
||||
key: () => ['current-workspace-summary'],
|
||||
queryOptions: (options: {
|
||||
select?: (workspace?: typeof mockCurrentWorkspaceResponse) => unknown
|
||||
}) => ({
|
||||
queryKey: ['current-workspace-summary'],
|
||||
queryFn: async () => {
|
||||
if (mockCurrentWorkspaceQueryState.isPending) return new Promise(() => {})
|
||||
|
||||
return mockCurrentWorkspaceQueryState.data
|
||||
},
|
||||
...options,
|
||||
}),
|
||||
return mockCurrentWorkspaceQueryState.data
|
||||
},
|
||||
...options,
|
||||
}),
|
||||
},
|
||||
},
|
||||
rbac: {
|
||||
myPermissions: {
|
||||
@@ -227,7 +223,6 @@ function ConsoleBootstrapProbe() {
|
||||
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
|
||||
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||
const refreshUserProfile = useSetAtom(refreshUserProfileAtom)
|
||||
const refreshCurrentWorkspace = useSetAtom(refreshCurrentWorkspaceAtom)
|
||||
const refreshPermissionsAfterMutationDenial = useSetAtom(
|
||||
refreshWorkspacePermissionKeysAfterMutationDenialAtom,
|
||||
)
|
||||
@@ -286,9 +281,6 @@ function ConsoleBootstrapProbe() {
|
||||
<button type="button" onClick={refreshUserProfile}>
|
||||
refresh user
|
||||
</button>
|
||||
<button type="button" onClick={refreshCurrentWorkspace}>
|
||||
refresh workspace
|
||||
</button>
|
||||
<button type="button" onClick={() => void refreshPermissionsAfterMutationDenial()}>
|
||||
refresh permissions after denial
|
||||
</button>
|
||||
@@ -425,8 +417,8 @@ describe('Console bootstrap', () => {
|
||||
renderConsoleBootstrap()
|
||||
|
||||
expect(await screen.findByText('user:user@example.com')).toBeInTheDocument()
|
||||
expect(screen.getByText(`workspace:${initialWorkspaceInfo.name}`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`role:${initialWorkspaceInfo.role}`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`workspace:${initialWorkspaceSummary.name}`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`role:${initialWorkspaceSummary.role}`)).toBeInTheDocument()
|
||||
expect(screen.getByText('keys:')).toBeInTheDocument()
|
||||
expect(screen.getByText('dataset keys:')).toBeInTheDocument()
|
||||
expect(screen.getByText('version://')).toBeInTheDocument()
|
||||
@@ -440,7 +432,7 @@ describe('Console bootstrap', () => {
|
||||
|
||||
renderConsoleBootstrap()
|
||||
|
||||
expect(await screen.findByText(`role:${initialWorkspaceInfo.role}`)).toBeInTheDocument()
|
||||
expect(await screen.findByText(`role:${initialWorkspaceSummary.role}`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should derive role flags from the current workspace role', async () => {
|
||||
@@ -469,15 +461,13 @@ describe('Console bootstrap', () => {
|
||||
})
|
||||
|
||||
describe('Refresh actions', () => {
|
||||
it('should invalidate the source queries when refresh actions are called', async () => {
|
||||
it('should invalidate the user profile query when refresh is called', async () => {
|
||||
const { queryClient } = renderConsoleBootstrap()
|
||||
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /refresh user/i }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /refresh workspace/i }))
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user-profile'] })
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['current-workspace'] })
|
||||
})
|
||||
|
||||
it('starts a fresh permission request without waiting for an older request', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import { atom } from 'jotai'
|
||||
import { atomEffect } from 'jotai-effect'
|
||||
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
|
||||
@@ -17,7 +17,7 @@ function buildAmplitudeProperties({
|
||||
currentWorkspace,
|
||||
userProfile,
|
||||
}: {
|
||||
currentWorkspace: ICurrentWorkspace
|
||||
currentWorkspace: GetWorkspacesCurrentSummaryResponse
|
||||
userProfile: GetAccountProfileResponse
|
||||
}) {
|
||||
const properties: AmplitudeProperties = {
|
||||
@@ -29,7 +29,7 @@ function buildAmplitudeProperties({
|
||||
if (currentWorkspace.id) {
|
||||
properties.workspace_id = currentWorkspace.id
|
||||
properties.workspace_name = currentWorkspace.name
|
||||
properties.workspace_plan = currentWorkspace.plan
|
||||
if (currentWorkspace.plan) properties.workspace_plan = currentWorkspace.plan
|
||||
properties.workspace_role = currentWorkspace.role
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { LangGeniusVersionInfo } from './app-context-types'
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
|
||||
export const initialLangGeniusVersionInfo: LangGeniusVersionInfo = {
|
||||
current_env: '',
|
||||
@@ -15,16 +15,10 @@ export const initialLangGeniusVersionInfo: LangGeniusVersionInfo = {
|
||||
can_auto_update: false,
|
||||
}
|
||||
|
||||
export const initialWorkspaceInfo: ICurrentWorkspace = {
|
||||
export const initialWorkspaceSummary: GetWorkspacesCurrentSummaryResponse = {
|
||||
id: '',
|
||||
name: '',
|
||||
plan: '',
|
||||
status: '',
|
||||
created_at: 0,
|
||||
plan: null,
|
||||
credits: null,
|
||||
role: 'normal',
|
||||
providers: [],
|
||||
trial_credits: 200,
|
||||
trial_credits_used: 0,
|
||||
trial_credits_exhausted_at: 0,
|
||||
next_credit_reset_date: 0,
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { GetVersionResponse } from '@dify/contracts/api/console/version/types.gen'
|
||||
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { LangGeniusVersionInfo } from './app-context-types'
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import { initialLangGeniusVersionInfo, initialWorkspaceInfo } from './app-context-defaults'
|
||||
import { initialLangGeniusVersionInfo, initialWorkspaceSummary } from './app-context-defaults'
|
||||
|
||||
const workspaceRoles = new Set<ICurrentWorkspace['role']>([
|
||||
const workspaceRoles = new Set<GetWorkspacesCurrentSummaryResponse['role']>([
|
||||
'owner',
|
||||
'admin',
|
||||
'editor',
|
||||
@@ -27,44 +26,30 @@ export type ProfileMeta = {
|
||||
}
|
||||
|
||||
function resolveWorkspaceRole(
|
||||
role: PostWorkspacesCurrentResponse['role'],
|
||||
): ICurrentWorkspace['role'] {
|
||||
if (role && workspaceRoles.has(role as ICurrentWorkspace['role']))
|
||||
return role as ICurrentWorkspace['role']
|
||||
role: GetWorkspacesCurrentSummaryResponse['role'],
|
||||
): GetWorkspacesCurrentSummaryResponse['role'] {
|
||||
if (workspaceRoles.has(role)) return role
|
||||
|
||||
return initialWorkspaceInfo.role
|
||||
return initialWorkspaceSummary.role
|
||||
}
|
||||
|
||||
export function normalizeCurrentWorkspace(
|
||||
workspace?: PostWorkspacesCurrentResponse,
|
||||
): ICurrentWorkspace {
|
||||
if (!workspace) return initialWorkspaceInfo
|
||||
export function normalizeCurrentWorkspaceSummary(
|
||||
workspace?: GetWorkspacesCurrentSummaryResponse,
|
||||
): GetWorkspacesCurrentSummaryResponse {
|
||||
if (!workspace) return initialWorkspaceSummary
|
||||
|
||||
return {
|
||||
id: workspace.id,
|
||||
name: workspace.name ?? initialWorkspaceInfo.name,
|
||||
plan: workspace.plan ?? initialWorkspaceInfo.plan,
|
||||
status: workspace.status ?? initialWorkspaceInfo.status,
|
||||
created_at: workspace.created_at ?? initialWorkspaceInfo.created_at,
|
||||
name: workspace.name,
|
||||
plan: workspace.plan,
|
||||
credits: workspace.credits,
|
||||
role: resolveWorkspaceRole(workspace.role),
|
||||
providers: initialWorkspaceInfo.providers,
|
||||
trial_credits: workspace.trial_credits ?? initialWorkspaceInfo.trial_credits,
|
||||
trial_credits_used: workspace.trial_credits_used ?? initialWorkspaceInfo.trial_credits_used,
|
||||
trial_credits_exhausted_at:
|
||||
workspace.trial_credits_exhausted_at ?? initialWorkspaceInfo.trial_credits_exhausted_at,
|
||||
next_credit_reset_date:
|
||||
workspace.next_credit_reset_date ?? initialWorkspaceInfo.next_credit_reset_date,
|
||||
trial_end_reason: workspace.trial_end_reason ?? undefined,
|
||||
custom_config: workspace.custom_config
|
||||
? {
|
||||
remove_webapp_brand: workspace.custom_config.remove_webapp_brand ?? undefined,
|
||||
replace_webapp_logo: workspace.custom_config.replace_webapp_logo ?? undefined,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkspaceRoleFlags(currentWorkspace: ICurrentWorkspace): WorkspaceRoleFlags {
|
||||
export function getWorkspaceRoleFlags(
|
||||
currentWorkspace: GetWorkspacesCurrentSummaryResponse,
|
||||
): WorkspaceRoleFlags {
|
||||
return {
|
||||
isCurrentWorkspaceManager: ['owner', 'admin'].includes(currentWorkspace.role),
|
||||
isCurrentWorkspaceOwner: currentWorkspace.role === 'owner',
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
|
||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { initialWorkspaceInfo } from './app-context-defaults'
|
||||
import { getWorkspaceRoleFlags, normalizeCurrentWorkspace } from './app-context-normalizers'
|
||||
import { initialWorkspaceSummary } from './app-context-defaults'
|
||||
import { getWorkspaceRoleFlags, normalizeCurrentWorkspaceSummary } from './app-context-normalizers'
|
||||
|
||||
const currentWorkspaceQueryAtom = atomWithQuery(() => {
|
||||
return consoleQuery.workspaces.current.post.queryOptions({
|
||||
select: normalizeCurrentWorkspace,
|
||||
return consoleQuery.workspaces.current.summary.get.queryOptions({
|
||||
select: normalizeCurrentWorkspaceSummary,
|
||||
})
|
||||
})
|
||||
|
||||
export const currentWorkspaceAtom = atom((get) => {
|
||||
return get(currentWorkspaceQueryAtom).data ?? initialWorkspaceInfo
|
||||
return get(currentWorkspaceQueryAtom).data ?? initialWorkspaceSummary
|
||||
})
|
||||
|
||||
export const currentWorkspaceIdAtom = atom((get) => {
|
||||
@@ -43,8 +43,3 @@ export const isCurrentWorkspaceDatasetOperatorAtom = atom((get) => {
|
||||
export const currentWorkspaceLoadingAtom = atom((get) => {
|
||||
return get(currentWorkspaceQueryAtom).isPending
|
||||
})
|
||||
|
||||
export const refreshCurrentWorkspaceAtom = atom(null, (get) => {
|
||||
const queryClient = get(queryClientAtom)
|
||||
queryClient.invalidateQueries({ queryKey: consoleQuery.workspaces.current.post.key() })
|
||||
})
|
||||
@@ -26,61 +26,6 @@ export type Member = Pick<GetAccountProfileResponse, 'id' | 'name' | 'email' | '
|
||||
roles: Role[]
|
||||
}
|
||||
|
||||
const ProviderName = {
|
||||
OPENAI: 'openai',
|
||||
AZURE_OPENAI: 'azure_openai',
|
||||
ANTHROPIC: 'anthropic',
|
||||
Replicate: 'replicate',
|
||||
HuggingfaceHub: 'huggingface_hub',
|
||||
MiniMax: 'minimax',
|
||||
Spark: 'spark',
|
||||
Tongyi: 'tongyi',
|
||||
ChatGLM: 'chatglm',
|
||||
} as const
|
||||
type ProviderName = (typeof ProviderName)[keyof typeof ProviderName]
|
||||
type ProviderAzureToken = {
|
||||
openai_api_base?: string
|
||||
openai_api_key?: string
|
||||
}
|
||||
type ProviderAnthropicToken = {
|
||||
anthropic_api_key?: string
|
||||
}
|
||||
type Provider = {
|
||||
[Name in ProviderName]: {
|
||||
provider_name: Name
|
||||
} & {
|
||||
provider_type: 'custom' | 'system'
|
||||
is_valid: boolean
|
||||
is_enabled: boolean
|
||||
last_used: string
|
||||
token?: string | ProviderAzureToken | ProviderAnthropicToken
|
||||
}
|
||||
}[ProviderName]
|
||||
|
||||
export type IWorkspace = {
|
||||
id: string
|
||||
name: string
|
||||
plan: string
|
||||
status: string
|
||||
created_at: number
|
||||
last_opened_at?: number | null
|
||||
current: boolean
|
||||
}
|
||||
|
||||
export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & {
|
||||
role: 'owner' | 'admin' | 'editor' | 'dataset_operator' | 'normal'
|
||||
providers: Provider[]
|
||||
trial_credits: number
|
||||
trial_credits_used: number
|
||||
trial_credits_exhausted_at: number
|
||||
next_credit_reset_date: number
|
||||
trial_end_reason?: string
|
||||
custom_config?: {
|
||||
remove_webapp_brand?: boolean
|
||||
replace_webapp_logo?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type DataSourceNotionPage = {
|
||||
page_icon: null | {
|
||||
type: string | null
|
||||
|
||||
+8
-15
@@ -1,3 +1,7 @@
|
||||
import type {
|
||||
PostWorkspacesInfoData,
|
||||
PostWorkspacesInfoResponse,
|
||||
} from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type {
|
||||
DefaultModelResponse,
|
||||
Model,
|
||||
@@ -7,7 +11,6 @@ import type {
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type {
|
||||
CommonResponse,
|
||||
ICurrentWorkspace,
|
||||
InitValidateStatusResponse,
|
||||
SetupStatusResponse,
|
||||
} from '@/models/common'
|
||||
@@ -105,24 +108,14 @@ export const ownershipTransfer = (
|
||||
export const fetchFilePreview = ({ fileID }: { fileID: string }): Promise<{ content: string }> => {
|
||||
return get<{ content: string }>(`/files/${fileID}/preview`)
|
||||
}
|
||||
export const updateCurrentWorkspace = ({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
body: Record<string, any>
|
||||
}): Promise<ICurrentWorkspace> => {
|
||||
return post<ICurrentWorkspace>(url, { body })
|
||||
}
|
||||
|
||||
export const updateWorkspaceInfo = ({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
body: Record<string, any>
|
||||
}): Promise<ICurrentWorkspace> => {
|
||||
return post<ICurrentWorkspace>(url, { body })
|
||||
url: PostWorkspacesInfoData['url']
|
||||
body: PostWorkspacesInfoData['body']
|
||||
}): Promise<PostWorkspacesInfoResponse> => {
|
||||
return post<PostWorkspacesInfoResponse>(url, { body })
|
||||
}
|
||||
|
||||
type InvitationCheckData = {
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import type { GetWorkspacesCurrentPermissionResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { get } from './base'
|
||||
|
||||
type WorkspacePermissions = {
|
||||
workspace_id: ICurrentWorkspace['id']
|
||||
allow_member_invite: boolean
|
||||
allow_owner_transfer: boolean
|
||||
}
|
||||
|
||||
export function useWorkspacePermissions(workspaceId: ICurrentWorkspace['id'], enabled: boolean) {
|
||||
export function useWorkspacePermissions(
|
||||
workspaceId: GetWorkspacesCurrentPermissionResponse['workspace_id'],
|
||||
enabled: boolean,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ['workspace-permissions', workspaceId],
|
||||
queryFn: () => get<WorkspacePermissions>('/workspaces/current/permission'),
|
||||
queryFn: () => get<GetWorkspacesCurrentPermissionResponse>('/workspaces/current/permission'),
|
||||
enabled: enabled && !!workspaceId,
|
||||
})
|
||||
}
|
||||
@@ -1,30 +1,25 @@
|
||||
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { QueryClient, QueryKey } from '@tanstack/react-query'
|
||||
|
||||
export const currentWorkspaceQueryKey = [
|
||||
['console', 'workspaces', 'current', 'post'],
|
||||
['console', 'workspaces', 'current', 'summary', 'get'],
|
||||
{ type: 'query' },
|
||||
] as const
|
||||
|
||||
const createCurrentWorkspaceFixture = (
|
||||
overrides: Partial<PostWorkspacesCurrentResponse> = {},
|
||||
): PostWorkspacesCurrentResponse => ({
|
||||
overrides: Partial<GetWorkspacesCurrentSummaryResponse> = {},
|
||||
): GetWorkspacesCurrentSummaryResponse => ({
|
||||
id: 'workspace-1',
|
||||
name: 'Workspace',
|
||||
plan: '',
|
||||
plan: null,
|
||||
role: 'owner',
|
||||
status: '',
|
||||
created_at: 0,
|
||||
trial_credits: 0,
|
||||
trial_credits_used: 0,
|
||||
trial_credits_exhausted_at: 0,
|
||||
next_credit_reset_date: 0,
|
||||
credits: null,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
export const seedCurrentWorkspaceQuery = (
|
||||
queryClient: QueryClient,
|
||||
workspace: Partial<PostWorkspacesCurrentResponse> = {},
|
||||
workspace: Partial<GetWorkspacesCurrentSummaryResponse> = {},
|
||||
queryKey: QueryKey = currentWorkspaceQueryKey,
|
||||
) => {
|
||||
const data = createCurrentWorkspaceFixture(workspace)
|
||||
@@ -34,10 +29,10 @@ export const seedCurrentWorkspaceQuery = (
|
||||
|
||||
export const ensureCurrentWorkspaceQuery = (
|
||||
queryClient: QueryClient,
|
||||
workspace: Partial<PostWorkspacesCurrentResponse> = {},
|
||||
workspace: Partial<GetWorkspacesCurrentSummaryResponse> = {},
|
||||
queryKey: QueryKey = currentWorkspaceQueryKey,
|
||||
) => {
|
||||
const existingWorkspace = queryClient.getQueryData<PostWorkspacesCurrentResponse>(queryKey)
|
||||
const existingWorkspace = queryClient.getQueryData<GetWorkspacesCurrentSummaryResponse>(queryKey)
|
||||
if (existingWorkspace === undefined)
|
||||
return seedCurrentWorkspaceQuery(queryClient, workspace, queryKey)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
GetSystemFeaturesLicenseResponse,
|
||||
GetSystemFeaturesResponse,
|
||||
} from '@dify/contracts/api/console/system-features/types.gen'
|
||||
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import type {
|
||||
RenderHookOptions,
|
||||
@@ -45,7 +45,9 @@ type AppDslVersionQueryProvider = {
|
||||
}
|
||||
|
||||
type CurrentWorkspaceQueryProvider = {
|
||||
post?: QueryKeyProvider
|
||||
summary?: {
|
||||
get?: QueryKeyProvider
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackTrialModelsQueryKey = ['console', 'trialModels', 'get'] as const
|
||||
@@ -71,7 +73,7 @@ const getCurrentWorkspaceQueryKey = () => {
|
||||
}
|
||||
).workspaces?.current
|
||||
|
||||
return currentWorkspaceQuery?.post?.queryKey() ?? currentWorkspaceQueryKey
|
||||
return currentWorkspaceQuery?.summary?.get?.queryKey() ?? currentWorkspaceQueryKey
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +142,7 @@ export type ConsoleQueryTestOptions = {
|
||||
*/
|
||||
systemFeatures?: DeepPartial<GetSystemFeaturesResponse> | null
|
||||
accountProfile?: Partial<GetAccountProfileResponse> | null
|
||||
currentWorkspace?: Partial<PostWorkspacesCurrentResponse> | null
|
||||
currentWorkspace?: Partial<GetWorkspacesCurrentSummaryResponse> | null
|
||||
trialModels?: readonly string[] | null
|
||||
workspacePermissionKeys?: readonly string[] | null
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { createStore } from 'jotai'
|
||||
import type { LangGeniusVersionInfo } from '@/context/app-context-types'
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import { atom } from 'jotai'
|
||||
import { createSystemFeaturesFixture } from '@/test/console/system-features'
|
||||
|
||||
@@ -13,12 +13,7 @@ export type ConsoleStateFixture = {
|
||||
avatar_url?: string | null
|
||||
is_password_set?: boolean
|
||||
} | null
|
||||
currentWorkspace?:
|
||||
| ({
|
||||
id?: string
|
||||
name?: string
|
||||
} & Partial<ICurrentWorkspace>)
|
||||
| null
|
||||
currentWorkspace?: Partial<GetWorkspacesCurrentSummaryResponse> | null
|
||||
isCurrentWorkspaceManager?: boolean
|
||||
isCurrentWorkspaceOwner?: boolean
|
||||
isCurrentWorkspaceEditor?: boolean
|
||||
@@ -51,16 +46,10 @@ const defaultUserProfile = {
|
||||
const defaultCurrentWorkspace = {
|
||||
id: 'workspace-1',
|
||||
name: 'Workspace',
|
||||
plan: '',
|
||||
status: '',
|
||||
created_at: 0,
|
||||
plan: null,
|
||||
credits: null,
|
||||
role: 'owner',
|
||||
providers: [],
|
||||
trial_credits: 0,
|
||||
trial_credits_used: 0,
|
||||
trial_credits_exhausted_at: 0,
|
||||
next_credit_reset_date: 0,
|
||||
} satisfies ICurrentWorkspace
|
||||
} satisfies GetWorkspacesCurrentSummaryResponse
|
||||
|
||||
const defaultLangGeniusVersionInfo = {
|
||||
current_env: 'CLOUD',
|
||||
@@ -83,7 +72,7 @@ const accountProfileMetaAtom = atom({ currentVersion: null, currentEnv: null })
|
||||
const refreshUserProfileCallbackAtom = atom({ callback: () => {} })
|
||||
const refreshUserProfileAtom = atom(null, (get) => get(refreshUserProfileCallbackAtom).callback())
|
||||
|
||||
const currentWorkspaceAtom = atom<ICurrentWorkspace>(defaultCurrentWorkspace)
|
||||
const currentWorkspaceAtom = atom<GetWorkspacesCurrentSummaryResponse>(defaultCurrentWorkspace)
|
||||
const currentWorkspaceIdAtom = atom((get) => get(currentWorkspaceAtom).id)
|
||||
const isCurrentWorkspaceManagerAtom = atom(false)
|
||||
const isCurrentWorkspaceOwnerAtom = atom(false)
|
||||
|
||||
Reference in new issue
Block a user