From e8dc7064144ec3bade3127776319d8e560fed520 Mon Sep 17 00:00:00 2001 From: L1nSn0w Date: Mon, 11 May 2026 09:57:56 +0800 Subject: [PATCH 01/22] fix(api): "File validation failed" on Chatflow follow-up with custom file type + memory (#35891) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/agent/base_agent_runner.py | 1 - api/core/memory/token_buffer_memory.py | 2 - api/factories/file_factory/message_files.py | 15 +- api/factories/file_factory/validation.py | 43 ++++- .../core/memory/test_token_buffer_memory.py | 42 +++++ .../factories/test_file_validation.py | 159 ++++++++++++++++++ 6 files changed, 244 insertions(+), 18 deletions(-) create mode 100644 api/tests/unit_tests/factories/test_file_validation.py diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py index c22102c2ba..cba4659483 100644 --- a/api/core/agent/base_agent_runner.py +++ b/api/core/agent/base_agent_runner.py @@ -532,7 +532,6 @@ class BaseAgentRunner(AppRunner): file_objs = file_factory.build_from_message_files( message_files=files, tenant_id=self.tenant_id, - config=file_extra_config, access_controller=_file_access_controller, ) if not file_objs: diff --git a/api/core/memory/token_buffer_memory.py b/api/core/memory/token_buffer_memory.py index d840ee213c..c41c175cca 100644 --- a/api/core/memory/token_buffer_memory.py +++ b/api/core/memory/token_buffer_memory.py @@ -86,12 +86,10 @@ class TokenBufferMemory: detail = ImagePromptMessageContent.DETAIL.HIGH if file_extra_config and app_record: - # Build files directly without filtering by belongs_to file_objs = [ file_factory.build_from_message_file( message_file=message_file, tenant_id=app_record.tenant_id, - config=file_extra_config, access_controller=_file_access_controller, ) for message_file in message_files diff --git a/api/factories/file_factory/message_files.py b/api/factories/file_factory/message_files.py index 4b3d514238..27441bdcc1 100644 --- a/api/factories/file_factory/message_files.py +++ b/api/factories/file_factory/message_files.py @@ -1,11 +1,18 @@ -"""Adapters from persisted message files to graph-layer file values.""" +"""Adapters from persisted message files to graph-layer file values. + +Replay paths only: files in conversation history were validated at upload time, +so these helpers deliberately do not accept (or forward) a ``FileUploadConfig`` — +re-validation here would break replays whenever workflow ``file_upload`` config +drifts between rounds. Mirrors ``build_file_from_stored_mapping`` in +``models/utils/file_input_compat.py``. +""" from __future__ import annotations from collections.abc import Sequence from core.app.file_access import FileAccessControllerProtocol -from graphon.file import File, FileBelongsTo, FileTransferMethod, FileUploadConfig +from graphon.file import File, FileBelongsTo, FileTransferMethod from models import MessageFile from .builders import build_from_mapping @@ -15,14 +22,12 @@ def build_from_message_files( *, message_files: Sequence[MessageFile], tenant_id: str, - config: FileUploadConfig | None = None, access_controller: FileAccessControllerProtocol, ) -> Sequence[File]: return [ build_from_message_file( message_file=message_file, tenant_id=tenant_id, - config=config, access_controller=access_controller, ) for message_file in message_files @@ -34,7 +39,6 @@ def build_from_message_file( *, message_file: MessageFile, tenant_id: str, - config: FileUploadConfig | None, access_controller: FileAccessControllerProtocol, ) -> File: mapping = { @@ -54,6 +58,5 @@ def build_from_message_file( return build_from_mapping( mapping=mapping, tenant_id=tenant_id, - config=config, access_controller=access_controller, ) diff --git a/api/factories/file_factory/validation.py b/api/factories/file_factory/validation.py index 4c4f6150e4..8c4e7ef1d4 100644 --- a/api/factories/file_factory/validation.py +++ b/api/factories/file_factory/validation.py @@ -2,9 +2,25 @@ from __future__ import annotations +from collections.abc import Iterable + from graphon.file import FileTransferMethod, FileType, FileUploadConfig +def _normalize_extension(extension: str) -> str: + s = extension.strip().lower() + if not s: + return "" + return s if s.startswith(".") else "." + s + + +def _extension_matches(extension: str, whitelist: Iterable[str]) -> bool: + normalized = _normalize_extension(extension) + if not normalized: + return False + return normalized in {_normalize_extension(e) for e in whitelist} + + def is_file_valid_with_config( *, input_file_type: str, @@ -12,22 +28,31 @@ def is_file_valid_with_config( file_transfer_method: FileTransferMethod, config: FileUploadConfig, ) -> bool: - # FIXME(QIN2DIM): Always allow tool files (files generated by the assistant/model) - # These are internally generated and should bypass user upload restrictions + """Return whether the file is allowed by the upload config. + + ``allowed_file_types`` lists the buckets a file may fall into; ``CUSTOM`` is + a fallback bucket gated by ``allowed_file_extensions`` (case- and + dot-insensitive). Tool-generated files bypass user-facing config. + """ if file_transfer_method == FileTransferMethod.TOOL_FILE: return True - if ( - config.allowed_file_types - and input_file_type not in config.allowed_file_types - and input_file_type != FileType.CUSTOM - ): + allowed_types = config.allowed_file_types or [] + custom_allowed = FileType.CUSTOM in allowed_types + type_allowed = not allowed_types or input_file_type in allowed_types + + if not type_allowed and not custom_allowed: return False + # When the file is in the CUSTOM bucket, the extension whitelist is authoritative. + # An explicitly set whitelist (including the empty list) is enforced; empty == deny — + # the UI never submits an empty list, so this guards against DSL/API paths that + # bypass the UI from accidentally widening the allowlist. + in_custom_bucket = input_file_type == FileType.CUSTOM or not type_allowed if ( - input_file_type == FileType.CUSTOM + in_custom_bucket and config.allowed_file_extensions is not None - and file_extension not in config.allowed_file_extensions + and not _extension_matches(file_extension, config.allowed_file_extensions) ): return False diff --git a/api/tests/unit_tests/core/memory/test_token_buffer_memory.py b/api/tests/unit_tests/core/memory/test_token_buffer_memory.py index f459250b8e..72c24bda96 100644 --- a/api/tests/unit_tests/core/memory/test_token_buffer_memory.py +++ b/api/tests/unit_tests/core/memory/test_token_buffer_memory.py @@ -198,6 +198,48 @@ class TestBuildPromptMessageWithFiles: assert isinstance(result.content[-1], TextPromptMessageContent) assert result.content[-1].data == "user text" + def test_replay_does_not_pass_config_to_file_factory(self): + """Replay contract: history files were validated on upload, so this + path must not forward a FileUploadConfig. The factory's signature + no longer accepts ``config``; this test guards against a future + regression that re-introduces it.""" + conv = _make_conversation(AppMode.CHAT) + mem = TokenBufferMemory(conversation=conv, model_instance=_make_model_instance()) + + mock_file_extra_config = MagicMock() + mock_file_extra_config.image_config = None + + real_image_content = ImagePromptMessageContent( + url="http://example.com/img.png", format="png", mime_type="image/png" + ) + mock_app_record = MagicMock() + mock_app_record.tenant_id = "tenant-1" + + with ( + patch( + "core.memory.token_buffer_memory.FileUploadConfigManager.convert", + return_value=mock_file_extra_config, + ), + patch( + "core.memory.token_buffer_memory.file_factory.build_from_message_file", + return_value=MagicMock(), + ) as mock_build, + patch( + "core.memory.token_buffer_memory.file_manager.to_prompt_message_content", + return_value=real_image_content, + ), + ): + mem._build_prompt_message_with_files( + message_files=[MagicMock()], + text_content="user text", + message=_make_message(), + app_record=mock_app_record, + is_user_message=True, + ) + + mock_build.assert_called_once() + assert "config" not in mock_build.call_args.kwargs + @pytest.mark.parametrize("mode", [AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.COMPLETION]) def test_chat_mode_with_files_assistant_message(self, mode): """When files are present, returns AssistantPromptMessage with list content.""" diff --git a/api/tests/unit_tests/factories/test_file_validation.py b/api/tests/unit_tests/factories/test_file_validation.py new file mode 100644 index 0000000000..61337fcf10 --- /dev/null +++ b/api/tests/unit_tests/factories/test_file_validation.py @@ -0,0 +1,159 @@ +"""Unit tests for is_file_valid_with_config.""" + +from __future__ import annotations + +import pytest + +from factories.file_factory.validation import is_file_valid_with_config +from graphon.file import FileTransferMethod, FileType, FileUploadConfig + + +def _validate( + *, + input_file_type: str, + file_extension: str = ".png", + file_transfer_method: FileTransferMethod = FileTransferMethod.LOCAL_FILE, + config: FileUploadConfig, +) -> bool: + return is_file_valid_with_config( + input_file_type=input_file_type, + file_extension=file_extension, + file_transfer_method=file_transfer_method, + config=config, + ) + + +@pytest.mark.parametrize( + ("input_file_type", "file_extension", "allowed_file_types", "allowed_file_extensions", "expected"), + [ + # round-1 happy path: literal "custom" mapping, ext whitelisted + ("custom", ".png", [FileType.CUSTOM], [".png"], True), + # round-2 replay: MessageFile.type is the resolved type, but config still allows CUSTOM + ("image", ".png", [FileType.CUSTOM], [".png"], True), + ("document", ".pdf", [FileType.CUSTOM], [".pdf"], True), + # mixed bucket [IMAGE, CUSTOM]: document falls into CUSTOM bucket via extension + ("document", ".pdf", [FileType.IMAGE, FileType.CUSTOM], [".pdf"], True), + ("document", ".exe", [FileType.IMAGE, FileType.CUSTOM], [".pdf"], False), + ("image", ".jpg", [FileType.IMAGE], [], True), + ("video", ".mp4", [FileType.IMAGE, FileType.DOCUMENT], [], False), + ("custom", ".exe", [FileType.CUSTOM], [".png"], False), + # empty allowed_file_types == no type restriction + ("video", ".mp4", [], [], True), + ], +) +def test_bucket_semantics(input_file_type, file_extension, allowed_file_types, allowed_file_extensions, expected): + config = FileUploadConfig( + allowed_file_types=allowed_file_types, + allowed_file_extensions=allowed_file_extensions, + ) + assert _validate(input_file_type=input_file_type, file_extension=file_extension, config=config) is expected + + +@pytest.mark.parametrize("whitelist_entry", [".png", ".PNG", "png", "PNG", " .Png ", "PnG"]) +def test_extension_match_is_case_and_dot_insensitive(whitelist_entry): + config = FileUploadConfig( + allowed_file_types=[FileType.CUSTOM], + allowed_file_extensions=[whitelist_entry], + ) + assert _validate(input_file_type="custom", file_extension=".png", config=config) is True + + +def test_extension_mismatch_still_rejected_after_normalization(): + config = FileUploadConfig( + allowed_file_types=[FileType.CUSTOM], + allowed_file_extensions=[".png", ".jpg"], + ) + assert _validate(input_file_type="custom", file_extension=".pdf", config=config) is False + + +def test_mixed_case_whitelist_replicating_real_user_config(): + config = FileUploadConfig( + allowed_file_types=[FileType.CUSTOM], + allowed_file_extensions=[".PNG", "png", "JPG", ".WEBP", "SVG", "GIF"], + ) + for ext in (".png", ".jpg", ".webp", ".svg", ".gif"): + assert _validate(input_file_type="custom", file_extension=ext, config=config) is True + + +def test_tool_file_always_passes(): + config = FileUploadConfig(allowed_file_types=[FileType.CUSTOM], allowed_file_extensions=[".pdf"]) + assert ( + _validate( + input_file_type="image", + file_extension=".png", + file_transfer_method=FileTransferMethod.TOOL_FILE, + config=config, + ) + is True + ) + + +def test_transfer_method_gate_for_non_image(): + config = FileUploadConfig( + allowed_file_types=[FileType.DOCUMENT], + allowed_file_upload_methods=[FileTransferMethod.LOCAL_FILE], + ) + assert ( + _validate( + input_file_type="document", + file_extension=".pdf", + file_transfer_method=FileTransferMethod.LOCAL_FILE, + config=config, + ) + is True + ) + assert ( + _validate( + input_file_type="document", + file_extension=".pdf", + file_transfer_method=FileTransferMethod.REMOTE_URL, + config=config, + ) + is False + ) + + +def test_history_replay_matches_round_1_outcome_under_unchanged_config(): + """A file that passes round 1 must pass history replay when config is unchanged.""" + config = FileUploadConfig( + allowed_file_types=[FileType.CUSTOM], + allowed_file_extensions=[".png"], + ) + assert _validate(input_file_type="custom", file_extension=".png", config=config) is True + assert _validate(input_file_type="image", file_extension=".png", config=config) is True + + +def test_empty_whitelist_in_custom_bucket_denies_by_default(): + """Defensive: when a file lands in the CUSTOM bucket, an empty + allowed_file_extensions list rejects. The UI never submits empty; + this guards DSL / API paths that bypass the UI from accidentally + widening what's accepted.""" + config = FileUploadConfig( + allowed_file_types=[FileType.CUSTOM], + allowed_file_extensions=[], + ) + assert _validate(input_file_type="custom", file_extension=".png", config=config) is False + assert _validate(input_file_type="image", file_extension=".png", config=config) is False + + +def test_normalize_handles_whitespace_and_empty_consistently(): + """Whitespace-only or empty entries in the whitelist must not match real + extensions (regression guard for _normalize_extension edge cases).""" + for noisy_entry in ("", " ", "\t"): + config = FileUploadConfig( + allowed_file_types=[FileType.CUSTOM], + allowed_file_extensions=[noisy_entry], + ) + assert _validate(input_file_type="custom", file_extension=".png", config=config) is False + + +def test_empty_extension_does_not_spuriously_match_empty_whitelist_entry(): + """Defensive: even if the whitelist contains an empty / whitespace entry + (e.g., a stray comma in DSL), an extensionless file must not pass via + a both-sides-empty match. Real entries in the same whitelist still match.""" + config = FileUploadConfig( + allowed_file_types=[FileType.CUSTOM], + allowed_file_extensions=["", ".png"], + ) + assert _validate(input_file_type="custom", file_extension=".png", config=config) is True + assert _validate(input_file_type="custom", file_extension="", config=config) is False From 0b70eec69512dd6bd2eaeedb3a6e62d30a2ca422 Mon Sep 17 00:00:00 2001 From: Blackoutta <37723456+Blackoutta@users.noreply.github.com> Date: Mon, 11 May 2026 10:16:29 +0800 Subject: [PATCH 02/22] feat(human-input): expose selected action value (#35451) --- api/services/workflow_service.py | 5 +++++ api/tests/unit_tests/services/test_workflow_service.py | 8 ++++++++ web/app/components/workflow/constants.ts | 4 ++++ .../nodes/human-input/__tests__/human-input.spec.tsx | 2 +- .../workflow/nodes/human-input/__tests__/panel.spec.tsx | 1 + .../components/__tests__/user-action.spec.tsx | 9 +++------ .../nodes/human-input/components/user-action.tsx | 8 ++++---- web/app/components/workflow/nodes/human-input/panel.tsx | 5 +++++ web/i18n/en-US/workflow.json | 4 ++-- 9 files changed, 33 insertions(+), 13 deletions(-) diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index b8c2ed5e6f..eb78e0a68b 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -1066,8 +1066,13 @@ class WorkflowService: ) rendered_content = node.render_form_content_before_submission() + selected_action = next( + (user_action for user_action in node_data.user_actions if user_action.id == action), + None, + ) outputs: dict[str, Any] = dict(form_inputs) outputs["__action_id"] = action + outputs["__action_value"] = selected_action.title if selected_action else "" outputs["__rendered_content"] = node.render_form_content_with_outputs( rendered_content, outputs, node_data.outputs_field_names() ) diff --git a/api/tests/unit_tests/services/test_workflow_service.py b/api/tests/unit_tests/services/test_workflow_service.py index 1711e66b23..e152ab923c 100644 --- a/api/tests/unit_tests/services/test_workflow_service.py +++ b/api/tests/unit_tests/services/test_workflow_service.py @@ -11,6 +11,7 @@ This test suite covers: import json import uuid +from types import SimpleNamespace from typing import Any, cast from unittest.mock import ANY, MagicMock, Mock, patch, sentinel @@ -2649,7 +2650,12 @@ class TestWorkflowServiceHumanInputOperations: mock_node = MagicMock() mock_node.node_data = MagicMock() + mock_node.node_data.user_actions = [ + SimpleNamespace(id="submit", title="card_visa_enterprise_001"), + ] mock_node.node_data.outputs_field_names.return_value = ["field1"] + mock_node.render_form_content_before_submission.return_value = "Ticket: {{#$output.field1#}}" + mock_node.render_form_content_with_outputs.return_value = "Ticket: val1" with ( patch("services.workflow_service.db"), @@ -2665,6 +2671,8 @@ class TestWorkflowServiceHumanInputOperations: app_model=app_model, account=account, node_id="node-1", form_inputs={"field1": "val1"}, action="submit" ) assert result["__action_id"] == "submit" + assert result["__action_value"] == "card_visa_enterprise_001" + assert result["__rendered_content"] == "Ticket: val1" mock_saver_cls.return_value.save.assert_called_once() def test_test_human_input_delivery_success(self, service: WorkflowService) -> None: diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 101d15a140..32c7c82e33 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -221,6 +221,10 @@ export const HUMAN_INPUT_OUTPUT_STRUCT: Var[] = [ variable: '__action_id', type: VarType.string, }, + { + variable: '__action_value', + type: VarType.string, + }, { variable: '__rendered_content', type: VarType.string, diff --git a/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx b/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx index cbc9cddd4a..8d41a45468 100644 --- a/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx @@ -516,7 +516,7 @@ describe('DSL Import with Human Input Node', () => { ]) }) - it('should return empty output variables when no form inputs exist', () => { + it('should return no output variables when no form inputs exist', () => { const payload = { ...humanInputDefault.defaultValue, inputs: [], diff --git a/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx index 6ff61dce3f..0f0a6839eb 100644 --- a/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx @@ -313,6 +313,7 @@ describe('human-input/panel', () => { expect(screen.getByText('approve:editable')).toBeInTheDocument() expect(screen.getByText('review_result:string:Form input value')).toBeInTheDocument() expect(screen.getByText('__action_id:string:Action ID user triggered')).toBeInTheDocument() + expect(screen.getByText('__action_value:string:Selected action value')).toBeInTheDocument() expect(screen.getByText('__rendered_content:string:Rendered content')).toBeInTheDocument() await user.click(screen.getByRole('button', { name: 'delivery-method:editable' })) diff --git a/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx b/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx index a47a012f49..5e9074a42b 100644 --- a/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx @@ -87,7 +87,7 @@ describe('UserActionItem', () => { fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.actionNamePlaceholder'), { target: { value: 'Approve action' } }) fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.actionNamePlaceholder'), { target: { value: '1invalid' } }) fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.actionNamePlaceholder'), { target: { value: 'averyveryveryverylongidentifier' } }) - fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.buttonTextPlaceholder'), { target: { value: 'A very very very long button title' } }) + fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.buttonTextPlaceholder'), { target: { value: 'card_visa_enterprise_001' } }) expect(onChange).toHaveBeenNthCalledWith(1, expect.objectContaining({ id: 'Approve_action', @@ -96,7 +96,7 @@ describe('UserActionItem', () => { id: 'averyveryveryverylon', })) expect(onChange).toHaveBeenNthCalledWith(3, expect.objectContaining({ - title: 'A very very very lon', + title: 'card_visa_enterprise_001', })) expect(mockNotify).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: 'error', @@ -106,10 +106,7 @@ describe('UserActionItem', () => { type: 'error', message: 'nodes.humanInput.userActions.actionIdTooLong', })) - expect(mockNotify).toHaveBeenNthCalledWith(3, expect.objectContaining({ - type: 'error', - message: 'nodes.humanInput.userActions.buttonTextTooLong', - })) + expect(mockNotify).toHaveBeenCalledTimes(2) }) it('should support clearing ids, updating button style, deleting, and readonly mode', () => { diff --git a/web/app/components/workflow/nodes/human-input/components/user-action.tsx b/web/app/components/workflow/nodes/human-input/components/user-action.tsx index a83ea4f8f2..94581281e8 100644 --- a/web/app/components/workflow/nodes/human-input/components/user-action.tsx +++ b/web/app/components/workflow/nodes/human-input/components/user-action.tsx @@ -12,7 +12,7 @@ import ButtonStyleDropdown from './button-style-dropdown' const i18nPrefix = 'nodes.humanInput' const ACTION_ID_MAX_LENGTH = 20 -const BUTTON_TEXT_MAX_LENGTH = 20 +const ACTION_VALUE_MAX_LENGTH = 100 type UserActionItemProps = { data: UserAction @@ -63,9 +63,9 @@ const UserActionItem: FC = ({ const handleTextChange = (e: React.ChangeEvent) => { let value = e.target.value - if (value.length > BUTTON_TEXT_MAX_LENGTH) { - value = value.slice(0, BUTTON_TEXT_MAX_LENGTH) - toast.error(t(`${i18nPrefix}.userActions.buttonTextTooLong`, { ns: 'workflow', maxLength: BUTTON_TEXT_MAX_LENGTH })) + if (value.length > ACTION_VALUE_MAX_LENGTH) { + value = value.slice(0, ACTION_VALUE_MAX_LENGTH) + toast.error(t(`${i18nPrefix}.userActions.buttonTextTooLong`, { ns: 'workflow', maxLength: ACTION_VALUE_MAX_LENGTH })) } onChange({ ...data, title: value }) } diff --git a/web/app/components/workflow/nodes/human-input/panel.tsx b/web/app/components/workflow/nodes/human-input/panel.tsx index fa0914c098..99b5da42eb 100644 --- a/web/app/components/workflow/nodes/human-input/panel.tsx +++ b/web/app/components/workflow/nodes/human-input/panel.tsx @@ -229,6 +229,11 @@ const Panel: FC> = ({ type="string" description="Action ID user triggered" /> + {{actionName}} has been triggered", "nodes.ifElse.addCondition": "Add Condition", "nodes.ifElse.addSubVariable": "Sub Variable", From 1e6dc6247002b2a5c988a0a291efc5126e6fdd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Mon, 11 May 2026 10:22:40 +0800 Subject: [PATCH 03/22] chore: separate websocket service (#35981) --- api/.env.example | 2 +- api/configs/feature/__init__.py | 2 +- api/services/feature_service.py | 2 +- docker/.env.example | 9 +++++-- docker/docker-compose-template.yaml | 26 ++++++++++++++++++++ docker/docker-compose.yaml | 26 ++++++++++++++++++++ docker/envs/core-services/shared.env.example | 8 ++++-- docker/envs/infrastructure/nginx.env.example | 1 + docker/nginx/conf.d/default.conf.template | 4 ++- 9 files changed, 72 insertions(+), 8 deletions(-) diff --git a/api/.env.example b/api/.env.example index 56ba8a6c5d..ba153e4c9c 100644 --- a/api/.env.example +++ b/api/.env.example @@ -34,7 +34,7 @@ TRIGGER_URL=http://localhost:5001 FILES_ACCESS_TIMEOUT=300 # Collaboration mode toggle -ENABLE_COLLABORATION_MODE=false +ENABLE_COLLABORATION_MODE=true # Access token expiration time in minutes ACCESS_TOKEN_EXPIRE_MINUTES=60 diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 52e33c1789..e9bb34fa75 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1298,7 +1298,7 @@ class PositionConfig(BaseSettings): class CollaborationConfig(BaseSettings): ENABLE_COLLABORATION_MODE: bool = Field( description="Whether to enable collaboration mode features across the workspace", - default=False, + default=True, ) diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 9477c28bf3..257c4bea9a 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -166,7 +166,7 @@ class SystemFeatureModel(BaseModel): enable_email_code_login: bool = False enable_email_password_login: bool = True enable_social_oauth_login: bool = False - enable_collaboration_mode: bool = False + enable_collaboration_mode: bool = True is_allow_register: bool = False is_allow_create_workspace: bool = False is_email_setup: bool = False diff --git a/docker/.env.example b/docker/.env.example index 82bd837ffb..d9891d842a 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -34,7 +34,8 @@ CHECK_UPDATE_URL=https://updates.dify.ai OPENAI_API_BASE=https://api.openai.com/v1 MIGRATION_ENABLED=true FILES_ACCESS_TIMEOUT=300 -ENABLE_COLLABORATION_MODE=false +# Remove `collaboration` from COMPOSE_PROFILES to stop the dedicated websocket service. +ENABLE_COLLABORATION_MODE=true # Logging and server workers LOG_LEVEL=INFO @@ -52,6 +53,9 @@ DIFY_PORT=5001 SERVER_WORKER_AMOUNT=1 SERVER_WORKER_CLASS=gevent SERVER_WORKER_CONNECTIONS=10 +API_WEBSOCKET_WORKER_CLASS=geventwebsocket.gunicorn.workers.GeventWebSocketWorker +API_WEBSOCKET_WORKER_CONNECTIONS=1000 +API_WEBSOCKET_GUNICORN_TIMEOUT=360 GUNICORN_TIMEOUT=360 CELERY_WORKER_CLASS= CELERY_WORKER_AMOUNT=4 @@ -246,6 +250,7 @@ NGINX_KEEPALIVE_TIMEOUT=65 NGINX_PROXY_READ_TIMEOUT=3600s NGINX_PROXY_SEND_TIMEOUT=3600s NGINX_ENABLE_CERTBOT_CHALLENGE=false +NGINX_SOCKET_IO_UPSTREAM=api_websocket:5001 EXPOSE_NGINX_PORT=80 EXPOSE_NGINX_SSL_PORT=443 -COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql} +COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql},collaboration diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 0f65c38098..72c9d4fd90 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -261,6 +261,31 @@ services: - ssrf_proxy_network - default + # WebSocket service for workflow collaboration. + api_websocket: + <<: *shared-api-worker-config + image: langgenius/dify-api:1.14.0 + profiles: + - collaboration + environment: + MODE: api + SERVER_WORKER_AMOUNT: 1 + SERVER_WORKER_CLASS: ${API_WEBSOCKET_WORKER_CLASS:-geventwebsocket.gunicorn.workers.GeventWebSocketWorker} + SERVER_WORKER_CONNECTIONS: ${API_WEBSOCKET_WORKER_CONNECTIONS:-1000} + GUNICORN_TIMEOUT: ${API_WEBSOCKET_GUNICORN_TIMEOUT:-360} + depends_on: + db_postgres: + condition: service_healthy + required: false + db_mysql: + condition: service_healthy + required: false + redis: + condition: service_started + networks: + - ssrf_proxy_network + - default + # worker service # The Celery worker for processing all queues (dataset, workflow, mail, etc.) worker: @@ -661,6 +686,7 @@ services: NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} NGINX_ENABLE_CERTBOT_CHALLENGE: ${NGINX_ENABLE_CERTBOT_CHALLENGE:-false} + NGINX_SOCKET_IO_UPSTREAM: ${NGINX_SOCKET_IO_UPSTREAM:-api_websocket:5001} CERTBOT_DOMAIN: ${CERTBOT_DOMAIN:-} depends_on: - api diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 0f8458a58f..c1d75e01f4 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -267,6 +267,31 @@ services: - ssrf_proxy_network - default + # WebSocket service for workflow collaboration. + api_websocket: + <<: *shared-api-worker-config + image: langgenius/dify-api:1.14.0 + profiles: + - collaboration + environment: + MODE: api + SERVER_WORKER_AMOUNT: 1 + SERVER_WORKER_CLASS: ${API_WEBSOCKET_WORKER_CLASS:-geventwebsocket.gunicorn.workers.GeventWebSocketWorker} + SERVER_WORKER_CONNECTIONS: ${API_WEBSOCKET_WORKER_CONNECTIONS:-1000} + GUNICORN_TIMEOUT: ${API_WEBSOCKET_GUNICORN_TIMEOUT:-360} + depends_on: + db_postgres: + condition: service_healthy + required: false + db_mysql: + condition: service_healthy + required: false + redis: + condition: service_started + networks: + - ssrf_proxy_network + - default + # worker service # The Celery worker for processing all queues (dataset, workflow, mail, etc.) worker: @@ -667,6 +692,7 @@ services: NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} NGINX_ENABLE_CERTBOT_CHALLENGE: ${NGINX_ENABLE_CERTBOT_CHALLENGE:-false} + NGINX_SOCKET_IO_UPSTREAM: ${NGINX_SOCKET_IO_UPSTREAM:-api_websocket:5001} CERTBOT_DOMAIN: ${CERTBOT_DOMAIN:-} depends_on: - api diff --git a/docker/envs/core-services/shared.env.example b/docker/envs/core-services/shared.env.example index 2a57f6954a..af1c3ce74e 100644 --- a/docker/envs/core-services/shared.env.example +++ b/docker/envs/core-services/shared.env.example @@ -16,7 +16,8 @@ CHECK_UPDATE_URL=https://updates.dify.ai OPENAI_API_BASE=https://api.openai.com/v1 MIGRATION_ENABLED=true FILES_ACCESS_TIMEOUT=300 -ENABLE_COLLABORATION_MODE=false +# Remove `collaboration` from COMPOSE_PROFILES to stop the dedicated websocket service. +ENABLE_COLLABORATION_MODE=true CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1 CELERY_TASK_ANNOTATIONS=null AZURE_BLOB_ACCOUNT_URL=https://.blob.core.windows.net @@ -87,6 +88,9 @@ DIFY_PORT=5001 SERVER_WORKER_AMOUNT=1 SERVER_WORKER_CLASS=gevent SERVER_WORKER_CONNECTIONS=10 +API_WEBSOCKET_WORKER_CLASS=geventwebsocket.gunicorn.workers.GeventWebSocketWorker +API_WEBSOCKET_WORKER_CONNECTIONS=1000 +API_WEBSOCKET_GUNICORN_TIMEOUT=360 CELERY_SENTINEL_PASSWORD= S3_ACCESS_KEY= S3_SECRET_KEY= @@ -399,7 +403,7 @@ TABLESTORE_ENDPOINT=https://instance-name.cn-hangzhou.ots.aliyuncs.com TABLESTORE_INSTANCE_NAME=instance-name CLICKZETTA_USERNAME= CLICKZETTA_VECTOR_DISTANCE_FUNCTION=cosine_distance -COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql} +COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql},collaboration EXPOSE_NGINX_PORT=80 EXPOSE_NGINX_SSL_PORT=443 POSITION_TOOL_PINS= diff --git a/docker/envs/infrastructure/nginx.env.example b/docker/envs/infrastructure/nginx.env.example index fbe86680ba..fcb369a47d 100644 --- a/docker/envs/infrastructure/nginx.env.example +++ b/docker/envs/infrastructure/nginx.env.example @@ -15,3 +15,4 @@ NGINX_KEEPALIVE_TIMEOUT=65 NGINX_PROXY_READ_TIMEOUT=3600s NGINX_PROXY_SEND_TIMEOUT=3600s NGINX_ENABLE_CERTBOT_CHALLENGE=false +NGINX_SOCKET_IO_UPSTREAM=api_websocket:5001 diff --git a/docker/nginx/conf.d/default.conf.template b/docker/nginx/conf.d/default.conf.template index 94a748290f..64c720ca2b 100644 --- a/docker/nginx/conf.d/default.conf.template +++ b/docker/nginx/conf.d/default.conf.template @@ -15,7 +15,9 @@ server { } location /socket.io/ { - proxy_pass http://api:5001; + resolver 127.0.0.11 valid=30s ipv6=off; + set $socket_io_upstream ${NGINX_SOCKET_IO_UPSTREAM}; + proxy_pass http://$socket_io_upstream; include proxy.conf; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; From bf117dd0c8b140f8390d6f447d84a9d2fddba10d Mon Sep 17 00:00:00 2001 From: kien duong Date: Mon, 11 May 2026 09:52:29 +0700 Subject: [PATCH 04/22] fix(trace): LangSmith trace_id mismatch in chatflow workflow traces (#35979) --- .../dify_trace_langsmith/langsmith_trace.py | 6 +- .../langsmith_trace/test_langsmith_trace.py | 84 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/langsmith_trace.py b/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/langsmith_trace.py index 145bd70dbc..045ec44e4e 100644 --- a/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/langsmith_trace.py +++ b/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/langsmith_trace.py @@ -64,7 +64,9 @@ class LangSmithDataTrace(BaseTraceInstance): self.generate_name_trace(trace_info) def workflow_trace(self, trace_info: WorkflowTraceInfo): - trace_id = trace_info.trace_id or trace_info.message_id or trace_info.workflow_run_id + # trace_id must equal the root run's run_id (LangSmith protocol); external trace_id + # cannot be used here as it would cause HTTP 400. + trace_id = trace_info.message_id or trace_info.workflow_run_id if trace_info.start_time is None: trace_info.start_time = datetime.now() message_dotted_order = ( @@ -77,6 +79,8 @@ class LangSmithDataTrace(BaseTraceInstance): ) metadata = trace_info.metadata metadata["workflow_app_log_id"] = trace_info.workflow_app_log_id + if trace_info.trace_id: + metadata["external_trace_id"] = trace_info.trace_id if trace_info.message_id: message_run = LangSmithRunModel( diff --git a/api/providers/trace/trace-langsmith/tests/unit_tests/langsmith_trace/test_langsmith_trace.py b/api/providers/trace/trace-langsmith/tests/unit_tests/langsmith_trace/test_langsmith_trace.py index ee59acb17e..edc4aafd87 100644 --- a/api/providers/trace/trace-langsmith/tests/unit_tests/langsmith_trace/test_langsmith_trace.py +++ b/api/providers/trace/trace-langsmith/tests/unit_tests/langsmith_trace/test_langsmith_trace.py @@ -208,13 +208,17 @@ def test_workflow_trace(trace_instance, monkeypatch: pytest.MonkeyPatch): assert call_args[0].id == "msg-1" assert call_args[0].name == TraceTaskName.MESSAGE_TRACE + # trace_id must equal root run's id (message_id), not the external trace_id "trace-1" + assert call_args[0].trace_id == "msg-1" assert call_args[1].id == "run-1" assert call_args[1].name == TraceTaskName.WORKFLOW_TRACE assert call_args[1].parent_run_id == "msg-1" + assert call_args[1].trace_id == "msg-1" assert call_args[2].id == "node-llm" assert call_args[2].run_type == LangSmithRunType.llm + assert call_args[2].trace_id == "msg-1" assert call_args[3].id == "node-other" assert call_args[3].run_type == LangSmithRunType.tool @@ -604,3 +608,83 @@ def test_get_project_url_error(trace_instance): trace_instance.langsmith_client.get_run_url.side_effect = Exception("error") with pytest.raises(ValueError, match="LangSmith get run url failed: error"): trace_instance.get_project_url() + + +def _make_workflow_trace_info( + *, message_id: str | None, workflow_run_id: str, trace_id: str | None +) -> WorkflowTraceInfo: + workflow_data = MagicMock() + workflow_data.created_at = _dt() + workflow_data.finished_at = _dt() + timedelta(seconds=1) + return WorkflowTraceInfo( + tenant_id="tenant-1", + workflow_id="wf-1", + workflow_run_id=workflow_run_id, + workflow_run_inputs={}, + workflow_run_outputs={}, + workflow_run_status="succeeded", + workflow_run_version="1.0", + workflow_run_elapsed_time=1.0, + total_tokens=0, + file_list=[], + query="q", + message_id=message_id, + conversation_id="conv-1" if message_id else None, + start_time=_dt(), + end_time=_dt() + timedelta(seconds=1), + trace_id=trace_id, + metadata={"app_id": "app-1"}, + workflow_app_log_id=None, + error=None, + workflow_data=workflow_data, + ) + + +def _patch_workflow_trace_deps(monkeypatch, trace_instance): + monkeypatch.setattr("dify_trace_langsmith.langsmith_trace.sessionmaker", lambda bind: lambda: MagicMock()) + monkeypatch.setattr("dify_trace_langsmith.langsmith_trace.db", MagicMock(engine="engine")) + repo = MagicMock() + repo.get_by_workflow_execution.return_value = [] + factory = MagicMock() + factory.create_workflow_node_execution_repository.return_value = repo + monkeypatch.setattr("dify_trace_langsmith.langsmith_trace.DifyCoreRepositoryFactory", factory) + monkeypatch.setattr(trace_instance, "get_service_account_with_tenant", lambda app_id: MagicMock()) + trace_instance.add_run = MagicMock() + + +def test_workflow_trace_id_uses_message_id_not_external(trace_instance, monkeypatch): + """Chatflow with external trace_id: LangSmith trace_id must be message_id, not external.""" + trace_info = _make_workflow_trace_info( + message_id="msg-abc", + workflow_run_id="run-xyz", + trace_id="external-999", + ) + _patch_workflow_trace_deps(monkeypatch, trace_instance) + + trace_instance.workflow_trace(trace_info) + + calls = [c[0][0] for c in trace_instance.add_run.call_args_list] + # message run (root) and workflow run (child) must both use message_id as trace_id + assert calls[0].id == "msg-abc" + assert calls[0].trace_id == "msg-abc" + assert calls[1].id == "run-xyz" + assert calls[1].trace_id == "msg-abc" + # external_trace_id preserved in metadata + assert trace_info.metadata.get("external_trace_id") == "external-999" + + +def test_workflow_trace_id_pure_workflow_uses_run_id(trace_instance, monkeypatch): + """Pure workflow (no message_id) with external trace_id: trace_id must be workflow_run_id.""" + trace_info = _make_workflow_trace_info( + message_id=None, + workflow_run_id="run-xyz", + trace_id="external-999", + ) + _patch_workflow_trace_deps(monkeypatch, trace_instance) + + trace_instance.workflow_trace(trace_info) + + calls = [c[0][0] for c in trace_instance.add_run.call_args_list] + # workflow run is the root; trace_id must equal its run_id + assert calls[0].id == "run-xyz" + assert calls[0].trace_id == "run-xyz" From 1a011dc14af6fcfecfbc2c9524cce48e24309902 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Mon, 11 May 2026 12:37:26 +0900 Subject: [PATCH 05/22] refactor: port DatasetProcessRule (#31004) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/indexing_runner.py | 9 +++-- .../processor/parent_child_index_processor.py | 3 +- api/models/dataset.py | 22 ++++++----- api/services/dataset_service.py | 12 +++--- .../test_dataset_service_retrieval.py | 15 ++++---- .../test_disable_segments_from_index_task.py | 37 ++++++++++--------- .../unit_tests/models/test_dataset_models.py | 4 +- 7 files changed, 54 insertions(+), 48 deletions(-) diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py index b6e33396d1..537b14388e 100644 --- a/api/core/indexing_runner.py +++ b/api/core/indexing_runner.py @@ -324,9 +324,10 @@ class IndexingRunner: # one extract_setting is one source document for extract_setting in extract_settings: # extract - processing_rule = DatasetProcessRule( - mode=tmp_processing_rule["mode"], rules=json.dumps(tmp_processing_rule["rules"]) - ) + processing_rule = { + "mode": tmp_processing_rule["mode"], + "rules": tmp_processing_rule.get("rules"), + } # Extract document content text_docs = index_processor.extract(extract_setting, process_rule_mode=tmp_processing_rule["mode"]) # Cleaning and segmentation @@ -334,7 +335,7 @@ class IndexingRunner: text_docs, current_user=None, embedding_model_instance=embedding_model_instance, - process_rule=processing_rule.to_dict(), + process_rule=processing_rule, tenant_id=tenant_id, doc_language=doc_language, preview=True, diff --git a/api/core/rag/index_processor/processor/parent_child_index_processor.py b/api/core/rag/index_processor/processor/parent_child_index_processor.py index ba277d5018..a26a900512 100644 --- a/api/core/rag/index_processor/processor/parent_child_index_processor.py +++ b/api/core/rag/index_processor/processor/parent_child_index_processor.py @@ -29,6 +29,7 @@ from libs import helper from models import Account from models.dataset import ChildChunk, Dataset, DatasetProcessRule, DocumentSegment from models.dataset import Document as DatasetDocument +from models.enums import ProcessRuleMode from services.account_service import AccountService from services.summary_index_service import SummaryIndexService @@ -325,7 +326,7 @@ class ParentChildIndexProcessor(BaseIndexProcessor): # update document parent mode dataset_process_rule = DatasetProcessRule( dataset_id=dataset.id, - mode="hierarchical", + mode=ProcessRuleMode.HIERARCHICAL, rules=json.dumps( { "parent_mode": parent_childs.parent_mode, diff --git a/api/models/dataset.py b/api/models/dataset.py index ed7727e0f1..f823e0aa10 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -11,7 +11,7 @@ import time from collections.abc import Sequence from datetime import datetime from json import JSONDecodeError -from typing import Any, TypedDict, cast +from typing import Any, ClassVar, TypedDict, cast from uuid import uuid4 import sqlalchemy as sa @@ -441,23 +441,27 @@ class Dataset(Base): return f"{dify_config.VECTOR_INDEX_NAME_PREFIX}_{normalized_dataset_id}_Node" -class DatasetProcessRule(Base): # bug +class DatasetProcessRule(TypeBase): __tablename__ = "dataset_process_rules" __table_args__ = ( sa.PrimaryKeyConstraint("id", name="dataset_process_rule_pkey"), sa.Index("dataset_process_rule_dataset_id_idx", "dataset_id"), ) - id = mapped_column(StringUUID, nullable=False, default=lambda: str(uuid4())) - dataset_id = mapped_column(StringUUID, nullable=False) - mode = mapped_column(EnumText(ProcessRuleMode, length=255), nullable=False, server_default=sa.text("'automatic'")) - rules = mapped_column(LongText, nullable=True) - created_by = mapped_column(StringUUID, nullable=False) - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + id: Mapped[str] = mapped_column(StringUUID, nullable=False, default_factory=lambda: str(uuid4()), init=False) + dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + mode: Mapped[ProcessRuleMode] = mapped_column( + EnumText(ProcessRuleMode, length=255), nullable=False, server_default=sa.text("'automatic'") + ) + rules: Mapped[str | None] = mapped_column(LongText, nullable=True) + created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=func.current_timestamp(), init=False + ) MODES = ["automatic", "custom", "hierarchical"] PRE_PROCESSING_RULES = ["remove_stopwords", "remove_extra_spaces", "remove_urls_emails"] - AUTOMATIC_RULES: AutomaticRulesConfig = { + AUTOMATIC_RULES: ClassVar[AutomaticRulesConfig] = { "pre_processing_rules": [ {"id": "remove_extra_spaces", "enabled": True}, {"id": "remove_urls_emails", "enabled": False}, diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index eef38f1ce2..383474f4f6 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -108,7 +108,7 @@ logger = logging.getLogger(__name__) class ProcessRulesDict(TypedDict): - mode: str + mode: ProcessRuleMode rules: dict[str, Any] @@ -204,7 +204,7 @@ class DatasetService: mode = dataset_process_rule.mode rules = dataset_process_rule.rules_dict or {} else: - mode = str(DocumentService.DEFAULT_RULES["mode"]) + mode = ProcessRuleMode(DocumentService.DEFAULT_RULES["mode"]) rules = dict(DocumentService.DEFAULT_RULES.get("rules") or {}) return {"mode": mode, "rules": rules} @@ -1984,7 +1984,7 @@ class DocumentService: if process_rule.rules: dataset_process_rule = DatasetProcessRule( dataset_id=dataset.id, - mode=process_rule.mode, + mode=ProcessRuleMode(process_rule.mode), rules=process_rule.rules.model_dump_json() if process_rule.rules else None, created_by=account.id, ) @@ -1995,7 +1995,7 @@ class DocumentService: elif process_rule.mode == ProcessRuleMode.AUTOMATIC: dataset_process_rule = DatasetProcessRule( dataset_id=dataset.id, - mode=process_rule.mode, + mode=ProcessRuleMode.AUTOMATIC, rules=json.dumps(DatasetProcessRule.AUTOMATIC_RULES), created_by=account.id, ) @@ -2572,14 +2572,14 @@ class DocumentService: if process_rule.mode in {ProcessRuleMode.CUSTOM, ProcessRuleMode.HIERARCHICAL}: dataset_process_rule = DatasetProcessRule( dataset_id=dataset.id, - mode=process_rule.mode, + mode=ProcessRuleMode(process_rule.mode), rules=process_rule.rules.model_dump_json() if process_rule.rules else None, created_by=account.id, ) elif process_rule.mode == ProcessRuleMode.AUTOMATIC: dataset_process_rule = DatasetProcessRule( dataset_id=dataset.id, - mode=process_rule.mode, + mode=ProcessRuleMode.AUTOMATIC, rules=json.dumps(DatasetProcessRule.AUTOMATIC_RULES), created_by=account.id, ) diff --git a/api/tests/test_containers_integration_tests/services/test_dataset_service_retrieval.py b/api/tests/test_containers_integration_tests/services/test_dataset_service_retrieval.py index 2f90d16176..0c610311bb 100644 --- a/api/tests/test_containers_integration_tests/services/test_dataset_service_retrieval.py +++ b/api/tests/test_containers_integration_tests/services/test_dataset_service_retrieval.py @@ -16,6 +16,7 @@ from uuid import uuid4 from sqlalchemy.orm import Session from core.rag.index_processor.constant.index_type import IndexTechniqueType +from models import AccountStatus, CreatorUserRole, TenantStatus from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.dataset import ( AppDatasetJoin, @@ -25,7 +26,7 @@ from models.dataset import ( DatasetProcessRule, DatasetQuery, ) -from models.enums import DatasetQuerySource, DataSourceType, ProcessRuleMode +from models.enums import DatasetQuerySource, DataSourceType, ProcessRuleMode, TagType from models.model import Tag, TagBinding from services.dataset_service import DatasetService, DocumentService @@ -42,11 +43,11 @@ class DatasetRetrievalTestDataFactory: email=f"{uuid4()}@example.com", name=f"user-{uuid4()}", interface_language="en-US", - status="active", + status=AccountStatus.ACTIVE, ) tenant = Tenant( name=f"tenant-{uuid4()}", - status="normal", + status=TenantStatus.NORMAL, ) db_session_with_containers.add_all([account, tenant]) db_session_with_containers.flush() @@ -72,7 +73,7 @@ class DatasetRetrievalTestDataFactory: email=f"{uuid4()}@example.com", name=f"user-{uuid4()}", interface_language="en-US", - status="active", + status=AccountStatus.ACTIVE, ) db_session_with_containers.add(account) db_session_with_containers.flush() @@ -130,7 +131,7 @@ class DatasetRetrievalTestDataFactory: @staticmethod def create_process_rule( - db_session_with_containers: Session, dataset_id: str, created_by: str, mode: str, rules: dict + db_session_with_containers: Session, dataset_id: str, created_by: str, mode: ProcessRuleMode, rules: dict ) -> DatasetProcessRule: """Create a dataset process rule.""" process_rule = DatasetProcessRule( @@ -153,7 +154,7 @@ class DatasetRetrievalTestDataFactory: content=content, source=DatasetQuerySource.APP, source_app_id=None, - created_by_role="account", + created_by_role=CreatorUserRole.ACCOUNT, created_by=created_by, ) db_session_with_containers.add(dataset_query) @@ -176,7 +177,7 @@ class DatasetRetrievalTestDataFactory: """Create a knowledge tag and bind it to the target dataset.""" tag = Tag( tenant_id=tenant_id, - type="knowledge", + type=TagType.KNOWLEDGE, name=f"tag-{uuid4()}", created_by=created_by, ) diff --git a/api/tests/test_containers_integration_tests/tasks/test_disable_segments_from_index_task.py b/api/tests/test_containers_integration_tests/tasks/test_disable_segments_from_index_task.py index 6bfb1e1f1e..6a95bfc425 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_disable_segments_from_index_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_disable_segments_from_index_task.py @@ -7,6 +7,7 @@ The task is responsible for removing document segments from the search index whe """ from unittest.mock import MagicMock, patch +from uuid import uuid4 from faker import Faker from sqlalchemy import select @@ -82,7 +83,7 @@ class TestDisableSegmentsFromIndexTask: return account - def _create_test_dataset(self, db_session_with_containers: Session, account, fake: Faker | None = None): + def _create_test_dataset(self, db_session_with_containers: Session, account: Account, fake: Faker | None = None): """ Helper method to create a test dataset with realistic data. @@ -117,7 +118,7 @@ class TestDisableSegmentsFromIndexTask: return dataset def _create_test_document( - self, db_session_with_containers: Session, dataset, account: Account, fake: Faker | None = None + self, db_session_with_containers: Session, dataset: Dataset, account: Account, fake: Faker | None = None ): """ Helper method to create a test document with realistic data. @@ -164,7 +165,7 @@ class TestDisableSegmentsFromIndexTask: return document def _create_test_segments( - self, db_session_with_containers: Session, document, dataset, account, count=3, fake=None + self, db_session_with_containers: Session, document, dataset: Dataset, account: Account, count=3, fake=None ): """ Helper method to create test document segments with realistic data. @@ -217,7 +218,9 @@ class TestDisableSegmentsFromIndexTask: return segments - def _create_dataset_process_rule(self, db_session_with_containers: Session, dataset, fake: Faker | None = None): + def _create_dataset_process_rule( + self, db_session_with_containers: Session, dataset: Dataset, fake: Faker | None = None + ): """ Helper method to create a dataset process rule. @@ -230,21 +233,19 @@ class TestDisableSegmentsFromIndexTask: DatasetProcessRule: Created process rule instance """ fake = fake or Faker() - process_rule = DatasetProcessRule() - process_rule.id = fake.uuid4() - process_rule.tenant_id = dataset.tenant_id - process_rule.dataset_id = dataset.id - process_rule.mode = ProcessRuleMode.AUTOMATIC - process_rule.rules = ( - "{" - '"mode": "automatic", ' - '"rules": {' - '"pre_processing_rules": [], "segmentation": ' - '{"separator": "\\n\\n", "max_tokens": 1000, "chunk_overlap": 50}}' - "}" + process_rule = DatasetProcessRule( + dataset_id=dataset.id, + mode=ProcessRuleMode.AUTOMATIC, + rules=( + "{" + '"mode": "automatic", ' + '"rules": {' + '"pre_processing_rules": [], "segmentation": ' + '{"separator": "\\n\\n", "max_tokens": 1000, "chunk_overlap": 50}}' + "}" + ), + created_by=str(uuid4()), ) - process_rule.created_by = dataset.created_by - process_rule.updated_by = dataset.updated_by db_session_with_containers.add(process_rule) db_session_with_containers.commit() diff --git a/api/tests/unit_tests/models/test_dataset_models.py b/api/tests/unit_tests/models/test_dataset_models.py index 3f14ebe8bf..f4ccfb4191 100644 --- a/api/tests/unit_tests/models/test_dataset_models.py +++ b/api/tests/unit_tests/models/test_dataset_models.py @@ -847,9 +847,7 @@ class TestDatasetProcessRule: # Act process_rule = DatasetProcessRule( - dataset_id=dataset_id, - mode=ProcessRuleMode.AUTOMATIC, - created_by=created_by, + dataset_id=dataset_id, mode=ProcessRuleMode.AUTOMATIC, created_by=created_by, rules=None ) # Assert From 837b5cad862e3cb8d62b2d1ca2385e1552154387 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 12:51:51 +0900 Subject: [PATCH 06/22] chore(deps): bump opentelemetry-exporter-otlp-proto-grpc from 1.41.0 to 1.41.1 in /api in the opentelemetry group (#36013) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/uv.lock | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/api/uv.lock b/api/uv.lock index ad9ce2c4a4..23cdf669bb 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -4265,32 +4265,32 @@ wheels = [ [[package]] name = "opentelemetry-exporter-otlp" -version = "1.41.0" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/b7/845565a2ab5d22c1486bc7729a06b05cd0964c61539d766e1f107c9eea0c/opentelemetry_exporter_otlp-1.41.0.tar.gz", hash = "sha256:97ff847321f8d4c919032a67d20d3137fb7b34eac0c47f13f71112858927fc5b", size = 6152, upload-time = "2026-04-09T14:38:35.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/84/d55baf8e1a222f40282956083e67de9fa92d5fa451108df4839505fa2a24/opentelemetry_exporter_otlp-1.41.1.tar.gz", hash = "sha256:299a2f0541ca175df186f5ac58fd5db177ba1e9b72b0826049062f750d55b47f", size = 6152, upload-time = "2026-04-24T13:15:40.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f2/f1076fff152858773f22cda146713f9ae3661795af6bacd411a76f2151ac/opentelemetry_exporter_otlp-1.41.0-py3-none-any.whl", hash = "sha256:443b6a45c990ae4c55e147f97049a86c5f5b704f3d78b48b44a073a886ec4d6e", size = 7022, upload-time = "2026-04-09T14:38:13.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/ea4aa7dfc458fd537bd9519ea0e7226eef2a6212dfe952694984167daaba/opentelemetry_exporter_otlp-1.41.1-py3-none-any.whl", hash = "sha256:db276c5a80c02b063994e80950d00ca1bfddcf6520f608335b7dc2db0c0eb9c6", size = 7025, upload-time = "2026-04-24T13:15:17.839Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.41.0" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/28/e8eca94966fe9a1465f6094dc5ddc5398473682180279c94020bc23b4906/opentelemetry_exporter_otlp_proto_common-1.41.0.tar.gz", hash = "sha256:966bbce537e9edb166154779a7c4f8ab6b8654a03a28024aeaf1a3eacb07d6ee", size = 20411, upload-time = "2026-04-09T14:38:36.572Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/fa/f9e3bd3c4d692b3ce9a2880a167d1f79681a1bea11f00d5bf76adc03e6ea/opentelemetry_exporter_otlp_proto_common-1.41.1.tar.gz", hash = "sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb", size = 20409, upload-time = "2026-04-24T13:15:40.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/c4/78b9bf2d9c1d5e494f44932988d9d91c51a66b9a7b48adf99b62f7c65318/opentelemetry_exporter_otlp_proto_common-1.41.0-py3-none-any.whl", hash = "sha256:7a99177bf61f85f4f9ed2072f54d676364719c066f6d11f515acc6c745c7acf0", size = 18366, upload-time = "2026-04-09T14:38:15.135Z" }, + { url = "https://files.pythonhosted.org/packages/29/48/bce76d3ea772b609757e9bc844e02ab408a6446609bf74fb562062ba6b71/opentelemetry_exporter_otlp_proto_common-1.41.1-py3-none-any.whl", hash = "sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9", size = 18366, upload-time = "2026-04-24T13:15:18.917Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.41.0" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -4301,14 +4301,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/46/d75a3f8c91915f2e58f61d0a2e4ada63891e7c7a37a20ff7949ba184a6b2/opentelemetry_exporter_otlp_proto_grpc-1.41.0.tar.gz", hash = "sha256:f704201251c6f65772b11bddea1c948000554459101bdbb0116e0a01b70592f6", size = 25754, upload-time = "2026-04-09T14:38:37.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/9b/e4503060b8695579dbaad187dc8cef4554188de68748c88060599b77489e/opentelemetry_exporter_otlp_proto_grpc-1.41.1.tar.gz", hash = "sha256:b05df8fa1333dc9a3fda36b676b96b5095ab6016d3f0c3296d430d629ba1443b", size = 25755, upload-time = "2026-04-24T13:15:41.93Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/f6/b09e2e0c9f0b5750cebc6eaf31527b910821453cef40a5a0fe93550422b2/opentelemetry_exporter_otlp_proto_grpc-1.41.0-py3-none-any.whl", hash = "sha256:3a1a86bd24806ccf136ec9737dbfa4c09b069f9130ff66b0acb014f9c5255fd1", size = 20299, upload-time = "2026-04-09T14:38:17.01Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f2/c54f33c92443d087703e57e52e55f22f111373a5c4c4aa349ea60efe512e/opentelemetry_exporter_otlp_proto_grpc-1.41.1-py3-none-any.whl", hash = "sha256:537926dcef951136992479af1d9cd88f25e33d56c530e9f020ed57774dca2f94", size = 20297, upload-time = "2026-04-24T13:15:20.212Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.41.0" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -4319,9 +4319,9 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/63/d9f43cd75f3fabb7e01148c89cfa9491fc18f6580a6764c554ff7c953c46/opentelemetry_exporter_otlp_proto_http-1.41.0.tar.gz", hash = "sha256:dcd6e0686f56277db4eecbadd5262124e8f2cc739cadbc3fae3d08a12c976cf5", size = 24139, upload-time = "2026-04-09T14:38:38.128Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/5b/9d3c7f70cca10136ba82a81e738dee626c8e7fc61c6887ea9a58bf34c606/opentelemetry_exporter_otlp_proto_http-1.41.1.tar.gz", hash = "sha256:4747a9604c8550ab38c6fd6180e2fcb80de3267060bef2c306bad3cb443302bc", size = 24139, upload-time = "2026-04-24T13:15:42.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b5/a214cd907eedc17699d1c2d602288ae17cb775526df04db3a3b3585329d2/opentelemetry_exporter_otlp_proto_http-1.41.0-py3-none-any.whl", hash = "sha256:a9c4ee69cce9c3f4d7ee736ad1b44e3c9654002c0816900abbafd9f3cf289751", size = 22673, upload-time = "2026-04-09T14:38:18.349Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4d/ef07ff2fc630849f2080ae0ae73a61f67257905b7ac79066640bfa0c5739/opentelemetry_exporter_otlp_proto_http-1.41.1-py3-none-any.whl", hash = "sha256:1a21e8f49c7a946d935551e90947d6c3eb39236723c6624401da0f33d68edcb4", size = 22673, upload-time = "2026-04-24T13:15:21.313Z" }, ] [[package]] @@ -4479,14 +4479,14 @@ wheels = [ [[package]] name = "opentelemetry-proto" -version = "1.41.0" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/d9/08e3dc6156878713e8c811682bc76151f5fe1a3cb7f3abda3966fd56e71e/opentelemetry_proto-1.41.0.tar.gz", hash = "sha256:95d2e576f9fb1800473a3e4cfcca054295d06bdb869fda4dc9f4f779dc68f7b6", size = 45669, upload-time = "2026-04-09T14:38:45.978Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz", hash = "sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", size = 45670, upload-time = "2026-04-24T13:15:49.768Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/8c/65ef7a9383a363864772022e822b5d5c6988e6f9dabeebb9278f5b86ebc3/opentelemetry_proto-1.41.0-py3-none-any.whl", hash = "sha256:b970ab537309f9eed296be482c3e7cca05d8aca8165346e929f658dbe153b247", size = 72074, upload-time = "2026-04-09T14:38:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl", hash = "sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", size = 72076, upload-time = "2026-04-24T13:15:32.542Z" }, ] [[package]] From 9e3e616391a22f263ec76d0503cdc1121a36c04a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 12:52:13 +0900 Subject: [PATCH 07/22] chore(deps): bump the storage group in /api with 2 updates (#36017) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/pyproject.toml | 4 ++-- api/uv.lock | 52 +++++++++++++++++++++++----------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 6c30779f9d..a88ad174fd 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -6,7 +6,7 @@ requires-python = "~=3.12.0" dependencies = [ # Legacy: mature and widely deployed "bleach>=6.3.0", - "boto3>=1.43.3", + "boto3>=1.43.6", "celery>=5.6.3", "croniter>=6.2.2", "flask>=3.1.3,<4.0.0", @@ -191,7 +191,7 @@ storage = [ "google-cloud-storage>=3.10.1", "opendal>=0.46.0", "oss2>=2.19.1", - "supabase>=2.29.0", + "supabase>=2.30.0", "tos>=2.9.0", ] diff --git a/api/uv.lock b/api/uv.lock index 23cdf669bb..cbb6440533 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -607,16 +607,16 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.3" +version = "1.43.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/50/ea184e159c4ac64fef816a72094fb8656eb071361a39ed22c0e3b15a35b4/boto3-1.43.3.tar.gz", hash = "sha256:7c7777862ffc898f05efa566032bbabfe226dbb810e35ec11125817f128bc5c5", size = 113111, upload-time = "2026-05-04T19:34:09.731Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/37/78c630d1308964aa9abf44951d9c4df776546ff37251ec2434944e205c4e/boto3-1.43.6.tar.gz", hash = "sha256:e6315effaf12b890b99956e6f8e2c3000a3f64e4ee91943cec3895ce9a836afb", size = 113153, upload-time = "2026-05-07T20:49:59.694Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/ad/8a6946a329f0127322108e537dc1c0d9f8eea4f1d1231702c073d2e85f46/boto3-1.43.3-py3-none-any.whl", hash = "sha256:fb9fe51849ef2a78198d582756fc06f14f7de27f73e0fa90275d6aa4171eb4d0", size = 140501, upload-time = "2026-05-04T19:34:07.991Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/3c2eef44f55eafab256836d1d9479bd6a74f70c26cbfdc0639a0e23e4327/boto3-1.43.6-py3-none-any.whl", hash = "sha256:179601ec2992726a718053bf41e43c223ceba397d31ceab11f64d9c910d9fc3a", size = 140502, upload-time = "2026-05-07T20:49:57.8Z" }, ] [[package]] @@ -639,16 +639,16 @@ bedrock-runtime = [ [[package]] name = "botocore" -version = "1.43.3" +version = "1.43.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/ac/cd55f886e17b6b952dbc95b792d3645a73d58586a1400ababe54406073bd/botocore-1.43.3.tar.gz", hash = "sha256:eac6da0fffccf87888ebf4d89f0b2378218a707efa748cd955b838995e944695", size = 15308705, upload-time = "2026-05-04T19:33:56.28Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/a7/23d0f5028011455096a1eeac0ddf3cbe147b3e855e127342f8202552194d/botocore-1.43.6.tar.gz", hash = "sha256:b1e395b347356860398da42e61c808cf1e34b6fa7180cf2b9d87d986e1a06ba0", size = 15336070, upload-time = "2026-05-07T20:49:48.14Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/99/1d9e296edf244f47e0508032f20999f8fd40704dd3c5b601fed099424eb6/botocore-1.43.3-py3-none-any.whl", hash = "sha256:ec0769eb0f7c5034856bb406a92698dbc02a3d4be0f78a384747106b161d8ea3", size = 14989027, upload-time = "2026-05-04T19:33:50.81Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c8/6f47223840e8d8cfa8c9f7c0ec1b77970417f257fc885169ff4f6326ce09/botocore-1.43.6-py3-none-any.whl", hash = "sha256:b6d1fdbc6f65a5fe0b7e947823aa37535d3f39f3ba4d21110fab1f55bbbcc04b", size = 15017094, upload-time = "2026-05-07T20:49:44.964Z" }, ] [[package]] @@ -1581,7 +1581,7 @@ requires-dist = [ { name = "aliyun-log-python-sdk", specifier = ">=0.9.44,<1.0.0" }, { name = "azure-identity", specifier = ">=1.25.3,<2.0.0" }, { name = "bleach", specifier = ">=6.3.0" }, - { name = "boto3", specifier = ">=1.43.3" }, + { name = "boto3", specifier = ">=1.43.6" }, { name = "celery", specifier = ">=5.6.3" }, { name = "croniter", specifier = ">=6.2.2" }, { name = "fastopenapi", extras = ["flask"], specifier = "~=0.7.0" }, @@ -1692,7 +1692,7 @@ storage = [ { name = "google-cloud-storage", specifier = ">=3.10.1" }, { name = "opendal", specifier = ">=0.46.0" }, { name = "oss2", specifier = ">=2.19.1" }, - { name = "supabase", specifier = ">=2.29.0" }, + { name = "supabase", specifier = ">=2.30.0" }, { name = "tos", specifier = ">=2.9.0" }, ] tools = [ @@ -4813,7 +4813,7 @@ wheels = [ [[package]] name = "postgrest" -version = "2.29.0" +version = "2.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecation" }, @@ -4821,9 +4821,9 @@ dependencies = [ { name = "pydantic" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/98/f216b8b5c4d116ab6a2fb21339b5821da279ee773e163612418e1c56c012/postgrest-2.29.0.tar.gz", hash = "sha256:a87081858f627fcd57e8e7137004a1ef0adbdf0dbdfed1384e9ea1d7a9c525ec", size = 14217, upload-time = "2026-04-24T13:13:00.281Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/7c/54e7be05adc9fd6fd98dc572ddfc8982d45bec314a55711e37277d440698/postgrest-2.30.0.tar.gz", hash = "sha256:4f89eec56ce605ab6fbddd9b96d526a9bb44962796d44a5d85cb77640eb766c3", size = 14430, upload-time = "2026-05-06T17:35:21.559Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/0b/08b670a93a90d625c557b9e64b8a5fdeec80c3542d2d0265f0b4d6b16646/postgrest-2.29.0-py3-none-any.whl", hash = "sha256:3ee48e146f726272733d20e2b12de354cdb6cb9dd9cc3a61ed97ce69047aeb96", size = 22735, upload-time = "2026-04-24T13:12:58.405Z" }, + { url = "https://files.pythonhosted.org/packages/22/aa/ff2e09f99f95ea96fddeb373646bf907dd89a24fc00b5d38e5674ca7c9ca/postgrest-2.30.0-py3-none-any.whl", hash = "sha256:30631e7993da542419f4217cf3b60aa641084731ea15e66a18526a3a52e40a7d", size = 23108, upload-time = "2026-05-06T17:35:20.531Z" }, ] [[package]] @@ -5726,16 +5726,16 @@ wheels = [ [[package]] name = "realtime" -version = "2.29.0" +version = "2.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/f1/08c42a42653942fadfbef495d5b0239356140e7186cc528704956c5f06d4/realtime-2.29.0.tar.gz", hash = "sha256:8efe4a1b3a548a5fda09de701bd041fa0970c5a2fe7d13db0b9861ce11828be2", size = 18715, upload-time = "2026-04-24T13:13:02.315Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/a2/0328d49d3b5fb427068e9200e7de5b0d708d021a1ad98d004bc685d2529e/realtime-2.30.0.tar.gz", hash = "sha256:7aa593da52ed5f92c34ec4e50e32043afa62f219c94f717ad64a66ab0ef9f1ba", size = 18718, upload-time = "2026-05-06T17:35:23.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/48/f6375c0a24923beb988f0c71c052604c96641cf43c2d22b91ec1df86afa0/realtime-2.29.0-py3-none-any.whl", hash = "sha256:1a4891e6c82e88ac9d96ac715e435e086f6f8c7665212a8717346de829cbb509", size = 22374, upload-time = "2026-04-24T13:13:01.103Z" }, + { url = "https://files.pythonhosted.org/packages/b4/75/1b2cfc949595e22d8c05a2aa2cfc222921f7f94177d7e8a90542f3f73b33/realtime-2.30.0-py3-none-any.whl", hash = "sha256:7c93b63d2cf99aa1da4fa8826b03b00cd32f7b38abb27ff47b19eb5dcb5707c6", size = 22376, upload-time = "2026-05-06T17:35:22.568Z" }, ] [[package]] @@ -6217,7 +6217,7 @@ wheels = [ [[package]] name = "storage3" -version = "2.29.0" +version = "2.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecation" }, @@ -6226,9 +6226,9 @@ dependencies = [ { name = "pyiceberg" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/be/771246434b5caf3c6187bfdc932eaede00bf5f2937b47475ab25209ede3e/storage3-2.29.0.tar.gz", hash = "sha256:b0cc2f6714655d725c998d2c5ae8c6fb4f56a513bd31e4f85770df557fe021e3", size = 20160, upload-time = "2026-04-24T13:13:04.626Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/b2/6df208d64630744704d00f2c07197170390d6b4d0098617740f6a7a4fa98/storage3-2.30.0.tar.gz", hash = "sha256:b74e3cac149f2c0553dcb5f4d55d8c35d420d88183a1a2df77727d482665972b", size = 20162, upload-time = "2026-05-06T17:35:25.71Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/c3/790c31866f52c13b26f108b45759bf50dafae3a0bafb4511fadc98ba7c33/storage3-2.29.0-py3-none-any.whl", hash = "sha256:043ef7ff27cc8b9da12be403cf78ee4586180edfcf62b227ff61e1bd79594b06", size = 28284, upload-time = "2026-04-24T13:13:03.338Z" }, + { url = "https://files.pythonhosted.org/packages/91/5c/bb8c8cc448cfae671c4ffee67f3651892ea59b341f27bed54666190eb8ef/storage3-2.30.0-py3-none-any.whl", hash = "sha256:2bd23a34011c018bd9c130d8a70a09ebd060ae80d946c6204a6fc08161ad728d", size = 28284, upload-time = "2026-05-06T17:35:24.659Z" }, ] [[package]] @@ -6254,7 +6254,7 @@ wheels = [ [[package]] name = "supabase" -version = "2.29.0" +version = "2.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -6265,37 +6265,37 @@ dependencies = [ { name = "supabase-functions" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/a0/2407d616fdf68e8632bbbfb063d1685c38377ac0199e8ca11deaea1f3bf0/supabase-2.29.0.tar.gz", hash = "sha256:a88c4a4eb50fbb903e2e962fbc7c27733b00589140139f9e837bc9fe30dd3615", size = 9689, upload-time = "2026-04-24T13:13:06.728Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/a6/d2b17021c2db1a9d219c383e0762ac03a62b25468e61ab126b6b561c2f21/supabase-2.30.0.tar.gz", hash = "sha256:efdba41d474038ed220736ba4e64946df56043057ad785c4c3499d27e459975c", size = 9689, upload-time = "2026-05-06T17:35:27.781Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/52/232f6bbf5326e04ae12e2ef04a24f011a0d7cab379a8b9698652bc8ff78f/supabase-2.29.0-py3-none-any.whl", hash = "sha256:16c3ec4b7094f6b92efc5cd3bb3f96826d3b6dd5d24fe15c89c81166efce88fe", size = 16633, upload-time = "2026-04-24T13:13:05.722Z" }, + { url = "https://files.pythonhosted.org/packages/f0/82/d213be7d0ce0bb18018744c0ee38ba0d6648d41dbc46ac8558cffe80541f/supabase-2.30.0-py3-none-any.whl", hash = "sha256:f9b259194554f7bfd2dca6c23261f2df588016ca18b18e774f4d85bc941edb03", size = 16634, upload-time = "2026-05-06T17:35:26.696Z" }, ] [[package]] name = "supabase-auth" -version = "2.29.0" +version = "2.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", extra = ["http2"] }, { name = "pydantic" }, { name = "pyjwt", extra = ["crypto"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/7f/7ceeb4c7a2caa188062e934897f0e08e1af0a0e47e376c7645c26b4c39d8/supabase_auth-2.29.0.tar.gz", hash = "sha256:46efc6a3455a23957b846dc974303a844ba0413718cfa899425477ac977f95b3", size = 39154, upload-time = "2026-04-24T13:13:08.509Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/8a/48bbbe0b6703d0670b67e45b90d6a791fd01aace67443d286f760bf48895/supabase_auth-2.30.0.tar.gz", hash = "sha256:6138a53a306a95ed59c03d4e4975469dfc3343a0ade33cc4b37e4ef967ad83f8", size = 39135, upload-time = "2026-05-06T17:35:30.371Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/ac/3c35cf52281f940b9497cf17abfc5c2050ca49f342d60cfafe22dac3482b/supabase_auth-2.29.0-py3-none-any.whl", hash = "sha256:64de6ef8cae80f97d3aa8d5ca507d5427dda5c89885c0bcfe9f8b0263b6fb9a4", size = 48379, upload-time = "2026-04-24T13:13:07.417Z" }, + { url = "https://files.pythonhosted.org/packages/db/40/a99cb4373353bcbf302d962e51da9eac78b3b0f257eb0362c0852b1667f4/supabase_auth-2.30.0-py3-none-any.whl", hash = "sha256:e85e1f51ec0de2172c3a2a8514205f71731a9914f9a770ed199ac0cf054bc82c", size = 48352, upload-time = "2026-05-06T17:35:28.936Z" }, ] [[package]] name = "supabase-functions" -version = "2.29.0" +version = "2.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", extra = ["http2"] }, { name = "strenum" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/19/1a1d22749f38f2a6cbca93a6f5a35c9f816c2c3c06bfaa077fa336e90537/supabase_functions-2.29.0.tar.gz", hash = "sha256:0f8a14a2ea9f12b1c208f61dc6f55e2f4b1121f81bf01c08f9b487d22888744d", size = 4683, upload-time = "2026-04-24T13:13:10.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/e6/5cd8559ec2bb332e6027840c1be292f9989c2fc7b47bf40800aec5586791/supabase_functions-2.30.0.tar.gz", hash = "sha256:025acfd25f1c000ba43d0f7b8e366b0d2e9dfc784b842528e21973eb33006113", size = 4683, upload-time = "2026-05-06T17:35:32.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/10/6f8ef0b408ade76b5a439afab588ce5849e9604a23040ca73cfe0b90cb9e/supabase_functions-2.29.0-py3-none-any.whl", hash = "sha256:6f08de52eec5820eae53616868b85e849e181beffaa5d05b8ea1708ceae5e48e", size = 8799, upload-time = "2026-04-24T13:13:09.214Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/9dedab32775df04cc22ca72f194b78e895d940f195bed3e02882a65daa9b/supabase_functions-2.30.0-py3-none-any.whl", hash = "sha256:92419459f102767b954cd034856e4ded8e34c78660b32442d66c8b2899c68011", size = 8803, upload-time = "2026-05-06T17:35:31.342Z" }, ] [[package]] From a2ee151e48633e2ed52d13905938c5e6d0c273c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 12:52:38 +0900 Subject: [PATCH 08/22] chore(deps): bump the github-actions-dependencies group with 2 updates (#36009) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/labeler.yml | 2 +- .github/workflows/translate-i18n-claude.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index f59cc6be48..aefcf1b5ac 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -9,6 +9,6 @@ jobs: pull-requests: write runs-on: depot-ubuntu-24.04 steps: - - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 + - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 with: sync-labels: true diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml index 7bb6fc1bbd..4e738df684 100644 --- a/.github/workflows/translate-i18n-claude.yml +++ b/.github/workflows/translate-i18n-claude.yml @@ -158,7 +158,7 @@ jobs: - name: Run Claude Code for Translation Sync if: steps.context.outputs.CHANGED_FILES != '' - uses: anthropics/claude-code-action@fefa07e9c665b7320f08c3b525980457f22f58aa # v1.0.111 + uses: anthropics/claude-code-action@476e359e6203e73dad705c8b322e333fabbd7416 # v1.0.119 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} From 9127209dd55830e56ce1a6cd74ae64a42f12c472 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Mon, 11 May 2026 11:52:47 +0800 Subject: [PATCH 09/22] chore(web): refresh agent skills (#36015) --- .agents/skills/component-refactoring/SKILL.md | 6 +- .../references/complexity-patterns.md | 4 +- .../references/component-splitting.md | 16 +- .../references/hook-extraction.md | 6 +- .../skills/e2e-cucumber-playwright/SKILL.md | 2 +- .../references/performance.md | 10 +- .../skills/frontend-query-mutation/SKILL.md | 46 - .../agents/openai.yaml | 4 - .../references/contract-patterns.md | 129 -- .../references/runtime-rules.md | 172 --- .agents/skills/frontend-testing/SKILL.md | 16 +- .../frontend-testing/references/mocking.md | 38 +- .../frontend-testing/references/workflow.md | 8 +- .../skills/how-to-write-component/SKILL.md | 63 + .agents/skills/tailwind-css-rules/SKILL.md | 367 ++++++ e2e/AGENTS.md | 18 +- .../generated/enterprise/orpc.gen.ts | 396 ------ .../generated/enterprise/types.gen.ts | 1097 ++--------------- .../contracts/generated/enterprise/zod.gen.ts | 1019 ++------------- web/AGENTS.md | 4 - web/contract/router.ts | 3 + web/docs/test.md | 18 +- 22 files changed, 733 insertions(+), 2709 deletions(-) delete mode 100644 .agents/skills/frontend-query-mutation/SKILL.md delete mode 100644 .agents/skills/frontend-query-mutation/agents/openai.yaml delete mode 100644 .agents/skills/frontend-query-mutation/references/contract-patterns.md delete mode 100644 .agents/skills/frontend-query-mutation/references/runtime-rules.md create mode 100644 .agents/skills/how-to-write-component/SKILL.md create mode 100644 .agents/skills/tailwind-css-rules/SKILL.md diff --git a/.agents/skills/component-refactoring/SKILL.md b/.agents/skills/component-refactoring/SKILL.md index 98a94592ab..a7cae67e8f 100644 --- a/.agents/skills/component-refactoring/SKILL.md +++ b/.agents/skills/component-refactoring/SKILL.md @@ -63,7 +63,7 @@ pnpm analyze-component --json ```typescript // ❌ Before: Complex state logic in component -const Configuration: FC = () => { +function Configuration() { const [modelConfig, setModelConfig] = useState(...) const [datasetConfigs, setDatasetConfigs] = useState(...) const [completionParams, setCompletionParams] = useState({}) @@ -85,7 +85,7 @@ export const useModelConfig = (appId: string) => { } // Component becomes cleaner -const Configuration: FC = () => { +function Configuration() { const { modelConfig, setModelConfig } = useModelConfig(appId) return
...
} @@ -189,8 +189,6 @@ const Template = useMemo(() => { **Dify Convention**: - This skill is for component decomposition, not query/mutation design. -- When refactoring data fetching, follow `web/AGENTS.md`. -- Use `frontend-query-mutation` for contracts, query shape, data-fetching wrappers, query/mutation call-site patterns, conditional queries, invalidation, and mutation error handling. - Do not introduce deprecated `useInvalid` / `useReset`. - Do not add thin passthrough `useQuery` wrappers during refactoring; only extract a custom hook when it truly orchestrates multiple queries/mutations or shared derived state. diff --git a/.agents/skills/component-refactoring/references/complexity-patterns.md b/.agents/skills/component-refactoring/references/complexity-patterns.md index 5a0a268f38..2873630d4b 100644 --- a/.agents/skills/component-refactoring/references/complexity-patterns.md +++ b/.agents/skills/component-refactoring/references/complexity-patterns.md @@ -60,8 +60,10 @@ const Template = useMemo(() => { **After** (complexity: ~3): ```typescript +import type { ComponentType } from 'react' + // Define lookup table outside component -const TEMPLATE_MAP: Record>> = { +const TEMPLATE_MAP: Record>> = { [AppModeEnum.CHAT]: { [LanguagesSupported[1]]: TemplateChatZh, [LanguagesSupported[7]]: TemplateChatJa, diff --git a/.agents/skills/component-refactoring/references/component-splitting.md b/.agents/skills/component-refactoring/references/component-splitting.md index 78a3389100..81c007e005 100644 --- a/.agents/skills/component-refactoring/references/component-splitting.md +++ b/.agents/skills/component-refactoring/references/component-splitting.md @@ -65,10 +65,10 @@ interface ConfigurationHeaderProps { onPublish: () => void } -const ConfigurationHeader: FC = ({ +function ConfigurationHeader({ isAdvancedMode, onPublish, -}) => { +}: ConfigurationHeaderProps) { const { t } = useTranslation() return ( @@ -136,7 +136,7 @@ const AppInfo = () => { } // ✅ After: Separate view components -const AppInfoExpanded: FC = ({ appDetail, onAction }) => { +function AppInfoExpanded({ appDetail, onAction }: AppInfoViewProps) { return (
{/* Clean, focused expanded view */} @@ -144,7 +144,7 @@ const AppInfoExpanded: FC = ({ appDetail, onAction }) => { ) } -const AppInfoCollapsed: FC = ({ appDetail, onAction }) => { +function AppInfoCollapsed({ appDetail, onAction }: AppInfoViewProps) { return (
{/* Clean, focused collapsed view */} @@ -203,12 +203,12 @@ interface AppInfoModalsProps { onSuccess: () => void } -const AppInfoModals: FC = ({ +function AppInfoModals({ appDetail, activeModal, onClose, onSuccess, -}) => { +}: AppInfoModalsProps) { const handleEdit = async (data) => { /* logic */ } const handleDuplicate = async (data) => { /* logic */ } const handleDelete = async () => { /* logic */ } @@ -296,7 +296,7 @@ interface OperationItemProps { onAction: (id: string) => void } -const OperationItem: FC = ({ operation, onAction }) => { +function OperationItem({ operation, onAction }: OperationItemProps) { return (
{operation.icon} @@ -435,7 +435,7 @@ interface ChildProps { onSubmit: () => void } -const Child: FC = ({ value, onChange, onSubmit }) => { +function Child({ value, onChange, onSubmit }: ChildProps) { return (
onChange(e.target.value)} /> diff --git a/.agents/skills/component-refactoring/references/hook-extraction.md b/.agents/skills/component-refactoring/references/hook-extraction.md index 0d567eb2a6..6fad2c8885 100644 --- a/.agents/skills/component-refactoring/references/hook-extraction.md +++ b/.agents/skills/component-refactoring/references/hook-extraction.md @@ -112,13 +112,13 @@ export const useModelConfig = ({ ```typescript // Before: 50+ lines of state management -const Configuration: FC = () => { +function Configuration() { const [modelConfig, setModelConfig] = useState(...) // ... lots of related state and effects } // After: Clean component -const Configuration: FC = () => { +function Configuration() { const { modelConfig, setModelConfig, @@ -159,8 +159,6 @@ const Configuration: FC = () => { When hook extraction touches query or mutation code, do not use this reference as the source of truth for data-layer patterns. -- Follow `web/AGENTS.md` first. -- Use `frontend-query-mutation` for contracts, query shape, data-fetching wrappers, query/mutation call-site patterns, conditional queries, invalidation, and mutation error handling. - Do not introduce deprecated `useInvalid` / `useReset`. - Do not extract thin passthrough `useQuery` hooks; only extract orchestration hooks. diff --git a/.agents/skills/e2e-cucumber-playwright/SKILL.md b/.agents/skills/e2e-cucumber-playwright/SKILL.md index de6b58f26d..dd7d204678 100644 --- a/.agents/skills/e2e-cucumber-playwright/SKILL.md +++ b/.agents/skills/e2e-cucumber-playwright/SKILL.md @@ -23,7 +23,7 @@ Use this skill for Dify's repository-level E2E suite in `e2e/`. Use [`e2e/AGENTS - `e2e/scripts/run-cucumber.ts` and `e2e/cucumber.config.ts` when tags or execution flow matter 3. Read [`references/playwright-best-practices.md`](references/playwright-best-practices.md) only when locator, assertion, isolation, or waiting choices are involved. 4. Read [`references/cucumber-best-practices.md`](references/cucumber-best-practices.md) only when scenario wording, step granularity, tags, or expression design are involved. -5. Re-check official docs with Context7 before introducing a new Playwright or Cucumber pattern. +5. Re-check official Playwright or Cucumber docs with the available documentation tools before introducing a new framework pattern. ## Local Rules diff --git a/.agents/skills/frontend-code-review/references/performance.md b/.agents/skills/frontend-code-review/references/performance.md index 2d60072f5c..0c33db46d0 100644 --- a/.agents/skills/frontend-code-review/references/performance.md +++ b/.agents/skills/frontend-code-review/references/performance.md @@ -9,18 +9,18 @@ Category: Performance When rendering React Flow, prefer `useNodes`/`useEdges` for UI consumption and rely on `useStoreApi` inside callbacks that mutate or read node/edge state. Avoid manually pulling Flow data outside of these hooks. -## Complex prop memoization +## Complex prop stability -IsUrgent: True +IsUrgent: False Category: Performance ### Description -Wrap complex prop values (objects, arrays, maps) in `useMemo` prior to passing them into child components to guarantee stable references and prevent unnecessary renders. +Only require stable object, array, or map props when there is a clear reason: the child is memoized, the value participates in effect/query dependencies, the value is part of a stable-reference API contract, or profiling/local behavior shows avoidable re-renders. Do not request `useMemo` for every inline object by default; `how-to-write-component` treats memoization as a targeted optimization. Update this file when adding, editing, or removing Performance rules so the catalog remains accurate. -Wrong: +Risky: ```tsx ``` -Right: +Better when stable identity matters: ```tsx const config = useMemo(() => ({ diff --git a/.agents/skills/frontend-query-mutation/SKILL.md b/.agents/skills/frontend-query-mutation/SKILL.md deleted file mode 100644 index 10c49d222e..0000000000 --- a/.agents/skills/frontend-query-mutation/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: frontend-query-mutation -description: Guide for implementing Dify frontend query and mutation patterns with TanStack Query and oRPC. Trigger when creating or updating contracts in web/contract, wiring router composition, consuming consoleQuery or marketplaceQuery in components or services, deciding whether to call queryOptions()/mutationOptions() directly or extract a helper or use-* hook, configuring oRPC experimental_defaults/default options, handling conditional queries, cache updates/invalidation, mutation error handling, or migrating legacy service calls to contract-first query and mutation helpers. ---- - -# Frontend Query & Mutation - -## Intent - -- Keep contract as the single source of truth in `web/contract/*`. -- Prefer contract-shaped `queryOptions()` and `mutationOptions()`. -- Keep default cache behavior with `consoleQuery`/`marketplaceQuery` setup, and keep business orchestration in feature vertical hooks when direct contract calls are not enough. -- Treat `web/service/use-*` query or mutation wrappers as legacy migration targets, not the preferred destination. -- Keep abstractions minimal to preserve TypeScript inference. - -## Workflow - -1. Identify the change surface. - - Read `references/contract-patterns.md` for contract files, router composition, client helpers, and query or mutation call-site shape. - - Read `references/runtime-rules.md` for conditional queries, default options, cache updates/invalidation, error handling, and legacy migrations. - - Read both references when a task spans contract shape and runtime behavior. -2. Implement the smallest abstraction that fits the task. - - Default to direct `useQuery(...)` or `useMutation(...)` calls with oRPC helpers at the call site. - - Extract a small shared query helper only when multiple call sites share the same extra options. - - Create or keep feature hooks only for real orchestration or shared domain behavior. - - When touching thin `web/service/use-*` wrappers, migrate them away when feasible. -3. Preserve Dify conventions. - - Keep contract inputs in `{ params, query?, body? }` shape. - - Bind default cache updates/invalidation in `createTanstackQueryUtils(...experimental_defaults...)`; use feature hooks only for workflows that cannot be expressed as default operation behavior. - - Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required. - -## Files Commonly Touched - -- `web/contract/console/*.ts` -- `web/contract/marketplace.ts` -- `web/contract/router.ts` -- `web/service/client.ts` -- legacy `web/service/use-*.ts` files when migrating wrappers away -- component and hook call sites using `consoleQuery` or `marketplaceQuery` - -## References - -- Use `references/contract-patterns.md` for contract shape, router registration, query and mutation helpers, and anti-patterns that degrade inference. -- Use `references/runtime-rules.md` for conditional queries, invalidation, `mutate` versus `mutateAsync`, and legacy migration rules. - -Treat this skill as the single query and mutation entry point for Dify frontend work. Keep detailed rules in the reference files instead of duplicating them in project docs. diff --git a/.agents/skills/frontend-query-mutation/agents/openai.yaml b/.agents/skills/frontend-query-mutation/agents/openai.yaml deleted file mode 100644 index 79e7e7d214..0000000000 --- a/.agents/skills/frontend-query-mutation/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Frontend Query & Mutation" - short_description: "Dify TanStack Query, oRPC, and default option patterns" - default_prompt: "Use this skill when implementing or reviewing Dify frontend contracts, query and mutation call sites, oRPC default options, conditional queries, cache updates/invalidation, or legacy query/mutation migrations." diff --git a/.agents/skills/frontend-query-mutation/references/contract-patterns.md b/.agents/skills/frontend-query-mutation/references/contract-patterns.md deleted file mode 100644 index 25ccfc81d7..0000000000 --- a/.agents/skills/frontend-query-mutation/references/contract-patterns.md +++ /dev/null @@ -1,129 +0,0 @@ -# Contract Patterns - -## Table of Contents - -- Intent -- Minimal structure -- Core workflow -- Query usage decision rule -- Mutation usage decision rule -- Thin hook decision rule -- Anti-patterns -- Contract rules -- Type export - -## Intent - -- Keep contract as the single source of truth in `web/contract/*`. -- Default query usage to call-site `useQuery(consoleQuery|marketplaceQuery.xxx.queryOptions(...))` when endpoint behavior maps 1:1 to the contract. -- Keep abstractions minimal and preserve TypeScript inference. - -## Minimal Structure - -```text -web/contract/ -├── base.ts -├── router.ts -├── marketplace.ts -└── console/ - ├── billing.ts - └── ...other domains -web/service/client.ts -``` - -## Core Workflow - -1. Define contract in `web/contract/console/{domain}.ts` or `web/contract/marketplace.ts`. - - Use `base.route({...}).output(type<...>())` as the baseline. - - Add `.input(type<...>())` only when the request has `params`, `query`, or `body`. - - For `GET` without input, omit `.input(...)`; do not use `.input(type())`. -2. Register contract in `web/contract/router.ts`. - - Import directly from domain files and nest by API prefix. -3. Consume from UI call sites via oRPC query utilities. - -```typescript -import { useQuery } from '@tanstack/react-query' -import { consoleQuery } from '@/service/client' - -const invoiceQuery = useQuery(consoleQuery.billing.invoices.queryOptions({ - staleTime: 5 * 60 * 1000, - throwOnError: true, - select: invoice => invoice.url, -})) -``` - -## Query Usage Decision Rule - -1. Default to direct `*.queryOptions(...)` usage at the call site. -2. If 3 or more call sites share the same extra options, extract a small query helper, not a `use-*` passthrough hook. -3. Create or keep feature hooks only for orchestration. - - Combine multiple queries or mutations. - - Share domain-level derived state or invalidation helpers. - - Prefer `web/features/{domain}/hooks/*` for feature-owned workflows. -4. Treat `web/service/use-{domain}.ts` as legacy. - - Do not create new thin service wrappers for oRPC contracts. - - When touching existing wrappers, inline direct `consoleQuery` or `marketplaceQuery` consumption when the wrapper is only a passthrough. - -```typescript -const invoicesBaseQueryOptions = () => - consoleQuery.billing.invoices.queryOptions({ retry: false }) - -const invoiceQuery = useQuery({ - ...invoicesBaseQueryOptions(), - throwOnError: true, -}) -``` - -## Mutation Usage Decision Rule - -1. Default to mutation helpers from `consoleQuery` or `marketplaceQuery`, for example `useMutation(consoleQuery.billing.bindPartnerStack.mutationOptions(...))`. -2. If the mutation flow is heavily custom, use oRPC clients as `mutationFn`, for example `consoleClient.xxx` or `marketplaceClient.xxx`, instead of handwritten non-oRPC mutation logic. - -```typescript -const createTagMutation = useMutation(consoleQuery.tags.create.mutationOptions()) -``` - -## Thin Hook Decision Rule - -Remove thin hooks when they only rename a single oRPC query or mutation helper. -Keep hooks when they orchestrate business behavior across multiple operations, own local workflow state, or normalize a feature-specific API. -Prefer feature vertical hooks for kept orchestration. Do not move new contract-first wrappers into `web/service/use-*`. - -Use: - -```typescript -const deleteTagMutation = useMutation(consoleQuery.tags.delete.mutationOptions()) -``` - -Keep: - -```typescript -const applyTagBindingsMutation = useApplyTagBindingsMutation() -``` - -`useApplyTagBindingsMutation` is acceptable because it coordinates bind and unbind requests, computes deltas, and exposes a feature-level workflow rather than a single endpoint passthrough. - -## Anti-Patterns - -- Do not wrap `useQuery` with `options?: Partial`. -- Do not split local `queryKey` and `queryFn` when oRPC `queryOptions` already exists and fits the use case. -- Do not create thin `use-*` passthrough hooks for a single endpoint. -- Do not create business-layer helpers whose only purpose is to call `consoleQuery.xxx.mutationOptions()` or `queryOptions()`. -- Do not introduce new `web/service/use-*` files for oRPC contract passthroughs. -- These patterns can degrade inference, especially around `throwOnError` and `select`, and add unnecessary indirection. - -## Contract Rules - -- Input structure: always use `{ params, query?, body? }`. -- No-input `GET`: omit `.input(...)`; do not use `.input(type())`. -- Path params: use `{paramName}` in the path and match it in the `params` object. -- Router nesting: group by API prefix, for example `/billing/*` becomes `billing: {}`. -- No barrel files: import directly from specific files. -- Types: import from `@/types/` and use the `type()` helper. -- Mutations: prefer `mutationOptions`; use explicit `mutationKey` mainly for defaults, filtering, and devtools. - -## Type Export - -```typescript -export type ConsoleInputs = InferContractRouterInputs -``` diff --git a/.agents/skills/frontend-query-mutation/references/runtime-rules.md b/.agents/skills/frontend-query-mutation/references/runtime-rules.md deleted file mode 100644 index 91b484d438..0000000000 --- a/.agents/skills/frontend-query-mutation/references/runtime-rules.md +++ /dev/null @@ -1,172 +0,0 @@ -# Runtime Rules - -## Table of Contents - -- Conditional queries -- oRPC default options -- Cache invalidation -- Key API guide -- `mutate` vs `mutateAsync` -- Legacy migration - -## Conditional Queries - -Prefer contract-shaped `queryOptions(...)`. -When required input is missing, prefer `input: skipToken` instead of placeholder params or non-null assertions. -Use `enabled` only for extra business gating after the input itself is already valid. - -```typescript -import { skipToken, useQuery } from '@tanstack/react-query' - -// Disable the query by skipping input construction. -function useAccessMode(appId: string | undefined) { - return useQuery(consoleQuery.accessControl.appAccessMode.queryOptions({ - input: appId - ? { params: { appId } } - : skipToken, - })) -} - -// Avoid runtime-only guards that bypass type checking. -function useBadAccessMode(appId: string | undefined) { - return useQuery(consoleQuery.accessControl.appAccessMode.queryOptions({ - input: { params: { appId: appId! } }, - enabled: !!appId, - })) -} -``` - -## oRPC Default Options - -Use `experimental_defaults` in `createTanstackQueryUtils` when a contract operation should always carry shared TanStack Query behavior, such as default stale time, mutation cache writes, or invalidation. - -Place defaults at the query utility creation point in `web/service/client.ts`: - -```typescript -export const consoleQuery = createTanstackQueryUtils(consoleClient, { - path: ['console'], - experimental_defaults: { - tags: { - create: { - mutationOptions: { - onSuccess: (tag, _variables, _result, context) => { - context.client.setQueryData( - consoleQuery.tags.list.queryKey({ - input: { - query: { - type: tag.type, - }, - }, - }), - (oldTags: Tag[] | undefined) => oldTags ? [tag, ...oldTags] : oldTags, - ) - }, - }, - }, - }, - }, -}) -``` - -Rules: - -- Keep defaults inline in the `consoleQuery` or `marketplaceQuery` initialization when they need sibling oRPC key builders. -- Do not create a wrapper function solely to host `createTanstackQueryUtils`. -- Do not split defaults into a vertical feature file if that forces handwritten operation paths such as `generateOperationKey(['console', ...])`. -- Keep feature-level orchestration in the feature vertical; keep query utility lifecycle defaults with the query utility. -- Prefer call-site callbacks for UI feedback only; shared cache behavior belongs in oRPC defaults when it is tied to a contract operation. - -## Cache Invalidation - -Bind shared invalidation in oRPC defaults when it is tied to a contract operation. -Use feature vertical hooks only for multi-operation workflows or domain orchestration that cannot live in a single operation default. -Components may add UI feedback in call-site callbacks, but they should not decide which queries to invalidate. - -Use: - -- `.key()` for namespace or prefix invalidation -- `.queryKey(...)` only for exact cache reads or writes such as `getQueryData` and `setQueryData` -- `queryClient.invalidateQueries(...)` in mutation `onSuccess` - -Do not use deprecated `useInvalid` from `use-base.ts`. - -```typescript -// Feature orchestration owns cache invalidation only when defaults are not enough. -export const useUpdateAccessMode = () => { - const queryClient = useQueryClient() - - return useMutation(consoleQuery.accessControl.updateAccessMode.mutationOptions({ - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: consoleQuery.accessControl.appWhitelistSubjects.key(), - }) - }, - })) -} - -// Component only adds UI behavior. -updateAccessMode({ appId, mode }, { - onSuccess: () => toast.success('...'), -}) - -// Avoid putting invalidation knowledge in the component. -mutate({ appId, mode }, { - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: consoleQuery.accessControl.appWhitelistSubjects.key(), - }) - }, -}) -``` - -## Key API Guide - -- `.key(...)` - - Use for partial matching operations. - - Prefer it for invalidation, refetch, and cancel patterns. - - Example: `queryClient.invalidateQueries({ queryKey: consoleQuery.billing.key() })` -- `.queryKey(...)` - - Use for a specific query's full key. - - Prefer it for exact cache addressing and direct reads or writes. -- `.mutationKey(...)` - - Use for a specific mutation's full key. - - Prefer it for mutation defaults registration, mutation-status filtering, and devtools grouping. - -## `mutate` vs `mutateAsync` - -Prefer `mutate` by default. -Use `mutateAsync` only when Promise semantics are truly required, such as parallel mutations or sequential steps with result dependencies. - -Rules: - -- Event handlers should usually call `mutate(...)` with `onSuccess` or `onError`. -- Every `await mutateAsync(...)` must be wrapped in `try/catch`. -- Do not use `mutateAsync` when callbacks already express the flow clearly. - -```typescript -// Default case. -mutation.mutate(data, { - onSuccess: result => router.push(result.url), -}) - -// Promise semantics are required. -try { - const order = await createOrder.mutateAsync(orderData) - await confirmPayment.mutateAsync({ orderId: order.id, token }) - router.push(`/orders/${order.id}`) -} -catch (error) { - toast.error(error instanceof Error ? error.message : 'Unknown error') -} -``` - -## Legacy Migration - -When touching old code, migrate it toward these rules: - -| Old pattern | New pattern | -|---|---| -| `useInvalid(key)` in service wrappers | oRPC defaults, or a feature vertical hook for real orchestration | -| component-triggered invalidation after mutation | move invalidation into oRPC defaults or a feature vertical hook | -| imperative fetch plus manual invalidation | wrap it in `useMutation(...mutationOptions(...))` | -| `await mutateAsync()` without `try/catch` | switch to `mutate(...)` or add `try/catch` | diff --git a/.agents/skills/frontend-testing/SKILL.md b/.agents/skills/frontend-testing/SKILL.md index 105c979c58..86675dfeba 100644 --- a/.agents/skills/frontend-testing/SKILL.md +++ b/.agents/skills/frontend-testing/SKILL.md @@ -5,7 +5,7 @@ description: Generate Vitest + React Testing Library tests for Dify frontend com # Dify Frontend Testing Skill -This skill enables Claude to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices. +This skill enables Codex to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices. > **⚠️ Authoritative Source**: This skill is derived from `web/docs/test.md`. Use Vitest mock/timer APIs (`vi.*`). @@ -24,23 +24,15 @@ Apply this skill when the user: **Do NOT apply** when: - User is asking about backend/API tests (Python/pytest) -- User is asking about E2E tests (Playwright/Cypress) +- User is asking about E2E tests (Cucumber + Playwright under `e2e/`) - User is only asking conceptual questions without code context ## Quick Reference -### Tech Stack - -| Tool | Version | Purpose | -|------|---------|---------| -| Vitest | 4.0.16 | Test runner | -| React Testing Library | 16.0 | Component testing | -| jsdom | - | Test environment | -| nock | 14.0 | HTTP mocking | -| TypeScript | 5.x | Type safety | - ### Key Commands +Run these commands from `web/`. From the repository root, prefix them with `pnpm -C web`. + ```bash # Run all tests pnpm test diff --git a/.agents/skills/frontend-testing/references/mocking.md b/.agents/skills/frontend-testing/references/mocking.md index 8c2f1c0c58..7723e4df21 100644 --- a/.agents/skills/frontend-testing/references/mocking.md +++ b/.agents/skills/frontend-testing/references/mocking.md @@ -56,7 +56,7 @@ See [Zustand Store Testing](#zustand-store-testing) section for full details. | Location | Purpose | |----------|---------| -| `web/vitest.setup.ts` | Global mocks shared by all tests (`react-i18next`, `next/image`, `zustand`) | +| `web/vitest.setup.ts` | Global mocks shared by all tests (`react-i18next`, `zustand`, clipboard, FloatingPortal, Monaco, localStorage`) | | `web/__mocks__/zustand.ts` | Zustand mock implementation (auto-resets stores after each test) | | `web/__mocks__/` | Reusable mock factories shared across multiple test files | | Test file | Test-specific mocks, inline with `vi.mock()` | @@ -216,28 +216,21 @@ describe('Component', () => { }) ``` -### 5. HTTP Mocking with Nock +### 5. HTTP and `fetch` Mocking ```typescript -import nock from 'nock' - -const GITHUB_HOST = 'https://api.github.com' -const GITHUB_PATH = '/repos/owner/repo' - -const mockGithubApi = (status: number, body: Record, delayMs = 0) => { - return nock(GITHUB_HOST) - .get(GITHUB_PATH) - .delay(delayMs) - .reply(status, body) -} - describe('GithubComponent', () => { - afterEach(() => { - nock.cleanAll() + beforeEach(() => { + vi.clearAllMocks() }) it('should display repo info', async () => { - mockGithubApi(200, { name: 'dify', stars: 1000 }) + vi.mocked(globalThis.fetch).mockResolvedValueOnce( + new Response(JSON.stringify({ name: 'dify', stars: 1000 }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ) render() @@ -247,7 +240,12 @@ describe('GithubComponent', () => { }) it('should handle API error', async () => { - mockGithubApi(500, { message: 'Server error' }) + vi.mocked(globalThis.fetch).mockResolvedValueOnce( + new Response(JSON.stringify({ message: 'Server error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }), + ) render() @@ -258,6 +256,8 @@ describe('GithubComponent', () => { }) ``` +Prefer mocking `@/service/*` modules or spying on `global.fetch` / `ky` clients with deterministic responses. Do not introduce an HTTP interception dependency such as `nock` or MSW unless it is already declared in the workspace or adding it is part of the task. + ### 6. Context Providers ```typescript @@ -332,7 +332,7 @@ const renderWithQueryClient = (ui: React.ReactElement) => { 1. **Don't mock Zustand store modules** - Use real stores with `setState()` 1. Don't mock components you can import directly 1. Don't create overly simplified mocks that miss conditional logic -1. Don't forget to clean up nock after each test +1. Don't leave HTTP mocks or service mock state leaking between tests 1. Don't use `any` types in mocks without necessity ### Mock Decision Tree diff --git a/.agents/skills/frontend-testing/references/workflow.md b/.agents/skills/frontend-testing/references/workflow.md index bc4ed8285a..27755d42a7 100644 --- a/.agents/skills/frontend-testing/references/workflow.md +++ b/.agents/skills/frontend-testing/references/workflow.md @@ -227,12 +227,12 @@ Failing tests compound: **Fix failures immediately before proceeding.** -## Integration with Claude's Todo Feature +## Integration with Codex's Todo Feature -When using Claude for multi-file testing: +When using Codex for multi-file testing: -1. **Ask Claude to create a todo list** before starting -1. **Request one file at a time** or ensure Claude processes incrementally +1. **Create a todo list** before starting +1. **Process one file at a time** 1. **Verify each test passes** before asking for the next 1. **Mark todos complete** as you progress diff --git a/.agents/skills/how-to-write-component/SKILL.md b/.agents/skills/how-to-write-component/SKILL.md new file mode 100644 index 0000000000..f33a9dd75e --- /dev/null +++ b/.agents/skills/how-to-write-component/SKILL.md @@ -0,0 +1,63 @@ +--- +name: how-to-write-component +description: React/TypeScript component style guide. Use when writing, refactoring, or reviewing React components, especially around props typing, state boundaries, shared local state with Jotai atoms, API types, query/mutation contracts, navigation, memoization, wrappers, and empty-state handling. +--- + +# How To Write A Component + +Use this as the decision guide for React/TypeScript component structure. Existing code is reference material, not automatic precedent; when it conflicts with these rules, adapt the approach instead of reproducing the violation. + +## Core Defaults + +- Search before adding UI, hooks, helpers, or styling patterns. Reuse existing base components, feature components, hooks, utilities, and design styles when they fit. +- Group code by feature workflow, route, or ownership area: components, hooks, local types, query helpers, atoms, constants, and small utilities should live near the code that changes with them. +- Promote code to shared only when multiple verticals need the same stable primitive. Otherwise keep it local and compose shared primitives inside the owning feature. +- Use Tailwind CSS v4.1+ rules via the `tailwind-css-rules` skill. Prefer v4 utilities, `gap`, `text-size/line-height`, `min-h-dvh`, and avoid deprecated utilities and `@apply`. + +## Ownership + +- Put local state, queries, mutations, handlers, and derived UI data in the lowest component that uses them. Extract a purpose-built owner component only when the logic has no natural home. +- Repeated TanStack query calls in sibling components are acceptable when each component independently consumes the data. Do not hoist a query only because it is duplicated; TanStack Query handles deduplication and cache sharing. +- Hoist state, queries, or callbacks to a parent only when the parent consumes the data, coordinates shared loading/error/empty UI, needs one consistent snapshot, or owns a workflow spanning children. +- Avoid prop drilling. One pass-through layer is acceptable; repeated forwarding means ownership should move down or into feature-scoped Jotai UI state. Keep server/cache state in query and API data flow. +- Keep callbacks in a parent only for workflow coordination such as form submission, shared selection, batch behavior, or navigation. Otherwise let the child or row own its action. +- Prefer uncontrolled DOM state and CSS variables before adding controlled props. + +## Components, Props, And Types + +- Type component signatures directly; do not use `FC` or `React.FC`. +- Prefer `function` for top-level components and module helpers. Use arrow functions for local callbacks, handlers, and lambda-style APIs. +- Prefer named exports. Use default exports only where the framework requires them, such as Next.js route files. +- Type simple one-off props inline. Use a named `Props` type only when reused, exported, complex, or clearer. +- Use API-generated or API-returned types at component boundaries. Keep small UI conversion helpers beside the component that needs them. +- Name values by their domain role and backend API contract, and keep that name stable across the call chain, especially IDs like `appInstanceId`. Normalize framework or route params at the boundary. +- Keep fallback and invariant checks at the lowest component that already handles that state; callers should pass raw values through instead of duplicating checks. + +## Queries And Mutations + +- Keep `web/contract/*` as the single source of truth for API shape; follow existing domain/router patterns and the `{ params, query?, body? }` input shape. +- Consume queries directly with `useQuery(consoleQuery.xxx.queryOptions(...))` or `useQuery(marketplaceQuery.xxx.queryOptions(...))`. +- Avoid pass-through hooks and thin `web/service/use-*` wrappers that only rename `queryOptions()` or `mutationOptions()`. Extract a small `queryOptions` helper only when repeated call-site options justify it. +- Keep feature hooks for real orchestration, workflow state, or shared domain behavior. +- For missing required query input, use `input: skipToken`; use `enabled` only for extra business gating after the input is valid. +- Consume mutations directly with `useMutation(consoleQuery.xxx.mutationOptions(...))` or `useMutation(marketplaceQuery.xxx.mutationOptions(...))`; use oRPC clients as `mutationFn` only for custom flows. +- Put shared cache behavior in `createTanstackQueryUtils(...experimental_defaults...)`; components may add UI feedback callbacks, but should not own shared invalidation rules. +- Do not use deprecated `useInvalid` or `useReset`. +- Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required, and wrap awaited calls in `try/catch`. + +## Component Boundaries + +- Use the first level below a page or tab to organize independent page sections when it adds real structure. This layer is layout/semantic first, not automatically the data owner. +- Split deeper components by the data and state each layer actually needs. Each component should access only necessary data, and ownership should stay at the lowest consumer. +- Keep cohesive forms, menu bodies, and one-off helpers local unless they need their own state, reuse, or semantic boundary. +- Separate hidden secondary surfaces from the trigger's main flow. For dialogs, dropdowns, popovers, and similar branches, extract a small local component that owns the trigger, open state, and hidden content when it would obscure the parent flow. +- Preserve composability by separating behavior ownership from layout ownership. A dropdown action may own its trigger, open state, and menu content; the caller owns placement such as slots, offsets, and alignment. +- Avoid unnecessary DOM hierarchy. Do not add wrapper elements unless they provide layout, semantics, accessibility, state ownership, or integration with a library API; prefer fragments or styling an existing element when possible. +- Avoid shallow wrappers and prop renaming unless the wrapper adds validation, orchestration, error handling, state ownership, or a real semantic boundary. + +## Navigation, Effects, And Performance + +- Prefer `Link` for normal navigation. Use router APIs only for command-flow side effects such as mutation success, guarded redirects, or form submission. +- Treat `useEffect` as a last resort. First try deriving values during render, moving event-driven work into handlers, or using existing hooks/APIs for persistence, subscriptions, media queries, timers, and DOM sync. +- Do not use `useEffect` directly in components. If unavoidable, encapsulate it in a purpose-built hook so the component consumes a declarative API. +- Avoid `memo`, `useMemo`, and `useCallback` unless there is a clear performance reason. diff --git a/.agents/skills/tailwind-css-rules/SKILL.md b/.agents/skills/tailwind-css-rules/SKILL.md new file mode 100644 index 0000000000..3528548036 --- /dev/null +++ b/.agents/skills/tailwind-css-rules/SKILL.md @@ -0,0 +1,367 @@ +--- +name: tailwind-css-rules +description: Tailwind CSS v4.1+ rules and best practices. Use when writing, reviewing, refactoring, or upgrading Tailwind CSS classes and styles, especially v4 utility migrations, layout spacing, typography, responsive variants, dark mode, gradients, CSS variables, and component styling. +--- + +# Tailwind CSS Rules and Best Practices + +## Core Principles + +- **Always use Tailwind CSS v4.1+** - Ensure the codebase is using the latest version +- **Do not use deprecated or removed utilities** - ALWAYS use the replacement +- **Never use `@apply`** - Use CSS variables, the `--spacing()` function, or framework components instead +- **Check for redundant classes** - Remove any classes that aren't necessary +- **Group elements logically** to simplify responsive tweaks later + +## Upgrading to Tailwind CSS v4 + +### Before Upgrading + +- **Always read the upgrade documentation first** - Read https://tailwindcss.com/docs/upgrade-guide and https://tailwindcss.com/blog/tailwindcss-v4 before starting an upgrade. +- Ensure the git repository is in a clean state before starting + +### Upgrade Process + +1. Run the upgrade command: `npx @tailwindcss/upgrade@latest` for both major and minor updates +2. The tool will convert JavaScript config files to the new CSS format +3. Review all changes extensively to clean up any false positives +4. Test thoroughly across your application + +## Breaking Changes Reference + +### Removed Utilities (NEVER use these in v4) + +| ❌ Deprecated | ✅ Replacement | +| ----------------------- | ------------------------------------------------- | +| `bg-opacity-*` | Use opacity modifiers like `bg-black/50` | +| `text-opacity-*` | Use opacity modifiers like `text-black/50` | +| `border-opacity-*` | Use opacity modifiers like `border-black/50` | +| `divide-opacity-*` | Use opacity modifiers like `divide-black/50` | +| `ring-opacity-*` | Use opacity modifiers like `ring-black/50` | +| `placeholder-opacity-*` | Use opacity modifiers like `placeholder-black/50` | +| `flex-shrink-*` | `shrink-*` | +| `flex-grow-*` | `grow-*` | +| `overflow-ellipsis` | `text-ellipsis` | +| `decoration-slice` | `box-decoration-slice` | +| `decoration-clone` | `box-decoration-clone` | + +### Renamed Utilities + +Use the v4 name when migrating code that still carries Tailwind v3 semantics. Do not blanket-replace existing v4 classes: classes such as `rounded-sm`, `shadow-sm`, `ring-1`, and `ring-2` are valid in this codebase when they intentionally represent the current design scale. + +| ❌ v3 pattern | ✅ v4 pattern | +| ------------------- | -------------------------------------------------- | +| `bg-gradient-*` | `bg-linear-*` | +| old shadow scale | verify against the current Tailwind/design scale | +| old blur scale | verify against the current Tailwind/design scale | +| old radius scale | use the Dify radius token mapping when applicable | +| `outline-none` | `outline-hidden` | +| bare `ring` utility | use an explicit ring width such as `ring-1`/`ring-2`/`ring-3` | + +For Figma radius tokens, follow `packages/dify-ui/AGENTS.md`. For example, `--radius/xs` maps to `rounded-sm`; do not rewrite it to `rounded-xs`. + +## Layout and Spacing Rules + +### Flexbox and Grid Spacing + +#### Always use gap utilities for internal spacing + +Gap provides consistent spacing without edge cases (no extra space on last items). It's cleaner and more maintainable than margins on children. + +```html + +
+
Item 1
+
Item 2
+
Item 3
+ +
+ + +
+
Item 1
+
Item 2
+
Item 3
+
+``` + +#### Gap vs Space utilities + +- **Never use `space-x-*` or `space-y-*` in flex/grid layouts** - always use gap +- Space utilities add margins to children and have issues with wrapped items +- Gap works correctly with flex-wrap and all flex directions + +```html + +
+ +
+ + +
+ +
+``` + +### General Spacing Guidelines + +- **Prefer top and left margins** over bottom and right margins (unless conditionally rendered) +- **Use padding on parent containers** instead of bottom margins on the last child +- **Always use `min-h-dvh` instead of `min-h-screen`** - `min-h-screen` is buggy on mobile Safari +- **Prefer `size-*` utilities** over separate `w-*` and `h-*` when setting equal dimensions +- For max-widths, prefer the container scale (e.g., `max-w-2xs` over `max-w-72`) + +## Typography Rules + +### Line Heights + +- **Never use `leading-*` classes** - Always use line height modifiers with text size +- **Always use fixed line heights from the spacing scale** - Don't use named values + +```html + +

Text with separate line height

+

Text with named line height

+ + +

Text with line height modifier

+

Text with specific line height

+``` + +### Font Size Reference + +Be precise with font sizes - know the actual pixel values: + +- `text-xs` = 12px +- `text-sm` = 14px +- `text-base` = 16px +- `text-lg` = 18px +- `text-xl` = 20px + +## Color and Opacity + +### Opacity Modifiers + +**Never use `bg-opacity-*`, `text-opacity-*`, etc.** - use the opacity modifier syntax: + +```html + +
Old opacity syntax
+ + +
Modern opacity syntax
+``` + +## Responsive Design + +### Breakpoint Optimization + +- **Check for redundant classes across breakpoints** +- **Only add breakpoint variants when values change** + +```html + +
+ +
+ + +
+ +
+``` + +## Dark Mode + +### Dark Mode Best Practices + +- Use the plain `dark:` variant pattern +- Put light mode styles first, then dark mode styles +- Ensure `dark:` variant comes before other variants + +```html + +
+ +
+``` + +## Gradient Utilities + +- **ALWAYS Use `bg-linear-*` instead of `bg-gradient-*` utilities** - The gradient utilities were renamed in v4 +- Use the new `bg-radial` or `bg-radial-[]` to create radial gradients +- Use the new `bg-conic` or `bg-conic-*` to create conic gradients + +```html + +
+
+
+ + +
+``` + +## Working with CSS Variables + +### Accessing Theme Values + +Tailwind CSS v4 exposes all theme values as CSS variables: + +```css +/* Access colors, and other theme values */ +.custom-element { + background: var(--color-red-500); + border-radius: var(--radius-lg); +} +``` + +### The `--spacing()` Function + +Use the dedicated `--spacing()` function for spacing calculations: + +```css +.custom-class { + margin-top: calc(100vh - --spacing(16)); +} +``` + +### Extending theme values + +Use CSS to extend theme values: + +```css +@import "tailwindcss"; + +@theme { + --color-mint-500: oklch(0.72 0.11 178); +} +``` + +```html +
+ +
+``` + +## New v4 Features + +### Container Queries + +Use the `@container` class and size variants: + +```html +
+
+ +
+ +
+
+
+``` + +### Container Query Units + +Use container-based units like `cqw` for responsive sizing: + +```html +
+

Responsive to container width

+
+``` + +### Text Shadows (v4.1) + +Use text-shadow-\* utilities from text-shadow-2xs to text-shadow-lg: + +```html + +

Large shadow

+

Small shadow with opacity

+``` + +### Masking (v4.1) + +Use the new composable mask utilities for image and gradient masks: + +```html + +
Top fade
+
Bottom gradient
+
+ Fade from white to black +
+ + +
+ Radial mask +
+``` + +## Component Patterns + +### Avoiding Utility Inheritance + +Don't add utilities to parents that you override in children: + +```html + +
+

Centered Heading

+
Left-aligned content
+
+ + +
+

Centered Heading

+
Left-aligned content
+
+``` + +### Component Extraction + +- Extract repeated patterns into framework components, not CSS classes +- Keep utility classes in templates/JSX +- Use data attributes for complex state-based styling + +## CSS Best Practices + +### Nesting Guidelines + +- Use nesting when styling both parent and children +- Avoid empty parent selectors + +```css +/* ✅ Good nesting - parent has styles */ +.card { + padding: --spacing(4); + + > .card-title { + font-weight: bold; + } +} + +/* ❌ Avoid empty parents */ +ul { + > li { + /* Parent has no styles */ + } +} +``` + +## Common Pitfalls to Avoid + +1. **Using old opacity utilities** - Always use `/opacity` syntax like `bg-red-500/60` +2. **Redundant breakpoint classes** - Only specify changes +3. **Space utilities in flex/grid** - Always use gap +4. **Leading utilities** - Use line-height modifiers like `text-sm/6` +5. **Arbitrary values** - Use the design scale +6. **@apply directive** - Use components or CSS variables +7. **min-h-screen on mobile** - Use min-h-dvh +8. **Separate width/height** - Use size utilities when equal +9. **Arbitrary values** - Always use Tailwind's predefined scale whenever possible (e.g., use `ml-4` over `ml-[16px]`) diff --git a/e2e/AGENTS.md b/e2e/AGENTS.md index e56aab20a7..c05b5105be 100644 --- a/e2e/AGENTS.md +++ b/e2e/AGENTS.md @@ -31,7 +31,7 @@ pnpm -C e2e check `pnpm install` is resolved through the repository workspace and uses the shared root lockfile plus `pnpm-workspace.yaml`. -Use `pnpm check` as the default local verification step after editing E2E TypeScript, Cucumber support code, or feature glue. It runs formatting, linting, and type checks for this package. +Use `pnpm -C e2e check` as the default local verification step after editing E2E TypeScript, Cucumber support code, or feature glue. It runs formatting, linting, and type checks for this package. Common commands: @@ -68,8 +68,8 @@ flowchart TD C --> D["Cucumber loads config, steps, and support modules"] D --> E["BeforeAll bootstraps shared auth state via /install"] E --> F{"Which command is running?"} - F -->|`pnpm e2e`| G["Run config default tags: not @fresh and not @skip"] - F -->|`pnpm e2e:full*`| H["Override tags to not @skip"] + F -->|`pnpm -C e2e e2e`| G["Run config default tags: not @fresh and not @skip"] + F -->|`pnpm -C e2e e2e:full*`| H["Override tags to not @skip"] G --> I["Per-scenario BrowserContext from shared browser"] H --> I I --> J["Failure artifacts written to cucumber-report/artifacts"] @@ -99,7 +99,7 @@ Behavior depends on instance state: - uninitialized instance: completes install and stores authenticated state - initialized instance: signs in and reuses authenticated state -Because of that, the `@fresh` install scenario only runs in the `pnpm e2e:full*` flows. The default `pnpm e2e*` flows exclude `@fresh` via Cucumber config tags so they can be re-run against an already initialized instance. +Because of that, the `@fresh` install scenario only runs in the `pnpm -C e2e e2e:full*` flows. The default `pnpm -C e2e e2e*` flows exclude `@fresh` via Cucumber config tags so they can be re-run against an already initialized instance. Reset all persisted E2E state: @@ -126,7 +126,7 @@ pnpm -C e2e e2e:middleware:up Stop the full middleware stack: ```bash -pnpm e2e:middleware:down +pnpm -C e2e e2e:middleware:down ``` The middleware stack includes: @@ -141,15 +141,15 @@ The middleware stack includes: Fresh install verification: ```bash -pnpm e2e:full +pnpm -C e2e e2e:full ``` Run the Cucumber suite against an already running middleware stack: ```bash -pnpm e2e:middleware:up -pnpm e2e -pnpm e2e:middleware:down +pnpm -C e2e e2e:middleware:up +pnpm -C e2e e2e +pnpm -C e2e e2e:middleware:down ``` Artifacts and diagnostics: diff --git a/packages/contracts/generated/enterprise/orpc.gen.ts b/packages/contracts/generated/enterprise/orpc.gen.ts index 73eb850001..6b9b76470a 100644 --- a/packages/contracts/generated/enterprise/orpc.gen.ts +++ b/packages/contracts/generated/enterprise/orpc.gen.ts @@ -7,63 +7,6 @@ import { zConsoleSsoOAuth2LoginResponse, zConsoleSsoOidcLoginResponse, zConsoleSsoSamlLoginResponse, - zEnterpriseAppDeployConsoleCancelRuntimeDeploymentBody, - zEnterpriseAppDeployConsoleCancelRuntimeDeploymentPath, - zEnterpriseAppDeployConsoleCancelRuntimeDeploymentResponse, - zEnterpriseAppDeployConsoleCreateAppInstanceBody, - zEnterpriseAppDeployConsoleCreateAppInstanceResponse, - zEnterpriseAppDeployConsoleCreateDeploymentBody, - zEnterpriseAppDeployConsoleCreateDeploymentPath, - zEnterpriseAppDeployConsoleCreateDeploymentResponse, - zEnterpriseAppDeployConsoleCreateDeveloperApiKeyBody, - zEnterpriseAppDeployConsoleCreateDeveloperApiKeyPath, - zEnterpriseAppDeployConsoleCreateDeveloperApiKeyResponse, - zEnterpriseAppDeployConsoleCreateReleaseBody, - zEnterpriseAppDeployConsoleCreateReleasePath, - zEnterpriseAppDeployConsoleCreateReleaseResponse, - zEnterpriseAppDeployConsoleDeleteAppInstancePath, - zEnterpriseAppDeployConsoleDeleteAppInstanceResponse, - zEnterpriseAppDeployConsoleDeleteDeveloperApiKeyPath, - zEnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponse, - zEnterpriseAppDeployConsoleGetAppInstanceAccessPath, - zEnterpriseAppDeployConsoleGetAppInstanceAccessResponse, - zEnterpriseAppDeployConsoleGetAppInstanceOverviewPath, - zEnterpriseAppDeployConsoleGetAppInstanceOverviewResponse, - zEnterpriseAppDeployConsoleGetAppInstanceSettingsPath, - zEnterpriseAppDeployConsoleGetAppInstanceSettingsResponse, - zEnterpriseAppDeployConsoleGetEnvironmentAccessPolicyPath, - zEnterpriseAppDeployConsoleGetEnvironmentAccessPolicyResponse, - zEnterpriseAppDeployConsoleListAppInstancesQuery, - zEnterpriseAppDeployConsoleListAppInstancesResponse, - zEnterpriseAppDeployConsoleListDeploymentBindingOptionsPath, - zEnterpriseAppDeployConsoleListDeploymentBindingOptionsResponse, - zEnterpriseAppDeployConsoleListDeploymentEnvironmentOptionsResponse, - zEnterpriseAppDeployConsoleListReleasesPath, - zEnterpriseAppDeployConsoleListReleasesQuery, - zEnterpriseAppDeployConsoleListReleasesResponse, - zEnterpriseAppDeployConsoleListRuntimeInstancesPath, - zEnterpriseAppDeployConsoleListRuntimeInstancesResponse, - zEnterpriseAppDeployConsolePreviewReleaseBody, - zEnterpriseAppDeployConsolePreviewReleasePath, - zEnterpriseAppDeployConsolePreviewReleaseResponse, - zEnterpriseAppDeployConsoleSearchAccessSubjectsPath, - zEnterpriseAppDeployConsoleSearchAccessSubjectsQuery, - zEnterpriseAppDeployConsoleSearchAccessSubjectsResponse, - zEnterpriseAppDeployConsoleUndeployRuntimeInstanceBody, - zEnterpriseAppDeployConsoleUndeployRuntimeInstancePath, - zEnterpriseAppDeployConsoleUndeployRuntimeInstanceResponse, - zEnterpriseAppDeployConsoleUpdateAccessChannelsBody, - zEnterpriseAppDeployConsoleUpdateAccessChannelsPath, - zEnterpriseAppDeployConsoleUpdateAccessChannelsResponse, - zEnterpriseAppDeployConsoleUpdateAppInstanceBody, - zEnterpriseAppDeployConsoleUpdateAppInstancePath, - zEnterpriseAppDeployConsoleUpdateAppInstanceResponse, - zEnterpriseAppDeployConsoleUpdateDeveloperApiBody, - zEnterpriseAppDeployConsoleUpdateDeveloperApiPath, - zEnterpriseAppDeployConsoleUpdateDeveloperApiResponse, - zEnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyBody, - zEnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyPath, - zEnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyResponse, zWebAppAuthGetGroupSubjectsQuery, zWebAppAuthGetGroupSubjectsResponse, zWebAppAuthGetWebAppAccessModeQuery, @@ -78,344 +21,6 @@ import { zWebAppAuthUpdateWebAppWhitelistSubjectsResponse, } from './zod.gen' -export const listAppInstances = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'EnterpriseAppDeployConsole_ListAppInstances', - path: '/enterprise/app-instances', - tags: ['EnterpriseAppDeployConsole'], - }) - .input(z.object({ query: zEnterpriseAppDeployConsoleListAppInstancesQuery.optional() })) - .output(zEnterpriseAppDeployConsoleListAppInstancesResponse) - -export const createAppInstance = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'EnterpriseAppDeployConsole_CreateAppInstance', - path: '/enterprise/app-instances', - tags: ['EnterpriseAppDeployConsole'], - }) - .input(z.object({ body: zEnterpriseAppDeployConsoleCreateAppInstanceBody })) - .output(zEnterpriseAppDeployConsoleCreateAppInstanceResponse) - -export const deleteAppInstance = oc - .route({ - inputStructure: 'detailed', - method: 'DELETE', - operationId: 'EnterpriseAppDeployConsole_DeleteAppInstance', - path: '/enterprise/app-instances/{appInstanceId}', - tags: ['EnterpriseAppDeployConsole'], - }) - .input(z.object({ params: zEnterpriseAppDeployConsoleDeleteAppInstancePath })) - .output(zEnterpriseAppDeployConsoleDeleteAppInstanceResponse) - -export const updateAppInstance = oc - .route({ - inputStructure: 'detailed', - method: 'PATCH', - operationId: 'EnterpriseAppDeployConsole_UpdateAppInstance', - path: '/enterprise/app-instances/{appInstanceId}', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - body: zEnterpriseAppDeployConsoleUpdateAppInstanceBody, - params: zEnterpriseAppDeployConsoleUpdateAppInstancePath, - }), - ) - .output(zEnterpriseAppDeployConsoleUpdateAppInstanceResponse) - -export const getAppInstanceAccess = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'EnterpriseAppDeployConsole_GetAppInstanceAccess', - path: '/enterprise/app-instances/{appInstanceId}/access', - tags: ['EnterpriseAppDeployConsole'], - }) - .input(z.object({ params: zEnterpriseAppDeployConsoleGetAppInstanceAccessPath })) - .output(zEnterpriseAppDeployConsoleGetAppInstanceAccessResponse) - -export const updateAccessChannels = oc - .route({ - inputStructure: 'detailed', - method: 'PATCH', - operationId: 'EnterpriseAppDeployConsole_UpdateAccessChannels', - path: '/enterprise/app-instances/{appInstanceId}/access-channels', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - body: zEnterpriseAppDeployConsoleUpdateAccessChannelsBody, - params: zEnterpriseAppDeployConsoleUpdateAccessChannelsPath, - }), - ) - .output(zEnterpriseAppDeployConsoleUpdateAccessChannelsResponse) - -export const searchAccessSubjects = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'EnterpriseAppDeployConsole_SearchAccessSubjects', - path: '/enterprise/app-instances/{appInstanceId}/access-subjects:search', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - params: zEnterpriseAppDeployConsoleSearchAccessSubjectsPath, - query: zEnterpriseAppDeployConsoleSearchAccessSubjectsQuery.optional(), - }), - ) - .output(zEnterpriseAppDeployConsoleSearchAccessSubjectsResponse) - -export const createDeveloperApiKey = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'EnterpriseAppDeployConsole_CreateDeveloperApiKey', - path: '/enterprise/app-instances/{appInstanceId}/api-keys', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - body: zEnterpriseAppDeployConsoleCreateDeveloperApiKeyBody, - params: zEnterpriseAppDeployConsoleCreateDeveloperApiKeyPath, - }), - ) - .output(zEnterpriseAppDeployConsoleCreateDeveloperApiKeyResponse) - -export const deleteDeveloperApiKey = oc - .route({ - inputStructure: 'detailed', - method: 'DELETE', - operationId: 'EnterpriseAppDeployConsole_DeleteDeveloperApiKey', - path: '/enterprise/app-instances/{appInstanceId}/api-keys/{apiKeyId}', - tags: ['EnterpriseAppDeployConsole'], - }) - .input(z.object({ params: zEnterpriseAppDeployConsoleDeleteDeveloperApiKeyPath })) - .output(zEnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponse) - -export const listDeploymentBindingOptions = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'EnterpriseAppDeployConsole_ListDeploymentBindingOptions', - path: '/enterprise/app-instances/{appInstanceId}/deployment-binding-options', - tags: ['EnterpriseAppDeployConsole'], - }) - .input(z.object({ params: zEnterpriseAppDeployConsoleListDeploymentBindingOptionsPath })) - .output(zEnterpriseAppDeployConsoleListDeploymentBindingOptionsResponse) - -export const createDeployment = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'EnterpriseAppDeployConsole_CreateDeployment', - path: '/enterprise/app-instances/{appInstanceId}/deployments', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - body: zEnterpriseAppDeployConsoleCreateDeploymentBody, - params: zEnterpriseAppDeployConsoleCreateDeploymentPath, - }), - ) - .output(zEnterpriseAppDeployConsoleCreateDeploymentResponse) - -export const updateDeveloperApi = oc - .route({ - inputStructure: 'detailed', - method: 'PATCH', - operationId: 'EnterpriseAppDeployConsole_UpdateDeveloperApi', - path: '/enterprise/app-instances/{appInstanceId}/developer-api', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - body: zEnterpriseAppDeployConsoleUpdateDeveloperApiBody, - params: zEnterpriseAppDeployConsoleUpdateDeveloperApiPath, - }), - ) - .output(zEnterpriseAppDeployConsoleUpdateDeveloperApiResponse) - -export const getEnvironmentAccessPolicy = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'EnterpriseAppDeployConsole_GetEnvironmentAccessPolicy', - path: '/enterprise/app-instances/{appInstanceId}/environments/{environmentId}/access-policy', - tags: ['EnterpriseAppDeployConsole'], - }) - .input(z.object({ params: zEnterpriseAppDeployConsoleGetEnvironmentAccessPolicyPath })) - .output(zEnterpriseAppDeployConsoleGetEnvironmentAccessPolicyResponse) - -export const updateEnvironmentAccessPolicy = oc - .route({ - inputStructure: 'detailed', - method: 'PUT', - operationId: 'EnterpriseAppDeployConsole_UpdateEnvironmentAccessPolicy', - path: '/enterprise/app-instances/{appInstanceId}/environments/{environmentId}/access-policy', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - body: zEnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyBody, - params: zEnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyPath, - }), - ) - .output(zEnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyResponse) - -export const getAppInstanceOverview = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'EnterpriseAppDeployConsole_GetAppInstanceOverview', - path: '/enterprise/app-instances/{appInstanceId}/overview', - tags: ['EnterpriseAppDeployConsole'], - }) - .input(z.object({ params: zEnterpriseAppDeployConsoleGetAppInstanceOverviewPath })) - .output(zEnterpriseAppDeployConsoleGetAppInstanceOverviewResponse) - -export const listReleases = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'EnterpriseAppDeployConsole_ListReleases', - path: '/enterprise/app-instances/{appInstanceId}/releases', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - params: zEnterpriseAppDeployConsoleListReleasesPath, - query: zEnterpriseAppDeployConsoleListReleasesQuery.optional(), - }), - ) - .output(zEnterpriseAppDeployConsoleListReleasesResponse) - -export const createRelease = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'EnterpriseAppDeployConsole_CreateRelease', - path: '/enterprise/app-instances/{appInstanceId}/releases', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - body: zEnterpriseAppDeployConsoleCreateReleaseBody, - params: zEnterpriseAppDeployConsoleCreateReleasePath, - }), - ) - .output(zEnterpriseAppDeployConsoleCreateReleaseResponse) - -export const previewRelease = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'EnterpriseAppDeployConsole_PreviewRelease', - path: '/enterprise/app-instances/{appInstanceId}/releases:preview', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - body: zEnterpriseAppDeployConsolePreviewReleaseBody, - params: zEnterpriseAppDeployConsolePreviewReleasePath, - }), - ) - .output(zEnterpriseAppDeployConsolePreviewReleaseResponse) - -export const listRuntimeInstances = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'EnterpriseAppDeployConsole_ListRuntimeInstances', - path: '/enterprise/app-instances/{appInstanceId}/runtime-instances', - tags: ['EnterpriseAppDeployConsole'], - }) - .input(z.object({ params: zEnterpriseAppDeployConsoleListRuntimeInstancesPath })) - .output(zEnterpriseAppDeployConsoleListRuntimeInstancesResponse) - -export const cancelRuntimeDeployment = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'EnterpriseAppDeployConsole_CancelRuntimeDeployment', - path: '/enterprise/app-instances/{appInstanceId}/runtime-instances/{runtimeInstanceId}/deployment:cancel', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - body: zEnterpriseAppDeployConsoleCancelRuntimeDeploymentBody, - params: zEnterpriseAppDeployConsoleCancelRuntimeDeploymentPath, - }), - ) - .output(zEnterpriseAppDeployConsoleCancelRuntimeDeploymentResponse) - -export const undeployRuntimeInstance = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'EnterpriseAppDeployConsole_UndeployRuntimeInstance', - path: '/enterprise/app-instances/{appInstanceId}/runtime-instances/{runtimeInstanceId}:undeploy', - tags: ['EnterpriseAppDeployConsole'], - }) - .input( - z.object({ - body: zEnterpriseAppDeployConsoleUndeployRuntimeInstanceBody, - params: zEnterpriseAppDeployConsoleUndeployRuntimeInstancePath, - }), - ) - .output(zEnterpriseAppDeployConsoleUndeployRuntimeInstanceResponse) - -export const getAppInstanceSettings = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'EnterpriseAppDeployConsole_GetAppInstanceSettings', - path: '/enterprise/app-instances/{appInstanceId}/settings', - tags: ['EnterpriseAppDeployConsole'], - }) - .input(z.object({ params: zEnterpriseAppDeployConsoleGetAppInstanceSettingsPath })) - .output(zEnterpriseAppDeployConsoleGetAppInstanceSettingsResponse) - -export const listDeploymentEnvironmentOptions = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'EnterpriseAppDeployConsole_ListDeploymentEnvironmentOptions', - path: '/enterprise/deployment-environment-options', - tags: ['EnterpriseAppDeployConsole'], - }) - .output(zEnterpriseAppDeployConsoleListDeploymentEnvironmentOptionsResponse) - -export const enterpriseAppDeployConsole = { - listAppInstances, - createAppInstance, - deleteAppInstance, - updateAppInstance, - getAppInstanceAccess, - updateAccessChannels, - searchAccessSubjects, - createDeveloperApiKey, - deleteDeveloperApiKey, - listDeploymentBindingOptions, - createDeployment, - updateDeveloperApi, - getEnvironmentAccessPolicy, - updateEnvironmentAccessPolicy, - getAppInstanceOverview, - listReleases, - createRelease, - previewRelease, - listRuntimeInstances, - cancelRuntimeDeployment, - undeployRuntimeInstance, - getAppInstanceSettings, - listDeploymentEnvironmentOptions, -} - export const oAuth2Login = oc .route({ inputStructure: 'detailed', @@ -528,7 +133,6 @@ export const webAppAuth = { } export const contract = { - enterpriseAppDeployConsole, consoleSso, webAppAuth, } diff --git a/packages/contracts/generated/enterprise/types.gen.ts b/packages/contracts/generated/enterprise/types.gen.ts index 56228f2738..b747c4baa8 100644 --- a/packages/contracts/generated/enterprise/types.gen.ts +++ b/packages/contracts/generated/enterprise/types.gen.ts @@ -4,46 +4,6 @@ export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}) } -export type AccessChannels = { - enabled?: boolean - webappRows?: Array - cli?: CliAccess -} - -export type AccessModeOption = { - mode?: string - label?: string - disabled?: boolean - selected?: boolean -} - -export type AccessPolicyDetail = { - accessMode?: string - subjects?: Array - options?: Array -} - -export type AccessStatus = { - accessChannelsEnabled?: boolean - webappUrl?: string - cliUrl?: string - developerApiEnabled?: boolean - apiKeyCount?: number -} - -export type AccessSubject = { - subjectId?: string - subjectType?: string -} - -export type AccessSubjectDisplay = { - id?: string - subjectType?: string - name?: string - avatarUrl?: string - memberCount?: string -} - export type Account = { id?: string email?: string @@ -70,104 +30,9 @@ export type AccountInWorkspace = { role?: string } -export type AckDeploymentReply = { - accepted?: boolean - newVersion?: string -} - -export type AckDeploymentReq = { - deploymentId?: string - instanceId?: string - expectedVersion?: string - status?: string - observedReleaseId?: string - lastError?: LastError -} - -export type AppInstanceBasicInfo = { +export type AddGroupAppsRequest = { id?: string - name?: string - description?: string - sourceAppId?: string - sourceAppName?: string - mode?: string - createdAt?: string -} - -export type AppInstanceCard = { - id?: string - name?: string - icon?: string - mode?: string - sourceAppName?: string - statuses?: Array - lastDeployedAt?: string -} - -export type AppRunnerBatchRuntimeArtifactReply = { - results?: Array -} - -export type AppRunnerBatchRuntimeArtifactRequest = { - artifacts?: Array -} - -export type AppRunnerBootstrapAssignment = { - appId?: string - environmentId?: string - workflowId?: string - instanceId?: string - workspaceId?: string - instanceVersion?: string - bindingSnapshotVersion?: string - executionTokenVersion?: string - executionToken?: string - releaseId?: string -} - -export type AppRunnerBootstrapReply = { - runnerId?: string - assignmentRevision?: string - assignments?: Array -} - -export type AppRunnerBootstrapRequest = { - runner?: AppRunnerRunnerInfo -} - -export type AppRunnerRunnerInfo = { - hostname?: string -} - -export type AppRunnerRuntimeArtifactReply = { - dslYaml?: string - bindingSnapshotVersion?: string - bindingSnapshot?: { - [key: string]: unknown - } -} - -export type AppRunnerRuntimeArtifactRequest = { - instanceId?: string - releaseId?: string - bindingSnapshotVersion?: string -} - -export type AppRunnerRuntimeArtifactResult = { - instanceId?: string - releaseId?: string - artifact?: AppRunnerRuntimeArtifactReply - errorCode?: string - errorMessage?: string -} - -export type AppRunnerTokenExchangeReply = { - accessToken?: string - expiresAt?: string -} - -export type AppRunnerTokenExchangeRequest = { - joinToken?: string + app_ids?: Array } export type AuthSettingsReply = { @@ -193,15 +58,6 @@ export type AuthSettingsReq = { ssoSettings?: SsoSettings } -export type BootstrapProgress = { - currentStep?: string - completedSteps?: Array - attemptCount?: number - lastAttemptAt?: string - lastErrorCode?: string - lastErrorMessage?: string -} - export type BrandingInfo = { enabled?: boolean applicationTitle?: string @@ -210,15 +66,6 @@ export type BrandingInfo = { favicon?: string } -export type CancelRuntimeDeploymentReply = { - status?: string -} - -export type CancelRuntimeDeploymentReq = { - appInstanceId?: string - runtimeInstanceId?: string -} - export type CheckPasswordStatusReply = { requirePasswordChange?: boolean changeReason?: number @@ -230,82 +77,10 @@ export type ClearDefaultWorkspaceReply = { [key: string]: unknown } -export type CliAccess = { - url?: string -} - -export type ConsoleEnvironment = { - id?: string - name?: string - runtime?: string - type?: string - status?: string -} - -export type ConsoleRelease = { - id?: string - name?: string - shortCommitId?: string - createdAt?: string -} - -export type ConsoleUser = { - id?: string - name?: string -} - -export type CreateAppInstanceReply = { - appInstanceId?: string - initialRelease?: ConsoleRelease -} - -export type CreateAppInstanceReq = { - sourceAppId?: string - name?: string - description?: string -} - export type CreateBearerTokenResponse = { token?: string } -export type CreateDeploymentReply = { - runtimeInstanceId?: string - deploymentId?: string - status?: string -} - -export type CreateDeploymentReq = { - appInstanceId?: string - environmentId?: string - releaseId?: string - bindings?: Array -} - -export type CreateDeveloperApiKeyReply = { - apiKey?: DeveloperApiKeyRow - token?: string -} - -export type CreateDeveloperApiKeyReq = { - appInstanceId?: string - environmentId?: string - name?: string -} - -export type CreateEnvironmentReply = { - environment?: Environment -} - -export type CreateEnvironmentReq = { - name?: string - description?: string - mode?: number - backend?: number - k8s?: K8sEnvironmentConfig - host?: HostEnvironmentConfig -} - export type CreateMemberReply = { id?: string password?: string @@ -329,12 +104,7 @@ export type CreateNewGroupsRes = { groups?: Array } -export type CreateReleaseReply = { - release?: ConsoleRelease -} - -export type CreateReleaseReq = { - appInstanceId?: string +export type CreateResourceGroupRequest = { name?: string description?: string } @@ -394,27 +164,10 @@ export type DashboardSsosamlLoginReply = { url?: string } -export type DeleteAppInstanceReply = { - [key: string]: unknown -} - -export type DeleteDeveloperApiKeyReply = { - [key: string]: unknown -} - -export type DeleteEnvironmentReply = { - [key: string]: unknown -} - export type DeleteGroupsRes = { message?: string } -export type DeleteGuard = { - canDelete?: boolean - disabledReason?: string -} - export type DeleteMemberReply = { account?: Account } @@ -431,70 +184,6 @@ export type DeleteWorkspaceReply = { [key: string]: unknown } -export type DeployedEnvironment = { - environmentId?: string - environmentName?: string -} - -export type DeploymentBindingOptionSlot = { - slot?: string - kind?: string - label?: string - required?: boolean - candidates?: Array - envVarCandidates?: Array -} - -export type DeploymentCredentialOption = { - credentialId?: string - displayName?: string - pluginId?: string - pluginName?: string - pluginVersion?: string -} - -export type DeploymentEnvVarOption = { - envVarId?: string - name?: string - valueType?: string - displayValue?: string -} - -export type DeploymentEnvironmentOption = { - id?: string - name?: string - type?: string - backend?: string - status?: string - managedBy?: string - deployable?: boolean - disabledReason?: string -} - -export type DeploymentRuntimeBinding = { - slot?: string - credentialId?: string - envVarId?: string -} - -export type DeploymentStatusRow = { - environment?: ConsoleEnvironment - release?: ConsoleRelease - status?: string -} - -export type DeveloperApiAccess = { - enabled?: boolean - apiKeys?: Array -} - -export type DeveloperApiKeyRow = { - id?: string - name?: string - environment?: ConsoleEnvironment - maskedKey?: string -} - export type EndpointReply = { mode?: number metricsEndpoint?: OtelExporterEndpoint @@ -507,55 +196,6 @@ export type EnterpriseSystemUserSettingReply = { enableEmailPasswordLogin?: boolean } -export type Environment = { - id?: string - name?: string - description?: string - mode?: number - namespace?: string - apiServer?: string - status?: number - statusMessage?: string - bootstrapProgress?: BootstrapProgress - managedBy?: string - createdAt?: string - updatedAt?: string - backend?: number - host?: string -} - -export type EnvironmentAccessRow = { - environment?: ConsoleEnvironment - currentRelease?: ConsoleRelease - accessMode?: string - accessModeLabel?: string - hint?: string -} - -export type EnvironmentFilter = { - id?: string - name?: string - kind?: string -} - -export type GetAppInstanceAccessReply = { - permissions?: Array - accessChannels?: AccessChannels - developerApi?: DeveloperApiAccess -} - -export type GetAppInstanceOverviewReply = { - instance?: AppInstanceBasicInfo - deployments?: Array - access?: AccessStatus -} - -export type GetAppInstanceSettingsReply = { - name?: string - description?: string - deleteGuard?: DeleteGuard -} - export type GetBearerTokenResponse = { maskedToken?: string } @@ -571,14 +211,6 @@ export type GetDefaultWorkspaceReply = { workspace?: Workspace } -export type GetEnvironmentAccessPolicyReply = { - policy?: AccessPolicyDetail -} - -export type GetEnvironmentReply = { - environment?: Environment -} - export type GetGroupSubjectsRes = { subjects?: Array } @@ -587,15 +219,6 @@ export type GetGroupsRes = { groups?: Array } -export type GetInstanceReply = { - instanceId?: string - status?: string - desiredReleaseId?: string - observedReleaseId?: string - currentDeploymentId?: string - version?: string -} - export type GetJoinedGroupsRes = { groups?: Array } @@ -652,16 +275,22 @@ export type GetWorkspaceReply = { workspace?: Workspace } +export type GroupAppItem = { + app_id?: string + app_name?: string + workspace_id?: string + workspace_name?: string + app_status?: number + token_usage?: string + rpm?: string + concurrency?: string +} + export type HealthzReply = { message?: string status?: string } -export type HostEnvironmentConfig = { - machineId?: string - joinTokenHash?: string -} - export type InfoConfigReply = { SSOEnforcedForSignin?: boolean SSOEnforcedForSigninProtocol?: string @@ -677,6 +306,11 @@ export type InfoConfigReply = { PluginInstallationPermission?: PluginInstallationPermissionInfo } +export type InnerAdmission = { + marker?: string + concurrencyGroupIds?: Array +} + export type InnerBatchGetWebAppAccessModesByIdReq = { appIds?: Array } @@ -698,42 +332,10 @@ export type InnerBatchIsUserAllowedToAccessWebAppRes = { } } -export type InnerCheckAppDeployAccessReply = { - allowed?: boolean - matchedPolicyId?: string - matchedScopeType?: string - reason?: string - cacheTtlSeconds?: number -} - -export type InnerCheckAppDeployAccessReq = { - appInstanceId?: string - environmentId?: string - principalType?: string - principalId?: string -} - export type InnerCleanAppRes = { message?: string } -export type InnerGetTokenRouteReply = { - environmentId?: string - namespace?: string - serviceName?: string - servicePort?: number - environmentStatus?: string - appId?: string - tenantId?: string - instanceId?: string - observedReleaseId?: string - instanceStatus?: string -} - -export type InnerGetTokenRouteReq = { - token?: string -} - export type InnerGetWebAppAccessModeByCodeRes = { accessMode?: string } @@ -742,10 +344,34 @@ export type InnerGetWebAppAccessModeByIdRes = { accessMode?: string } +export type InnerGroupConfig = { + id?: string + enabled?: boolean + membershipId?: string + limits?: Array +} + export type InnerIsUserAllowedToAccessWebAppRes = { result?: boolean } +export type InnerReleaseAdmissionRequest = { + admission?: InnerAdmission +} + +export type InnerReleaseAdmissionResponse = { + [key: string]: unknown +} + +export type InnerResolveResponse = { + appId?: string + groups?: Array + blocked?: boolean + blockGroupId?: string + blockReason?: string + admission?: InnerAdmission +} + export type InnerTryAddAccountToDefaultWorkspaceReply = { workspaceId?: string joined?: boolean @@ -770,20 +396,6 @@ export type JoinWorkspaceReq = { role?: string } -export type K8sEnvironmentConfig = { - namespace?: string - apiServer?: string - caBundle?: string - bearerToken?: string -} - -export type LastError = { - phase?: string - code?: string - message?: string - releaseId?: string -} - export type LicenseInfo = { uuid?: string expiredAt?: string @@ -798,28 +410,21 @@ export type LicenseStatus = { workspaces?: ResourceQuota } +export type LimitConfig = { + type?: number + threshold?: string + action?: number + reached?: boolean +} + export type LimitFields = { workspaceMembers?: number workspaces?: ResourceQuota } -export type ListAppInstancesReply = { - filters?: Array - data?: Array - pagination?: Pagination -} - -export type ListDeploymentBindingOptionsReply = { - slots?: Array -} - -export type ListDeploymentEnvironmentOptionsReply = { - environments?: Array -} - -export type ListEnvironmentsReply = { - data?: Array - pagination?: Pagination +export type ListGroupAppsResponse = { + items?: Array + total?: string } export type ListMembersReply = { @@ -827,13 +432,9 @@ export type ListMembersReply = { pagination?: Pagination } -export type ListReleasesReply = { - data?: Array - pagination?: Pagination -} - -export type ListRuntimeInstancesReply = { - data?: Array +export type ListResourceGroupsResponse = { + items?: Array + total?: string } export type ListSecretKeysReply = { @@ -981,31 +582,6 @@ export type PluginInstallationSettingsReply = { restrictToMarketplaceOnly?: boolean } -export type PreviewReleaseReply = { - release?: ConsoleRelease - bindings?: Array -} - -export type PreviewReleaseReq = { - appInstanceId?: string - releaseId?: string -} - -export type ReleaseRow = { - id?: string - name?: string - createdAt?: string - createdBy?: ConsoleUser - deployedTo?: Array -} - -export type ReleaseRuntimeBinding = { - kind?: string - label?: string - displayValue?: string - valueType?: string -} - export type ResetMemberPasswordReply = { id?: string password?: string @@ -1034,21 +610,35 @@ export type ResetUserPasswordReq = { id?: string } -export type ResolveCredentialsReply = { - resolved?: Array +export type ResourceGroupDetail = { + id?: string + name?: string + description?: string + enabled?: boolean + rpm_limit?: number + rpm_action?: number + concurrency_limit?: number + concurrency_action?: number + token_quota?: string + token_action?: number + created_at?: string + updated_at?: string } -export type ResolveCredentialsReq = { - instanceId?: string - deploymentId?: string - slots?: Array -} - -export type ResolvedCredential = { - slot?: string - credentialId?: string - envVarId?: string - value?: string +export type ResourceGroupItem = { + id?: string + name?: string + description?: string + enabled?: boolean + rpm_limit?: number + concurrency_limit?: number + token_quota?: string + token_usage?: string + app_count?: string + rpm_status?: number + conc_status?: number + created_at?: string + updated_at?: string } export type ResourceQuota = { @@ -1057,36 +647,6 @@ export type ResourceQuota = { enabled?: boolean } -export type RetryEnvironmentReply = { - environment?: Environment -} - -export type RetryEnvironmentReq = { - id?: string -} - -export type RuntimeEndpoints = { - run?: string - health?: string -} - -export type RuntimeInstanceDetail = { - deploymentName?: string - replicas?: number - runtimeMode?: string - runtimeNote?: string - endpoints?: RuntimeEndpoints - bindings?: Array -} - -export type RuntimeInstanceRow = { - id?: string - environment?: ConsoleEnvironment - status?: string - currentRelease?: ConsoleRelease - detail?: RuntimeInstanceDetail -} - export type SamlConfig = { idpSsoUrl?: string certificate?: string @@ -1119,8 +679,21 @@ export type ScimSettings = { lastSyncTime?: string } -export type SearchAccessSubjectsReply = { - data?: Array +export type SearchAppItem = { + app_id?: string + app_name?: string + workspace_id?: string + workspace_name?: string + app_status?: number + icon?: string + icon_type?: string + icon_background?: string + created_by_name?: string +} + +export type SearchAppsResponse = { + items?: Array + total?: string } export type SearchForWhilteListCandidatesRes = { @@ -1145,11 +718,6 @@ export type SetDefaultWorkspaceReq = { id?: string } -export type StatusCount = { - status?: string - count?: number -} - export type Subject = { subjectId?: string subjectType?: string @@ -1185,42 +753,10 @@ export type TestConnectionReply = { error?: string } -export type TestEnvironmentConnectionReply = { - ok?: boolean - reachableServerVersion?: string - namespaceExists?: boolean - missingPermissions?: Array - error?: string - probedAt?: string -} - -export type TestEnvironmentConnectionReq = { - id?: string -} - export type ToggleEndpointRequest = { enabled?: boolean } -export type UndeployRuntimeInstanceReply = { - deploymentId?: string - status?: string -} - -export type UndeployRuntimeInstanceReq = { - appInstanceId?: string - runtimeInstanceId?: string -} - -export type UpdateAccessChannelsReply = { - accessChannels?: AccessChannels -} - -export type UpdateAccessChannelsReq = { - appInstanceId?: string - enabled?: boolean -} - export type UpdateAccessModeReq = { appId?: string accessMode?: string @@ -1230,16 +766,6 @@ export type UpdateAccessModeRes = { message?: string } -export type UpdateAppInstanceReply = { - appInstanceId?: string -} - -export type UpdateAppInstanceReq = { - appInstanceId?: string - name?: string - description?: string -} - export type UpdateBrandingInfoReq = { enabled?: boolean applicationTitle?: string @@ -1248,36 +774,6 @@ export type UpdateBrandingInfoReq = { favicon?: string } -export type UpdateDeveloperApiReply = { - developerApi?: DeveloperApiAccess -} - -export type UpdateDeveloperApiReq = { - appInstanceId?: string - enabled?: boolean -} - -export type UpdateEnvironmentAccessPolicyReply = { - permission?: EnvironmentAccessRow -} - -export type UpdateEnvironmentAccessPolicyReq = { - appInstanceId?: string - environmentId?: string - accessMode?: string - subjects?: Array -} - -export type UpdateEnvironmentReply = { - environment?: Environment -} - -export type UpdateEnvironmentReq = { - id?: string - name?: string - description?: string -} - export type UpdateGroupSubjectsReq = { groupId?: string subjects?: Array @@ -1358,6 +854,19 @@ export type UpdatePluginInstallationSettingsRequest = { restrictToMarketplaceOnly?: boolean } +export type UpdateResourceGroupRequest = { + id?: string + name?: string + description?: string + enabled?: boolean + rpm_limit?: number + rpm_action?: number + concurrency_limit?: number + concurrency_action?: number + token_quota?: string + token_action?: number +} + export type UpdateUserReply = { account?: AccountDetail } @@ -1410,11 +919,6 @@ export type UpdateWorkspaceReq = { status?: string } -export type WebAppAccessRow = { - environment?: ConsoleEnvironment - url?: string -} - export type WebAppAuthInfo = { allowSso?: boolean allowEmailCodeLogin?: boolean @@ -1459,385 +963,6 @@ export type Pagination = { totalPages?: number } -export type EnterpriseAppDeployConsoleListAppInstancesData = { - body?: never - path?: never - query?: { - environmentId?: string - notDeployed?: boolean - query?: string - pageNumber?: number - resultsPerPage?: number - } - url: '/enterprise/app-instances' -} - -export type EnterpriseAppDeployConsoleListAppInstancesResponses = { - 200: ListAppInstancesReply -} - -export type EnterpriseAppDeployConsoleListAppInstancesResponse - = EnterpriseAppDeployConsoleListAppInstancesResponses[keyof EnterpriseAppDeployConsoleListAppInstancesResponses] - -export type EnterpriseAppDeployConsoleCreateAppInstanceData = { - body: CreateAppInstanceReq - path?: never - query?: never - url: '/enterprise/app-instances' -} - -export type EnterpriseAppDeployConsoleCreateAppInstanceResponses = { - 200: CreateAppInstanceReply -} - -export type EnterpriseAppDeployConsoleCreateAppInstanceResponse - = EnterpriseAppDeployConsoleCreateAppInstanceResponses[keyof EnterpriseAppDeployConsoleCreateAppInstanceResponses] - -export type EnterpriseAppDeployConsoleDeleteAppInstanceData = { - body?: never - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}' -} - -export type EnterpriseAppDeployConsoleDeleteAppInstanceResponses = { - 200: DeleteAppInstanceReply -} - -export type EnterpriseAppDeployConsoleDeleteAppInstanceResponse - = EnterpriseAppDeployConsoleDeleteAppInstanceResponses[keyof EnterpriseAppDeployConsoleDeleteAppInstanceResponses] - -export type EnterpriseAppDeployConsoleUpdateAppInstanceData = { - body: UpdateAppInstanceReq - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}' -} - -export type EnterpriseAppDeployConsoleUpdateAppInstanceResponses = { - 200: UpdateAppInstanceReply -} - -export type EnterpriseAppDeployConsoleUpdateAppInstanceResponse - = EnterpriseAppDeployConsoleUpdateAppInstanceResponses[keyof EnterpriseAppDeployConsoleUpdateAppInstanceResponses] - -export type EnterpriseAppDeployConsoleGetAppInstanceAccessData = { - body?: never - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/access' -} - -export type EnterpriseAppDeployConsoleGetAppInstanceAccessResponses = { - 200: GetAppInstanceAccessReply -} - -export type EnterpriseAppDeployConsoleGetAppInstanceAccessResponse - = EnterpriseAppDeployConsoleGetAppInstanceAccessResponses[keyof EnterpriseAppDeployConsoleGetAppInstanceAccessResponses] - -export type EnterpriseAppDeployConsoleUpdateAccessChannelsData = { - body: UpdateAccessChannelsReq - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/access-channels' -} - -export type EnterpriseAppDeployConsoleUpdateAccessChannelsResponses = { - 200: UpdateAccessChannelsReply -} - -export type EnterpriseAppDeployConsoleUpdateAccessChannelsResponse - = EnterpriseAppDeployConsoleUpdateAccessChannelsResponses[keyof EnterpriseAppDeployConsoleUpdateAccessChannelsResponses] - -export type EnterpriseAppDeployConsoleSearchAccessSubjectsData = { - body?: never - path: { - appInstanceId: string - } - query?: { - keyword?: string - subjectTypes?: Array - } - url: '/enterprise/app-instances/{appInstanceId}/access-subjects:search' -} - -export type EnterpriseAppDeployConsoleSearchAccessSubjectsResponses = { - 200: SearchAccessSubjectsReply -} - -export type EnterpriseAppDeployConsoleSearchAccessSubjectsResponse - = EnterpriseAppDeployConsoleSearchAccessSubjectsResponses[keyof EnterpriseAppDeployConsoleSearchAccessSubjectsResponses] - -export type EnterpriseAppDeployConsoleCreateDeveloperApiKeyData = { - body: CreateDeveloperApiKeyReq - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/api-keys' -} - -export type EnterpriseAppDeployConsoleCreateDeveloperApiKeyResponses = { - 200: CreateDeveloperApiKeyReply -} - -export type EnterpriseAppDeployConsoleCreateDeveloperApiKeyResponse - = EnterpriseAppDeployConsoleCreateDeveloperApiKeyResponses[keyof EnterpriseAppDeployConsoleCreateDeveloperApiKeyResponses] - -export type EnterpriseAppDeployConsoleDeleteDeveloperApiKeyData = { - body?: never - path: { - appInstanceId: string - apiKeyId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/api-keys/{apiKeyId}' -} - -export type EnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponses = { - 200: DeleteDeveloperApiKeyReply -} - -export type EnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponse - = EnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponses[keyof EnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponses] - -export type EnterpriseAppDeployConsoleListDeploymentBindingOptionsData = { - body?: never - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/deployment-binding-options' -} - -export type EnterpriseAppDeployConsoleListDeploymentBindingOptionsResponses = { - 200: ListDeploymentBindingOptionsReply -} - -export type EnterpriseAppDeployConsoleListDeploymentBindingOptionsResponse - = EnterpriseAppDeployConsoleListDeploymentBindingOptionsResponses[keyof EnterpriseAppDeployConsoleListDeploymentBindingOptionsResponses] - -export type EnterpriseAppDeployConsoleCreateDeploymentData = { - body: CreateDeploymentReq - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/deployments' -} - -export type EnterpriseAppDeployConsoleCreateDeploymentResponses = { - 200: CreateDeploymentReply -} - -export type EnterpriseAppDeployConsoleCreateDeploymentResponse - = EnterpriseAppDeployConsoleCreateDeploymentResponses[keyof EnterpriseAppDeployConsoleCreateDeploymentResponses] - -export type EnterpriseAppDeployConsoleUpdateDeveloperApiData = { - body: UpdateDeveloperApiReq - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/developer-api' -} - -export type EnterpriseAppDeployConsoleUpdateDeveloperApiResponses = { - 200: UpdateDeveloperApiReply -} - -export type EnterpriseAppDeployConsoleUpdateDeveloperApiResponse - = EnterpriseAppDeployConsoleUpdateDeveloperApiResponses[keyof EnterpriseAppDeployConsoleUpdateDeveloperApiResponses] - -export type EnterpriseAppDeployConsoleGetEnvironmentAccessPolicyData = { - body?: never - path: { - appInstanceId: string - environmentId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/environments/{environmentId}/access-policy' -} - -export type EnterpriseAppDeployConsoleGetEnvironmentAccessPolicyResponses = { - 200: GetEnvironmentAccessPolicyReply -} - -export type EnterpriseAppDeployConsoleGetEnvironmentAccessPolicyResponse - = EnterpriseAppDeployConsoleGetEnvironmentAccessPolicyResponses[keyof EnterpriseAppDeployConsoleGetEnvironmentAccessPolicyResponses] - -export type EnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyData = { - body: UpdateEnvironmentAccessPolicyReq - path: { - appInstanceId: string - environmentId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/environments/{environmentId}/access-policy' -} - -export type EnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyResponses = { - 200: UpdateEnvironmentAccessPolicyReply -} - -export type EnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyResponse - = EnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyResponses[keyof EnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyResponses] - -export type EnterpriseAppDeployConsoleGetAppInstanceOverviewData = { - body?: never - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/overview' -} - -export type EnterpriseAppDeployConsoleGetAppInstanceOverviewResponses = { - 200: GetAppInstanceOverviewReply -} - -export type EnterpriseAppDeployConsoleGetAppInstanceOverviewResponse - = EnterpriseAppDeployConsoleGetAppInstanceOverviewResponses[keyof EnterpriseAppDeployConsoleGetAppInstanceOverviewResponses] - -export type EnterpriseAppDeployConsoleListReleasesData = { - body?: never - path: { - appInstanceId: string - } - query?: { - pageNumber?: number - resultsPerPage?: number - } - url: '/enterprise/app-instances/{appInstanceId}/releases' -} - -export type EnterpriseAppDeployConsoleListReleasesResponses = { - 200: ListReleasesReply -} - -export type EnterpriseAppDeployConsoleListReleasesResponse - = EnterpriseAppDeployConsoleListReleasesResponses[keyof EnterpriseAppDeployConsoleListReleasesResponses] - -export type EnterpriseAppDeployConsoleCreateReleaseData = { - body: CreateReleaseReq - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/releases' -} - -export type EnterpriseAppDeployConsoleCreateReleaseResponses = { - 200: CreateReleaseReply -} - -export type EnterpriseAppDeployConsoleCreateReleaseResponse - = EnterpriseAppDeployConsoleCreateReleaseResponses[keyof EnterpriseAppDeployConsoleCreateReleaseResponses] - -export type EnterpriseAppDeployConsolePreviewReleaseData = { - body: PreviewReleaseReq - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/releases:preview' -} - -export type EnterpriseAppDeployConsolePreviewReleaseResponses = { - 200: PreviewReleaseReply -} - -export type EnterpriseAppDeployConsolePreviewReleaseResponse - = EnterpriseAppDeployConsolePreviewReleaseResponses[keyof EnterpriseAppDeployConsolePreviewReleaseResponses] - -export type EnterpriseAppDeployConsoleListRuntimeInstancesData = { - body?: never - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/runtime-instances' -} - -export type EnterpriseAppDeployConsoleListRuntimeInstancesResponses = { - 200: ListRuntimeInstancesReply -} - -export type EnterpriseAppDeployConsoleListRuntimeInstancesResponse - = EnterpriseAppDeployConsoleListRuntimeInstancesResponses[keyof EnterpriseAppDeployConsoleListRuntimeInstancesResponses] - -export type EnterpriseAppDeployConsoleCancelRuntimeDeploymentData = { - body: CancelRuntimeDeploymentReq - path: { - appInstanceId: string - runtimeInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/runtime-instances/{runtimeInstanceId}/deployment:cancel' -} - -export type EnterpriseAppDeployConsoleCancelRuntimeDeploymentResponses = { - 200: CancelRuntimeDeploymentReply -} - -export type EnterpriseAppDeployConsoleCancelRuntimeDeploymentResponse - = EnterpriseAppDeployConsoleCancelRuntimeDeploymentResponses[keyof EnterpriseAppDeployConsoleCancelRuntimeDeploymentResponses] - -export type EnterpriseAppDeployConsoleUndeployRuntimeInstanceData = { - body: UndeployRuntimeInstanceReq - path: { - appInstanceId: string - runtimeInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/runtime-instances/{runtimeInstanceId}:undeploy' -} - -export type EnterpriseAppDeployConsoleUndeployRuntimeInstanceResponses = { - 200: UndeployRuntimeInstanceReply -} - -export type EnterpriseAppDeployConsoleUndeployRuntimeInstanceResponse - = EnterpriseAppDeployConsoleUndeployRuntimeInstanceResponses[keyof EnterpriseAppDeployConsoleUndeployRuntimeInstanceResponses] - -export type EnterpriseAppDeployConsoleGetAppInstanceSettingsData = { - body?: never - path: { - appInstanceId: string - } - query?: never - url: '/enterprise/app-instances/{appInstanceId}/settings' -} - -export type EnterpriseAppDeployConsoleGetAppInstanceSettingsResponses = { - 200: GetAppInstanceSettingsReply -} - -export type EnterpriseAppDeployConsoleGetAppInstanceSettingsResponse - = EnterpriseAppDeployConsoleGetAppInstanceSettingsResponses[keyof EnterpriseAppDeployConsoleGetAppInstanceSettingsResponses] - -export type EnterpriseAppDeployConsoleListDeploymentEnvironmentOptionsData = { - body?: never - path?: never - query?: never - url: '/enterprise/deployment-environment-options' -} - -export type EnterpriseAppDeployConsoleListDeploymentEnvironmentOptionsResponses = { - 200: ListDeploymentEnvironmentOptionsReply -} - -export type EnterpriseAppDeployConsoleListDeploymentEnvironmentOptionsResponse - = EnterpriseAppDeployConsoleListDeploymentEnvironmentOptionsResponses[keyof EnterpriseAppDeployConsoleListDeploymentEnvironmentOptionsResponses] - export type ConsoleSsoOAuth2LoginData = { body?: never path?: never diff --git a/packages/contracts/generated/enterprise/zod.gen.ts b/packages/contracts/generated/enterprise/zod.gen.ts index 1e7e3d44ae..cef500a906 100644 --- a/packages/contracts/generated/enterprise/zod.gen.ts +++ b/packages/contracts/generated/enterprise/zod.gen.ts @@ -2,44 +2,6 @@ import * as z from 'zod' -export const zAccessModeOption = z.object({ - mode: z.string().optional(), - label: z.string().optional(), - disabled: z.boolean().optional(), - selected: z.boolean().optional(), -}) - -export const zAccessStatus = z.object({ - accessChannelsEnabled: z.boolean().optional(), - webappUrl: z.string().optional(), - cliUrl: z.string().optional(), - developerApiEnabled: z.boolean().optional(), - apiKeyCount: z - .int() - .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) - .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) - .optional(), -}) - -export const zAccessSubject = z.object({ - subjectId: z.string().optional(), - subjectType: z.string().optional(), -}) - -export const zAccessSubjectDisplay = z.object({ - id: z.string().optional(), - subjectType: z.string().optional(), - name: z.string().optional(), - avatarUrl: z.string().optional(), - memberCount: z.string().optional(), -}) - -export const zAccessPolicyDetail = z.object({ - accessMode: z.string().optional(), - subjects: z.array(zAccessSubjectDisplay).optional(), - options: z.array(zAccessModeOption).optional(), -}) - /** * Account represents a basic user account */ @@ -75,101 +37,9 @@ export const zAccountDetail = z.object({ groups: z.array(zAccountDetailGroup).optional(), }) -export const zAckDeploymentReply = z.object({ - accepted: z.boolean().optional(), - newVersion: z.string().optional(), -}) - -export const zAppInstanceBasicInfo = z.object({ +export const zAddGroupAppsRequest = z.object({ id: z.string().optional(), - name: z.string().optional(), - description: z.string().optional(), - sourceAppId: z.string().optional(), - sourceAppName: z.string().optional(), - mode: z.string().optional(), - createdAt: z.iso.datetime().optional(), -}) - -export const zAppRunnerBootstrapAssignment = z.object({ - appId: z.string().optional(), - environmentId: z.string().optional(), - workflowId: z.string().optional(), - instanceId: z.string().optional(), - workspaceId: z.string().optional(), - instanceVersion: z.string().optional(), - bindingSnapshotVersion: z.string().optional(), - executionTokenVersion: z.string().optional(), - executionToken: z.string().optional(), - releaseId: z.string().optional(), -}) - -export const zAppRunnerBootstrapReply = z.object({ - runnerId: z.string().optional(), - assignmentRevision: z.string().optional(), - assignments: z.array(zAppRunnerBootstrapAssignment).optional(), -}) - -export const zAppRunnerRunnerInfo = z.object({ - hostname: z.string().optional(), -}) - -export const zAppRunnerBootstrapRequest = z.object({ - runner: zAppRunnerRunnerInfo.optional(), -}) - -export const zAppRunnerRuntimeArtifactReply = z.object({ - dslYaml: z.string().optional(), - bindingSnapshotVersion: z.string().optional(), - bindingSnapshot: z.record(z.string(), z.unknown()).optional(), -}) - -export const zAppRunnerRuntimeArtifactRequest = z.object({ - instanceId: z.string().optional(), - releaseId: z.string().optional(), - bindingSnapshotVersion: z.string().optional(), -}) - -export const zAppRunnerBatchRuntimeArtifactRequest = z.object({ - artifacts: z.array(zAppRunnerRuntimeArtifactRequest).optional(), -}) - -export const zAppRunnerRuntimeArtifactResult = z.object({ - instanceId: z.string().optional(), - releaseId: z.string().optional(), - artifact: zAppRunnerRuntimeArtifactReply.optional(), - errorCode: z.string().optional(), - errorMessage: z.string().optional(), -}) - -export const zAppRunnerBatchRuntimeArtifactReply = z.object({ - results: z.array(zAppRunnerRuntimeArtifactResult).optional(), -}) - -export const zAppRunnerTokenExchangeReply = z.object({ - accessToken: z.string().optional(), - expiresAt: z.iso.datetime().optional(), -}) - -export const zAppRunnerTokenExchangeRequest = z.object({ - joinToken: z.string().optional(), -}) - -/** - * BootstrapProgress is step-list-agnostic. Reconcilers emit step names as - * strings owned by each executor (e.g. "connectivity", "namespace"), so adding - * or removing steps does not break the API. - */ -export const zBootstrapProgress = z.object({ - currentStep: z.string().optional(), - completedSteps: z.array(z.string()).optional(), - attemptCount: z - .int() - .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) - .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) - .optional(), - lastAttemptAt: z.iso.datetime().optional(), - lastErrorCode: z.string().optional(), - lastErrorMessage: z.string().optional(), + app_ids: z.array(z.string()).optional(), }) export const zBrandingInfo = z.object({ @@ -180,15 +50,6 @@ export const zBrandingInfo = z.object({ favicon: z.string().optional(), }) -export const zCancelRuntimeDeploymentReply = z.object({ - status: z.string().optional(), -}) - -export const zCancelRuntimeDeploymentReq = z.object({ - appInstanceId: z.string().optional(), - runtimeInstanceId: z.string().optional(), -}) - export const zCheckPasswordStatusReply = z.object({ requirePasswordChange: z.boolean().optional(), changeReason: z.int().optional(), @@ -202,57 +63,10 @@ export const zCheckPasswordStatusReply = z.object({ export const zClearDefaultWorkspaceReply = z.record(z.string(), z.unknown()) -export const zCliAccess = z.object({ - url: z.string().optional(), -}) - -export const zConsoleEnvironment = z.object({ - id: z.string().optional(), - name: z.string().optional(), - runtime: z.string().optional(), - type: z.string().optional(), - status: z.string().optional(), -}) - -export const zConsoleRelease = z.object({ - id: z.string().optional(), - name: z.string().optional(), - shortCommitId: z.string().optional(), - createdAt: z.iso.datetime().optional(), -}) - -export const zConsoleUser = z.object({ - id: z.string().optional(), - name: z.string().optional(), -}) - -export const zCreateAppInstanceReply = z.object({ - appInstanceId: z.string().optional(), - initialRelease: zConsoleRelease.optional(), -}) - -export const zCreateAppInstanceReq = z.object({ - sourceAppId: z.string().optional(), - name: z.string().optional(), - description: z.string().optional(), -}) - export const zCreateBearerTokenResponse = z.object({ token: z.string().optional(), }) -export const zCreateDeploymentReply = z.object({ - runtimeInstanceId: z.string().optional(), - deploymentId: z.string().optional(), - status: z.string().optional(), -}) - -export const zCreateDeveloperApiKeyReq = z.object({ - appInstanceId: z.string().optional(), - environmentId: z.string().optional(), - name: z.string().optional(), -}) - export const zCreateMemberReply = z.object({ id: z.string().optional(), password: z.string().optional(), @@ -275,12 +89,7 @@ export const zCreateNewGroupsReq = z.object({ groups: z.array(zCreateNewGroupsReqGroup).optional(), }) -export const zCreateReleaseReply = z.object({ - release: zConsoleRelease.optional(), -}) - -export const zCreateReleaseReq = z.object({ - appInstanceId: z.string().optional(), +export const zCreateResourceGroupRequest = z.object({ name: z.string().optional(), description: z.string().optional(), }) @@ -342,21 +151,10 @@ export const zDashboardSsosamlLoginReply = z.object({ url: z.string().optional(), }) -export const zDeleteAppInstanceReply = z.record(z.string(), z.unknown()) - -export const zDeleteDeveloperApiKeyReply = z.record(z.string(), z.unknown()) - -export const zDeleteEnvironmentReply = z.record(z.string(), z.unknown()) - export const zDeleteGroupsRes = z.object({ message: z.string().optional(), }) -export const zDeleteGuard = z.object({ - canDelete: z.boolean().optional(), - disabledReason: z.string().optional(), -}) - export const zDeleteMemberReply = z.object({ account: zAccount.optional(), }) @@ -371,82 +169,6 @@ export const zDeleteUserReply = z.object({ export const zDeleteWorkspaceReply = z.record(z.string(), z.unknown()) -export const zDeployedEnvironment = z.object({ - environmentId: z.string().optional(), - environmentName: z.string().optional(), -}) - -export const zDeploymentCredentialOption = z.object({ - credentialId: z.string().optional(), - displayName: z.string().optional(), - pluginId: z.string().optional(), - pluginName: z.string().optional(), - pluginVersion: z.string().optional(), -}) - -export const zDeploymentEnvVarOption = z.object({ - envVarId: z.string().optional(), - name: z.string().optional(), - valueType: z.string().optional(), - displayValue: z.string().optional(), -}) - -export const zDeploymentBindingOptionSlot = z.object({ - slot: z.string().optional(), - kind: z.string().optional(), - label: z.string().optional(), - required: z.boolean().optional(), - candidates: z.array(zDeploymentCredentialOption).optional(), - envVarCandidates: z.array(zDeploymentEnvVarOption).optional(), -}) - -export const zDeploymentEnvironmentOption = z.object({ - id: z.string().optional(), - name: z.string().optional(), - type: z.string().optional(), - backend: z.string().optional(), - status: z.string().optional(), - managedBy: z.string().optional(), - deployable: z.boolean().optional(), - disabledReason: z.string().optional(), -}) - -export const zDeploymentRuntimeBinding = z.object({ - slot: z.string().optional(), - credentialId: z.string().optional(), - envVarId: z.string().optional(), -}) - -export const zCreateDeploymentReq = z.object({ - appInstanceId: z.string().optional(), - environmentId: z.string().optional(), - releaseId: z.string().optional(), - bindings: z.array(zDeploymentRuntimeBinding).optional(), -}) - -export const zDeploymentStatusRow = z.object({ - environment: zConsoleEnvironment.optional(), - release: zConsoleRelease.optional(), - status: z.string().optional(), -}) - -export const zDeveloperApiKeyRow = z.object({ - id: z.string().optional(), - name: z.string().optional(), - environment: zConsoleEnvironment.optional(), - maskedKey: z.string().optional(), -}) - -export const zCreateDeveloperApiKeyReply = z.object({ - apiKey: zDeveloperApiKeyRow.optional(), - token: z.string().optional(), -}) - -export const zDeveloperApiAccess = z.object({ - enabled: z.boolean().optional(), - apiKeys: z.array(zDeveloperApiKeyRow).optional(), -}) - /** * System user setting messages */ @@ -456,53 +178,6 @@ export const zEnterpriseSystemUserSettingReply = z.object({ enableEmailPasswordLogin: z.boolean().optional(), }) -export const zEnvironment = z.object({ - id: z.string().optional(), - name: z.string().optional(), - description: z.string().optional(), - mode: z.int().optional(), - namespace: z.string().optional(), - apiServer: z.string().optional(), - status: z.int().optional(), - statusMessage: z.string().optional(), - bootstrapProgress: zBootstrapProgress.optional(), - managedBy: z.string().optional(), - createdAt: z.iso.datetime().optional(), - updatedAt: z.iso.datetime().optional(), - backend: z.int().optional(), - host: z.string().optional(), -}) - -export const zCreateEnvironmentReply = z.object({ - environment: zEnvironment.optional(), -}) - -export const zEnvironmentAccessRow = z.object({ - environment: zConsoleEnvironment.optional(), - currentRelease: zConsoleRelease.optional(), - accessMode: z.string().optional(), - accessModeLabel: z.string().optional(), - hint: z.string().optional(), -}) - -export const zEnvironmentFilter = z.object({ - id: z.string().optional(), - name: z.string().optional(), - kind: z.string().optional(), -}) - -export const zGetAppInstanceOverviewReply = z.object({ - instance: zAppInstanceBasicInfo.optional(), - deployments: z.array(zDeploymentStatusRow).optional(), - access: zAccessStatus.optional(), -}) - -export const zGetAppInstanceSettingsReply = z.object({ - name: z.string().optional(), - description: z.string().optional(), - deleteGuard: zDeleteGuard.optional(), -}) - export const zGetBearerTokenResponse = z.object({ maskedToken: z.string().optional(), }) @@ -513,23 +188,6 @@ export const zGetClusterInfoReply = z.object({ verifyMode: z.string().optional(), }) -export const zGetEnvironmentAccessPolicyReply = z.object({ - policy: zAccessPolicyDetail.optional(), -}) - -export const zGetEnvironmentReply = z.object({ - environment: zEnvironment.optional(), -}) - -export const zGetInstanceReply = z.object({ - instanceId: z.string().optional(), - status: z.string().optional(), - desiredReleaseId: z.string().optional(), - observedReleaseId: z.string().optional(), - currentDeploymentId: z.string().optional(), - version: z.string().optional(), -}) - export const zGetLicenseStatusReply = z.object({ status: z.string().optional(), }) @@ -565,14 +223,25 @@ export const zGetWebAppWhitelistSubjectsResMember = z.object({ avatar: z.string().optional(), }) +export const zGroupAppItem = z.object({ + app_id: z.string().optional(), + app_name: z.string().optional(), + workspace_id: z.string().optional(), + workspace_name: z.string().optional(), + app_status: z.int().optional(), + token_usage: z.string().optional(), + rpm: z.string().optional(), + concurrency: z.string().optional(), +}) + export const zHealthzReply = z.object({ message: z.string().optional(), status: z.string().optional(), }) -export const zHostEnvironmentConfig = z.object({ - machineId: z.string().optional(), - joinTokenHash: z.string().optional(), +export const zInnerAdmission = z.object({ + marker: z.string().optional(), + concurrencyGroupIds: z.array(z.string()).optional(), }) export const zInnerBatchGetWebAppAccessModesByIdReq = z.object({ @@ -592,50 +261,10 @@ export const zInnerBatchIsUserAllowedToAccessWebAppRes = z.object({ permissions: z.record(z.string(), z.boolean()).optional(), }) -export const zInnerCheckAppDeployAccessReply = z.object({ - allowed: z.boolean().optional(), - matchedPolicyId: z.string().optional(), - matchedScopeType: z.string().optional(), - reason: z.string().optional(), - cacheTtlSeconds: z - .int() - .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) - .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) - .optional(), -}) - -export const zInnerCheckAppDeployAccessReq = z.object({ - appInstanceId: z.string().optional(), - environmentId: z.string().optional(), - principalType: z.string().optional(), - principalId: z.string().optional(), -}) - export const zInnerCleanAppRes = z.object({ message: z.string().optional(), }) -export const zInnerGetTokenRouteReply = z.object({ - environmentId: z.string().optional(), - namespace: z.string().optional(), - serviceName: z.string().optional(), - servicePort: z - .int() - .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) - .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) - .optional(), - environmentStatus: z.string().optional(), - appId: z.string().optional(), - tenantId: z.string().optional(), - instanceId: z.string().optional(), - observedReleaseId: z.string().optional(), - instanceStatus: z.string().optional(), -}) - -export const zInnerGetTokenRouteReq = z.object({ - token: z.string().optional(), -}) - export const zInnerGetWebAppAccessModeByCodeRes = z.object({ accessMode: z.string().optional(), }) @@ -648,6 +277,12 @@ export const zInnerIsUserAllowedToAccessWebAppRes = z.object({ result: z.boolean().optional(), }) +export const zInnerReleaseAdmissionRequest = z.object({ + admission: zInnerAdmission.optional(), +}) + +export const zInnerReleaseAdmissionResponse = z.record(z.string(), z.unknown()) + export const zInnerTryAddAccountToDefaultWorkspaceReply = z.object({ workspaceId: z.string().optional(), joined: z.boolean().optional(), @@ -678,48 +313,32 @@ export const zJoinWorkspaceReq = z.object({ role: z.string().optional(), }) -export const zK8sEnvironmentConfig = z.object({ - namespace: z.string().optional(), - apiServer: z.string().optional(), - caBundle: z.string().optional(), - bearerToken: z.string().optional(), +export const zLimitConfig = z.object({ + type: z.int().optional(), + threshold: z.string().optional(), + action: z.int().optional(), + reached: z.boolean().optional(), }) -/** - * Field-level validation only; target (api_server) and RBAC validation happen - * in the bootstrap reconciler. - */ -export const zCreateEnvironmentReq = z.object({ - name: z.string().optional(), - description: z.string().optional(), - mode: z.int().optional(), - backend: z.int().optional(), - k8s: zK8sEnvironmentConfig.optional(), - host: zHostEnvironmentConfig.optional(), +export const zInnerGroupConfig = z.object({ + id: z.string().optional(), + enabled: z.boolean().optional(), + membershipId: z.string().optional(), + limits: z.array(zLimitConfig).optional(), }) -export const zLastError = z.object({ - phase: z.string().optional(), - code: z.string().optional(), - message: z.string().optional(), - releaseId: z.string().optional(), +export const zInnerResolveResponse = z.object({ + appId: z.string().optional(), + groups: z.array(zInnerGroupConfig).optional(), + blocked: z.boolean().optional(), + blockGroupId: z.string().optional(), + blockReason: z.string().optional(), + admission: zInnerAdmission.optional(), }) -export const zAckDeploymentReq = z.object({ - deploymentId: z.string().optional(), - instanceId: z.string().optional(), - expectedVersion: z.string().optional(), - status: z.string().optional(), - observedReleaseId: z.string().optional(), - lastError: zLastError.optional(), -}) - -export const zListDeploymentBindingOptionsReply = z.object({ - slots: z.array(zDeploymentBindingOptionSlot).optional(), -}) - -export const zListDeploymentEnvironmentOptionsReply = z.object({ - environments: z.array(zDeploymentEnvironmentOption).optional(), +export const zListGroupAppsResponse = z.object({ + items: z.array(zGroupAppItem).optional(), + total: z.string().optional(), }) export const zLoginTypesReply = z.object({ @@ -871,31 +490,6 @@ export const zPluginInstallationSettingsReply = z.object({ restrictToMarketplaceOnly: z.boolean().optional(), }) -export const zPreviewReleaseReq = z.object({ - appInstanceId: z.string().optional(), - releaseId: z.string().optional(), -}) - -export const zReleaseRow = z.object({ - id: z.string().optional(), - name: z.string().optional(), - createdAt: z.iso.datetime().optional(), - createdBy: zConsoleUser.optional(), - deployedTo: z.array(zDeployedEnvironment).optional(), -}) - -export const zReleaseRuntimeBinding = z.object({ - kind: z.string().optional(), - label: z.string().optional(), - displayValue: z.string().optional(), - valueType: z.string().optional(), -}) - -export const zPreviewReleaseReply = z.object({ - release: zConsoleRelease.optional(), - bindings: z.array(zReleaseRuntimeBinding).optional(), -}) - export const zResetMemberPasswordReply = z.object({ id: z.string().optional(), password: z.string().optional(), @@ -930,26 +524,56 @@ export const zResetUserPasswordReq = z.object({ id: z.string().optional(), }) -export const zResolveCredentialsReq = z.object({ - instanceId: z.string().optional(), - deploymentId: z.string().optional(), - slots: z.array(z.string()).optional(), +export const zResourceGroupDetail = z.object({ + id: z.string().optional(), + name: z.string().optional(), + description: z.string().optional(), + enabled: z.boolean().optional(), + rpm_limit: z + .int() + .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) + .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) + .optional(), + rpm_action: z.int().optional(), + concurrency_limit: z + .int() + .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) + .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) + .optional(), + concurrency_action: z.int().optional(), + token_quota: z.string().optional(), + token_action: z.int().optional(), + created_at: z.string().optional(), + updated_at: z.string().optional(), }) -/** - * Exactly one of credential_id / env_var_id is populated; model/plugin slots - * carry credential_id (pool A), env_var slots carry env_var_id (pool B). - * See design §4.1. - */ -export const zResolvedCredential = z.object({ - slot: z.string().optional(), - credentialId: z.string().optional(), - envVarId: z.string().optional(), - value: z.string().optional(), +export const zResourceGroupItem = z.object({ + id: z.string().optional(), + name: z.string().optional(), + description: z.string().optional(), + enabled: z.boolean().optional(), + rpm_limit: z + .int() + .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) + .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) + .optional(), + concurrency_limit: z + .int() + .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) + .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) + .optional(), + token_quota: z.string().optional(), + token_usage: z.string().optional(), + app_count: z.string().optional(), + rpm_status: z.int().optional(), + conc_status: z.int().optional(), + created_at: z.string().optional(), + updated_at: z.string().optional(), }) -export const zResolveCredentialsReply = z.object({ - resolved: z.array(zResolvedCredential).optional(), +export const zListResourceGroupsResponse = z.object({ + items: z.array(zResourceGroupItem).optional(), + total: z.string().optional(), }) /** @@ -1002,44 +626,6 @@ export const zGetLicenseReply = z.object({ license: zLicenseInfo.optional(), }) -export const zRetryEnvironmentReply = z.object({ - environment: zEnvironment.optional(), -}) - -export const zRetryEnvironmentReq = z.object({ - id: z.string().optional(), -}) - -export const zRuntimeEndpoints = z.object({ - run: z.string().optional(), - health: z.string().optional(), -}) - -export const zRuntimeInstanceDetail = z.object({ - deploymentName: z.string().optional(), - replicas: z - .int() - .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) - .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) - .optional(), - runtimeMode: z.string().optional(), - runtimeNote: z.string().optional(), - endpoints: zRuntimeEndpoints.optional(), - bindings: z.array(zReleaseRuntimeBinding).optional(), -}) - -export const zRuntimeInstanceRow = z.object({ - id: z.string().optional(), - environment: zConsoleEnvironment.optional(), - status: z.string().optional(), - currentRelease: zConsoleRelease.optional(), - detail: zRuntimeInstanceDetail.optional(), -}) - -export const zListRuntimeInstancesReply = z.object({ - data: z.array(zRuntimeInstanceRow).optional(), -}) - /** * SSO Configuration messages */ @@ -1102,8 +688,21 @@ export const zScimSettings = z.object({ lastSyncTime: z.iso.datetime().optional(), }) -export const zSearchAccessSubjectsReply = z.object({ - data: z.array(zAccessSubjectDisplay).optional(), +export const zSearchAppItem = z.object({ + app_id: z.string().optional(), + app_name: z.string().optional(), + workspace_id: z.string().optional(), + workspace_name: z.string().optional(), + app_status: z.int().optional(), + icon: z.string().optional(), + icon_type: z.string().optional(), + icon_background: z.string().optional(), + created_by_name: z.string().optional(), +}) + +export const zSearchAppsResponse = z.object({ + items: z.array(zSearchAppItem).optional(), + total: z.string().optional(), }) export const zSecretKey = z.object({ @@ -1122,25 +721,6 @@ export const zSetDefaultWorkspaceReq = z.object({ id: z.string().optional(), }) -export const zStatusCount = z.object({ - status: z.string().optional(), - count: z - .int() - .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) - .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) - .optional(), -}) - -export const zAppInstanceCard = z.object({ - id: z.string().optional(), - name: z.string().optional(), - icon: z.string().optional(), - mode: z.string().optional(), - sourceAppName: z.string().optional(), - statuses: z.array(zStatusCount).optional(), - lastDeployedAt: z.iso.datetime().optional(), -}) - export const zSubjectAccountData = z.object({ id: z.string().optional(), name: z.string().optional(), @@ -1214,38 +794,10 @@ export const zTestConnectionReply = z.object({ error: z.string().optional(), }) -export const zTestEnvironmentConnectionReply = z.object({ - ok: z.boolean().optional(), - reachableServerVersion: z.string().optional(), - namespaceExists: z.boolean().optional(), - missingPermissions: z.array(z.string()).optional(), - error: z.string().optional(), - probedAt: z.iso.datetime().optional(), -}) - -export const zTestEnvironmentConnectionReq = z.object({ - id: z.string().optional(), -}) - export const zToggleEndpointRequest = z.object({ enabled: z.boolean().optional(), }) -export const zUndeployRuntimeInstanceReply = z.object({ - deploymentId: z.string().optional(), - status: z.string().optional(), -}) - -export const zUndeployRuntimeInstanceReq = z.object({ - appInstanceId: z.string().optional(), - runtimeInstanceId: z.string().optional(), -}) - -export const zUpdateAccessChannelsReq = z.object({ - appInstanceId: z.string().optional(), - enabled: z.boolean().optional(), -}) - export const zUpdateAccessModeReq = z.object({ appId: z.string().optional(), accessMode: z.string().optional(), @@ -1255,16 +807,6 @@ export const zUpdateAccessModeRes = z.object({ message: z.string().optional(), }) -export const zUpdateAppInstanceReply = z.object({ - appInstanceId: z.string().optional(), -}) - -export const zUpdateAppInstanceReq = z.object({ - appInstanceId: z.string().optional(), - name: z.string().optional(), - description: z.string().optional(), -}) - export const zUpdateBrandingInfoReq = z.object({ enabled: z.boolean().optional(), applicationTitle: z.string().optional(), @@ -1273,36 +815,6 @@ export const zUpdateBrandingInfoReq = z.object({ favicon: z.string().optional(), }) -export const zUpdateDeveloperApiReply = z.object({ - developerApi: zDeveloperApiAccess.optional(), -}) - -export const zUpdateDeveloperApiReq = z.object({ - appInstanceId: z.string().optional(), - enabled: z.boolean().optional(), -}) - -export const zUpdateEnvironmentAccessPolicyReply = z.object({ - permission: zEnvironmentAccessRow.optional(), -}) - -export const zUpdateEnvironmentAccessPolicyReq = z.object({ - appInstanceId: z.string().optional(), - environmentId: z.string().optional(), - accessMode: z.string().optional(), - subjects: z.array(zAccessSubject).optional(), -}) - -export const zUpdateEnvironmentReply = z.object({ - environment: zEnvironment.optional(), -}) - -export const zUpdateEnvironmentReq = z.object({ - id: z.string().optional(), - name: z.string().optional(), - description: z.string().optional(), -}) - export const zUpdateGroupSubjectsReq = z.object({ groupId: z.string().optional(), subjects: z.array(zSubject).optional(), @@ -1386,6 +898,27 @@ export const zUpdatePluginInstallationSettingsRequest = z.object({ restrictToMarketplaceOnly: z.boolean().optional(), }) +export const zUpdateResourceGroupRequest = z.object({ + id: z.string().optional(), + name: z.string().optional(), + description: z.string().optional(), + enabled: z.boolean().optional(), + rpm_limit: z + .int() + .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) + .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) + .optional(), + rpm_action: z.int().optional(), + concurrency_limit: z + .int() + .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) + .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) + .optional(), + concurrency_action: z.int().optional(), + token_quota: z.string().optional(), + token_action: z.int().optional(), +}) + export const zUpdateUserReply = z.object({ account: zAccountDetail.optional(), }) @@ -1430,27 +963,6 @@ export const zUpdateWorkspaceReq = z.object({ status: z.string().optional(), }) -export const zWebAppAccessRow = z.object({ - environment: zConsoleEnvironment.optional(), - url: z.string().optional(), -}) - -export const zAccessChannels = z.object({ - enabled: z.boolean().optional(), - webappRows: z.array(zWebAppAccessRow).optional(), - cli: zCliAccess.optional(), -}) - -export const zGetAppInstanceAccessReply = z.object({ - permissions: z.array(zEnvironmentAccessRow).optional(), - accessChannels: zAccessChannels.optional(), - developerApi: zDeveloperApiAccess.optional(), -}) - -export const zUpdateAccessChannelsReply = z.object({ - accessChannels: zAccessChannels.optional(), -}) - export const zWebAppAuthInfo = z.object({ allowSso: z.boolean().optional(), allowEmailCodeLogin: z.boolean().optional(), @@ -1572,27 +1084,11 @@ export const zPagination = z.object({ .optional(), }) -export const zListAppInstancesReply = z.object({ - filters: z.array(zEnvironmentFilter).optional(), - data: z.array(zAppInstanceCard).optional(), - pagination: zPagination.optional(), -}) - -export const zListEnvironmentsReply = z.object({ - data: z.array(zEnvironment).optional(), - pagination: zPagination.optional(), -}) - export const zListMembersReply = z.object({ data: z.array(zAccountDetail).optional(), pagination: zPagination.optional(), }) -export const zListReleasesReply = z.object({ - data: z.array(zReleaseRow).optional(), - pagination: zPagination.optional(), -}) - export const zListSecretKeysReply = z.object({ data: z.array(zSecretKey).optional(), pagination: zPagination.optional(), @@ -1608,271 +1104,6 @@ export const zListWorkspacesReply = z.object({ pagination: zPagination.optional(), }) -export const zEnterpriseAppDeployConsoleListAppInstancesQuery = z.object({ - environmentId: z.string().optional(), - notDeployed: z.boolean().optional(), - query: z.string().optional(), - pageNumber: z - .int() - .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) - .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) - .optional(), - resultsPerPage: z - .int() - .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) - .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) - .optional(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleListAppInstancesResponse = zListAppInstancesReply - -export const zEnterpriseAppDeployConsoleCreateAppInstanceBody = zCreateAppInstanceReq - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleCreateAppInstanceResponse = zCreateAppInstanceReply - -export const zEnterpriseAppDeployConsoleDeleteAppInstancePath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleDeleteAppInstanceResponse = zDeleteAppInstanceReply - -export const zEnterpriseAppDeployConsoleUpdateAppInstanceBody = zUpdateAppInstanceReq - -export const zEnterpriseAppDeployConsoleUpdateAppInstancePath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleUpdateAppInstanceResponse = zUpdateAppInstanceReply - -export const zEnterpriseAppDeployConsoleGetAppInstanceAccessPath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleGetAppInstanceAccessResponse = zGetAppInstanceAccessReply - -export const zEnterpriseAppDeployConsoleUpdateAccessChannelsBody = zUpdateAccessChannelsReq - -export const zEnterpriseAppDeployConsoleUpdateAccessChannelsPath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleUpdateAccessChannelsResponse = zUpdateAccessChannelsReply - -export const zEnterpriseAppDeployConsoleSearchAccessSubjectsPath = z.object({ - appInstanceId: z.string(), -}) - -export const zEnterpriseAppDeployConsoleSearchAccessSubjectsQuery = z.object({ - keyword: z.string().optional(), - subjectTypes: z.array(z.string()).optional(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleSearchAccessSubjectsResponse = zSearchAccessSubjectsReply - -export const zEnterpriseAppDeployConsoleCreateDeveloperApiKeyBody = zCreateDeveloperApiKeyReq - -export const zEnterpriseAppDeployConsoleCreateDeveloperApiKeyPath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleCreateDeveloperApiKeyResponse = zCreateDeveloperApiKeyReply - -export const zEnterpriseAppDeployConsoleDeleteDeveloperApiKeyPath = z.object({ - appInstanceId: z.string(), - apiKeyId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponse = zDeleteDeveloperApiKeyReply - -export const zEnterpriseAppDeployConsoleListDeploymentBindingOptionsPath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleListDeploymentBindingOptionsResponse - = zListDeploymentBindingOptionsReply - -export const zEnterpriseAppDeployConsoleCreateDeploymentBody = zCreateDeploymentReq - -export const zEnterpriseAppDeployConsoleCreateDeploymentPath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleCreateDeploymentResponse = zCreateDeploymentReply - -export const zEnterpriseAppDeployConsoleUpdateDeveloperApiBody = zUpdateDeveloperApiReq - -export const zEnterpriseAppDeployConsoleUpdateDeveloperApiPath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleUpdateDeveloperApiResponse = zUpdateDeveloperApiReply - -export const zEnterpriseAppDeployConsoleGetEnvironmentAccessPolicyPath = z.object({ - appInstanceId: z.string(), - environmentId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleGetEnvironmentAccessPolicyResponse - = zGetEnvironmentAccessPolicyReply - -export const zEnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyBody - = zUpdateEnvironmentAccessPolicyReq - -export const zEnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyPath = z.object({ - appInstanceId: z.string(), - environmentId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleUpdateEnvironmentAccessPolicyResponse - = zUpdateEnvironmentAccessPolicyReply - -export const zEnterpriseAppDeployConsoleGetAppInstanceOverviewPath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleGetAppInstanceOverviewResponse - = zGetAppInstanceOverviewReply - -export const zEnterpriseAppDeployConsoleListReleasesPath = z.object({ - appInstanceId: z.string(), -}) - -export const zEnterpriseAppDeployConsoleListReleasesQuery = z.object({ - pageNumber: z - .int() - .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) - .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) - .optional(), - resultsPerPage: z - .int() - .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) - .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) - .optional(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleListReleasesResponse = zListReleasesReply - -export const zEnterpriseAppDeployConsoleCreateReleaseBody = zCreateReleaseReq - -export const zEnterpriseAppDeployConsoleCreateReleasePath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleCreateReleaseResponse = zCreateReleaseReply - -export const zEnterpriseAppDeployConsolePreviewReleaseBody = zPreviewReleaseReq - -export const zEnterpriseAppDeployConsolePreviewReleasePath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsolePreviewReleaseResponse = zPreviewReleaseReply - -export const zEnterpriseAppDeployConsoleListRuntimeInstancesPath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleListRuntimeInstancesResponse = zListRuntimeInstancesReply - -export const zEnterpriseAppDeployConsoleCancelRuntimeDeploymentBody = zCancelRuntimeDeploymentReq - -export const zEnterpriseAppDeployConsoleCancelRuntimeDeploymentPath = z.object({ - appInstanceId: z.string(), - runtimeInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleCancelRuntimeDeploymentResponse - = zCancelRuntimeDeploymentReply - -export const zEnterpriseAppDeployConsoleUndeployRuntimeInstanceBody = zUndeployRuntimeInstanceReq - -export const zEnterpriseAppDeployConsoleUndeployRuntimeInstancePath = z.object({ - appInstanceId: z.string(), - runtimeInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleUndeployRuntimeInstanceResponse - = zUndeployRuntimeInstanceReply - -export const zEnterpriseAppDeployConsoleGetAppInstanceSettingsPath = z.object({ - appInstanceId: z.string(), -}) - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleGetAppInstanceSettingsResponse - = zGetAppInstanceSettingsReply - -/** - * OK - */ -export const zEnterpriseAppDeployConsoleListDeploymentEnvironmentOptionsResponse - = zListDeploymentEnvironmentOptionsReply - /** * OK */ diff --git a/web/AGENTS.md b/web/AGENTS.md index 2f7e0f6cda..a2ca3857e7 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -9,10 +9,6 @@ - In new or modified code, use only overlay primitives from `@langgenius/dify-ui/*`. - Do not introduce overlay imports from `@/app/components/base/*`; when touching existing callers, migrate them. -## Query & Mutation (Mandatory) - -- `frontend-query-mutation` is the source of truth for Dify frontend contracts, query and mutation call-site patterns, conditional queries, invalidation, and mutation error handling. - ## Design Token Mapping - When translating Figma designs to code, read `../packages/dify-ui/AGENTS.md` for the Figma `--radius/*` token to Tailwind `rounded-*` class mapping. The two scales are offset by one step. diff --git a/web/contract/router.ts b/web/contract/router.ts index 953678a910..37438d028d 100644 --- a/web/contract/router.ts +++ b/web/contract/router.ts @@ -63,6 +63,9 @@ export const marketplaceRouterContract = { export type MarketPlaceInputs = InferContractRouterInputs export const consoleRouterContract = { + // `enterprise` is the only backend-generated contract wired in here. Community API contracts + // are generated too, but backend definitions are not complete enough to consume directly yet, + // so those routes stay manually maintained for now. enterprise: enterpriseContract, account: { avatar: accountAvatarContract, diff --git a/web/docs/test.md b/web/docs/test.md index b7c6a5f5a3..402a24d30f 100644 --- a/web/docs/test.md +++ b/web/docs/test.md @@ -4,16 +4,10 @@ This document is the complete testing specification for the Dify frontend projec Goal: Readable, change-friendly, reusable, and debuggable tests. When I ask you to write/refactor/fix tests, follow these rules by default. -## Tech Stack - -- **Framework**: Next.js 15 + React 19 + TypeScript -- **Testing Tools**: Vitest 4.0.16 + React Testing Library 16.0 -- **Test Environment**: happy-dom -- **File Naming**: `ComponentName.spec.tsx` inside a same-level `__tests__/` directory -- **Placement Rule**: Component, hook, and utility tests must live in a sibling `__tests__/` folder at the same level as the source under test. For example, `foo/index.tsx` maps to `foo/__tests__/index.spec.tsx`, and `foo/bar.ts` maps to `foo/__tests__/bar.spec.ts`. - ## Running Tests +Run these commands from `web/`. From the repository root, prefix them with `pnpm -C web`. + ```bash # Run all tests pnpm test @@ -31,6 +25,8 @@ pnpm test path/to/file.spec.tsx ## Project Test Setup - **Configuration**: `vite.config.ts` sets the `happy-dom` environment, loads the Testing Library presets, and respects our path aliases (`@/...`). Check this file before adding new transformers or module name mappers. +- **File naming**: `ComponentName.spec.tsx` inside a same-level `__tests__/` directory. +- **Placement rule**: Component, hook, and utility tests must live in a sibling `__tests__/` folder at the same level as the source under test. For example, `foo/index.tsx` maps to `foo/__tests__/index.spec.tsx`, and `foo/bar.ts` maps to `foo/__tests__/bar.spec.ts`. - **Global setup**: `vitest.setup.ts` already imports `@testing-library/jest-dom`, runs `cleanup()` after every test, and defines shared mocks (for example `react-i18next`). Add any environment-level mocks (for example `ResizeObserver`, `matchMedia`, `IntersectionObserver`, `TextEncoder`, `crypto`) here so they are shared consistently. - **Reusable mocks**: Place shared mock factories inside `web/__mocks__/` and use `vi.mock('module-name')` to point to them rather than redefining mocks in every spec. - **Mocking behavior**: Modules are not mocked automatically. Use `vi.mock(...)` in tests, or place global mocks in `vitest.setup.ts`. @@ -216,8 +212,8 @@ Simulate the interactions that matter to users—primary clicks, change events, **Guidelines**: -- Prefer spying on `global.fetch`/`axios`/`ky` and returning deterministic responses over reaching out to the network. -- Use MSW (`msw` is already installed) when you need declarative request handlers across multiple specs. +- Prefer mocking `@/service/*` modules or spying on `global.fetch` / `ky` clients with deterministic responses over reaching out to the network. +- Do not introduce an HTTP interception dependency such as MSW unless it is already declared in the workspace or adding it is part of the task. - Keep async assertions inside `await waitFor(...)` blocks or the async `findBy*` queries to avoid race conditions. ### 7. Next.js Routing @@ -281,7 +277,7 @@ For complex inputs/entities, use Builders with solid defaults and chainable over Reserve snapshots for static, deterministic fragments (icons, badges, layout chrome). Keep them tight, prefer explicit assertions for behavior, and review any snapshot updates deliberately instead of accepting them wholesale. -**Note**: Dify is a desktop application. **No need for** responsive/mobile testing. +**Note**: Dify primarily targets desktop workflows, but the supported browsers list includes mobile browsers. Do not add responsive/mobile assertions to ordinary unit tests unless the component has responsive behavior, mobile-specific behavior, or accessibility behavior that must be covered. ## Code Style From e134c1e0d590ee6da69ec7ac1b3d34cdf2988ea3 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 11 May 2026 11:53:03 +0800 Subject: [PATCH 10/22] feat(web): improve a11y and remove data-testid (#35999) --- .agents/skills/frontend-testing/SKILL.md | 9 +- .../step-definitions/apps/share-app.steps.ts | 2 +- .../base/notion-page-selector-flow.test.tsx | 4 +- .../text-generation-run-once-flow.test.tsx | 10 +- .../(commonLayout)/datasets/layout.spec.tsx | 12 +- .../(commonLayout)/role-route-guard.spec.tsx | 16 +- .../app/annotation/__tests__/filter.spec.tsx | 5 +- .../app/annotation/batch-action.tsx | 20 ++- .../__tests__/csv-uploader.spec.tsx | 4 +- .../__tests__/index.spec.tsx | 8 + .../csv-uploader.tsx | 19 ++- .../batch-add-annotation-modal/index.tsx | 11 +- .../__tests__/access-control.spec.tsx | 8 +- .../specific-groups-or-members.spec.tsx | 6 +- .../add-member-or-group-pop.tsx | 30 +++- .../specific-groups-or-members.tsx | 13 +- .../__tests__/version-info-modal.spec.tsx | 16 ++ .../app/app-publisher/version-info-modal.tsx | 11 +- .../config-var/__tests__/index.spec.tsx | 22 +-- .../config-var/__tests__/var-item.spec.tsx | 2 +- .../config-select/__tests__/index.spec.tsx | 9 +- .../config-var/config-select/index.tsx | 11 +- .../app/configuration/config-var/var-item.tsx | 23 ++- .../agent-tools/__tests__/index.spec.tsx | 2 +- .../config/agent/agent-tools/index.tsx | 24 +-- .../__tests__/instruction-editor.spec.tsx | 2 +- .../config/automatic/instruction-editor.tsx | 8 +- .../app/configuration/configuration-view.tsx | 1 - .../ctrl-btn-group/__tests__/index.spec.tsx | 8 +- .../configuration/ctrl-btn-group/index.tsx | 4 +- .../settings-modal/__tests__/index.spec.tsx | 2 +- .../dataset-config/settings-modal/index.tsx | 8 +- .../__tests__/index.spec.tsx | 2 +- .../prompt-value-panel/index.tsx | 12 +- .../__tests__/index.spec.tsx | 26 +-- .../create-app-dialog/app-list/sidebar.tsx | 39 +++-- .../components/app/create-app-modal/index.tsx | 14 +- .../__tests__/uploader.spec.tsx | 2 +- .../app/create-from-dsl-modal/uploader.tsx | 8 +- .../duplicate-modal/__tests__/index.spec.tsx | 21 +++ .../components/app/duplicate-modal/index.tsx | 11 +- .../log-annotation/__tests__/index.spec.tsx | 16 +- .../app/log/__tests__/empty-element.spec.tsx | 5 +- .../app/log/__tests__/filter.spec.tsx | 2 +- .../app/overview/app-card-sections.tsx | 70 ++++---- .../customize/__tests__/index.spec.tsx | 2 +- .../app/overview/customize/index.tsx | 2 +- .../switch-app-modal/__tests__/index.spec.tsx | 9 + .../components/app/switch-app-modal/index.tsx | 19 ++- .../workflow-log/__tests__/detail.spec.tsx | 13 +- .../workflow-log/__tests__/filter.spec.tsx | 12 +- .../components/app/workflow-log/detail.tsx | 15 +- web/app/components/app/workflow-log/list.tsx | 9 +- .../components/base/__tests__/alert.spec.tsx | 5 +- .../agent-log-modal/__tests__/detail.spec.tsx | 6 +- .../agent-log-modal/__tests__/index.spec.tsx | 2 +- .../base/agent-log-modal/detail.tsx | 18 +- .../components/base/agent-log-modal/index.tsx | 11 +- web/app/components/base/alert.tsx | 13 +- .../app-icon-picker/__tests__/index.spec.tsx | 4 +- web/app/components/base/audio-btn/index.tsx | 3 +- .../base/audio-gallery/AudioPlayer.tsx | 10 +- .../__tests__/AudioPlayer.spec.tsx | 41 +++-- .../__tests__/chat-wrapper.spec.tsx | 4 +- .../__tests__/header-in-mobile.spec.tsx | 8 +- .../mobile-operation-dropdown.spec.tsx | 12 +- .../header/mobile-operation-dropdown.tsx | 17 +- .../sidebar/__tests__/item.spec.tsx | 36 ++-- .../chat/__tests__/chat-log-modals.spec.tsx | 2 +- .../chat/chat/__tests__/question.spec.tsx | 68 ++++---- .../chat/answer/__tests__/operation.spec.tsx | 16 +- .../__tests__/human-input-form.spec.tsx | 2 +- .../human-input-content/human-input-form.tsx | 1 - .../base/chat/chat/answer/operation.tsx | 3 +- .../chat-input-area/__tests__/index.spec.tsx | 30 ++-- .../__tests__/operation.spec.tsx | 9 +- .../chat/chat/chat-input-area/operation.tsx | 11 +- .../chat/citation/__tests__/popup.spec.tsx | 32 ++-- .../base/chat/chat/citation/popup.tsx | 3 +- .../components/base/chat/chat/question.tsx | 14 +- .../header/__tests__/index.spec.tsx | 20 +-- .../chat/embedded-chatbot/header/index.tsx | 36 ++-- .../inputs-form/__tests__/index.spec.tsx | 14 +- .../embedded-chatbot/inputs-form/index.tsx | 3 - .../copy-feedback/__tests__/index.spec.tsx | 22 ++- .../components/base/copy-feedback/index.tsx | 18 +- .../base/copy-icon/__tests__/index.spec.tsx | 13 +- web/app/components/base/copy-icon/index.tsx | 4 +- .../date-picker/__tests__/index.spec.tsx | 5 +- .../date-picker/index.tsx | 9 +- .../time-picker/index.tsx | 9 +- .../components/base/emoji-picker/Inner.tsx | 60 +++++-- .../emoji-picker/__tests__/Inner.spec.tsx | 24 +-- .../emoji-picker/__tests__/index.spec.tsx | 4 +- .../__tests__/modal.spec.tsx | 10 +- .../conversation-opener/modal.tsx | 19 +-- .../__tests__/setting-content.spec.tsx | 10 +- .../file-upload/setting-content.tsx | 19 +-- .../moderation-setting-modal.spec.tsx | 16 +- .../moderation/moderation-setting-modal.tsx | 18 +- .../__tests__/param-config-content.spec.tsx | 4 +- .../text-to-speech/param-config-content.tsx | 8 +- .../__tests__/audio-preview.spec.tsx | 6 +- .../__tests__/video-preview.spec.tsx | 10 +- .../base/file-uploader/audio-preview.tsx | 13 +- .../base/file-uploader/file-list-in-log.tsx | 19 ++- .../__tests__/file-item.spec.tsx | 4 +- .../__tests__/file-image-item.spec.tsx | 93 ++-------- .../__tests__/file-item.spec.tsx | 22 +-- .../file-image-item.tsx | 35 ++-- .../file-uploader-in-chat-input/file-item.tsx | 19 ++- .../base/file-uploader/video-preview.tsx | 13 +- .../__tests__/index.spec.tsx | 2 +- .../base/float-right-container/index.tsx | 1 - .../field/__tests__/checkbox.spec.tsx | 2 +- .../base/form/components/field/checkbox.tsx | 1 + .../__tests__/audio-preview.spec.tsx | 5 +- .../__tests__/image-list.spec.tsx | 11 +- .../__tests__/image-preview.spec.tsx | 12 +- .../__tests__/video-preview.spec.tsx | 2 +- .../base/image-uploader/audio-preview.tsx | 14 +- .../base/image-uploader/image-list.tsx | 18 +- .../base/image-uploader/image-preview.tsx | 14 +- .../base/image-uploader/video-preview.tsx | 13 +- .../input-with-copy/__tests__/index.spec.tsx | 17 +- .../components/base/input-with-copy/index.tsx | 7 +- .../base/input/__tests__/index.spec.tsx | 2 +- web/app/components/base/input/index.tsx | 11 +- .../markdown-blocks/__tests__/form.spec.tsx | 4 +- .../base/mermaid/__tests__/index.spec.tsx | 4 +- .../__tests__/index.spec.tsx | 2 +- .../base/message-log-modal/index.tsx | 11 +- .../base/new-audio-button/index.tsx | 3 +- .../__tests__/base.spec.tsx | 6 +- .../__tests__/index.spec.tsx | 4 +- .../credential-selector/index.tsx | 2 +- .../search-input/__tests__/index.spec.tsx | 8 +- .../search-input/index.tsx | 19 ++- .../components/base/prompt-editor/hooks.ts | 6 +- .../__tests__/component.spec.tsx | 2 +- .../plugins/context-block/component.tsx | 23 ++- .../__tests__/component-ui.spec.tsx | 73 ++------ .../__tests__/type-switch.spec.tsx | 4 +- .../plugins/hitl-input-block/component-ui.tsx | 6 +- .../plugins/hitl-input-block/input-field.tsx | 4 +- .../plugins/hitl-input-block/type-switch.tsx | 10 +- .../prompt-log-modal/__tests__/index.spec.tsx | 4 +- .../base/prompt-log-modal/index.tsx | 13 +- .../base/qrcode/__tests__/index.spec.tsx | 22 +-- web/app/components/base/qrcode/index.tsx | 45 ++--- .../base/radio-card/__tests__/index.spec.tsx | 4 +- web/app/components/base/radio-card/index.tsx | 18 +- .../base/sort/__tests__/index.spec.tsx | 11 +- web/app/components/base/sort/index.tsx | 13 +- .../base/svg-gallery/__tests__/index.spec.tsx | 2 +- .../base/tag-input/__tests__/index.spec.tsx | 6 +- web/app/components/base/tag-input/index.tsx | 11 +- .../base/video-gallery/VideoPlayer.tsx | 21 ++- .../__tests__/VideoPlayer.spec.tsx | 21 ++- .../billing/annotation-full/modal.tsx | 2 +- .../custom-page/__tests__/index.spec.tsx | 2 +- .../components/custom/custom-page/index.tsx | 8 +- .../__tests__/status-with-action.spec.tsx | 14 +- .../status-with-action.tsx | 11 +- .../image-list/__tests__/index.spec.tsx | 2 +- .../common/image-list/__tests__/more.spec.tsx | 6 +- .../datasets/common/image-list/more.tsx | 14 +- .../__tests__/index.spec.tsx | 6 +- .../__tests__/uploader.spec.tsx | 4 +- .../create-from-dsl-modal/uploader.tsx | 8 +- .../__tests__/index.spec.tsx | 10 +- .../empty-dataset-creation-modal/index.tsx | 7 +- .../file-preview/__tests__/index.spec.tsx | 18 +- .../datasets/create/file-preview/index.tsx | 11 +- .../__tests__/index.spec.tsx | 13 +- .../create/notion-page-preview/index.tsx | 11 +- .../__tests__/index.spec.tsx | 64 +++---- .../create/stop-embedding-modal/index.tsx | 7 +- .../create/website/__tests__/preview.spec.tsx | 14 +- .../jina-reader/__tests__/base.spec.tsx | 12 +- .../website/jina-reader/base/url-input.tsx | 2 +- .../datasets/create/website/preview.tsx | 11 +- .../documents/components/operations.tsx | 46 ++--- .../file-list/__tests__/index.spec.tsx | 4 +- .../file-list/header/__tests__/index.spec.tsx | 10 +- .../documents/detail/__tests__/index.spec.tsx | 6 +- .../detail/__tests__/new-segment.spec.tsx | 15 +- .../__tests__/csv-uploader.spec.tsx | 25 ++- .../detail/batch-modal/csv-uploader.tsx | 19 ++- .../__tests__/child-segment-detail.spec.tsx | 12 +- .../__tests__/new-child-segment.spec.tsx | 12 +- .../__tests__/segment-detail.spec.tsx | 12 +- .../detail/completed/child-segment-detail.tsx | 24 ++- .../components/__tests__/menu-bar.spec.tsx | 2 +- .../detail/completed/new-child-segment.tsx | 22 ++- .../segment-card/__tests__/index.spec.tsx | 14 +- .../detail/completed/segment-card/index.tsx | 16 +- .../detail/completed/segment-detail.tsx | 24 ++- .../datasets/documents/detail/index.tsx | 3 +- .../__tests__/doc-type-selector.spec.tsx | 2 +- .../metadata/components/doc-type-selector.tsx | 8 +- .../datasets/documents/detail/new-segment.tsx | 22 ++- .../segment-add/__tests__/index.spec.tsx | 4 +- .../documents/detail/segment-add/index.tsx | 20 ++- .../datasets/documents/style.module.css | 2 +- .../__tests__/modify-retrieval-modal.spec.tsx | 14 +- .../__tests__/chunk-detail-modal.spec.tsx | 2 +- .../__tests__/result-item-footer.spec.tsx | 3 +- .../components/chunk-detail-modal.tsx | 1 - .../components/result-item-external.tsx | 2 +- .../components/result-item-footer.tsx | 9 +- .../hit-testing/modify-retrieval-modal.tsx | 11 +- .../base/__tests__/date-picker.spec.tsx | 71 ++++---- .../datasets/metadata/base/date-picker.tsx | 62 ++++--- .../__tests__/edited-beacon.spec.tsx | 14 +- .../edit-metadata-batch/edited-beacon.tsx | 11 +- .../metadata/edit-metadata-batch/modal.tsx | 2 +- .../__tests__/create-content.spec.tsx | 4 +- .../dataset-metadata-drawer.spec.tsx | 83 ++------- .../__tests__/select-metadata.spec.tsx | 20 +-- .../metadata-dataset/create-content.tsx | 8 +- .../dataset-metadata-drawer.tsx | 24 ++- .../metadata-dataset/select-metadata.tsx | 29 ++-- .../__tests__/info-group.spec.tsx | 34 ++-- .../metadata/metadata-document/info-group.tsx | 11 +- .../rename-modal/__tests__/index.spec.tsx | 12 +- .../datasets/rename-modal/index.tsx | 11 +- .../__tests__/index.spec.tsx | 2 +- .../develop/__tests__/ApiServer.spec.tsx | 16 +- .../secret-key/__tests__/input-copy.spec.tsx | 9 +- .../develop/secret-key/input-copy.tsx | 15 +- .../explore/app-list/__tests__/index.spec.tsx | 2 +- .../try-app/app/__tests__/chat.spec.tsx | 6 +- .../components/explore/try-app/app/chat.tsx | 8 +- .../__tests__/maintenance-notice.spec.tsx | 6 +- .../account-about/__tests__/index.spec.tsx | 8 +- .../components/header/account-about/index.tsx | 11 +- .../collapse/__tests__/index.spec.tsx | 8 +- .../header/account-setting/collapse/index.tsx | 21 ++- .../install-from-marketplace.tsx | 11 +- .../members-page/__tests__/index.spec.tsx | 2 +- .../__tests__/index.spec.tsx | 18 +- .../edit-workspace-modal/index.tsx | 6 +- .../account-setting/members-page/index.tsx | 12 +- .../invite-modal/__tests__/index.spec.tsx | 4 +- .../__tests__/role-selector.spec.tsx | 39 +++-- .../members-page/invite-modal/index.tsx | 10 +- .../invite-modal/role-selector.tsx | 44 ++--- .../__tests__/invitation-link.spec.tsx | 6 +- .../invited-modal/invitation-link.tsx | 23 ++- .../__tests__/index.spec.tsx | 18 +- .../transfer-ownership-modal/index.tsx | 21 ++- .../__tests__/index.spec.tsx | 6 +- .../provider-added-card/index.tsx | 5 +- .../components/header/maintenance-notice.tsx | 21 ++- .../install-plugin/install-bundle/index.tsx | 2 +- .../install-from-github/index.tsx | 2 +- .../install-from-local-package/index.tsx | 2 +- .../install-from-marketplace/index.tsx | 2 +- .../__tests__/add-oauth-button.spec.tsx | 6 +- .../__tests__/api-key-modal.spec.tsx | 2 +- .../__tests__/authorize-components.spec.tsx | 17 +- .../__tests__/oauth-client-settings.spec.tsx | 4 +- .../authorize/add-oauth-button.tsx | 43 +++-- .../plugin-auth/authorize/api-key-modal.tsx | 1 - .../authorize/oauth-client-settings.tsx | 2 - .../create/__tests__/common-modal.spec.tsx | 2 +- .../subscription-list/create/common-modal.tsx | 1 - .../subscription-list/create/oauth-client.tsx | 2 +- .../edit/__tests__/index.spec.tsx | 161 +++++++++--------- .../edit/apikey-edit-modal.tsx | 5 +- .../edit/manual-edit-modal.tsx | 4 +- .../edit/oauth-edit-modal.tsx | 4 +- .../plugins/plugin-mutation-model/index.tsx | 2 +- .../plugin-page/__tests__/index.spec.tsx | 4 +- .../components/plugins/plugin-page/index.tsx | 4 +- .../plugins/plugin-page/plugin-info.tsx | 2 +- .../__tests__/index.spec.tsx | 19 +-- .../__tests__/plugins-picker.spec.tsx | 2 +- .../auto-update-setting/index.tsx | 15 +- .../auto-update-setting/plugins-picker.tsx | 8 +- .../plugins/reference-setting-modal/index.tsx | 2 +- .../components/__tests__/index.spec.tsx | 2 +- ...blish-as-knowledge-pipeline-modal.spec.tsx | 2 +- .../editor/__tests__/index.spec.tsx | 4 +- .../form/__tests__/show-all-settings.spec.tsx | 2 +- .../editor/form/show-all-settings.tsx | 10 +- .../panel/input-field/editor/index.tsx | 6 +- .../field-list/__tests__/index.spec.tsx | 10 +- .../panel/input-field/field-list/index.tsx | 6 +- .../publish-as-knowledge-pipeline-modal.tsx | 11 +- .../components/update-dsl-modal.tsx | 11 +- .../text-generation-result-panel.spec.tsx | 4 +- .../share/text-generation/info-modal.tsx | 2 +- .../run-once/__tests__/index.spec.tsx | 6 +- .../share/text-generation/run-once/index.tsx | 1 - .../text-generation-result-panel.tsx | 8 +- .../signin/__tests__/countdown.spec.tsx | 16 +- web/app/components/signin/countdown.tsx | 10 +- .../tools/__tests__/provider-list.spec.tsx | 2 +- web/app/components/tools/labels/filter.tsx | 9 +- .../mcp/__tests__/mcp-server-modal.spec.tsx | 10 +- .../tools/mcp/__tests__/modal.spec.tsx | 12 +- .../components/tools/mcp/mcp-server-modal.tsx | 11 +- web/app/components/tools/mcp/modal.tsx | 11 +- .../__tests__/configure-button.spec.tsx | 2 +- .../confirm-modal/__tests__/index.spec.tsx | 12 +- .../workflow-tool/confirm-modal/index.tsx | 11 +- .../components/tools/workflow-tool/index.tsx | 1 - .../tools/workflow-tool/method-selector.tsx | 20 ++- .../__tests__/selection-contextmenu.spec.tsx | 8 +- .../__tests__/update-dsl-modal.spec.tsx | 9 + .../__tests__/index-bar.spec.tsx | 2 +- .../workflow/block-selector/index-bar.tsx | 9 +- .../_base/components/before-run-form/form.tsx | 13 +- .../var-reference-picker.branches.spec.tsx | 2 +- .../__tests__/var-reference-picker.spec.tsx | 2 +- .../var-reference-picker.trigger.spec.tsx | 4 +- .../variable/var-reference-picker.trigger.tsx | 24 ++- .../nodes/code/__tests__/panel.spec.tsx | 12 +- .../components/workflow/nodes/code/panel.tsx | 28 ++- .../nodes/end/__tests__/panel.spec.tsx | 4 +- .../components/workflow/nodes/end/panel.tsx | 11 +- .../human-input/__tests__/panel.spec.tsx | 7 +- .../__tests__/single-run-form.spec.tsx | 102 +++++++++++ .../__tests__/test-email-sender.spec.tsx | 2 +- .../delivery-method/test-email-sender.tsx | 19 ++- .../components/single-run-form.tsx | 10 +- .../workflow/nodes/human-input/panel.tsx | 21 ++- .../__tests__/integration.spec.tsx | 4 +- .../components/add-dataset.tsx | 6 +- .../components/dataset-item.tsx | 2 - .../condition-list/condition-date.tsx | 67 ++++---- .../json-importer.tsx | 11 +- .../generated-result.tsx | 11 +- .../json-schema-generator/prompt-editor.tsx | 11 +- .../components/workflow/nodes/llm/panel.tsx | 11 +- .../__tests__/integration.spec.tsx | 6 +- .../__tests__/update.spec.tsx | 4 +- .../components/extract-parameter/update.tsx | 11 +- .../components/__tests__/class-list.spec.tsx | 4 +- .../components/class-list.tsx | 11 +- .../nodes/start/__tests__/panel.spec.tsx | 2 +- .../components/__tests__/var-item.spec.tsx | 38 +++++ .../nodes/start/components/var-item.tsx | 22 ++- .../components/workflow/nodes/start/panel.tsx | 11 +- .../__tests__/integration.spec.tsx | 4 +- .../__tests__/panel.spec.tsx | 4 +- .../nodes/template-transform/panel.tsx | 11 +- .../panel/__tests__/workflow-preview.spec.tsx | 4 +- .../conversation-variable-modal.spec.tsx | 3 +- .../conversation-variable-modal.tsx | 22 ++- .../workflow/panel/workflow-preview.tsx | 11 +- .../run/__tests__/loop-result-panel.spec.tsx | 9 +- .../run/__tests__/result-text.spec.tsx | 2 +- .../workflow/run/loop-result-panel.tsx | 38 +++-- .../components/workflow/run/result-text.tsx | 8 +- .../workflow/selection-contextmenu.tsx | 3 - .../components/workflow/update-dsl-modal.tsx | 11 +- .../variable-inspect/__tests__/group.spec.tsx | 10 +- .../workflow/variable-inspect/group.tsx | 53 +++--- .../education-apply/expire-notice-modal.tsx | 2 +- web/docs/test.md | 16 +- .../__tests__/dataset-card-tags.spec.tsx | 22 +-- .../__tests__/tag-filter.spec.tsx | 15 +- .../__tests__/tag-item-editor.spec.tsx | 42 +++-- .../__tests__/tag-management-modal.spec.tsx | 25 ++- .../__tests__/tag-panel.spec.tsx | 9 +- .../__tests__/tag-selector.spec.tsx | 2 +- .../__tests__/tag-trigger.spec.tsx | 5 +- .../__tests__/app-card-tags.spec.tsx | 15 +- .../tag-management/components/tag-filter.tsx | 9 +- .../components/tag-item-editor.tsx | 23 ++- .../components/tag-management-modal.tsx | 4 +- .../tag-management/components/tag-panel.tsx | 2 - .../tag-management/components/tag-trigger.tsx | 7 +- web/i18n/en-US/common.json | 7 + 377 files changed, 2806 insertions(+), 2135 deletions(-) create mode 100644 web/app/components/workflow/nodes/human-input/components/__tests__/single-run-form.spec.tsx create mode 100644 web/app/components/workflow/nodes/start/components/__tests__/var-item.spec.tsx diff --git a/.agents/skills/frontend-testing/SKILL.md b/.agents/skills/frontend-testing/SKILL.md index 86675dfeba..21c46d75bc 100644 --- a/.agents/skills/frontend-testing/SKILL.md +++ b/.agents/skills/frontend-testing/SKILL.md @@ -38,13 +38,13 @@ Run these commands from `web/`. From the repository root, prefix them with `pnpm pnpm test # Watch mode -pnpm test:watch +pnpm test --watch # Run specific file pnpm test path/to/file.spec.tsx # Generate coverage report -pnpm test:coverage +pnpm test --coverage # Analyze component complexity pnpm analyze-component @@ -220,7 +220,10 @@ Every test should clearly separate: ### 2. Black-Box Testing - Test observable behavior, not implementation details -- Use semantic queries (getByRole, getByLabelText) +- Use semantic queries (`getByRole` with accessible `name`, `getByLabelText`, `getByPlaceholderText`, `getByText`, and scoped `within(...)`) +- Treat `getByTestId` as a last resort. If a control cannot be found by role/name, label, landmark, or dialog scope, fix the component accessibility first instead of adding or relying on `data-testid`. +- Remove production `data-testid` attributes when semantic selectors can cover the behavior. Keep them only for non-visual mocked boundaries, editor/browser shims such as Monaco, canvas/chart output, or third-party widgets with no accessible DOM in the test environment. +- Do not assert decorative icons by test id. Assert the named control that contains them, or mark decorative icons `aria-hidden`. - Avoid testing internal state directly - **Prefer pattern matching over hardcoded strings** in assertions: diff --git a/e2e/features/step-definitions/apps/share-app.steps.ts b/e2e/features/step-definitions/apps/share-app.steps.ts index d5742bdaa8..3ec038b065 100644 --- a/e2e/features/step-definitions/apps/share-app.steps.ts +++ b/e2e/features/step-definitions/apps/share-app.steps.ts @@ -40,7 +40,7 @@ Then('the shared app page should be accessible', async function (this: DifyWorld When('I run the shared workflow app', async function (this: DifyWorld) { const page = this.getPage() - const runButton = page.getByTestId('run-button') + const runButton = page.getByRole('button', { name: 'Execute' }) await expect(runButton).toBeEnabled({ timeout: 15_000 }) await runButton.click() diff --git a/web/__tests__/base/notion-page-selector-flow.test.tsx b/web/__tests__/base/notion-page-selector-flow.test.tsx index 6295d2dc00..ef813ee4bc 100644 --- a/web/__tests__/base/notion-page-selector-flow.test.tsx +++ b/web/__tests__/base/notion-page-selector-flow.test.tsx @@ -111,7 +111,7 @@ describe('Base Notion Page Selector Flow', () => { await user.type(screen.getByTestId('notion-search-input'), 'missing-page') expect(screen.getByText('common.dataSource.notion.selector.noSearchResult')).toBeInTheDocument() - await user.click(screen.getByTestId('notion-search-input-clear')) + await user.click(screen.getByRole('button', { name: 'common.operation.clear' })) expect(screen.getByTestId('notion-page-name-root-1')).toBeInTheDocument() await user.click(screen.getByTestId('notion-page-preview-root-1')) @@ -134,7 +134,7 @@ describe('Base Notion Page Selector Flow', () => { expect(onSelectCredential).toHaveBeenCalledWith('c1') - await user.click(screen.getByTestId('notion-credential-selector-btn')) + await user.click(screen.getByRole('combobox', { name: /Workspace 1/ })) await user.click(screen.getByTestId('notion-credential-item-c2')) expect(mockInvalidPreImportNotionPages).toHaveBeenCalledWith({ datasetId: 'dataset-1', credentialId: 'c2' }) diff --git a/web/__tests__/share/text-generation-run-once-flow.test.tsx b/web/__tests__/share/text-generation-run-once-flow.test.tsx index 2a5d1b882c..1471effa2d 100644 --- a/web/__tests__/share/text-generation-run-once-flow.test.tsx +++ b/web/__tests__/share/text-generation-run-once-flow.test.tsx @@ -119,7 +119,7 @@ describe('RunOnce – integration flow', () => { fireEvent.change(screen.getByPlaceholderText('Bio'), { target: { value: 'Hello' } }) // Phase 3 – submit - fireEvent.click(screen.getByTestId('run-button')) + fireEvent.click(screen.getByRole('button', { name: 'share.generation.run' })) expect(onSend).toHaveBeenCalledTimes(1) // Phase 4 – simulate "running" state @@ -132,7 +132,7 @@ describe('RunOnce – integration flow', () => { />, ) - const stopBtn = screen.getByTestId('stop-button') + const stopBtn = screen.getByRole('button', { name: 'share.generation.stopRun:{"defaultValue":"Stop Run"}' }) expect(stopBtn).toBeInTheDocument() fireEvent.click(stopBtn) expect(onStop).toHaveBeenCalledTimes(1) @@ -145,7 +145,7 @@ describe('RunOnce – integration flow', () => { runControl={{ onStop, isStopping: true }} />, ) - expect(screen.getByTestId('stop-button')).toBeDisabled() + expect(screen.getByRole('button', { name: 'share.generation.stopRun:{"defaultValue":"Stop Run"}' })).toBeDisabled() }) it('clear resets all field types and allows re-submit', async () => { @@ -174,7 +174,7 @@ describe('RunOnce – integration flow', () => { // Re-fill and submit fireEvent.change(screen.getByPlaceholderText('Question'), { target: { value: 'New' } }) - fireEvent.click(screen.getByTestId('run-button')) + fireEvent.click(screen.getByRole('button', { name: 'share.generation.run' })) expect(onSend).toHaveBeenCalledTimes(1) }) @@ -212,7 +212,7 @@ describe('RunOnce – integration flow', () => { fireEvent.change(screen.getByPlaceholderText('Text'), { target: { value: 'hello' } }) fireEvent.change(screen.getByTestId('code-editor'), { target: { value: '{"a":1}' } }) - fireEvent.click(screen.getByTestId('run-button')) + fireEvent.click(screen.getByRole('button', { name: 'share.generation.run' })) expect(onSend).toHaveBeenCalledTimes(1) }) }) diff --git a/web/app/(commonLayout)/datasets/layout.spec.tsx b/web/app/(commonLayout)/datasets/layout.spec.tsx index 9c01cffba8..7abc2253ce 100644 --- a/web/app/(commonLayout)/datasets/layout.spec.tsx +++ b/web/app/(commonLayout)/datasets/layout.spec.tsx @@ -63,12 +63,12 @@ describe('DatasetsLayout', () => { render(( -
datasets
+
datasets
)) expect(screen.getByRole('status')).toBeInTheDocument() - expect(screen.queryByTestId('datasets-content')).not.toBeInTheDocument() + expect(screen.queryByText('datasets')).not.toBeInTheDocument() expect(mockReplace).not.toHaveBeenCalled() }) @@ -80,11 +80,11 @@ describe('DatasetsLayout', () => { render(( -
datasets
+
datasets
)) - expect(screen.queryByTestId('datasets-content')).not.toBeInTheDocument() + expect(screen.queryByText('datasets')).not.toBeInTheDocument() await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith('/apps') }) @@ -98,11 +98,11 @@ describe('DatasetsLayout', () => { render(( -
datasets
+
datasets
)) - expect(screen.getByTestId('datasets-content')).toBeInTheDocument() + expect(screen.getByText('datasets')).toBeInTheDocument() expect(mockReplace).not.toHaveBeenCalled() }) }) diff --git a/web/app/(commonLayout)/role-route-guard.spec.tsx b/web/app/(commonLayout)/role-route-guard.spec.tsx index ca1550f0b8..ef409393b0 100644 --- a/web/app/(commonLayout)/role-route-guard.spec.tsx +++ b/web/app/(commonLayout)/role-route-guard.spec.tsx @@ -48,12 +48,12 @@ describe('RoleRouteGuard', () => { render(( -
content
+
content
)) expect(screen.getByRole('status')).toBeInTheDocument() - expect(screen.queryByTestId('guarded-content')).not.toBeInTheDocument() + expect(screen.queryByText('content')).not.toBeInTheDocument() expect(mockReplace).not.toHaveBeenCalled() }) @@ -64,11 +64,11 @@ describe('RoleRouteGuard', () => { render(( -
content
+
content
)) - expect(screen.queryByTestId('guarded-content')).not.toBeInTheDocument() + expect(screen.queryByText('content')).not.toBeInTheDocument() await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith('/datasets') }) @@ -82,11 +82,11 @@ describe('RoleRouteGuard', () => { render(( -
content
+
content
)) - expect(screen.getByTestId('guarded-content')).toBeInTheDocument() + expect(screen.getByText('content')).toBeInTheDocument() expect(mockReplace).not.toHaveBeenCalled() }) @@ -98,11 +98,11 @@ describe('RoleRouteGuard', () => { render(( -
content
+
content
)) - expect(screen.getByTestId('guarded-content')).toBeInTheDocument() + expect(screen.getByText('content')).toBeInTheDocument() expect(screen.queryByRole('status')).not.toBeInTheDocument() expect(mockReplace).not.toHaveBeenCalled() }) diff --git a/web/app/components/app/annotation/__tests__/filter.spec.tsx b/web/app/components/app/annotation/__tests__/filter.spec.tsx index 8b69494e3f..5353a32c4b 100644 --- a/web/app/components/app/annotation/__tests__/filter.spec.tsx +++ b/web/app/components/app/annotation/__tests__/filter.spec.tsx @@ -243,10 +243,7 @@ describe('Filter', () => { ) // Act - const input = screen.getByPlaceholderText('common.operation.search') - const clearButton = input.parentElement?.querySelector('div.cursor-pointer') - if (clearButton) - fireEvent.click(clearButton) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.clear' })) // Assert expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: '' }) diff --git a/web/app/components/app/annotation/batch-action.tsx b/web/app/components/app/annotation/batch-action.tsx index 938dcb03bd..961f313746 100644 --- a/web/app/components/app/annotation/batch-action.tsx +++ b/web/app/components/app/annotation/batch-action.tsx @@ -55,15 +55,23 @@ const BatchAction: FC = ({ {t(`${i18nPrefix}.selected`, { ns: 'appAnnotation' })}
-
- - -
+ + -
diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx index 5fc1cd25e1..8e6dd6cc28 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/csv-uploader.spec.tsx @@ -54,7 +54,7 @@ describe('CSVUploader', () => { const clickSpy = vi.spyOn(HTMLInputElement.prototype, 'click') renderComponent() - fireEvent.click(screen.getByText('appAnnotation.batchModal.browse')) + fireEvent.click(screen.getByRole('button', { name: 'appAnnotation.batchModal.browse' })) expect(clickSpy).toHaveBeenCalledTimes(1) clickSpy.mockRestore() @@ -137,7 +137,7 @@ describe('CSVUploader', () => { clickSpy.mockRestore() const valueSetter = vi.spyOn(fileInput, 'value', 'set') - const removeTrigger = screen.getByTestId('remove-file-button') + const removeTrigger = screen.getByRole('button', { name: /operation\.delete$/ }) fireEvent.click(removeTrigger) expect(updateFile).toHaveBeenCalledWith() diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx index c5d7232e12..74b59ff79f 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx @@ -115,6 +115,14 @@ describe('BatchModal', () => { expect(props.onCancel).toHaveBeenCalledTimes(1) }) + it('should call onCancel when close button is clicked', () => { + const { props } = renderComponent() + + fireEvent.click(screen.getByRole('button', { name: /operation\.close$/ })) + + expect(props.onCancel).toHaveBeenCalledTimes(1) + }) + it('should submit the csv file, poll status, and notify when import completes', async () => { vi.useFakeTimers({ shouldAdvanceTime: true }) const { props } = renderComponent() diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx index dc63b5c9be..75c3e8a66c 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx @@ -97,7 +97,13 @@ const CSVUploader: FC = ({
{t('batchModal.csvUploadTitle', { ns: 'appAnnotation' })} - {t('batchModal.browse', { ns: 'appAnnotation' })} +
{dragging &&
} @@ -113,9 +119,14 @@ const CSVUploader: FC = ({
-
- -
+
)} diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx index 0f6c27fd5a..7f1905c025 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx @@ -91,9 +91,14 @@ const BatchModal: FC = ({
{t('batchModal.title', { ns: 'appAnnotation' })}
-
- -
+ { expect(screen.getByText(baseMember.name)).toBeInTheDocument() }) - const groupItem = screen.getByText(baseGroup.name).closest('div') - const groupRemove = groupItem?.querySelector('.h-4.w-4.cursor-pointer') as HTMLElement + const groupRemove = screen.getAllByRole('button', { name: /operation\.remove$/ })[0]! + fireEvent.click(groupRemove) await waitFor(() => { expect(screen.queryByText(baseGroup.name)).not.toBeInTheDocument() }) - const memberItem = screen.getByText(baseMember.name).closest('div') - const memberRemove = memberItem?.querySelector('.h-4.w-4.cursor-pointer') as HTMLElement + const memberRemove = screen.getAllByRole('button', { name: /operation\.remove$/ })[0]! + fireEvent.click(memberRemove) await waitFor(() => { diff --git a/web/app/components/app/app-access-control/__tests__/specific-groups-or-members.spec.tsx b/web/app/components/app/app-access-control/__tests__/specific-groups-or-members.spec.tsx index 7b198c4e66..e763521940 100644 --- a/web/app/components/app/app-access-control/__tests__/specific-groups-or-members.spec.tsx +++ b/web/app/components/app/app-access-control/__tests__/specific-groups-or-members.spec.tsx @@ -86,11 +86,13 @@ describe('SpecificGroupsOrMembers', () => { expect(screen.getByText(baseMember.name)).toBeInTheDocument() }) - const groupRemove = screen.getByText(baseGroup.name).closest('div')?.querySelector('.h-4.w-4.cursor-pointer') as HTMLElement + const removeButtons = screen.getAllByRole('button', { name: /operation\.remove$/ }) + const groupRemove = removeButtons[0]! + const memberRemove = removeButtons[1]! + fireEvent.click(groupRemove) expect(useAccessControlStore.getState().specificGroups).toEqual([]) - const memberRemove = screen.getByText(baseMember.name).closest('div')?.querySelector('.h-4.w-4.cursor-pointer') as HTMLElement fireEvent.click(memberRemove) expect(useAccessControlStore.getState().specificMembers).toEqual([]) }) diff --git a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx index 38f9c2ab50..8d9bf19ea3 100644 --- a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx +++ b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx @@ -119,14 +119,40 @@ function SelectedGroupsBreadCrumb() { const handleReset = useCallback(() => { setSelectedGroupsForBreadcrumb([]) }, [setSelectedGroupsForBreadcrumb]) + const hasBreadcrumb = selectedGroupsForBreadcrumb.length > 0 + return (
- 0 && 'cursor-pointer text-text-accent')} onClick={handleReset}>{t('accessControlDialog.operateGroupAndMember.allMembers', { ns: 'app' })} + {hasBreadcrumb + ? ( + + ) + : ( + {t('accessControlDialog.operateGroupAndMember.allMembers', { ns: 'app' })} + )} {selectedGroupsForBreadcrumb.map((group, index) => { + const isLastGroup = index === selectedGroupsForBreadcrumb.length - 1 + return (
/ - handleBreadCrumbClick(index)}>{group.name} + {isLastGroup + ? {group.name} + : ( + + )}
) })} diff --git a/web/app/components/app/app-access-control/specific-groups-or-members.tsx b/web/app/components/app/app-access-control/specific-groups-or-members.tsx index 2cacd2cf03..ce6619ec80 100644 --- a/web/app/components/app/app-access-control/specific-groups-or-members.tsx +++ b/web/app/components/app/app-access-control/specific-groups-or-members.tsx @@ -120,6 +120,8 @@ type BaseItemProps = { onRemove?: () => void } function BaseItem({ icon, onRemove, children }: BaseItemProps) { + const { t } = useTranslation() + return (
@@ -128,9 +130,14 @@ function BaseItem({ icon, onRemove, children }: BaseItemProps) {
{children} -
- -
+
) } diff --git a/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx b/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx index 942a199a87..252eddb47e 100644 --- a/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx @@ -103,6 +103,22 @@ describe('VersionInfoModal', () => { expect(handleClose).toHaveBeenCalledTimes(1) }) + it('should close when the close button is clicked', () => { + const handleClose = vi.fn() + + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'operation.close' })) + + expect(handleClose).toHaveBeenCalledTimes(1) + }) + it('should validate release note length and clear previous errors before publishing', () => { const handlePublish = vi.fn() const handleClose = vi.fn() diff --git a/web/app/components/app/app-publisher/version-info-modal.tsx b/web/app/components/app/app-publisher/version-info-modal.tsx index 264975a08b..4e5493d1b2 100644 --- a/web/app/components/app/app-publisher/version-info-modal.tsx +++ b/web/app/components/app/app-publisher/version-info-modal.tsx @@ -79,9 +79,14 @@ const VersionInfoModal: FC = ({
{versionInfo?.marked_name ? t('versionHistory.editVersionInfo', { ns: 'workflow' }) : t('versionHistory.nameThisVersion', { ns: 'workflow' })}
-
- -
+
diff --git a/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx index 51683ad948..604db8288a 100644 --- a/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx @@ -233,9 +233,7 @@ describe('ConfigVar', () => { const item = screen.getByTitle('name · Name') const itemContainer = item.closest('div.group') expect(itemContainer).not.toBeNull() - const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') - expect(actionButtons).toHaveLength(2) - fireEvent.click(actionButtons[0]!) + fireEvent.click(within(itemContainer as HTMLElement).getByRole('button', { name: 'common.operation.edit' })) const editDialog = await screen.findByRole('dialog') const saveButton = within(editDialog).getByRole('button', { name: 'common.operation.save' }) @@ -259,9 +257,7 @@ describe('ConfigVar', () => { const item = screen.getByTitle('first · First') const itemContainer = item.closest('div.group') expect(itemContainer).not.toBeNull() - const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') - expect(actionButtons).toHaveLength(2) - fireEvent.click(actionButtons[0]!) + fireEvent.click(within(itemContainer as HTMLElement).getByRole('button', { name: 'common.operation.edit' })) const inputs = await screen.findAllByPlaceholderText('appDebug.variableConfig.inputPlaceholder') fireEvent.change(inputs[0]!, { target: { value: 'second' } }) @@ -285,9 +281,7 @@ describe('ConfigVar', () => { const item = screen.getByTitle('first · First') const itemContainer = item.closest('div.group') expect(itemContainer).not.toBeNull() - const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') - expect(actionButtons).toHaveLength(2) - fireEvent.click(actionButtons[0]!) + fireEvent.click(within(itemContainer as HTMLElement).getByRole('button', { name: 'common.operation.edit' })) const inputs = await screen.findAllByPlaceholderText('appDebug.variableConfig.inputPlaceholder') fireEvent.change(inputs[1]!, { target: { value: 'Second' } }) @@ -318,7 +312,7 @@ describe('ConfigVar', () => { onPromptVariablesChange, }) - const removeBtn = screen.getByTestId('var-item-delete-btn') + const removeBtn = screen.getByRole('button', { name: 'common.operation.delete' }) fireEvent.click(removeBtn) expect(onPromptVariablesChange).toHaveBeenCalledWith([]) @@ -343,7 +337,7 @@ describe('ConfigVar', () => { }, ) - const deleteBtn = screen.getByTestId('var-item-delete-btn') + const deleteBtn = screen.getByRole('button', { name: 'common.operation.delete' }) fireEvent.click(deleteBtn) // confirmation modal should show up fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) @@ -411,8 +405,7 @@ describe('ConfigVar', () => { const itemContainer = item.closest('div.group') expect(itemContainer).not.toBeNull() - const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') - fireEvent.click(actionButtons[0]!) + fireEvent.click(within(itemContainer as HTMLElement).getByRole('button', { name: 'common.operation.edit' })) const modalState = setShowExternalDataToolModal.mock.calls.at(-1)?.[0] @@ -460,8 +453,7 @@ describe('ConfigVar', () => { const itemContainer = item.closest('div.group') expect(itemContainer).not.toBeNull() - const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') - fireEvent.click(actionButtons[0]!) + fireEvent.click(within(itemContainer as HTMLElement).getByRole('button', { name: 'common.operation.edit' })) const modalState = setShowExternalDataToolModal.mock.calls.at(-1)?.[0] diff --git a/web/app/components/app/configuration/config-var/__tests__/var-item.spec.tsx b/web/app/components/app/configuration/config-var/__tests__/var-item.spec.tsx index aae00bb2b7..6f4fe5f11a 100644 --- a/web/app/components/app/configuration/config-var/__tests__/var-item.spec.tsx +++ b/web/app/components/app/configuration/config-var/__tests__/var-item.spec.tsx @@ -39,7 +39,7 @@ describe('VarItem', () => { />, ) - fireEvent.click(screen.getByTestId('var-item-delete-btn')) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.delete' })) expect(onRemove).toHaveBeenCalledTimes(1) }) diff --git a/web/app/components/app/configuration/config-var/config-select/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-var/config-select/__tests__/index.spec.tsx index 337b3bfe1c..24517eb341 100644 --- a/web/app/components/app/configuration/config-var/config-select/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/config-select/__tests__/index.spec.tsx @@ -44,12 +44,7 @@ describe('ConfigSelect Component', () => { it('handles option deletion', () => { render() - const optionContainer = screen.getByDisplayValue('Option 1').closest('div') - const deleteButton = optionContainer?.querySelector('div[role="button"]') - - if (!deleteButton) - return - fireEvent.click(deleteButton) + fireEvent.click(screen.getAllByRole('button', { name: 'common.operation.delete' })[0]!) expect(defaultProps.onChange).toHaveBeenCalledWith(['Option 2']) }) @@ -86,7 +81,7 @@ describe('ConfigSelect Component', () => { it('applies delete hover styles', () => { render() const optionContainer = screen.getByDisplayValue('Option 1').closest('div') - const deleteButton = optionContainer?.querySelector('div[role="button"]') + const deleteButton = screen.getAllByRole('button', { name: 'common.operation.delete' })[0] if (!deleteButton) return diff --git a/web/app/components/app/configuration/config-var/config-select/index.tsx b/web/app/components/app/configuration/config-var/config-select/index.tsx index 24bc3b4a06..42878852d9 100644 --- a/web/app/components/app/configuration/config-var/config-select/index.tsx +++ b/web/app/components/app/configuration/config-var/config-select/index.tsx @@ -67,9 +67,10 @@ const ConfigSelect: FC = ({ onFocus={() => setFocusID(index)} onBlur={() => setFocusID(null)} /> -
{ onChange(options.filter((_, i) => index !== i)) setDeletingID(null) @@ -77,8 +78,8 @@ const ConfigSelect: FC = ({ onMouseEnter={() => setDeletingID(index)} onMouseLeave={() => setDeletingID(null)} > - -
+
))} diff --git a/web/app/components/app/configuration/config-var/var-item.tsx b/web/app/components/app/configuration/config-var/var-item.tsx index 80c0bf6ac6..17568683d2 100644 --- a/web/app/components/app/configuration/config-var/var-item.tsx +++ b/web/app/components/app/configuration/config-var/var-item.tsx @@ -9,6 +9,7 @@ import { } from '@remixicon/react' import * as React from 'react' import { useState } from 'react' +import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { BracketsX as VarIcon } from '@/app/components/base/icons/src/vender/line/development' import IconTypeIcon from './input-type-icon' @@ -36,6 +37,7 @@ const VarItem: FC = ({ onRemove, canDrag, }) => { + const { t } = useTranslation() const [isDeleting, setIsDeleting] = useState(false) return ( @@ -58,21 +60,24 @@ const VarItem: FC = ({
-
- -
- +
diff --git a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx index d668737a0a..628e34bbe0 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx @@ -368,7 +368,7 @@ describe('AgentTools', () => { it('should remove tool when delete action is clicked', async () => { const { getModelConfig } = renderAgentTools() - const deleteButton = screen.getByTestId('delete-removed-tool') + const deleteButton = screen.getByRole('button', { name: /operation\.delete/i }) if (!deleteButton) throw new Error('Delete button not found') await userEvent.click(deleteButton) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index 3242bcdcf8..8ad9ad1f8f 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -96,6 +96,7 @@ const AgentTools: FC = () => { } const [isDeleting, setIsDeleting] = useState(-1) + const getDeleteToolLabel = (tool: AgentTool) => `${t('operation.delete', { ns: 'common' })} ${tool.tool_label || tool.tool_name}` const getToolValue = (tool: ToolDefaultValue) => { const currToolInCollections = collectionList.find(c => c.id === tool.provider_id) const currToolWithConfigs = currToolInCollections?.tools.find(t => t.name === tool.tool_name) @@ -249,7 +250,7 @@ const AgentTools: FC = () => {
{t('toolNameUsageTip', { ns: 'tools' })}
)} {!item.isDeleted && !readonly && ( @@ -320,8 +323,10 @@ const AgentTools: FC = () => { )} -
{ const newModelConfig = produce(modelConfig, (draft) => { draft.agentConfig.tools.splice(index, 1) @@ -331,10 +336,9 @@ const AgentTools: FC = () => { }} onMouseOver={() => setIsDeleting(index)} onMouseLeave={() => setIsDeleting(-1)} - data-testid="delete-removed-tool" > - -
+