mirror of
https://github.com/langgenius/dify.git
synced 2026-06-24 21:11:16 +08:00
Merge branch 'main' into feat/refine-snippet-siderbar
This commit is contained in:
commit
9e1f5bddbc
@ -36,25 +36,27 @@ Use this as the decision guide for React/TypeScript component structure. Existin
|
||||
- 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.
|
||||
- Do not replace prop drilling with one top-level hook that returns a large view model and then thread that object through section props. Move each hook, query, derived value, and handler to the concrete section that consumes it, or use feature-scoped Jotai atoms for simple shared form/UI state when siblings need the same source of truth.
|
||||
- When using feature-scoped Jotai state for a form, drawer, or other secondary surface, scope the store to that surface instance when stale cross-instance state is possible. Initialize stable config at the owning boundary, then let descendants read only the atoms or purpose-named hooks they actually need.
|
||||
- For Jotai-backed surfaces, put shared query atoms, mutation atoms, derived state, and write actions in the feature state file when they coordinate multiple descendants. The lowest-owner rule still applies to independent visual surfaces that do not participate in shared state.
|
||||
- For Jotai-backed surfaces, put shared query atoms, mutation atoms, derived state, and write actions in the feature state file when they coordinate multiple descendants. Do not create a query or mutation atom only because the surrounding feature uses Jotai. If the query or mutation does not read atom state, feed another atom, or participate in shared workflow orchestration, use `useQuery` or `useMutation` directly at the lowest owner.
|
||||
- For repeated row/menu action surfaces that need reset, hydrate the stable identity at the surface entry and scope only the primitives that truly need per-instance reset, such as open flags, drafts, or selected local options.
|
||||
- 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.
|
||||
|
||||
## Feature-Scoped Jotai State
|
||||
|
||||
- A module's feature-local state lives in one state file for Jotai-backed features: primitive atoms, query atoms, derived atoms, write-only action atoms, mutation atoms, submission orchestration, provider exports, and optional scope configuration.
|
||||
- Keep state local when one component owns it, even inside Jotai-backed features. Dialog open flags, menu/popover visibility, confirmation visibility, form/input drafts, row-local pending flags, and in-flight refs usually belong in component state.
|
||||
- A module's feature-local state lives in one state file for Jotai-backed features: primitive atoms, shared query atoms, derived atoms, write-only action atoms, shared mutation atoms, submission orchestration, provider exports, and optional scope configuration.
|
||||
- Keep synchronous UI state local when one component owns it, even inside Jotai-backed features. Dialog open flags, menu/popover visibility, confirmation visibility, form/input drafts, and selected local options usually belong in component state.
|
||||
- In Jotai-backed feature surfaces, never hand-roll async loading, error, or in-flight guards with `useState` or `useRef`. For async work that depends on atom state, feeds derived atoms, or participates in shared submission orchestration, model the work with `atomWithQuery` or `atomWithMutation`; write atoms should only update the inputs that drive those atoms. For component-owned remote work that does not participate in atom state, use TanStack Query hooks directly.
|
||||
- Row-local async state should belong to the row owner. Use `useQuery` or `useMutation` directly for row actions that do not depend on atom state and are not consumed by other atoms. Use a per-instance query or mutation atom only when the row action participates in a Jotai-backed shared workflow or needs atom-scoped reset semantics.
|
||||
- Promote UI state to an atom only when siblings need the same source of truth, the value drives a query or mutation atom, a parent workflow coordinates the state, or the state intentionally persists across hidden or unmounted descendants within a scoped surface.
|
||||
- Reflect atom-backed surface-wide locks or invariants in every affected trigger. If only one row, menu, or dialog should be disabled, keep the pending or lock state local to that row, menu, or dialog.
|
||||
- Reflect atom-backed surface-wide locks or invariants in every affected trigger. If only one row, menu, or dialog should be disabled, keep the pending or lock scope local to that row, menu, or dialog with the lowest-owner query/mutation hook unless it genuinely participates in shared atom state.
|
||||
- Atom order in the state file follows the dependency graph: types/constants, editable primitives, query atoms, query-data derived atoms, readiness/business derived atoms, write actions, mutation atoms, submission orchestration, provider exports.
|
||||
- Derived atom names read as business facts. Write atom names read as user or workflow commands.
|
||||
- UI components read and write the exact atom they use with `useAtomValue` or `useSetAtom`. Repeated workflow semantics live in named derived atoms or write atoms.
|
||||
- Non-query derived atoms return a narrow value with a clear domain name; avoid pass-through aliases or bundling unrelated UI facts. Query atoms expose the TanStack Query result object so loading, error, fetch, and pagination state stay attached to the query contract.
|
||||
- Write-only atoms own synchronous state transitions that update multiple primitives, reset dependent state, or advance the workflow. Async work with loading, error, caching, retry, or stale-result concerns should be modeled as query or mutation atoms, with write atoms only changing the inputs that drive them.
|
||||
- Write-only atoms own synchronous state transitions that update multiple primitives, reset dependent state, or advance the workflow. Async work with loading, error, caching, retry, stale-result, or in-flight concerns should be modeled as query or mutation atoms, with write atoms only changing the inputs that drive them.
|
||||
- Avoid feature hooks that aggregate form values, query results, derived state, and commands for sibling components. Prefer named derived atoms and write atoms so UI components read the exact shared fact or command they need.
|
||||
- When a form library owns validation, keep submit orchestration in feature state when post-submit result or error state is shared by the surface. Avoid duplicating validation gates or request shaping in UI hooks.
|
||||
- `jotai-tanstack-query` atoms use the same QueryClient as the React Query provider. Query atoms belong in feature state when atoms are the feature's local state surface.
|
||||
- `jotai-tanstack-query` atoms use the same QueryClient as the React Query provider. Query atoms belong in feature state only when they need atom inputs, provide data to derived atoms, or coordinate a shared Jotai-backed workflow.
|
||||
- Jotai scope is an optional instance-isolation tool for secondary surfaces with independent local state. Query and mutation atoms keep shared cache behavior through the shared QueryClient.
|
||||
- Do not put `atomWithQuery`, `atomWithInfiniteQuery`, `atomWithMutation`, or broad derived orchestration atoms in a `ScopeProvider` just to reset a surface. Scoped derived atoms implicitly scope their dependencies, which can duplicate query client access and break shared invalidation. Leave query/mutation atoms unscoped; let them read scoped primitive inputs.
|
||||
- Scope providers should list resettable primitive atoms and explicit hydration tuples. If a derived atom must be scoped, confirm that every dependency it implicitly scopes is meant to be private to that surface.
|
||||
@ -102,6 +104,7 @@ Use this as the decision guide for React/TypeScript component structure. Existin
|
||||
|
||||
- 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(...))`.
|
||||
- Do not promote a query or mutation to an atom just because the feature already has a state file. Use `atomWithQuery` or `atomWithMutation` only when the query/mutation reads atom state, is consumed by another atom, or is part of shared workflow orchestration.
|
||||
- In `atomWithQuery` and `atomWithInfiniteQuery`, return generated `queryOptions()` or `infiniteOptions()` directly. Pass `enabled`, `retry`, `placeholderData`, `select`, and pagination options into that call instead of spreading generated options into a hand-built object.
|
||||
- In `atomWithMutation`, return generated `mutationOptions()` directly when using generated clients. Put request shaping and submit orchestration in write atoms; do not rebuild mutation option objects just to pass through the generated mutation function.
|
||||
- For custom query functions that do not come from generated clients, wrap the options object with TanStack `queryOptions(...)` so query atoms still return a query options contract.
|
||||
@ -110,7 +113,7 @@ Use this as the decision guide for React/TypeScript component structure. Existin
|
||||
- For TanStack cache data, use generated or query-derived types; do not create local wrappers for `getQueryData` or `getQueriesData`.
|
||||
- For generated oRPC `queryOptions()` / `infiniteOptions()`, keep returning the generated options directly. When required input is missing, use a whole-input branch such as `input: condition ? validInput : skipToken` together with `enabled: Boolean(condition)` so no request runs and no fake payload is built.
|
||||
- Do not put `skipToken` inside a nested placeholder payload, such as `{ params: { appInstanceId: skipToken } }`. Do not create hand-written "missing queryOptions" objects or coerce required IDs to `''`.
|
||||
- Consume mutations directly with `useMutation(consoleQuery.xxx.mutationOptions(...))` or `useMutation(marketplaceQuery.xxx.mutationOptions(...))`; use oRPC clients as `mutationFn` only for custom flows.
|
||||
- Consume mutations directly with `useMutation(consoleQuery.xxx.mutationOptions(...))` or `useMutation(marketplaceQuery.xxx.mutationOptions(...))` when the mutation is owned by one component, menu, dialog, or row and its pending/error state is not consumed by feature atoms. In Jotai-backed workflow orchestration, expose mutations from feature state with `atomWithMutation` so pending/error state stays attached to the mutation atom. For component-owned custom mutation functions, use `useMutation(mutationOptions(...))` at the owner.
|
||||
- Put shared cache behavior in `createTanstackQueryUtils(...experimental_defaults...)`; components may add UI feedback callbacks, but should not own shared invalidation rules.
|
||||
- Component or atom mutation callbacks can handle local UI feedback such as toasts, closing dialogs, or navigation. They should not replace shared invalidation or add local cache patches for shared server state.
|
||||
- Do not use deprecated `useInvalid` or `useReset`.
|
||||
|
||||
@ -28,9 +28,9 @@ from libs.login import login_required
|
||||
from models.model import App, AppMode
|
||||
from services.agent.composer_service import AgentComposerService
|
||||
from services.agent.composer_validator import ComposerConfigValidator
|
||||
from services.entities.agent_entities import ComposerSavePayload
|
||||
from services.entities.agent_entities import ComposerSavePayload, WorkflowComposerCopyFromRosterPayload
|
||||
|
||||
register_schema_models(console_ns, ComposerSavePayload)
|
||||
register_schema_models(console_ns, ComposerSavePayload, WorkflowComposerCopyFromRosterPayload)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
AgentAppComposerResponse,
|
||||
@ -91,6 +91,38 @@ class WorkflowAgentComposerApi(Resource):
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/agent-composer/copy-from-roster")
|
||||
class WorkflowAgentComposerCopyFromRosterApi(Resource):
|
||||
@console_ns.expect(console_ns.models[WorkflowComposerCopyFromRosterPayload.__name__])
|
||||
@console_ns.response(
|
||||
200,
|
||||
"Workflow roster agent copied to inline agent",
|
||||
console_ns.models[WorkflowAgentComposerResponse.__name__],
|
||||
)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, account_id: str, app_model: App, node_id: str):
|
||||
payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {})
|
||||
return dump_response(
|
||||
WorkflowAgentComposerResponse,
|
||||
AgentComposerService.copy_workflow_composer_from_roster(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
node_id=node_id,
|
||||
account_id=account_id,
|
||||
source_agent_id=payload.source_agent_id,
|
||||
source_snapshot_id=payload.source_snapshot_id,
|
||||
idempotency_key=payload.idempotency_key,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/agent-composer/validate")
|
||||
class WorkflowAgentComposerValidateApi(Resource):
|
||||
@console_ns.expect(console_ns.models[ComposerSavePayload.__name__])
|
||||
|
||||
@ -341,8 +341,8 @@ class MessageFeedbackExportApi(Resource):
|
||||
|
||||
try:
|
||||
export_data = FeedbackService.export_feedbacks(
|
||||
db.session(),
|
||||
app_id=app_model.id,
|
||||
app_model.id,
|
||||
session=db.session(),
|
||||
from_source=args.from_source,
|
||||
rating=args.rating,
|
||||
has_comment=args.has_comment,
|
||||
|
||||
@ -5,6 +5,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from controllers.common.fields import SimpleResultResponse
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.login import login_required
|
||||
from services.auth.api_key_auth_service import ApiKeyAuthService
|
||||
@ -58,7 +59,7 @@ class ApiKeyAuthDataSource(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str):
|
||||
data_source_api_key_bindings = ApiKeyAuthService.get_provider_auth_list(current_tenant_id)
|
||||
data_source_api_key_bindings = ApiKeyAuthService.get_provider_auth_list(db.session(), current_tenant_id)
|
||||
if data_source_api_key_bindings:
|
||||
return {
|
||||
"sources": [
|
||||
@ -92,7 +93,7 @@ class ApiKeyAuthDataSourceBinding(Resource):
|
||||
data = payload.model_dump()
|
||||
ApiKeyAuthService.validate_api_key_auth_args(data)
|
||||
try:
|
||||
ApiKeyAuthService.create_provider_auth(current_tenant_id, data)
|
||||
ApiKeyAuthService.create_provider_auth(db.session(), current_tenant_id, data)
|
||||
except Exception as e:
|
||||
raise ApiKeyAuthFailedError(str(e))
|
||||
return {"result": "success"}, 200
|
||||
@ -109,6 +110,6 @@ class ApiKeyAuthDataSourceBindingDelete(Resource):
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, binding_id: UUID):
|
||||
# The role of the current user in the table must be admin or owner
|
||||
ApiKeyAuthService.delete_provider_auth(current_tenant_id, str(binding_id))
|
||||
ApiKeyAuthService.delete_provider_auth(db.session(), current_tenant_id, str(binding_id))
|
||||
|
||||
return "", 204
|
||||
|
||||
@ -17,6 +17,7 @@ from controllers.console.wraps import (
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from fields.dataset_fields import (
|
||||
DatasetMetadataBuiltInFieldsResponse,
|
||||
DatasetMetadataListResponse,
|
||||
@ -65,7 +66,9 @@ class DatasetMetadataCreateApi(Resource):
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
metadata = MetadataService.create_metadata(dataset_id_str, metadata_args, current_user, current_tenant_id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db.session(), dataset_id_str, metadata_args, current_user, current_tenant_id
|
||||
)
|
||||
return dump_response(DatasetMetadataResponse, metadata), 201
|
||||
|
||||
@setup_required
|
||||
@ -81,7 +84,7 @@ class DatasetMetadataCreateApi(Resource):
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
metadata = MetadataService.get_dataset_metadatas(dataset)
|
||||
metadata = MetadataService.get_dataset_metadatas(db.session(), dataset)
|
||||
return dump_response(DatasetMetadataListResponse, metadata), 200
|
||||
|
||||
|
||||
@ -108,7 +111,7 @@ class DatasetMetadataApi(Resource):
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
metadata = MetadataService.update_metadata_name(
|
||||
dataset_id_str, metadata_id_str, name, current_user, current_tenant_id
|
||||
db.session(), dataset_id_str, metadata_id_str, name, current_user, current_tenant_id
|
||||
)
|
||||
return dump_response(DatasetMetadataResponse, metadata), 200
|
||||
|
||||
@ -127,7 +130,7 @@ class DatasetMetadataApi(Resource):
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
MetadataService.delete_metadata(dataset_id_str, metadata_id_str)
|
||||
MetadataService.delete_metadata(db.session(), dataset_id_str, metadata_id_str)
|
||||
# Frontend callers only await success and invalidate metadata caches; no response body is consumed.
|
||||
return "", 204
|
||||
|
||||
@ -166,9 +169,9 @@ class DatasetMetadataBuiltInFieldActionApi(Resource):
|
||||
|
||||
match action:
|
||||
case "enable":
|
||||
MetadataService.enable_built_in_field(dataset)
|
||||
MetadataService.enable_built_in_field(db.session(), dataset)
|
||||
case "disable":
|
||||
MetadataService.disable_built_in_field(dataset)
|
||||
MetadataService.disable_built_in_field(db.session(), dataset)
|
||||
# Frontend callers only await success and invalidate metadata caches; no response body is consumed.
|
||||
return "", 204
|
||||
|
||||
@ -195,7 +198,7 @@ class DocumentMetadataEditApi(Resource):
|
||||
|
||||
metadata_args = MetadataOperationData.model_validate(console_ns.payload or {})
|
||||
|
||||
MetadataService.update_documents_metadata(dataset, metadata_args, current_user)
|
||||
MetadataService.update_documents_metadata(db.session(), dataset, metadata_args, current_user)
|
||||
|
||||
# Frontend callers only await success and invalidate caches; no response body is consumed.
|
||||
return "", 204
|
||||
|
||||
@ -8,6 +8,7 @@ from controllers.common.controller_schemas import MetadataUpdatePayload
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_model, register_schema_models
|
||||
from controllers.service_api import service_api_ns
|
||||
from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_rate_limit_check
|
||||
from extensions.ext_database import db
|
||||
from fields.dataset_fields import (
|
||||
DatasetMetadataActionResponse,
|
||||
DatasetMetadataBuiltInFieldsResponse,
|
||||
@ -85,7 +86,7 @@ class DatasetMetadataCreateServiceApi(DatasetApiResource):
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
metadata = MetadataService.create_metadata(dataset_id_str, metadata_args)
|
||||
metadata = MetadataService.create_metadata(db.session(), dataset_id_str, metadata_args)
|
||||
return dump_response(DatasetMetadataResponse, metadata), 201
|
||||
|
||||
@service_api_ns.doc(
|
||||
@ -118,7 +119,7 @@ class DatasetMetadataCreateServiceApi(DatasetApiResource):
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
metadata = MetadataService.get_dataset_metadatas(dataset)
|
||||
metadata = MetadataService.get_dataset_metadatas(db.session(), dataset)
|
||||
return dump_response(DatasetMetadataListResponse, metadata), 200
|
||||
|
||||
|
||||
@ -158,7 +159,7 @@ class DatasetMetadataServiceApi(DatasetApiResource):
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
metadata = MetadataService.update_metadata_name(dataset_id_str, metadata_id_str, payload.name)
|
||||
metadata = MetadataService.update_metadata_name(db.session(), dataset_id_str, metadata_id_str, payload.name)
|
||||
return dump_response(DatasetMetadataResponse, metadata), 200
|
||||
|
||||
@service_api_ns.doc(
|
||||
@ -193,7 +194,7 @@ class DatasetMetadataServiceApi(DatasetApiResource):
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user)
|
||||
|
||||
MetadataService.delete_metadata(dataset_id_str, metadata_id_str)
|
||||
MetadataService.delete_metadata(db.session(), dataset_id_str, metadata_id_str)
|
||||
return "", 204
|
||||
|
||||
|
||||
@ -263,9 +264,9 @@ class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource):
|
||||
|
||||
match action:
|
||||
case "enable":
|
||||
MetadataService.enable_built_in_field(dataset)
|
||||
MetadataService.enable_built_in_field(db.session(), dataset)
|
||||
case "disable":
|
||||
MetadataService.disable_built_in_field(dataset)
|
||||
MetadataService.disable_built_in_field(db.session(), dataset)
|
||||
return dump_response(DatasetMetadataActionResponse, {"result": "success"}), 200
|
||||
|
||||
|
||||
@ -309,6 +310,6 @@ class DocumentMetadataEditServiceApi(DatasetApiResource):
|
||||
|
||||
metadata_args = MetadataOperationData.model_validate(service_api_ns.payload or {})
|
||||
|
||||
MetadataService.update_documents_metadata(dataset, metadata_args)
|
||||
MetadataService.update_documents_metadata(db.session(), dataset, metadata_args)
|
||||
|
||||
return dump_response(DatasetMetadataActionResponse, {"result": "success"}), 200
|
||||
|
||||
@ -197,7 +197,7 @@ class AgentAppRuntimeRequestBuilder:
|
||||
def _plugin_daemon_plugin_id(*, plugin_id: str, model_provider: str) -> str:
|
||||
"""Return the transport plugin id expected by plugin-daemon headers."""
|
||||
if plugin_id.count("/") == 1:
|
||||
return plugin_id
|
||||
return plugin_id.split(":", 1)[0].split("@", 1)[0]
|
||||
if plugin_id:
|
||||
return ModelProviderID(plugin_id).plugin_id
|
||||
return ModelProviderID(model_provider).plugin_id
|
||||
|
||||
@ -265,7 +265,7 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
def _plugin_daemon_plugin_id(*, plugin_id: str, model_provider: str) -> str:
|
||||
"""Return the transport plugin id expected by plugin-daemon headers."""
|
||||
if plugin_id.count("/") == 1:
|
||||
return plugin_id
|
||||
return plugin_id.split(":", 1)[0].split("@", 1)[0]
|
||||
if plugin_id:
|
||||
return ModelProviderID(plugin_id).plugin_id
|
||||
return ModelProviderID(model_provider).plugin_id
|
||||
|
||||
@ -3807,6 +3807,26 @@ Submit human input form preview for workflow
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Workflow agent composer candidates | **application/json**: [AgentComposerCandidatesResponse](#agentcomposercandidatesresponse)<br> |
|
||||
|
||||
### [POST] /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/copy-from-roster
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string (uuid) |
|
||||
| node_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [WorkflowComposerCopyFromRosterPayload](#workflowcomposercopyfromrosterpayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Workflow roster agent copied to inline agent | **application/json**: [WorkflowAgentComposerResponse](#workflowagentcomposerresponse)<br> |
|
||||
|
||||
### [POST] /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/impact
|
||||
#### Parameters
|
||||
|
||||
@ -14385,9 +14405,14 @@ Button styles for user actions.
|
||||
| agent_soul | [AgentSoulConfig](#agentsoulconfig) | | No |
|
||||
| binding | [ComposerBindingPayload](#composerbindingpayload) | | No |
|
||||
| client_revision_id | string | | No |
|
||||
| description | string | | No |
|
||||
| icon | string | | No |
|
||||
| icon_background | string | | No |
|
||||
| icon_type | [AgentIconType](#agenticontype) | | No |
|
||||
| idempotency_key | string | | No |
|
||||
| new_agent_name | string | | No |
|
||||
| node_job | [WorkflowNodeJobConfig](#workflownodejobconfig) | | No |
|
||||
| role | string | | No |
|
||||
| save_strategy | [ComposerSaveStrategy](#composersavestrategy) | | Yes |
|
||||
| soul_lock | [ComposerSoulLockPayload](#composersoullockpayload) | | No |
|
||||
| variant | [ComposerVariant](#composervariant) | | Yes |
|
||||
@ -20560,6 +20585,14 @@ How a workflow node is bound to an Agent.
|
||||
| position_x | number | Comment X position | No |
|
||||
| position_y | number | Comment Y position | No |
|
||||
|
||||
#### WorkflowComposerCopyFromRosterPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| idempotency_key | string | | No |
|
||||
| source_agent_id | string | | Yes |
|
||||
| source_snapshot_id | string | | No |
|
||||
|
||||
#### WorkflowConversationVariableResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@ -4,15 +4,18 @@ from typing import Any
|
||||
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.sql.elements import ColumnElement
|
||||
|
||||
from extensions.ext_database import db
|
||||
from libs.helper import to_timestamp
|
||||
from models import Account
|
||||
from models.agent import (
|
||||
Agent,
|
||||
AgentConfigRevision,
|
||||
AgentConfigRevisionOperation,
|
||||
AgentConfigSnapshot,
|
||||
AgentDriveFile,
|
||||
AgentIconType,
|
||||
AgentKind,
|
||||
AgentScope,
|
||||
AgentSource,
|
||||
@ -20,9 +23,7 @@ from models.agent import (
|
||||
WorkflowAgentBindingType,
|
||||
WorkflowAgentNodeBinding,
|
||||
)
|
||||
from models.agent_config_entities import (
|
||||
DeclaredOutputConfig,
|
||||
)
|
||||
from models.agent_config_entities import DeclaredOutputConfig
|
||||
from models.agent_config_entities import (
|
||||
effective_declared_outputs as _effective_declared_outputs,
|
||||
)
|
||||
@ -32,8 +33,12 @@ from services.agent.composer_validator import ComposerConfigValidator
|
||||
from services.agent.errors import (
|
||||
AgentNameConflictError,
|
||||
AgentNotFoundError,
|
||||
AgentVersionConflictError,
|
||||
AgentVersionNotFoundError,
|
||||
InvalidComposerConfigError,
|
||||
)
|
||||
from services.agent.roster_service import AgentRosterService
|
||||
from services.app_service import AppService, CreateAppParams
|
||||
from services.entities.agent_entities import (
|
||||
AgentSoulConfig,
|
||||
ComposerCandidatesResponse,
|
||||
@ -172,6 +177,86 @@ class AgentComposerService:
|
||||
)
|
||||
return state
|
||||
|
||||
@classmethod
|
||||
def copy_workflow_composer_from_roster(
|
||||
cls,
|
||||
*,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
node_id: str,
|
||||
account_id: str,
|
||||
source_agent_id: str,
|
||||
source_snapshot_id: str | None = None,
|
||||
idempotency_key: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
workflow = cls._get_draft_workflow(tenant_id=tenant_id, app_id=app_id)
|
||||
binding = cls._require_binding(
|
||||
cls._get_workflow_binding(tenant_id=tenant_id, workflow_id=workflow.id, node_id=node_id)
|
||||
)
|
||||
|
||||
if binding.binding_type == WorkflowAgentBindingType.INLINE_AGENT and idempotency_key:
|
||||
agent = cls._get_agent_if_present(tenant_id=tenant_id, agent_id=binding.agent_id)
|
||||
version = cls._get_version_if_present(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent.id if agent else None,
|
||||
version_id=binding.current_snapshot_id,
|
||||
)
|
||||
return cls._serialize_workflow_state(binding=binding, agent=agent, version=version)
|
||||
|
||||
if binding.binding_type != WorkflowAgentBindingType.ROSTER_AGENT:
|
||||
raise InvalidComposerConfigError("Workflow agent node must be bound to a roster agent.")
|
||||
if binding.agent_id != source_agent_id:
|
||||
raise InvalidComposerConfigError("Source agent does not match the current workflow node binding.")
|
||||
|
||||
source_agent = cls._require_agent(tenant_id=tenant_id, agent_id=source_agent_id)
|
||||
if source_agent.scope != AgentScope.ROSTER or source_agent.status != AgentStatus.ACTIVE:
|
||||
raise InvalidComposerConfigError("Source agent must be an active roster agent.")
|
||||
source_version = cls._require_version(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=source_agent.id,
|
||||
version_id=source_agent.active_config_snapshot_id,
|
||||
)
|
||||
if source_snapshot_id and source_snapshot_id != source_version.id:
|
||||
raise AgentVersionConflictError()
|
||||
|
||||
agent_soul = AgentSoulConfig.model_validate(source_version.config_snapshot_dict)
|
||||
inline_agent = cls._create_workflow_only_agent(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
workflow_id=workflow.id,
|
||||
node_id=node_id,
|
||||
account_id=account_id,
|
||||
agent_soul=agent_soul,
|
||||
name=source_agent.name,
|
||||
description=source_agent.description,
|
||||
role=source_agent.role,
|
||||
icon_type=source_agent.icon_type,
|
||||
icon=source_agent.icon,
|
||||
icon_background=source_agent.icon_background,
|
||||
)
|
||||
cls._copy_agent_drive_rows(
|
||||
tenant_id=tenant_id,
|
||||
source_agent_id=source_agent.id,
|
||||
target_agent_id=inline_agent.id,
|
||||
account_id=account_id,
|
||||
agent_soul=agent_soul,
|
||||
node_job=WorkflowNodeJobConfig.model_validate(binding.node_job_config_dict),
|
||||
)
|
||||
|
||||
binding.binding_type = WorkflowAgentBindingType.INLINE_AGENT
|
||||
binding.agent_id = inline_agent.id
|
||||
binding.current_snapshot_id = inline_agent.active_config_snapshot_id
|
||||
binding.updated_by = account_id
|
||||
db.session.flush()
|
||||
db.session.commit()
|
||||
|
||||
version = cls._require_version(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=inline_agent.id,
|
||||
version_id=inline_agent.active_config_snapshot_id,
|
||||
)
|
||||
return cls._serialize_workflow_state(binding=binding, agent=inline_agent, version=version)
|
||||
|
||||
@classmethod
|
||||
def load_agent_app_composer(cls, *, tenant_id: str, app_id: str) -> dict[str, Any]:
|
||||
agent = db.session.scalar(
|
||||
@ -849,6 +934,11 @@ class AgentComposerService:
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
name=agent_name,
|
||||
description=payload.description or "",
|
||||
role=payload.role or "",
|
||||
icon_type=payload.icon_type,
|
||||
icon=payload.icon,
|
||||
icon_background=payload.icon_background,
|
||||
agent_soul=payload.agent_soul,
|
||||
operation=AgentConfigRevisionOperation.SAVE_NEW_AGENT,
|
||||
version_note=payload.version_note,
|
||||
@ -894,10 +984,25 @@ class AgentComposerService:
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
name=agent_name,
|
||||
description=payload.description if payload.description is not None else source_agent.description,
|
||||
role=payload.role if payload.role is not None else source_agent.role,
|
||||
icon_type=payload.icon_type if payload.icon_type is not None else source_agent.icon_type,
|
||||
icon=payload.icon if payload.icon is not None else source_agent.icon,
|
||||
icon_background=payload.icon_background
|
||||
if payload.icon_background is not None
|
||||
else source_agent.icon_background,
|
||||
agent_soul=agent_soul,
|
||||
operation=AgentConfigRevisionOperation.SAVE_TO_ROSTER,
|
||||
version_note=payload.version_note,
|
||||
)
|
||||
cls._copy_agent_drive_rows(
|
||||
tenant_id=tenant_id,
|
||||
source_agent_id=source_agent.id,
|
||||
target_agent_id=roster_agent.id,
|
||||
account_id=account_id,
|
||||
agent_soul=agent_soul,
|
||||
node_job=payload.node_job or WorkflowNodeJobConfig.model_validate(binding.node_job_config_dict),
|
||||
)
|
||||
binding.binding_type = WorkflowAgentBindingType.ROSTER_AGENT
|
||||
binding.agent_id = roster_agent.id
|
||||
binding.current_snapshot_id = roster_agent.active_config_snapshot_id
|
||||
@ -916,11 +1021,21 @@ class AgentComposerService:
|
||||
node_id: str,
|
||||
account_id: str,
|
||||
agent_soul: AgentSoulConfig,
|
||||
name: str | None = None,
|
||||
description: str = "",
|
||||
role: str = "",
|
||||
icon_type: Any | None = None,
|
||||
icon: str | None = None,
|
||||
icon_background: str | None = None,
|
||||
) -> Agent:
|
||||
agent = Agent(
|
||||
tenant_id=tenant_id,
|
||||
name=f"Workflow Agent {node_id}",
|
||||
description="",
|
||||
name=name or f"Workflow Agent {node_id}",
|
||||
description=description,
|
||||
role=role,
|
||||
icon_type=icon_type,
|
||||
icon=icon,
|
||||
icon_background=icon_background,
|
||||
agent_kind=AgentKind.DIFY_AGENT,
|
||||
scope=AgentScope.WORKFLOW_ONLY,
|
||||
source=AgentSource.WORKFLOW,
|
||||
@ -945,6 +1060,98 @@ class AgentComposerService:
|
||||
agent.active_config_has_model = agent_soul_has_model(agent_soul)
|
||||
return agent
|
||||
|
||||
@classmethod
|
||||
def _copy_agent_drive_rows(
|
||||
cls,
|
||||
*,
|
||||
tenant_id: str,
|
||||
source_agent_id: str,
|
||||
target_agent_id: str,
|
||||
account_id: str,
|
||||
agent_soul: AgentSoulConfig,
|
||||
node_job: WorkflowNodeJobConfig | None = None,
|
||||
) -> None:
|
||||
exact_keys, prefixes = cls._drive_copy_scopes_from_agent_configs(agent_soul=agent_soul, node_job=node_job)
|
||||
predicates: list[ColumnElement[bool]] = []
|
||||
if exact_keys:
|
||||
predicates.append(AgentDriveFile.key.in_(sorted(exact_keys)))
|
||||
predicates.extend(AgentDriveFile.key.startswith(prefix) for prefix in sorted(prefixes))
|
||||
if not predicates:
|
||||
return
|
||||
|
||||
source_rows = list(
|
||||
db.session.scalars(
|
||||
select(AgentDriveFile).where(
|
||||
AgentDriveFile.tenant_id == tenant_id,
|
||||
AgentDriveFile.agent_id == source_agent_id,
|
||||
or_(*predicates),
|
||||
)
|
||||
).all()
|
||||
)
|
||||
if not source_rows:
|
||||
return
|
||||
|
||||
existing_target_keys = set(
|
||||
db.session.scalars(
|
||||
select(AgentDriveFile.key).where(
|
||||
AgentDriveFile.tenant_id == tenant_id,
|
||||
AgentDriveFile.agent_id == target_agent_id,
|
||||
AgentDriveFile.key.in_([row.key for row in source_rows]),
|
||||
)
|
||||
).all()
|
||||
)
|
||||
for row in source_rows:
|
||||
if row.key in existing_target_keys:
|
||||
continue
|
||||
db.session.add(
|
||||
AgentDriveFile(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=target_agent_id,
|
||||
key=row.key,
|
||||
file_kind=row.file_kind,
|
||||
file_id=row.file_id,
|
||||
value_owned_by_drive=row.value_owned_by_drive,
|
||||
is_skill=row.is_skill,
|
||||
skill_metadata=row.skill_metadata,
|
||||
size=row.size,
|
||||
hash=row.hash,
|
||||
mime_type=row.mime_type,
|
||||
created_by=account_id,
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _drive_copy_scopes_from_agent_configs(
|
||||
*, agent_soul: AgentSoulConfig, node_job: WorkflowNodeJobConfig | None = None
|
||||
) -> tuple[set[str], set[str]]:
|
||||
from services.agent.prompt_mentions import MentionKind, parse_prompt_mentions
|
||||
from services.agent_drive_service import decode_drive_mention_ref
|
||||
|
||||
exact_keys: set[str] = set()
|
||||
prefixes: set[str] = set()
|
||||
|
||||
for mention in parse_prompt_mentions(agent_soul.prompt.system_prompt):
|
||||
if mention.kind not in {MentionKind.SKILL, MentionKind.FILE}:
|
||||
continue
|
||||
drive_key = decode_drive_mention_ref(mention.ref_id)
|
||||
if not drive_key:
|
||||
continue
|
||||
if mention.kind == MentionKind.SKILL and "/" in drive_key:
|
||||
prefixes.add(f"{drive_key.rsplit('/', 1)[0]}/")
|
||||
else:
|
||||
exact_keys.add(drive_key)
|
||||
|
||||
if node_job is not None:
|
||||
for file_ref in node_job.metadata.file_refs or []:
|
||||
if file_ref.drive_key:
|
||||
exact_keys.add(file_ref.drive_key)
|
||||
for output in node_job.declared_outputs:
|
||||
benchmark_ref = output.check.benchmark_file_ref if output.check and output.check.enabled else None
|
||||
if benchmark_ref and benchmark_ref.drive_key:
|
||||
exact_keys.add(benchmark_ref.drive_key)
|
||||
|
||||
return exact_keys, prefixes
|
||||
|
||||
@classmethod
|
||||
def _create_roster_agent_for_composer(
|
||||
cls,
|
||||
@ -955,27 +1162,42 @@ class AgentComposerService:
|
||||
agent_soul: AgentSoulConfig,
|
||||
operation: AgentConfigRevisionOperation,
|
||||
version_note: str | None,
|
||||
description: str = "",
|
||||
role: str = "",
|
||||
icon_type: AgentIconType | None = None,
|
||||
icon: str | None = None,
|
||||
icon_background: str | None = None,
|
||||
) -> Agent:
|
||||
agent = Agent(
|
||||
tenant_id=tenant_id,
|
||||
name=name,
|
||||
description="",
|
||||
agent_kind=AgentKind.DIFY_AGENT,
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.WORKFLOW,
|
||||
status=AgentStatus.ACTIVE,
|
||||
created_by=account_id,
|
||||
updated_by=account_id,
|
||||
)
|
||||
db.session.add(agent)
|
||||
account = cls._require_account(account_id=account_id)
|
||||
try:
|
||||
db.session.flush()
|
||||
app = AppService().create_app(
|
||||
tenant_id,
|
||||
CreateAppParams(
|
||||
name=name,
|
||||
description=description,
|
||||
mode="agent",
|
||||
agent_role=role,
|
||||
icon_type=icon_type.value if isinstance(icon_type, AgentIconType) else icon_type,
|
||||
icon=icon,
|
||||
icon_background=icon_background,
|
||||
),
|
||||
account,
|
||||
)
|
||||
except IntegrityError as exc:
|
||||
db.session.rollback()
|
||||
raise AgentNameConflictError() from exc
|
||||
version = cls._create_config_version(
|
||||
|
||||
agent = AgentRosterService(db.session).get_app_backing_agent(tenant_id=tenant_id, app_id=app.id)
|
||||
if agent is None:
|
||||
raise AgentNotFoundError()
|
||||
|
||||
current_snapshot = cls._require_version(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent.id,
|
||||
version_id=agent.active_config_snapshot_id,
|
||||
)
|
||||
version = cls._update_current_version(
|
||||
current_snapshot=current_snapshot,
|
||||
account_id=account_id,
|
||||
agent_soul=agent_soul,
|
||||
operation=operation,
|
||||
@ -983,6 +1205,7 @@ class AgentComposerService:
|
||||
)
|
||||
agent.active_config_snapshot_id = version.id
|
||||
agent.active_config_has_model = agent_soul_has_model(agent_soul)
|
||||
agent.updated_by = account_id
|
||||
return agent
|
||||
|
||||
@classmethod
|
||||
@ -1111,6 +1334,13 @@ class AgentComposerService:
|
||||
raise AgentNotFoundError()
|
||||
return agent
|
||||
|
||||
@classmethod
|
||||
def _require_account(cls, *, account_id: str) -> Account:
|
||||
account = db.session.get(Account, account_id)
|
||||
if not account:
|
||||
raise ValueError("Account not found")
|
||||
return account
|
||||
|
||||
@classmethod
|
||||
def _get_agent_if_present(cls, *, tenant_id: str, agent_id: str | None) -> Agent | None:
|
||||
if not agent_id:
|
||||
|
||||
@ -17,6 +17,10 @@ class AgentArchivedError(Conflict):
|
||||
description = "Archived agent cannot be modified."
|
||||
|
||||
|
||||
class AgentVersionConflictError(Conflict):
|
||||
description = "Agent config version changed. Please reload and try again."
|
||||
|
||||
|
||||
class AgentSoulLockedError(BadRequest):
|
||||
description = "Agent Soul is locked for this workflow node."
|
||||
|
||||
|
||||
@ -837,6 +837,7 @@ class AgentRosterService:
|
||||
if agent.source == AgentSource.AGENT_APP:
|
||||
return {
|
||||
AgentConfigRevisionOperation.SAVE_NEW_VERSION,
|
||||
AgentConfigRevisionOperation.SAVE_TO_ROSTER,
|
||||
AgentConfigRevisionOperation.RESTORE_VERSION,
|
||||
}
|
||||
return {
|
||||
|
||||
@ -2,17 +2,17 @@ import json
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.helper import encrypter
|
||||
from extensions.ext_database import db
|
||||
from models.source import DataSourceApiKeyAuthBinding
|
||||
from services.auth.api_key_auth_factory import ApiKeyAuthFactory
|
||||
|
||||
|
||||
class ApiKeyAuthService:
|
||||
@staticmethod
|
||||
def get_provider_auth_list(tenant_id: str):
|
||||
data_source_api_key_bindings = db.session.scalars(
|
||||
def get_provider_auth_list(session: Session, tenant_id: str):
|
||||
data_source_api_key_bindings = session.scalars(
|
||||
select(DataSourceApiKeyAuthBinding).where(
|
||||
DataSourceApiKeyAuthBinding.tenant_id == tenant_id, DataSourceApiKeyAuthBinding.disabled.is_(False)
|
||||
)
|
||||
@ -20,7 +20,7 @@ class ApiKeyAuthService:
|
||||
return data_source_api_key_bindings
|
||||
|
||||
@staticmethod
|
||||
def create_provider_auth(tenant_id: str, args: dict[str, Any]):
|
||||
def create_provider_auth(session: Session, tenant_id: str, args: dict[str, Any]):
|
||||
auth_result = ApiKeyAuthFactory(args["provider"], args["credentials"]).validate_credentials()
|
||||
if auth_result:
|
||||
# Encrypt the api key
|
||||
@ -31,12 +31,12 @@ class ApiKeyAuthService:
|
||||
tenant_id=tenant_id, category=args["category"], provider=args["provider"]
|
||||
)
|
||||
data_source_api_key_binding.credentials = json.dumps(args["credentials"], ensure_ascii=False)
|
||||
db.session.add(data_source_api_key_binding)
|
||||
db.session.commit()
|
||||
session.add(data_source_api_key_binding)
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def get_auth_credentials(tenant_id: str, category: str, provider: str):
|
||||
data_source_api_key_bindings = db.session.scalar(
|
||||
def get_auth_credentials(session: Session, tenant_id: str, category: str, provider: str):
|
||||
data_source_api_key_bindings = session.scalar(
|
||||
select(DataSourceApiKeyAuthBinding).where(
|
||||
DataSourceApiKeyAuthBinding.tenant_id == tenant_id,
|
||||
DataSourceApiKeyAuthBinding.category == category,
|
||||
@ -52,16 +52,16 @@ class ApiKeyAuthService:
|
||||
return credentials
|
||||
|
||||
@staticmethod
|
||||
def delete_provider_auth(tenant_id: str, binding_id: str):
|
||||
data_source_api_key_binding = db.session.scalar(
|
||||
def delete_provider_auth(session: Session, tenant_id: str, binding_id: str):
|
||||
data_source_api_key_binding = session.scalar(
|
||||
select(DataSourceApiKeyAuthBinding).where(
|
||||
DataSourceApiKeyAuthBinding.tenant_id == tenant_id,
|
||||
DataSourceApiKeyAuthBinding.id == binding_id,
|
||||
)
|
||||
)
|
||||
if data_source_api_key_binding:
|
||||
db.session.delete(data_source_api_key_binding)
|
||||
db.session.commit()
|
||||
session.delete(data_source_api_key_binding)
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def validate_api_key_auth_args(cls, args):
|
||||
|
||||
@ -42,6 +42,11 @@ class ComposerSavePayload(BaseModel):
|
||||
idempotency_key: str | None = None
|
||||
client_revision_id: str | None = None
|
||||
new_agent_name: str | None = Field(default=None, min_length=1, max_length=255)
|
||||
description: str | None = None
|
||||
role: str | None = Field(default=None, max_length=255)
|
||||
icon_type: AgentIconType | None = None
|
||||
icon: str | None = Field(default=None, max_length=255)
|
||||
icon_background: str | None = Field(default=None, max_length=255)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_variant_sections(self) -> "ComposerSavePayload":
|
||||
@ -58,6 +63,12 @@ class ComposerSavePayload(BaseModel):
|
||||
return self
|
||||
|
||||
|
||||
class WorkflowComposerCopyFromRosterPayload(BaseModel):
|
||||
source_agent_id: str = Field(min_length=1, max_length=255)
|
||||
source_snapshot_id: str | None = Field(default=None, max_length=255)
|
||||
idempotency_key: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class RosterAgentCreatePayload(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
mode: Literal["agent"] = "agent"
|
||||
|
||||
@ -14,8 +14,9 @@ from models.model import Account, App, Conversation, Message, MessageFeedback
|
||||
class FeedbackService:
|
||||
@staticmethod
|
||||
def export_feedbacks(
|
||||
session: Session,
|
||||
app_id: str,
|
||||
*,
|
||||
session: Session,
|
||||
from_source: str | None = None,
|
||||
rating: str | None = None,
|
||||
has_comment: bool | None = None,
|
||||
@ -28,6 +29,7 @@ class FeedbackService:
|
||||
|
||||
Args:
|
||||
app_id: Application ID
|
||||
session: Database session used to run the export query
|
||||
from_source: Filter by feedback source ('user' or 'admin')
|
||||
rating: Filter by rating ('like' or 'dislike')
|
||||
has_comment: Only include feedback with comments
|
||||
|
||||
@ -2,9 +2,9 @@ import copy
|
||||
import logging
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.rag.index_processor.constant.built_in_field import BuiltInField, MetadataDataSource
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from libs.login import resolve_account_fallback
|
||||
@ -23,6 +23,7 @@ logger = logging.getLogger(__name__)
|
||||
class MetadataService:
|
||||
@staticmethod
|
||||
def create_metadata(
|
||||
session: Session,
|
||||
dataset_id: str,
|
||||
metadata_args: MetadataArgs,
|
||||
current_user: Account | None = None, # TODO: the service_api is not migrated yet
|
||||
@ -33,7 +34,7 @@ class MetadataService:
|
||||
raise ValueError("Metadata name cannot exceed 255 characters.")
|
||||
current_user, current_tenant_id = resolve_account_fallback(current_user, current_tenant_id)
|
||||
# check if metadata name already exists
|
||||
if db.session.scalar(
|
||||
if session.scalar(
|
||||
select(DatasetMetadata)
|
||||
.where(
|
||||
DatasetMetadata.tenant_id == current_tenant_id,
|
||||
@ -53,12 +54,13 @@ class MetadataService:
|
||||
name=metadata_args.name,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
db.session.add(metadata)
|
||||
db.session.commit()
|
||||
session.add(metadata)
|
||||
session.commit()
|
||||
return metadata
|
||||
|
||||
@staticmethod
|
||||
def update_metadata_name(
|
||||
session: Session,
|
||||
dataset_id: str,
|
||||
metadata_id: str,
|
||||
name: str,
|
||||
@ -72,7 +74,7 @@ class MetadataService:
|
||||
lock_key = f"dataset_metadata_lock_{dataset_id}"
|
||||
# check if metadata name already exists
|
||||
current_user, current_tenant_id = resolve_account_fallback(current_user, current_tenant_id)
|
||||
if db.session.scalar(
|
||||
if session.scalar(
|
||||
select(DatasetMetadata)
|
||||
.where(
|
||||
DatasetMetadata.tenant_id == current_tenant_id,
|
||||
@ -87,7 +89,7 @@ class MetadataService:
|
||||
raise ValueError("Metadata name already exists in Built-in fields.")
|
||||
try:
|
||||
MetadataService.knowledge_base_metadata_lock_check(dataset_id, None)
|
||||
metadata = db.session.scalar(
|
||||
metadata = session.scalar(
|
||||
select(DatasetMetadata)
|
||||
.where(DatasetMetadata.id == metadata_id, DatasetMetadata.dataset_id == dataset_id)
|
||||
.limit(1)
|
||||
@ -100,7 +102,7 @@ class MetadataService:
|
||||
metadata.updated_at = naive_utc_now()
|
||||
|
||||
# update related documents
|
||||
dataset_metadata_bindings = db.session.scalars(
|
||||
dataset_metadata_bindings = session.scalars(
|
||||
select(DatasetMetadataBinding).where(DatasetMetadataBinding.metadata_id == metadata_id)
|
||||
).all()
|
||||
if dataset_metadata_bindings:
|
||||
@ -114,8 +116,8 @@ class MetadataService:
|
||||
value = doc_metadata.pop(old_name, None)
|
||||
doc_metadata[name] = value
|
||||
document.doc_metadata = doc_metadata
|
||||
db.session.add(document)
|
||||
db.session.commit()
|
||||
session.add(document)
|
||||
session.commit()
|
||||
return metadata
|
||||
except Exception:
|
||||
logger.exception("Update metadata name failed")
|
||||
@ -124,21 +126,21 @@ class MetadataService:
|
||||
redis_client.delete(lock_key)
|
||||
|
||||
@staticmethod
|
||||
def delete_metadata(dataset_id: str, metadata_id: str):
|
||||
def delete_metadata(session: Session, dataset_id: str, metadata_id: str):
|
||||
lock_key = f"dataset_metadata_lock_{dataset_id}"
|
||||
try:
|
||||
MetadataService.knowledge_base_metadata_lock_check(dataset_id, None)
|
||||
metadata = db.session.scalar(
|
||||
metadata = session.scalar(
|
||||
select(DatasetMetadata)
|
||||
.where(DatasetMetadata.id == metadata_id, DatasetMetadata.dataset_id == dataset_id)
|
||||
.limit(1)
|
||||
)
|
||||
if metadata is None:
|
||||
raise ValueError("Metadata not found.")
|
||||
db.session.delete(metadata)
|
||||
session.delete(metadata)
|
||||
|
||||
# deal related documents
|
||||
dataset_metadata_bindings = db.session.scalars(
|
||||
dataset_metadata_bindings = session.scalars(
|
||||
select(DatasetMetadataBinding).where(DatasetMetadataBinding.metadata_id == metadata_id)
|
||||
).all()
|
||||
if dataset_metadata_bindings:
|
||||
@ -151,8 +153,8 @@ class MetadataService:
|
||||
doc_metadata = copy.deepcopy(document.doc_metadata)
|
||||
doc_metadata.pop(metadata.name, None)
|
||||
document.doc_metadata = doc_metadata
|
||||
db.session.add(document)
|
||||
db.session.commit()
|
||||
session.add(document)
|
||||
session.commit()
|
||||
return metadata
|
||||
except Exception:
|
||||
logger.exception("Delete metadata failed")
|
||||
@ -170,13 +172,13 @@ class MetadataService:
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def enable_built_in_field(dataset: Dataset):
|
||||
def enable_built_in_field(session: Session, dataset: Dataset):
|
||||
if dataset.built_in_field_enabled:
|
||||
return
|
||||
lock_key = f"dataset_metadata_lock_{dataset.id}"
|
||||
try:
|
||||
MetadataService.knowledge_base_metadata_lock_check(dataset.id, None)
|
||||
db.session.add(dataset)
|
||||
session.add(dataset)
|
||||
documents = DocumentService.get_working_documents_by_dataset_id(dataset.id)
|
||||
if documents:
|
||||
for document in documents:
|
||||
@ -190,22 +192,22 @@ class MetadataService:
|
||||
doc_metadata[BuiltInField.last_update_date] = document.last_update_date.timestamp()
|
||||
doc_metadata[BuiltInField.source] = MetadataDataSource[document.data_source_type]
|
||||
document.doc_metadata = doc_metadata
|
||||
db.session.add(document)
|
||||
session.add(document)
|
||||
dataset.built_in_field_enabled = True
|
||||
db.session.commit()
|
||||
session.commit()
|
||||
except Exception:
|
||||
logger.exception("Enable built-in field failed")
|
||||
finally:
|
||||
redis_client.delete(lock_key)
|
||||
|
||||
@staticmethod
|
||||
def disable_built_in_field(dataset: Dataset):
|
||||
def disable_built_in_field(session: Session, dataset: Dataset):
|
||||
if not dataset.built_in_field_enabled:
|
||||
return
|
||||
lock_key = f"dataset_metadata_lock_{dataset.id}"
|
||||
try:
|
||||
MetadataService.knowledge_base_metadata_lock_check(dataset.id, None)
|
||||
db.session.add(dataset)
|
||||
session.add(dataset)
|
||||
documents = DocumentService.get_working_documents_by_dataset_id(dataset.id)
|
||||
document_ids = []
|
||||
if documents:
|
||||
@ -220,10 +222,10 @@ class MetadataService:
|
||||
doc_metadata.pop(BuiltInField.last_update_date, None)
|
||||
doc_metadata.pop(BuiltInField.source, None)
|
||||
document.doc_metadata = doc_metadata
|
||||
db.session.add(document)
|
||||
session.add(document)
|
||||
document_ids.append(document.id)
|
||||
dataset.built_in_field_enabled = False
|
||||
db.session.commit()
|
||||
session.commit()
|
||||
except Exception:
|
||||
logger.exception("Disable built-in field failed")
|
||||
finally:
|
||||
@ -231,6 +233,7 @@ class MetadataService:
|
||||
|
||||
@staticmethod
|
||||
def update_documents_metadata(
|
||||
session: Session,
|
||||
dataset: Dataset,
|
||||
metadata_args: MetadataOperationData,
|
||||
current_user: Account | None = None, # TODO: the service_api is not migrated yet
|
||||
@ -259,11 +262,11 @@ class MetadataService:
|
||||
doc_metadata[BuiltInField.last_update_date] = document.last_update_date.timestamp()
|
||||
doc_metadata[BuiltInField.source] = MetadataDataSource[document.data_source_type]
|
||||
document.doc_metadata = doc_metadata
|
||||
db.session.add(document)
|
||||
session.add(document)
|
||||
|
||||
# deal metadata binding (in the same transaction as the doc_metadata update)
|
||||
if not operation.partial_update:
|
||||
db.session.execute(
|
||||
session.execute(
|
||||
delete(DatasetMetadataBinding).where(
|
||||
DatasetMetadataBinding.document_id == operation.document_id
|
||||
)
|
||||
@ -272,7 +275,7 @@ class MetadataService:
|
||||
for metadata_value in operation.metadata_list:
|
||||
# check if binding already exists
|
||||
if operation.partial_update:
|
||||
existing_binding = db.session.scalar(
|
||||
existing_binding = session.scalar(
|
||||
select(DatasetMetadataBinding)
|
||||
.where(
|
||||
DatasetMetadataBinding.document_id == operation.document_id,
|
||||
@ -290,10 +293,10 @@ class MetadataService:
|
||||
metadata_id=metadata_value.id,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
db.session.add(dataset_metadata_binding)
|
||||
db.session.commit()
|
||||
session.add(dataset_metadata_binding)
|
||||
session.commit()
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
session.rollback()
|
||||
logger.exception("Update documents metadata failed")
|
||||
raise
|
||||
finally:
|
||||
@ -313,14 +316,14 @@ class MetadataService:
|
||||
redis_client.set(lock_key, 1, ex=3600)
|
||||
|
||||
@staticmethod
|
||||
def get_dataset_metadatas(dataset: Dataset):
|
||||
def get_dataset_metadatas(session: Session, dataset: Dataset):
|
||||
return {
|
||||
"doc_metadata": [
|
||||
{
|
||||
"id": item.get("id"),
|
||||
"name": item.get("name"),
|
||||
"type": item.get("type"),
|
||||
"count": db.session.scalar(
|
||||
"count": session.scalar(
|
||||
select(func.count(DatasetMetadataBinding.id)).where(
|
||||
DatasetMetadataBinding.metadata_id == item.get("id"),
|
||||
DatasetMetadataBinding.dataset_id == dataset.id,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""Controller integration tests for API key data source auth routes."""
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
from flask.testing import FlaskClient
|
||||
from sqlalchemy import select
|
||||
@ -85,7 +85,7 @@ def test_create_binding_successful(
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.get_json() == {"result": "success"}
|
||||
create_auth.assert_called_once_with(tenant_id, payload)
|
||||
create_auth.assert_called_once_with(ANY, tenant_id, payload)
|
||||
|
||||
|
||||
def test_create_binding_failure(
|
||||
|
||||
@ -51,7 +51,7 @@ class TestApiKeyAuthService:
|
||||
self._create_binding(db_session_with_containers, tenant_id=tenant_id, category=category, provider=provider)
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
result = ApiKeyAuthService.get_provider_auth_list(tenant_id)
|
||||
result = ApiKeyAuthService.get_provider_auth_list(db_session_with_containers, tenant_id)
|
||||
|
||||
assert len(result) >= 1
|
||||
tenant_results = [r for r in result if r.tenant_id == tenant_id]
|
||||
@ -61,7 +61,7 @@ class TestApiKeyAuthService:
|
||||
def test_get_provider_auth_list_empty(
|
||||
self, flask_app_with_containers: Flask, db_session_with_containers: Session, tenant_id
|
||||
):
|
||||
result = ApiKeyAuthService.get_provider_auth_list(tenant_id)
|
||||
result = ApiKeyAuthService.get_provider_auth_list(db_session_with_containers, tenant_id)
|
||||
|
||||
tenant_results = [r for r in result if r.tenant_id == tenant_id]
|
||||
assert tenant_results == []
|
||||
@ -74,7 +74,7 @@ class TestApiKeyAuthService:
|
||||
)
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
result = ApiKeyAuthService.get_provider_auth_list(tenant_id)
|
||||
result = ApiKeyAuthService.get_provider_auth_list(db_session_with_containers, tenant_id)
|
||||
|
||||
tenant_results = [r for r in result if r.tenant_id == tenant_id]
|
||||
assert tenant_results == []
|
||||
@ -95,7 +95,7 @@ class TestApiKeyAuthService:
|
||||
mock_factory.return_value = mock_auth_instance
|
||||
mock_encrypter.encrypt_token.return_value = "encrypted_test_key_123"
|
||||
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id, mock_args)
|
||||
ApiKeyAuthService.create_provider_auth(db_session_with_containers, tenant_id, mock_args)
|
||||
|
||||
mock_factory.assert_called_once()
|
||||
mock_auth_instance.validate_credentials.assert_called_once()
|
||||
@ -118,7 +118,7 @@ class TestApiKeyAuthService:
|
||||
mock_auth_instance.validate_credentials.return_value = False
|
||||
mock_factory.return_value = mock_auth_instance
|
||||
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id, mock_args)
|
||||
ApiKeyAuthService.create_provider_auth(db_session_with_containers, tenant_id, mock_args)
|
||||
|
||||
db_session_with_containers.expire_all()
|
||||
bindings = db_session_with_containers.query(DataSourceApiKeyAuthBinding).filter_by(tenant_id=tenant_id).all()
|
||||
@ -142,7 +142,7 @@ class TestApiKeyAuthService:
|
||||
|
||||
original_key = mock_args["credentials"]["config"]["api_key"]
|
||||
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id, mock_args)
|
||||
ApiKeyAuthService.create_provider_auth(db_session_with_containers, tenant_id, mock_args)
|
||||
|
||||
assert mock_args["credentials"]["config"]["api_key"] == "encrypted_test_key_123"
|
||||
assert mock_args["credentials"]["config"]["api_key"] != original_key
|
||||
@ -166,14 +166,14 @@ class TestApiKeyAuthService:
|
||||
)
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
result = ApiKeyAuthService.get_auth_credentials(tenant_id, category, provider)
|
||||
result = ApiKeyAuthService.get_auth_credentials(db_session_with_containers, tenant_id, category, provider)
|
||||
|
||||
assert result == mock_credentials
|
||||
|
||||
def test_get_auth_credentials_not_found(
|
||||
self, flask_app_with_containers: Flask, db_session_with_containers: Session, tenant_id, category, provider
|
||||
):
|
||||
result = ApiKeyAuthService.get_auth_credentials(tenant_id, category, provider)
|
||||
result = ApiKeyAuthService.get_auth_credentials(db_session_with_containers, tenant_id, category, provider)
|
||||
|
||||
assert result is None
|
||||
|
||||
@ -190,7 +190,7 @@ class TestApiKeyAuthService:
|
||||
)
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
result = ApiKeyAuthService.get_auth_credentials(tenant_id, category, provider)
|
||||
result = ApiKeyAuthService.get_auth_credentials(db_session_with_containers, tenant_id, category, provider)
|
||||
|
||||
assert result == special_credentials
|
||||
assert result["config"]["api_key"] == "key_with_中文_and_special_chars_!@#$%"
|
||||
@ -204,7 +204,7 @@ class TestApiKeyAuthService:
|
||||
binding_id = binding.id
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
ApiKeyAuthService.delete_provider_auth(tenant_id, binding_id)
|
||||
ApiKeyAuthService.delete_provider_auth(db_session_with_containers, tenant_id, binding_id)
|
||||
|
||||
db_session_with_containers.expire_all()
|
||||
remaining = db_session_with_containers.query(DataSourceApiKeyAuthBinding).filter_by(id=binding_id).first()
|
||||
@ -214,7 +214,7 @@ class TestApiKeyAuthService:
|
||||
self, flask_app_with_containers: Flask, db_session_with_containers: Session, tenant_id
|
||||
):
|
||||
# Should not raise when binding not found
|
||||
ApiKeyAuthService.delete_provider_auth(tenant_id, str(uuid4()))
|
||||
ApiKeyAuthService.delete_provider_auth(db_session_with_containers, tenant_id, str(uuid4()))
|
||||
|
||||
def test_validate_api_key_auth_args_success(self, mock_args):
|
||||
ApiKeyAuthService.validate_api_key_auth_args(mock_args)
|
||||
@ -288,16 +288,16 @@ class TestApiKeyAuthService:
|
||||
mock_factory.return_value = mock_auth_instance
|
||||
mock_encrypter.encrypt_token.return_value = "encrypted_key"
|
||||
|
||||
with patch("services.auth.api_key_auth_service.db.session") as mock_session:
|
||||
mock_session.commit.side_effect = Exception("Database error")
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id, mock_args)
|
||||
mock_session = MagicMock()
|
||||
mock_session.commit.side_effect = Exception("Database error")
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
ApiKeyAuthService.create_provider_auth(mock_session, tenant_id, mock_args)
|
||||
|
||||
@patch("services.auth.api_key_auth_service.ApiKeyAuthFactory")
|
||||
def test_create_provider_auth_factory_exception(self, mock_factory: MagicMock, tenant_id, mock_args):
|
||||
mock_factory.side_effect = Exception("Factory error")
|
||||
with pytest.raises(Exception, match="Factory error"):
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id, mock_args)
|
||||
ApiKeyAuthService.create_provider_auth(MagicMock(), tenant_id, mock_args)
|
||||
|
||||
@patch("services.auth.api_key_auth_service.ApiKeyAuthFactory")
|
||||
@patch("services.auth.api_key_auth_service.encrypter")
|
||||
@ -307,7 +307,7 @@ class TestApiKeyAuthService:
|
||||
mock_factory.return_value = mock_auth_instance
|
||||
mock_encrypter.encrypt_token.side_effect = Exception("Encryption error")
|
||||
with pytest.raises(Exception, match="Encryption error"):
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id, mock_args)
|
||||
ApiKeyAuthService.create_provider_auth(MagicMock(), tenant_id, mock_args)
|
||||
|
||||
def test_validate_api_key_auth_args_none_input(self):
|
||||
with pytest.raises(TypeError):
|
||||
|
||||
@ -13,6 +13,7 @@ import pytest
|
||||
from flask import Flask
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from extensions.ext_database import db
|
||||
from models.source import DataSourceApiKeyAuthBinding
|
||||
from services.auth.api_key_auth_factory import ApiKeyAuthFactory
|
||||
from services.auth.api_key_auth_service import ApiKeyAuthService
|
||||
@ -56,7 +57,7 @@ class TestAuthIntegration:
|
||||
mock_encrypt.return_value = "encrypted_fc_test_key_123"
|
||||
|
||||
args = {"category": category, "provider": AuthType.FIRECRAWL, "credentials": firecrawl_credentials}
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id_1, args)
|
||||
ApiKeyAuthService.create_provider_auth(db_session_with_containers, tenant_id_1, args)
|
||||
|
||||
mock_http.assert_called_once()
|
||||
call_args = mock_http.call_args
|
||||
@ -100,15 +101,15 @@ class TestAuthIntegration:
|
||||
mock_encrypt.return_value = "encrypted_key"
|
||||
|
||||
args1 = {"category": category, "provider": AuthType.FIRECRAWL, "credentials": firecrawl_credentials}
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id_1, args1)
|
||||
ApiKeyAuthService.create_provider_auth(db_session_with_containers, tenant_id_1, args1)
|
||||
|
||||
args2 = {"category": category, "provider": AuthType.JINA, "credentials": jina_credentials}
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id_2, args2)
|
||||
ApiKeyAuthService.create_provider_auth(db_session_with_containers, tenant_id_2, args2)
|
||||
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
result1 = ApiKeyAuthService.get_provider_auth_list(tenant_id_1)
|
||||
result2 = ApiKeyAuthService.get_provider_auth_list(tenant_id_2)
|
||||
result1 = ApiKeyAuthService.get_provider_auth_list(db_session_with_containers, tenant_id_1)
|
||||
result2 = ApiKeyAuthService.get_provider_auth_list(db_session_with_containers, tenant_id_2)
|
||||
|
||||
assert len(result1) == 1
|
||||
assert result1[0].tenant_id == tenant_id_1
|
||||
@ -118,7 +119,9 @@ class TestAuthIntegration:
|
||||
def test_cross_tenant_access_prevention(
|
||||
self, flask_app_with_containers: Flask, db_session_with_containers: Session, tenant_id_2, category
|
||||
):
|
||||
result = ApiKeyAuthService.get_auth_credentials(tenant_id_2, category, AuthType.FIRECRAWL)
|
||||
result = ApiKeyAuthService.get_auth_credentials(
|
||||
db_session_with_containers, tenant_id_2, category, AuthType.FIRECRAWL
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
@ -160,7 +163,7 @@ class TestAuthIntegration:
|
||||
"provider": AuthType.FIRECRAWL,
|
||||
"credentials": {"auth_type": "bearer", "config": {"api_key": "fc_test_key_123"}},
|
||||
}
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id_1, thread_args)
|
||||
ApiKeyAuthService.create_provider_auth(db.session(), tenant_id_1, thread_args)
|
||||
results.append("success")
|
||||
except Exception as e:
|
||||
exceptions.append(e)
|
||||
@ -213,7 +216,7 @@ class TestAuthIntegration:
|
||||
args = {"category": category, "provider": AuthType.FIRECRAWL, "credentials": firecrawl_credentials}
|
||||
|
||||
with pytest.raises(httpx.RequestError):
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id_1, args)
|
||||
ApiKeyAuthService.create_provider_auth(db_session_with_containers, tenant_id_1, args)
|
||||
|
||||
db_session_with_containers.expire_all()
|
||||
bindings = db_session_with_containers.query(DataSourceApiKeyAuthBinding).filter_by(tenant_id=tenant_id_1).all()
|
||||
@ -250,11 +253,13 @@ class TestAuthIntegration:
|
||||
mock_encrypt.return_value = "encrypted_key"
|
||||
|
||||
args = {"category": category, "provider": AuthType.FIRECRAWL, "credentials": firecrawl_credentials}
|
||||
ApiKeyAuthService.create_provider_auth(tenant_id_1, args)
|
||||
ApiKeyAuthService.create_provider_auth(db_session_with_containers, tenant_id_1, args)
|
||||
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
result = ApiKeyAuthService.get_auth_credentials(tenant_id_1, category, AuthType.FIRECRAWL)
|
||||
result = ApiKeyAuthService.get_auth_credentials(
|
||||
db_session_with_containers, tenant_id_1, category, AuthType.FIRECRAWL
|
||||
)
|
||||
assert result is not None
|
||||
assert result["config"]["api_key"] == "encrypted_key"
|
||||
|
||||
|
||||
@ -97,8 +97,9 @@ class TestFeedbackService:
|
||||
)
|
||||
|
||||
# Test CSV export
|
||||
result = FeedbackService.export_feedbacks(mock_db_session, app_id=sample_data["app"].id, format_type="csv")
|
||||
|
||||
result = FeedbackService.export_feedbacks(
|
||||
app_id=sample_data["app"].id, session=mock_db_session, format_type="csv"
|
||||
)
|
||||
# Verify response structure
|
||||
assert hasattr(result, "headers")
|
||||
assert "text/csv" in result.headers["Content-Type"]
|
||||
@ -128,7 +129,9 @@ class TestFeedbackService:
|
||||
)
|
||||
|
||||
# Test JSON export
|
||||
result = FeedbackService.export_feedbacks(mock_db_session, app_id=sample_data["app"].id, format_type="json")
|
||||
result = FeedbackService.export_feedbacks(
|
||||
app_id=sample_data["app"].id, session=mock_db_session, format_type="json"
|
||||
)
|
||||
|
||||
# Verify response structure
|
||||
assert hasattr(result, "headers")
|
||||
@ -158,8 +161,8 @@ class TestFeedbackService:
|
||||
|
||||
# Test with filters
|
||||
result = FeedbackService.export_feedbacks(
|
||||
mock_db_session,
|
||||
app_id=sample_data["app"].id,
|
||||
session=mock_db_session,
|
||||
from_source=FeedbackFromSource.ADMIN,
|
||||
rating=FeedbackRating.DISLIKE,
|
||||
has_comment=True,
|
||||
@ -175,7 +178,9 @@ class TestFeedbackService:
|
||||
"""Test exporting feedback when no data exists."""
|
||||
mock_db_session.execute.return_value = _execute_result([])
|
||||
|
||||
result = FeedbackService.export_feedbacks(mock_db_session, app_id=sample_data["app"].id, format_type="csv")
|
||||
result = FeedbackService.export_feedbacks(
|
||||
app_id=sample_data["app"].id, session=mock_db_session, format_type="csv"
|
||||
)
|
||||
|
||||
# Should return an empty CSV with headers only
|
||||
assert hasattr(result, "headers")
|
||||
@ -194,13 +199,13 @@ class TestFeedbackService:
|
||||
# Test with invalid start_date
|
||||
with pytest.raises(ValueError, match="Invalid start_date format"):
|
||||
FeedbackService.export_feedbacks(
|
||||
mock_db_session, app_id=sample_data["app"].id, start_date="invalid-date-format"
|
||||
app_id=sample_data["app"].id, session=mock_db_session, start_date="invalid-date-format"
|
||||
)
|
||||
|
||||
# Test with invalid end_date
|
||||
with pytest.raises(ValueError, match="Invalid end_date format"):
|
||||
FeedbackService.export_feedbacks(
|
||||
mock_db_session, app_id=sample_data["app"].id, end_date="invalid-date-format"
|
||||
app_id=sample_data["app"].id, session=mock_db_session, end_date="invalid-date-format"
|
||||
)
|
||||
|
||||
def test_export_feedbacks_invalid_format(self, mock_db_session, sample_data):
|
||||
@ -208,8 +213,8 @@ class TestFeedbackService:
|
||||
|
||||
with pytest.raises(ValueError, match="Unsupported format"):
|
||||
FeedbackService.export_feedbacks(
|
||||
mock_db_session,
|
||||
app_id=sample_data["app"].id,
|
||||
session=mock_db_session,
|
||||
format_type="xml", # Unsupported format
|
||||
)
|
||||
|
||||
@ -239,7 +244,9 @@ class TestFeedbackService:
|
||||
)
|
||||
|
||||
# Test export
|
||||
result = FeedbackService.export_feedbacks(mock_db_session, app_id=sample_data["app"].id, format_type="json")
|
||||
result = FeedbackService.export_feedbacks(
|
||||
app_id=sample_data["app"].id, session=mock_db_session, format_type="json"
|
||||
)
|
||||
|
||||
# Check JSON content
|
||||
json_content = json.loads(result.get_data(as_text=True))
|
||||
@ -290,7 +297,9 @@ class TestFeedbackService:
|
||||
)
|
||||
|
||||
# Test export
|
||||
result = FeedbackService.export_feedbacks(mock_db_session, app_id=sample_data["app"].id, format_type="csv")
|
||||
result = FeedbackService.export_feedbacks(
|
||||
app_id=sample_data["app"].id, session=mock_db_session, format_type="csv"
|
||||
)
|
||||
|
||||
# Check that unicode content is preserved
|
||||
csv_content = result.get_data(as_text=True)
|
||||
@ -320,7 +329,9 @@ class TestFeedbackService:
|
||||
)
|
||||
|
||||
# Test export
|
||||
result = FeedbackService.export_feedbacks(mock_db_session, app_id=sample_data["app"].id, format_type="json")
|
||||
result = FeedbackService.export_feedbacks(
|
||||
app_id=sample_data["app"].id, session=mock_db_session, format_type="json"
|
||||
)
|
||||
|
||||
# Check JSON content for emoji ratings
|
||||
json_content = json.loads(result.get_data(as_text=True))
|
||||
|
||||
@ -95,7 +95,7 @@ class TestMetadataPartialUpdate:
|
||||
)
|
||||
metadata_args = MetadataOperationData(operation_data=[operation])
|
||||
|
||||
MetadataService.update_documents_metadata(dataset, metadata_args, current_account)
|
||||
MetadataService.update_documents_metadata(db_session_with_containers, dataset, metadata_args, current_account)
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
updated_doc = db_session_with_containers.get(Document, document.id)
|
||||
@ -126,7 +126,7 @@ class TestMetadataPartialUpdate:
|
||||
)
|
||||
metadata_args = MetadataOperationData(operation_data=[operation])
|
||||
|
||||
MetadataService.update_documents_metadata(dataset, metadata_args, current_account)
|
||||
MetadataService.update_documents_metadata(db_session_with_containers, dataset, metadata_args, current_account)
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
updated_doc = db_session_with_containers.get(Document, document.id)
|
||||
@ -168,7 +168,7 @@ class TestMetadataPartialUpdate:
|
||||
)
|
||||
metadata_args = MetadataOperationData(operation_data=[operation])
|
||||
|
||||
MetadataService.update_documents_metadata(dataset, metadata_args, current_account)
|
||||
MetadataService.update_documents_metadata(db_session_with_containers, dataset, metadata_args, current_account)
|
||||
db_session_with_containers.expire_all()
|
||||
|
||||
bindings = db_session_with_containers.scalars(
|
||||
@ -202,6 +202,8 @@ class TestMetadataPartialUpdate:
|
||||
)
|
||||
metadata_args = MetadataOperationData(operation_data=[operation])
|
||||
|
||||
with patch("services.metadata_service.db.session.commit", side_effect=RuntimeError("database connection lost")):
|
||||
with patch.object(db_session_with_containers, "commit", side_effect=RuntimeError("database connection lost")):
|
||||
with pytest.raises(RuntimeError, match="database connection lost"):
|
||||
MetadataService.update_documents_metadata(dataset, metadata_args, current_account)
|
||||
MetadataService.update_documents_metadata(
|
||||
db_session_with_containers, dataset, metadata_args, current_account
|
||||
)
|
||||
|
||||
@ -183,7 +183,9 @@ class TestMetadataService:
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
result = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -218,7 +220,7 @@ class TestMetadataService:
|
||||
|
||||
# Act & Assert: Verify proper error handling
|
||||
with pytest.raises(ValueError, match="Metadata name cannot exceed 255 characters."):
|
||||
MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
MetadataService.create_metadata(db_session_with_containers, dataset.id, metadata_args, account, tenant.id)
|
||||
|
||||
def test_create_metadata_name_already_exists(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps
|
||||
@ -236,14 +238,16 @@ class TestMetadataService:
|
||||
|
||||
# Create first metadata
|
||||
first_metadata_args = MetadataArgs(type="string", name="duplicate_name")
|
||||
MetadataService.create_metadata(dataset.id, first_metadata_args, account, tenant.id)
|
||||
MetadataService.create_metadata(db_session_with_containers, dataset.id, first_metadata_args, account, tenant.id)
|
||||
|
||||
# Try to create second metadata with same name
|
||||
second_metadata_args = MetadataArgs(type="number", name="duplicate_name")
|
||||
|
||||
# Act & Assert: Verify proper error handling
|
||||
with pytest.raises(ValueError, match="Metadata name already exists."):
|
||||
MetadataService.create_metadata(dataset.id, second_metadata_args, account, tenant.id)
|
||||
MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, second_metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
def test_create_metadata_name_conflicts_with_built_in_field(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps
|
||||
@ -265,7 +269,7 @@ class TestMetadataService:
|
||||
|
||||
# Act & Assert: Verify proper error handling
|
||||
with pytest.raises(ValueError, match="Metadata name already exists in Built-in fields."):
|
||||
MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
MetadataService.create_metadata(db_session_with_containers, dataset.id, metadata_args, account, tenant.id)
|
||||
|
||||
def test_update_metadata_name_success(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps
|
||||
@ -283,11 +287,15 @@ class TestMetadataService:
|
||||
|
||||
# Create metadata first
|
||||
metadata_args = MetadataArgs(type="string", name="old_name")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Act: Execute the method under test
|
||||
new_name = "new_name"
|
||||
result = MetadataService.update_metadata_name(dataset.id, metadata.id, new_name, account, tenant.id)
|
||||
result = MetadataService.update_metadata_name(
|
||||
db_session_with_containers, dataset.id, metadata.id, new_name, account, tenant.id
|
||||
)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -316,14 +324,18 @@ class TestMetadataService:
|
||||
|
||||
# Create metadata first
|
||||
metadata_args = MetadataArgs(type="string", name="old_name")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Try to update with too long name
|
||||
long_name = "a" * 256 # 256 characters, exceeding 255 limit
|
||||
|
||||
# Act & Assert: Verify proper error handling
|
||||
with pytest.raises(ValueError, match="Metadata name cannot exceed 255 characters."):
|
||||
MetadataService.update_metadata_name(dataset.id, metadata.id, long_name, account, tenant.id)
|
||||
MetadataService.update_metadata_name(
|
||||
db_session_with_containers, dataset.id, metadata.id, long_name, account, tenant.id
|
||||
)
|
||||
|
||||
def test_update_metadata_name_already_exists(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps
|
||||
@ -341,14 +353,20 @@ class TestMetadataService:
|
||||
|
||||
# Create two metadata entries
|
||||
first_metadata_args = MetadataArgs(type="string", name="first_metadata")
|
||||
first_metadata = MetadataService.create_metadata(dataset.id, first_metadata_args, account, tenant.id)
|
||||
first_metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, first_metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
second_metadata_args = MetadataArgs(type="number", name="second_metadata")
|
||||
second_metadata = MetadataService.create_metadata(dataset.id, second_metadata_args, account, tenant.id)
|
||||
second_metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, second_metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Try to update first metadata with second metadata's name
|
||||
with pytest.raises(ValueError, match="Metadata name already exists."):
|
||||
MetadataService.update_metadata_name(dataset.id, first_metadata.id, "second_metadata", account, tenant.id)
|
||||
MetadataService.update_metadata_name(
|
||||
db_session_with_containers, dataset.id, first_metadata.id, "second_metadata", account, tenant.id
|
||||
)
|
||||
|
||||
def test_update_metadata_name_conflicts_with_built_in_field(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps
|
||||
@ -366,13 +384,17 @@ class TestMetadataService:
|
||||
|
||||
# Create metadata first
|
||||
metadata_args = MetadataArgs(type="string", name="old_name")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Try to update with built-in field name
|
||||
built_in_field_name = BuiltInField.document_name
|
||||
|
||||
with pytest.raises(ValueError, match="Metadata name already exists in Built-in fields."):
|
||||
MetadataService.update_metadata_name(dataset.id, metadata.id, built_in_field_name, account, tenant.id)
|
||||
MetadataService.update_metadata_name(
|
||||
db_session_with_containers, dataset.id, metadata.id, built_in_field_name, account, tenant.id
|
||||
)
|
||||
|
||||
def test_update_metadata_name_not_found(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps
|
||||
@ -395,7 +417,9 @@ class TestMetadataService:
|
||||
new_name = "new_name"
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = MetadataService.update_metadata_name(dataset.id, fake_metadata_id, new_name, account, tenant.id)
|
||||
result = MetadataService.update_metadata_name(
|
||||
db_session_with_containers, dataset.id, fake_metadata_id, new_name, account, tenant.id
|
||||
)
|
||||
|
||||
# Assert: Verify the method returns None when metadata is not found
|
||||
assert result is None
|
||||
@ -416,10 +440,12 @@ class TestMetadataService:
|
||||
|
||||
# Create metadata first
|
||||
metadata_args = MetadataArgs(type="string", name="to_be_deleted")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = MetadataService.delete_metadata(dataset.id, metadata.id)
|
||||
result = MetadataService.delete_metadata(db_session_with_containers, dataset.id, metadata.id)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -450,7 +476,7 @@ class TestMetadataService:
|
||||
fake_metadata_id = str(uuid.uuid4()) # Use valid UUID format
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = MetadataService.delete_metadata(dataset.id, fake_metadata_id)
|
||||
result = MetadataService.delete_metadata(db_session_with_containers, dataset.id, fake_metadata_id)
|
||||
|
||||
# Assert: Verify the method returns None when metadata is not found
|
||||
assert result is None
|
||||
@ -474,7 +500,9 @@ class TestMetadataService:
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Create metadata binding
|
||||
binding = DatasetMetadataBinding(
|
||||
@ -494,7 +522,7 @@ class TestMetadataService:
|
||||
db_session_with_containers.commit()
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = MetadataService.delete_metadata(dataset.id, metadata.id)
|
||||
result = MetadataService.delete_metadata(db_session_with_containers, dataset.id, metadata.id)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -559,7 +587,7 @@ class TestMetadataService:
|
||||
assert dataset.built_in_field_enabled is False
|
||||
|
||||
# Act: Execute the method under test
|
||||
MetadataService.enable_built_in_field(dataset)
|
||||
MetadataService.enable_built_in_field(db_session_with_containers, dataset)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
|
||||
@ -595,7 +623,7 @@ class TestMetadataService:
|
||||
]()
|
||||
|
||||
# Act: Execute the method under test
|
||||
MetadataService.enable_built_in_field(dataset)
|
||||
MetadataService.enable_built_in_field(db_session_with_containers, dataset)
|
||||
|
||||
# Assert: Verify the method returns early without changes
|
||||
db_session_with_containers.refresh(dataset)
|
||||
@ -621,7 +649,7 @@ class TestMetadataService:
|
||||
]()
|
||||
|
||||
# Act: Execute the method under test
|
||||
MetadataService.enable_built_in_field(dataset)
|
||||
MetadataService.enable_built_in_field(db_session_with_containers, dataset)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
|
||||
@ -668,7 +696,7 @@ class TestMetadataService:
|
||||
]
|
||||
|
||||
# Act: Execute the method under test
|
||||
MetadataService.disable_built_in_field(dataset)
|
||||
MetadataService.disable_built_in_field(db_session_with_containers, dataset)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
db_session_with_containers.refresh(dataset)
|
||||
@ -700,7 +728,7 @@ class TestMetadataService:
|
||||
]()
|
||||
|
||||
# Act: Execute the method under test
|
||||
MetadataService.disable_built_in_field(dataset)
|
||||
MetadataService.disable_built_in_field(db_session_with_containers, dataset)
|
||||
|
||||
# Assert: Verify the method returns early without changes
|
||||
|
||||
@ -733,7 +761,7 @@ class TestMetadataService:
|
||||
]()
|
||||
|
||||
# Act: Execute the method under test
|
||||
MetadataService.disable_built_in_field(dataset)
|
||||
MetadataService.disable_built_in_field(db_session_with_containers, dataset)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
db_session_with_containers.refresh(dataset)
|
||||
@ -758,7 +786,9 @@ class TestMetadataService:
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Mock DocumentService.get_document
|
||||
mock_external_service_dependencies["document_service"].get_document.return_value = document
|
||||
@ -777,7 +807,7 @@ class TestMetadataService:
|
||||
operation_data = MetadataOperationData(operation_data=[operation])
|
||||
|
||||
# Act: Execute the method under test
|
||||
MetadataService.update_documents_metadata(dataset, operation_data, account)
|
||||
MetadataService.update_documents_metadata(db_session_with_containers, dataset, operation_data, account)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
|
||||
@ -822,7 +852,9 @@ class TestMetadataService:
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Mock DocumentService.get_document
|
||||
mock_external_service_dependencies["document_service"].get_document.return_value = document
|
||||
@ -841,7 +873,7 @@ class TestMetadataService:
|
||||
operation_data = MetadataOperationData(operation_data=[operation])
|
||||
|
||||
# Act: Execute the method under test
|
||||
MetadataService.update_documents_metadata(dataset, operation_data, account)
|
||||
MetadataService.update_documents_metadata(db_session_with_containers, dataset, operation_data, account)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
# Verify document metadata was updated with both custom and built-in fields
|
||||
@ -869,7 +901,9 @@ class TestMetadataService:
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Create metadata operation data
|
||||
from services.entities.knowledge_entities.knowledge_entities import (
|
||||
@ -890,7 +924,7 @@ class TestMetadataService:
|
||||
# Act & Assert: The method should raise ValueError("Document not found.")
|
||||
# because the exception is now re-raised after rollback
|
||||
with pytest.raises(ValueError, match="Document not found"):
|
||||
MetadataService.update_documents_metadata(dataset, operation_data, account)
|
||||
MetadataService.update_documents_metadata(db_session_with_containers, dataset, operation_data, account)
|
||||
|
||||
def test_knowledge_base_metadata_lock_check_dataset_id(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps
|
||||
@ -986,7 +1020,9 @@ class TestMetadataService:
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Create document and metadata binding
|
||||
document = self._create_test_document(
|
||||
@ -1005,7 +1041,7 @@ class TestMetadataService:
|
||||
db_session_with_containers.commit()
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = MetadataService.get_dataset_metadatas(dataset)
|
||||
result = MetadataService.get_dataset_metadatas(db_session_with_containers, dataset)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -1045,10 +1081,12 @@ class TestMetadataService:
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id)
|
||||
metadata = MetadataService.create_metadata(
|
||||
db_session_with_containers, dataset.id, metadata_args, account, tenant.id
|
||||
)
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = MetadataService.get_dataset_metadatas(dataset)
|
||||
result = MetadataService.get_dataset_metadatas(db_session_with_containers, dataset)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -1077,7 +1115,7 @@ class TestMetadataService:
|
||||
)
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = MetadataService.get_dataset_metadatas(dataset)
|
||||
result = MetadataService.get_dataset_metadatas(db_session_with_containers, dataset)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
|
||||
@ -15,6 +15,7 @@ from controllers.console.agent.composer import (
|
||||
AgentComposerValidateApi,
|
||||
WorkflowAgentComposerApi,
|
||||
WorkflowAgentComposerCandidatesApi,
|
||||
WorkflowAgentComposerCopyFromRosterApi,
|
||||
WorkflowAgentComposerImpactApi,
|
||||
WorkflowAgentComposerSaveToRosterApi,
|
||||
WorkflowAgentComposerValidateApi,
|
||||
@ -1017,6 +1018,58 @@ def test_workflow_composer_get_put_validate_candidates_impact_and_save(
|
||||
)["save_options"] == ["node_job_only"]
|
||||
|
||||
|
||||
def test_workflow_composer_copy_from_roster(app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str) -> None:
|
||||
app_model = SimpleNamespace(id="app-1")
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_copy_from_roster(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return _workflow_composer_response(
|
||||
binding={
|
||||
"id": "binding-1",
|
||||
"binding_type": "inline_agent",
|
||||
"agent_id": "inline-agent-1",
|
||||
"current_snapshot_id": "inline-version-1",
|
||||
"workflow_id": "workflow-1",
|
||||
"node_id": kwargs["node_id"],
|
||||
},
|
||||
agent={
|
||||
"id": "inline-agent-1",
|
||||
"name": "Nadia",
|
||||
"description": "",
|
||||
"scope": "workflow_only",
|
||||
"status": "active",
|
||||
},
|
||||
active_config_snapshot={"id": "inline-version-1", "version": 1},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
composer_controller.AgentComposerService, "copy_workflow_composer_from_roster", fake_copy_from_roster
|
||||
)
|
||||
|
||||
with app.test_request_context(
|
||||
json={
|
||||
"source_agent_id": "roster-agent-1",
|
||||
"source_snapshot_id": "roster-version-1",
|
||||
"idempotency_key": "copy-1",
|
||||
}
|
||||
):
|
||||
result = unwrap(WorkflowAgentComposerCopyFromRosterApi.post)(
|
||||
WorkflowAgentComposerCopyFromRosterApi(), "tenant-1", account_id, app_model, "node-1"
|
||||
)
|
||||
|
||||
assert result["binding"]["binding_type"] == "inline_agent"
|
||||
assert captured == {
|
||||
"tenant_id": "tenant-1",
|
||||
"app_id": "app-1",
|
||||
"node_id": "node-1",
|
||||
"account_id": account_id,
|
||||
"source_agent_id": "roster-agent-1",
|
||||
"source_snapshot_id": "roster-version-1",
|
||||
"idempotency_key": "copy-1",
|
||||
}
|
||||
|
||||
|
||||
def test_workflow_impact_returns_empty_without_version(app: Flask) -> None:
|
||||
payload = {"variant": ComposerVariant.WORKFLOW.value, "save_strategy": ComposerSaveStrategy.NODE_JOB_ONLY.value}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from datetime import UTC, datetime
|
||||
from inspect import unwrap
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import PropertyMock, patch
|
||||
from unittest.mock import ANY, PropertyMock, patch
|
||||
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.auth.data_source_bearer_auth import (
|
||||
@ -34,13 +34,16 @@ def test_list_data_source_auth_uses_injected_tenant_id() -> None:
|
||||
updated_at=datetime(2026, 1, 2, tzinfo=UTC),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"controllers.console.auth.data_source_bearer_auth.ApiKeyAuthService.get_provider_auth_list",
|
||||
return_value=[binding],
|
||||
) as get_provider_auth_list:
|
||||
with (
|
||||
patch("controllers.console.auth.data_source_bearer_auth.db"),
|
||||
patch(
|
||||
"controllers.console.auth.data_source_bearer_auth.ApiKeyAuthService.get_provider_auth_list",
|
||||
return_value=[binding],
|
||||
) as get_provider_auth_list,
|
||||
):
|
||||
result = method(api, "tenant-1")
|
||||
|
||||
get_provider_auth_list.assert_called_once_with("tenant-1")
|
||||
get_provider_auth_list.assert_called_once_with(ANY, "tenant-1")
|
||||
assert result["sources"][0]["id"] == "binding-1"
|
||||
assert result["sources"][0]["provider"] == "custom"
|
||||
|
||||
@ -56,12 +59,13 @@ def test_create_data_source_auth_binding_uses_injected_tenant_id() -> None:
|
||||
|
||||
with (
|
||||
_payload_patch(payload),
|
||||
patch("controllers.console.auth.data_source_bearer_auth.db"),
|
||||
patch("controllers.console.auth.data_source_bearer_auth.ApiKeyAuthService.validate_api_key_auth_args"),
|
||||
patch("controllers.console.auth.data_source_bearer_auth.ApiKeyAuthService.create_provider_auth") as create_auth,
|
||||
):
|
||||
result, status = method(api, "tenant-1")
|
||||
|
||||
create_auth.assert_called_once_with("tenant-1", payload)
|
||||
create_auth.assert_called_once_with(ANY, "tenant-1", payload)
|
||||
assert result == {"result": "success"}
|
||||
assert status == 200
|
||||
|
||||
@ -70,11 +74,14 @@ def test_delete_data_source_auth_binding_uses_injected_tenant_id() -> None:
|
||||
api = ApiKeyAuthDataSourceBindingDelete()
|
||||
method = unwrap(api.delete)
|
||||
|
||||
with patch(
|
||||
"controllers.console.auth.data_source_bearer_auth.ApiKeyAuthService.delete_provider_auth"
|
||||
) as delete_provider_auth:
|
||||
with (
|
||||
patch("controllers.console.auth.data_source_bearer_auth.db"),
|
||||
patch(
|
||||
"controllers.console.auth.data_source_bearer_auth.ApiKeyAuthService.delete_provider_auth"
|
||||
) as delete_provider_auth,
|
||||
):
|
||||
result, status = method(api, "tenant-1", "binding-1")
|
||||
|
||||
delete_provider_auth.assert_called_once_with("tenant-1", "binding-1")
|
||||
delete_provider_auth.assert_called_once_with(ANY, "tenant-1", "binding-1")
|
||||
assert result == ""
|
||||
assert status == 204
|
||||
|
||||
@ -17,7 +17,7 @@ Decorator strategy:
|
||||
|
||||
import uuid
|
||||
from inspect import unwrap
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import ANY, Mock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
@ -408,7 +408,7 @@ class TestDatasetMetadataBuiltInFieldAction:
|
||||
|
||||
assert status == 200
|
||||
assert response["result"] == "success"
|
||||
mock_meta_svc.enable_built_in_field.assert_called_once_with(mock_dataset)
|
||||
mock_meta_svc.enable_built_in_field.assert_called_once_with(ANY, mock_dataset)
|
||||
|
||||
@patch("controllers.service_api.dataset.metadata.MetadataService")
|
||||
@patch("controllers.service_api.dataset.metadata.DatasetService")
|
||||
@ -439,7 +439,7 @@ class TestDatasetMetadataBuiltInFieldAction:
|
||||
)
|
||||
|
||||
assert status == 200
|
||||
mock_meta_svc.disable_built_in_field.assert_called_once_with(mock_dataset)
|
||||
mock_meta_svc.disable_built_in_field.assert_called_once_with(ANY, mock_dataset)
|
||||
|
||||
@patch("controllers.service_api.dataset.metadata.DatasetService")
|
||||
def test_action_dataset_not_found(
|
||||
|
||||
@ -144,6 +144,22 @@ class TestAgentAppRuntimeRequestBuilder:
|
||||
assert result.redacted_request["composition"]["layers"][-1]["config"]["credentials"] == "[REDACTED]"
|
||||
assert result.metadata["conversation_id"] == "conv-1"
|
||||
|
||||
def test_build_normalizes_marketplace_model_plugin_id(self):
|
||||
soul = _soul_with_model()
|
||||
soul.model.plugin_id = (
|
||||
"langgenius/openai:0.4.2@21195ee1321849e0a7d4b3f6b2fd8c2be23ea6c7182e1b444ecc4c1711b52468"
|
||||
)
|
||||
builder = AgentAppRuntimeRequestBuilder(
|
||||
credentials_provider=_FakeCredentialsProvider(),
|
||||
plugin_tools_builder=_NoToolsBuilder(), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
result = builder.build(_ctx(soul))
|
||||
|
||||
llm = next(layer for layer in result.request.composition.layers if layer.name == "llm")
|
||||
assert llm.config.plugin_id == "langgenius/openai"
|
||||
assert llm.config.model_provider == "openai"
|
||||
|
||||
def test_build_maps_agent_soul_knowledge_to_knowledge_layer(self):
|
||||
soul = AgentSoulConfig.model_validate(
|
||||
{
|
||||
|
||||
@ -189,7 +189,7 @@ def test_normalizes_langgenius_model_provider_for_agent_backend_transport():
|
||||
context.snapshot.config_snapshot = AgentSoulConfig(
|
||||
prompt={"system_prompt": "You are careful."},
|
||||
model=AgentSoulModelConfig(
|
||||
plugin_id="langgenius/openai/openai",
|
||||
plugin_id="langgenius/openai:0.4.2@21195ee1321849e0a7d4b3f6b2fd8c2be23ea6c7182e1b444ecc4c1711b52468",
|
||||
model_provider="langgenius/openai/openai",
|
||||
model="gpt-test",
|
||||
),
|
||||
|
||||
@ -105,6 +105,28 @@ def test_agent_app_soul_allows_app_features_and_variables():
|
||||
assert payload.agent_soul.app_variables[0].name == "company_name"
|
||||
|
||||
|
||||
def test_composer_save_payload_accepts_new_roster_metadata():
|
||||
payload = ComposerSavePayload.model_validate(
|
||||
{
|
||||
"variant": ComposerVariant.WORKFLOW,
|
||||
"save_strategy": ComposerSaveStrategy.SAVE_TO_ROSTER,
|
||||
"new_agent_name": "Research Agent",
|
||||
"description": "Finds relevant sources.",
|
||||
"role": "Research Assistant",
|
||||
"icon_type": "emoji",
|
||||
"icon": "search",
|
||||
"icon_background": "#E0F2FE",
|
||||
}
|
||||
)
|
||||
|
||||
assert payload.new_agent_name == "Research Agent"
|
||||
assert payload.description == "Finds relevant sources."
|
||||
assert payload.role == "Research Assistant"
|
||||
assert payload.icon_type == "emoji"
|
||||
assert payload.icon == "search"
|
||||
assert payload.icon_background == "#E0F2FE"
|
||||
|
||||
|
||||
def test_knowledge_query_mode_uses_stable_backend_enums():
|
||||
config = AgentSoulConfig.model_validate(
|
||||
{
|
||||
|
||||
@ -3,6 +3,7 @@ from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from core.workflow.nodes.agent_v2.validators import WorkflowAgentNodeValidationError
|
||||
from models.agent import (
|
||||
@ -10,6 +11,7 @@ from models.agent import (
|
||||
AgentConfigRevisionOperation,
|
||||
AgentConfigSnapshot,
|
||||
AgentDebugConversation,
|
||||
AgentDriveFile,
|
||||
AgentKind,
|
||||
AgentScope,
|
||||
AgentSource,
|
||||
@ -31,7 +33,12 @@ from services.agent import composer_service, roster_service
|
||||
from services.agent.agent_soul_state import agent_soul_has_model
|
||||
from services.agent.composer_service import AgentComposerService
|
||||
from services.agent.composer_validator import ComposerConfigValidator
|
||||
from services.agent.errors import InvalidComposerConfigError
|
||||
from services.agent.errors import (
|
||||
AgentNameConflictError,
|
||||
AgentNotFoundError,
|
||||
AgentVersionConflictError,
|
||||
InvalidComposerConfigError,
|
||||
)
|
||||
from services.agent.roster_service import AgentRosterService
|
||||
from services.agent.workflow_publish_service import WorkflowAgentPublishService
|
||||
from services.app_service import AppListParams, AppService
|
||||
@ -415,9 +422,34 @@ def test_composer_save_helpers_create_and_rebind_agents(monkeypatch: pytest.Monk
|
||||
fake_session = FakeSession()
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
workflow_agent = SimpleNamespace(id="inline-agent-1", active_config_snapshot_id="inline-version-1")
|
||||
roster_agent = SimpleNamespace(id="roster-agent-1", active_config_snapshot_id="roster-version-1", name="Roster")
|
||||
roster_agent = SimpleNamespace(
|
||||
id="roster-agent-1",
|
||||
active_config_snapshot_id="roster-version-1",
|
||||
name="Roster",
|
||||
description="Source description",
|
||||
role="Source role",
|
||||
icon_type="emoji",
|
||||
icon="source",
|
||||
icon_background="#FFFFFF",
|
||||
)
|
||||
create_roster_calls = []
|
||||
copy_drive_calls = []
|
||||
monkeypatch.setattr(AgentComposerService, "_create_workflow_only_agent", lambda **kwargs: workflow_agent)
|
||||
monkeypatch.setattr(AgentComposerService, "_create_roster_agent_for_composer", lambda **kwargs: roster_agent)
|
||||
|
||||
def fake_create_roster_agent_for_composer(**kwargs):
|
||||
create_roster_calls.append(kwargs)
|
||||
return roster_agent
|
||||
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
"_create_roster_agent_for_composer",
|
||||
fake_create_roster_agent_for_composer,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
"_copy_agent_drive_rows",
|
||||
lambda **kwargs: copy_drive_calls.append(kwargs),
|
||||
)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_agent", lambda **kwargs: roster_agent)
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
@ -443,6 +475,11 @@ def test_composer_save_helpers_create_and_rebind_agents(monkeypatch: pytest.Monk
|
||||
"agent_soul": {"prompt": {"system_prompt": "new"}},
|
||||
"node_job": {"workflow_prompt": "use prior output"},
|
||||
"new_agent_name": "Copied Agent",
|
||||
"description": "Copied description",
|
||||
"role": "Copied role",
|
||||
"icon_type": "emoji",
|
||||
"icon": "copied",
|
||||
"icon_background": "#E0F2FE",
|
||||
}
|
||||
)
|
||||
existing_binding = WorkflowAgentNodeBinding(agent_id="inline-agent-1", current_snapshot_id="inline-version-1")
|
||||
@ -500,6 +537,24 @@ def test_composer_save_helpers_create_and_rebind_agents(monkeypatch: pytest.Monk
|
||||
assert new_agent_binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT
|
||||
assert save_to_roster_binding.agent_id == "roster-agent-1"
|
||||
assert new_version_binding.current_snapshot_id == "new-version-1"
|
||||
assert create_roster_calls[0]["description"] == "Copied description"
|
||||
assert create_roster_calls[0]["role"] == "Copied role"
|
||||
assert create_roster_calls[0]["icon"] == "copied"
|
||||
assert create_roster_calls[0]["icon_background"] == "#E0F2FE"
|
||||
assert create_roster_calls[1]["description"] == "Copied description"
|
||||
assert create_roster_calls[1]["role"] == "Copied role"
|
||||
assert create_roster_calls[1]["icon"] == "copied"
|
||||
assert create_roster_calls[1]["icon_background"] == "#E0F2FE"
|
||||
assert copy_drive_calls == [
|
||||
{
|
||||
"tenant_id": "tenant-1",
|
||||
"source_agent_id": "roster-agent-1",
|
||||
"target_agent_id": "roster-agent-1",
|
||||
"account_id": "account-1",
|
||||
"agent_soul": payload.agent_soul,
|
||||
"node_job": payload.node_job,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_node_job_only_updates_inline_agent_soul(monkeypatch: pytest.MonkeyPatch):
|
||||
@ -715,9 +770,464 @@ def test_node_job_only_rejects_inline_binding_pointing_to_roster_agent(monkeypat
|
||||
)
|
||||
|
||||
|
||||
def test_copy_workflow_composer_from_roster_creates_inline_agent_and_preserves_node_job(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
fake_session = FakeSession()
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
workflow = SimpleNamespace(id="workflow-1")
|
||||
node_job = WorkflowNodeJobConfig(workflow_prompt="keep this node task")
|
||||
binding = WorkflowAgentNodeBinding(
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
workflow_id="workflow-1",
|
||||
workflow_version="draft",
|
||||
node_id="node-1",
|
||||
binding_type=WorkflowAgentBindingType.ROSTER_AGENT,
|
||||
agent_id="roster-agent-1",
|
||||
current_snapshot_id="old-roster-version",
|
||||
node_job_config=node_job,
|
||||
)
|
||||
roster_agent = Agent(
|
||||
id="roster-agent-1",
|
||||
tenant_id="tenant-1",
|
||||
name="Nadia",
|
||||
description="Clarification Drafter",
|
||||
role="Clarifies tenders",
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=AgentStatus.ACTIVE,
|
||||
active_config_snapshot_id="roster-version-2",
|
||||
)
|
||||
source_version = AgentConfigSnapshot(
|
||||
id="roster-version-2",
|
||||
tenant_id="tenant-1",
|
||||
agent_id="roster-agent-1",
|
||||
version=2,
|
||||
config_snapshot='{"prompt":{"system_prompt":"copy me"}}',
|
||||
)
|
||||
inline_agent = Agent(
|
||||
id="inline-agent-1",
|
||||
tenant_id="tenant-1",
|
||||
name="Nadia",
|
||||
description="Clarification Drafter",
|
||||
role="Clarifies tenders",
|
||||
scope=AgentScope.WORKFLOW_ONLY,
|
||||
source=AgentSource.WORKFLOW,
|
||||
status=AgentStatus.ACTIVE,
|
||||
active_config_snapshot_id="inline-version-1",
|
||||
)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
monkeypatch.setattr(AgentComposerService, "_get_draft_workflow", lambda **kwargs: workflow)
|
||||
monkeypatch.setattr(AgentComposerService, "_get_workflow_binding", lambda **kwargs: binding)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_agent", lambda **kwargs: roster_agent)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_version", lambda **kwargs: source_version)
|
||||
|
||||
def fake_create_workflow_only_agent(**kwargs):
|
||||
captured["create"] = kwargs
|
||||
return inline_agent
|
||||
|
||||
def fake_copy_drive_rows(**kwargs):
|
||||
captured["drive"] = kwargs
|
||||
|
||||
monkeypatch.setattr(AgentComposerService, "_create_workflow_only_agent", fake_create_workflow_only_agent)
|
||||
monkeypatch.setattr(AgentComposerService, "_copy_agent_drive_rows", fake_copy_drive_rows)
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
"_serialize_workflow_state",
|
||||
lambda **kwargs: {
|
||||
"binding": {
|
||||
"binding_type": kwargs["binding"].binding_type.value,
|
||||
"agent_id": kwargs["binding"].agent_id,
|
||||
"current_snapshot_id": kwargs["binding"].current_snapshot_id,
|
||||
},
|
||||
"node_job": kwargs["binding"].node_job_config_dict,
|
||||
},
|
||||
)
|
||||
|
||||
state = AgentComposerService.copy_workflow_composer_from_roster(
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
node_id="node-1",
|
||||
account_id="account-1",
|
||||
source_agent_id="roster-agent-1",
|
||||
source_snapshot_id="roster-version-2",
|
||||
)
|
||||
|
||||
assert state["binding"]["binding_type"] == WorkflowAgentBindingType.INLINE_AGENT.value
|
||||
assert state["binding"]["agent_id"] == "inline-agent-1"
|
||||
assert state["node_job"]["workflow_prompt"] == "keep this node task"
|
||||
assert binding.node_job_config is node_job
|
||||
create_kwargs = captured["create"]
|
||||
assert create_kwargs["agent_soul"].prompt.system_prompt == "copy me"
|
||||
assert create_kwargs["name"] == "Nadia"
|
||||
assert create_kwargs["role"] == "Clarifies tenders"
|
||||
drive_kwargs = captured["drive"]
|
||||
assert drive_kwargs["source_agent_id"] == "roster-agent-1"
|
||||
assert drive_kwargs["target_agent_id"] == "inline-agent-1"
|
||||
assert fake_session.commits == 1
|
||||
|
||||
|
||||
def test_copy_workflow_composer_from_roster_rejects_stale_source_snapshot(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(AgentComposerService, "_get_draft_workflow", lambda **kwargs: SimpleNamespace(id="workflow-1"))
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
"_get_workflow_binding",
|
||||
lambda **kwargs: WorkflowAgentNodeBinding(
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
workflow_id="workflow-1",
|
||||
workflow_version="draft",
|
||||
node_id="node-1",
|
||||
binding_type=WorkflowAgentBindingType.ROSTER_AGENT,
|
||||
agent_id="roster-agent-1",
|
||||
current_snapshot_id="roster-version-1",
|
||||
node_job_config=WorkflowNodeJobConfig(),
|
||||
),
|
||||
)
|
||||
roster_agent = Agent(
|
||||
id="roster-agent-1",
|
||||
tenant_id="tenant-1",
|
||||
name="Nadia",
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=AgentStatus.ACTIVE,
|
||||
active_config_snapshot_id="roster-version-2",
|
||||
)
|
||||
source_version = AgentConfigSnapshot(
|
||||
id="roster-version-2",
|
||||
tenant_id="tenant-1",
|
||||
agent_id="roster-agent-1",
|
||||
version=2,
|
||||
config_snapshot='{"prompt":{"system_prompt":"copy me"}}',
|
||||
)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_agent", lambda **kwargs: roster_agent)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_version", lambda **kwargs: source_version)
|
||||
|
||||
with pytest.raises(AgentVersionConflictError):
|
||||
AgentComposerService.copy_workflow_composer_from_roster(
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
node_id="node-1",
|
||||
account_id="account-1",
|
||||
source_agent_id="roster-agent-1",
|
||||
source_snapshot_id="roster-version-1",
|
||||
)
|
||||
|
||||
|
||||
def test_copy_workflow_composer_from_roster_is_idempotent_when_already_inline(monkeypatch: pytest.MonkeyPatch):
|
||||
inline_binding = WorkflowAgentNodeBinding(
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
workflow_id="workflow-1",
|
||||
workflow_version="draft",
|
||||
node_id="node-1",
|
||||
binding_type=WorkflowAgentBindingType.INLINE_AGENT,
|
||||
agent_id="inline-agent-1",
|
||||
current_snapshot_id="inline-version-1",
|
||||
)
|
||||
inline_agent = Agent(
|
||||
id="inline-agent-1",
|
||||
tenant_id="tenant-1",
|
||||
name="Inline",
|
||||
scope=AgentScope.WORKFLOW_ONLY,
|
||||
source=AgentSource.WORKFLOW,
|
||||
status=AgentStatus.ACTIVE,
|
||||
active_config_snapshot_id="inline-version-1",
|
||||
)
|
||||
inline_version = AgentConfigSnapshot(
|
||||
id="inline-version-1",
|
||||
tenant_id="tenant-1",
|
||||
agent_id="inline-agent-1",
|
||||
version=1,
|
||||
config_snapshot='{"prompt":{"system_prompt":"inline"}}',
|
||||
)
|
||||
monkeypatch.setattr(composer_service.db, "session", FakeSession())
|
||||
monkeypatch.setattr(AgentComposerService, "_get_draft_workflow", lambda **kwargs: SimpleNamespace(id="workflow-1"))
|
||||
monkeypatch.setattr(AgentComposerService, "_get_workflow_binding", lambda **kwargs: inline_binding)
|
||||
monkeypatch.setattr(AgentComposerService, "_get_agent_if_present", lambda **kwargs: inline_agent)
|
||||
monkeypatch.setattr(AgentComposerService, "_get_version_if_present", lambda **kwargs: inline_version)
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
"_serialize_workflow_state",
|
||||
lambda **kwargs: {"binding_type": kwargs["binding"].binding_type.value},
|
||||
)
|
||||
|
||||
state = AgentComposerService.copy_workflow_composer_from_roster(
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
node_id="node-1",
|
||||
account_id="account-1",
|
||||
source_agent_id="roster-agent-1",
|
||||
idempotency_key="same-click",
|
||||
)
|
||||
|
||||
assert state == {"binding_type": WorkflowAgentBindingType.INLINE_AGENT.value}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("binding_agent_id", "binding_type", "source_scope", "source_status", "expected_message"),
|
||||
[
|
||||
(
|
||||
"roster-agent-1",
|
||||
WorkflowAgentBindingType.INLINE_AGENT,
|
||||
AgentScope.ROSTER,
|
||||
AgentStatus.ACTIVE,
|
||||
"must be bound to a roster agent",
|
||||
),
|
||||
(
|
||||
"other-agent",
|
||||
WorkflowAgentBindingType.ROSTER_AGENT,
|
||||
AgentScope.ROSTER,
|
||||
AgentStatus.ACTIVE,
|
||||
"does not match",
|
||||
),
|
||||
(
|
||||
"roster-agent-1",
|
||||
WorkflowAgentBindingType.ROSTER_AGENT,
|
||||
AgentScope.WORKFLOW_ONLY,
|
||||
AgentStatus.ACTIVE,
|
||||
"must be an active roster agent",
|
||||
),
|
||||
(
|
||||
"roster-agent-1",
|
||||
WorkflowAgentBindingType.ROSTER_AGENT,
|
||||
AgentScope.ROSTER,
|
||||
AgentStatus.ARCHIVED,
|
||||
"must be an active roster agent",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_copy_workflow_composer_from_roster_rejects_invalid_source_binding(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
binding_agent_id: str,
|
||||
binding_type: WorkflowAgentBindingType,
|
||||
source_scope: AgentScope,
|
||||
source_status: AgentStatus,
|
||||
expected_message: str,
|
||||
):
|
||||
binding = WorkflowAgentNodeBinding(
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
workflow_id="workflow-1",
|
||||
workflow_version="draft",
|
||||
node_id="node-1",
|
||||
binding_type=binding_type,
|
||||
agent_id=binding_agent_id,
|
||||
current_snapshot_id="version-1",
|
||||
node_job_config=WorkflowNodeJobConfig(),
|
||||
)
|
||||
source_agent = Agent(
|
||||
id="roster-agent-1",
|
||||
tenant_id="tenant-1",
|
||||
name="Source",
|
||||
scope=source_scope,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=source_status,
|
||||
active_config_snapshot_id="version-1",
|
||||
)
|
||||
monkeypatch.setattr(AgentComposerService, "_get_draft_workflow", lambda **kwargs: SimpleNamespace(id="workflow-1"))
|
||||
monkeypatch.setattr(AgentComposerService, "_get_workflow_binding", lambda **kwargs: binding)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_agent", lambda **kwargs: source_agent)
|
||||
|
||||
with pytest.raises(InvalidComposerConfigError, match=expected_message):
|
||||
AgentComposerService.copy_workflow_composer_from_roster(
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
node_id="node-1",
|
||||
account_id="account-1",
|
||||
source_agent_id="roster-agent-1",
|
||||
)
|
||||
|
||||
|
||||
def test_copy_agent_drive_rows_copies_skill_prefix_and_files(monkeypatch: pytest.MonkeyPatch):
|
||||
skill_row = AgentDriveFile(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="roster-agent-1",
|
||||
key="tender-analyzer/SKILL.md",
|
||||
file_kind="tool_file",
|
||||
file_id="tool-file-1",
|
||||
value_owned_by_drive=True,
|
||||
is_skill=True,
|
||||
skill_metadata='{"name":"Tender Analyzer"}',
|
||||
size=10,
|
||||
mime_type="text/markdown",
|
||||
)
|
||||
script_row = AgentDriveFile(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="roster-agent-1",
|
||||
key="tender-analyzer/scripts/run.sh",
|
||||
file_kind="tool_file",
|
||||
file_id="tool-file-2",
|
||||
value_owned_by_drive=True,
|
||||
size=20,
|
||||
mime_type="text/x-shellscript",
|
||||
)
|
||||
file_row = AgentDriveFile(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="roster-agent-1",
|
||||
key="files/qna.pdf",
|
||||
file_kind="upload_file",
|
||||
file_id="upload-file-1",
|
||||
value_owned_by_drive=False,
|
||||
size=30,
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
fake_session = FakeSession(scalars=[[skill_row, script_row, file_row], []])
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
agent_soul = AgentSoulConfig.model_validate(
|
||||
{
|
||||
"prompt": {
|
||||
"system_prompt": "[§skill:tender-analyzer/SKILL.md:Tender Analyzer§]",
|
||||
},
|
||||
}
|
||||
)
|
||||
node_job = WorkflowNodeJobConfig.model_validate(
|
||||
{"metadata": {"file_refs": [{"name": "qna.pdf", "drive_key": "files/qna.pdf"}]}}
|
||||
)
|
||||
|
||||
AgentComposerService._copy_agent_drive_rows(
|
||||
tenant_id="tenant-1",
|
||||
source_agent_id="roster-agent-1",
|
||||
target_agent_id="inline-agent-1",
|
||||
account_id="account-1",
|
||||
agent_soul=agent_soul,
|
||||
node_job=node_job,
|
||||
)
|
||||
|
||||
copied = [row for row in fake_session.added if isinstance(row, AgentDriveFile)]
|
||||
assert [row.key for row in copied] == [
|
||||
"tender-analyzer/SKILL.md",
|
||||
"tender-analyzer/scripts/run.sh",
|
||||
"files/qna.pdf",
|
||||
]
|
||||
assert {row.agent_id for row in copied} == {"inline-agent-1"}
|
||||
assert copied[0].file_id == "tool-file-1"
|
||||
assert copied[0].is_skill is True
|
||||
assert copied[2].value_owned_by_drive is False
|
||||
|
||||
|
||||
def test_copy_agent_drive_rows_skips_when_no_referenced_drive_keys(monkeypatch: pytest.MonkeyPatch):
|
||||
fake_session = FakeSession()
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
agent_soul = AgentSoulConfig.model_validate({"prompt": {"system_prompt": "No drive mentions."}})
|
||||
|
||||
AgentComposerService._copy_agent_drive_rows(
|
||||
tenant_id="tenant-1",
|
||||
source_agent_id="roster-agent-1",
|
||||
target_agent_id="inline-agent-1",
|
||||
account_id="account-1",
|
||||
agent_soul=agent_soul,
|
||||
)
|
||||
|
||||
assert fake_session.added == []
|
||||
|
||||
|
||||
def test_copy_agent_drive_rows_skips_existing_target_keys(monkeypatch: pytest.MonkeyPatch):
|
||||
source_row = AgentDriveFile(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="roster-agent-1",
|
||||
key="files/qna.pdf",
|
||||
file_kind="upload_file",
|
||||
file_id="upload-file-1",
|
||||
value_owned_by_drive=False,
|
||||
size=30,
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
fake_session = FakeSession(scalars=[[source_row], ["files/qna.pdf"]])
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
agent_soul = AgentSoulConfig.model_validate({"prompt": {"system_prompt": "[§file:files/qna.pdf:qna.pdf§]"}})
|
||||
|
||||
AgentComposerService._copy_agent_drive_rows(
|
||||
tenant_id="tenant-1",
|
||||
source_agent_id="roster-agent-1",
|
||||
target_agent_id="inline-agent-1",
|
||||
account_id="account-1",
|
||||
agent_soul=agent_soul,
|
||||
)
|
||||
|
||||
assert [row for row in fake_session.added if isinstance(row, AgentDriveFile)] == []
|
||||
|
||||
|
||||
def test_drive_copy_scopes_include_declared_output_benchmark_files():
|
||||
agent_soul = AgentSoulConfig.model_validate(
|
||||
{
|
||||
"prompt": {
|
||||
"system_prompt": (
|
||||
"[§file:files/source.pdf:source.pdf§] "
|
||||
"[§knowledge:dataset-1:Docs§] "
|
||||
"[§skill:tender-analyzer/SKILL.md:Tender Analyzer§]"
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
node_job = WorkflowNodeJobConfig.model_validate(
|
||||
{
|
||||
"declared_outputs": [
|
||||
{
|
||||
"name": "qna_report",
|
||||
"type": "file",
|
||||
"check": {
|
||||
"enabled": True,
|
||||
"prompt": "Compare the generated file with the benchmark.",
|
||||
"benchmark_file_ref": {"name": "expected.pdf", "drive_key": "files/expected.pdf"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "summary",
|
||||
"type": "string",
|
||||
"check": {"enabled": False, "benchmark_file_ref": {"drive_key": "files/ignored.pdf"}},
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
exact_keys, prefixes = AgentComposerService._drive_copy_scopes_from_agent_configs(
|
||||
agent_soul=agent_soul,
|
||||
node_job=node_job,
|
||||
)
|
||||
|
||||
assert exact_keys == {"files/source.pdf", "files/expected.pdf"}
|
||||
assert prefixes == {"tender-analyzer/"}
|
||||
|
||||
|
||||
def test_composer_create_agents_syncs_active_config_has_model(monkeypatch: pytest.MonkeyPatch):
|
||||
fake_session = FakeSession()
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
created_apps = []
|
||||
backing_agent = Agent(
|
||||
id="roster-agent-1",
|
||||
tenant_id="tenant-1",
|
||||
name="Ready Agent",
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
app_id="app-agent-1",
|
||||
active_config_snapshot_id="empty-version-1",
|
||||
)
|
||||
|
||||
class FakeAppService:
|
||||
def create_app(self, tenant_id, params, account):
|
||||
created_apps.append((tenant_id, params, account))
|
||||
return SimpleNamespace(id="app-agent-1")
|
||||
|
||||
class FakeAgentRosterService:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def get_app_backing_agent(self, *, tenant_id, app_id):
|
||||
assert tenant_id == "tenant-1"
|
||||
assert app_id == "app-agent-1"
|
||||
return backing_agent
|
||||
|
||||
monkeypatch.setattr(composer_service, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(composer_service, "AgentRosterService", FakeAgentRosterService)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_account", lambda **kwargs: SimpleNamespace(id="account-1"))
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
"_require_version",
|
||||
lambda **kwargs: SimpleNamespace(id="empty-version-1", tenant_id="tenant-1", agent_id="roster-agent-1"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
AgentComposerService,
|
||||
"_create_config_version",
|
||||
@ -745,6 +1255,81 @@ def test_composer_create_agents_syncs_active_config_has_model(monkeypatch: pytes
|
||||
assert workflow_agent.active_config_has_model is True
|
||||
assert roster_agent.active_config_snapshot_id == "version-with-model"
|
||||
assert roster_agent.active_config_has_model is True
|
||||
assert roster_agent.source == AgentSource.AGENT_APP
|
||||
assert roster_agent.app_id == "app-agent-1"
|
||||
created_tenant_id, created_params, created_account = created_apps[0]
|
||||
assert created_tenant_id == "tenant-1"
|
||||
assert created_params.mode == "agent"
|
||||
assert created_params.name == "Ready Agent"
|
||||
assert created_account.id == "account-1"
|
||||
|
||||
|
||||
def test_composer_require_account(monkeypatch: pytest.MonkeyPatch):
|
||||
account = SimpleNamespace(id="account-1")
|
||||
monkeypatch.setattr(composer_service.db, "session", SimpleNamespace(get=lambda model, account_id: account))
|
||||
|
||||
assert AgentComposerService._require_account(account_id="account-1") is account
|
||||
|
||||
|
||||
def test_composer_require_account_raises_when_missing(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(composer_service.db, "session", SimpleNamespace(get=lambda model, account_id: None))
|
||||
|
||||
with pytest.raises(ValueError, match="Account not found"):
|
||||
AgentComposerService._require_account(account_id="missing-account")
|
||||
|
||||
|
||||
def test_composer_create_roster_agent_rolls_back_name_conflict(monkeypatch: pytest.MonkeyPatch):
|
||||
fake_session = FakeSession()
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
|
||||
class FakeAppService:
|
||||
def create_app(self, tenant_id, params, account):
|
||||
raise IntegrityError("insert apps", params, Exception("duplicate"))
|
||||
|
||||
monkeypatch.setattr(composer_service, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_account", lambda **kwargs: SimpleNamespace(id="account-1"))
|
||||
|
||||
with pytest.raises(AgentNameConflictError):
|
||||
AgentComposerService._create_roster_agent_for_composer(
|
||||
tenant_id="tenant-1",
|
||||
account_id="account-1",
|
||||
name="Duplicate Agent",
|
||||
agent_soul=_agent_soul_with_model(),
|
||||
operation=AgentConfigRevisionOperation.CREATE_VERSION,
|
||||
version_note=None,
|
||||
)
|
||||
|
||||
assert fake_session.rollbacks == 1
|
||||
|
||||
|
||||
def test_composer_create_roster_agent_raises_when_backing_agent_missing(monkeypatch: pytest.MonkeyPatch):
|
||||
fake_session = FakeSession()
|
||||
monkeypatch.setattr(composer_service.db, "session", fake_session)
|
||||
|
||||
class FakeAppService:
|
||||
def create_app(self, tenant_id, params, account):
|
||||
return SimpleNamespace(id="app-agent-1")
|
||||
|
||||
class FakeAgentRosterService:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def get_app_backing_agent(self, *, tenant_id, app_id):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(composer_service, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(composer_service, "AgentRosterService", FakeAgentRosterService)
|
||||
monkeypatch.setattr(AgentComposerService, "_require_account", lambda **kwargs: SimpleNamespace(id="account-1"))
|
||||
|
||||
with pytest.raises(AgentNotFoundError):
|
||||
AgentComposerService._create_roster_agent_for_composer(
|
||||
tenant_id="tenant-1",
|
||||
account_id="account-1",
|
||||
name="Missing Backing Agent",
|
||||
agent_soul=_agent_soul_with_model(),
|
||||
operation=AgentConfigRevisionOperation.CREATE_VERSION,
|
||||
version_note=None,
|
||||
)
|
||||
|
||||
|
||||
def test_composer_version_helpers_and_lookup_errors(monkeypatch: pytest.MonkeyPatch):
|
||||
@ -1318,6 +1903,7 @@ def test_agent_app_visible_versions_exclude_draft_saves():
|
||||
|
||||
assert agent_app_operations == {
|
||||
AgentConfigRevisionOperation.SAVE_NEW_VERSION,
|
||||
AgentConfigRevisionOperation.SAVE_TO_ROSTER,
|
||||
AgentConfigRevisionOperation.RESTORE_VERSION,
|
||||
}
|
||||
assert AgentConfigRevisionOperation.SAVE_CURRENT_VERSION not in agent_app_operations
|
||||
|
||||
@ -48,13 +48,15 @@ class TestMetadataBugCompleteValidation:
|
||||
account = _make_account()
|
||||
# Should crash with TypeError
|
||||
with pytest.raises(TypeError, match="object of type 'NoneType' has no len"):
|
||||
MetadataService.create_metadata("dataset-123", mock_metadata_args, account, "tenant-123")
|
||||
MetadataService.create_metadata(Mock(), "dataset-123", mock_metadata_args, account, "tenant-123")
|
||||
|
||||
# Test update method as well
|
||||
account = _make_account()
|
||||
none_name = cast(str, None)
|
||||
with pytest.raises(TypeError, match="object of type 'NoneType' has no len"):
|
||||
MetadataService.update_metadata_name("dataset-123", "metadata-456", none_name, account, "tenant-123")
|
||||
MetadataService.update_metadata_name(
|
||||
Mock(), "dataset-123", "metadata-456", none_name, account, "tenant-123"
|
||||
)
|
||||
|
||||
def test_3_database_constraints_verification(self) -> None:
|
||||
"""Test Layer 3: Verify database model has nullable=False constraints."""
|
||||
@ -97,7 +99,7 @@ class TestMetadataBugCompleteValidation:
|
||||
|
||||
account = _make_account()
|
||||
with pytest.raises(TypeError, match="object of type 'NoneType' has no len"):
|
||||
MetadataService.create_metadata("dataset-123", mock_metadata_args, account, "tenant-123")
|
||||
MetadataService.create_metadata(Mock(), "dataset-123", mock_metadata_args, account, "tenant-123")
|
||||
|
||||
def test_7_end_to_end_validation_layers(self) -> None:
|
||||
"""Test all validation layers work together correctly."""
|
||||
|
||||
@ -37,7 +37,7 @@ class TestMetadataNullableBug:
|
||||
account = _make_account()
|
||||
# This should crash with TypeError when calling len(None)
|
||||
with pytest.raises(TypeError, match="object of type 'NoneType' has no len"):
|
||||
MetadataService.create_metadata("dataset-123", mock_metadata_args, account, "tenant-123")
|
||||
MetadataService.create_metadata(Mock(), "dataset-123", mock_metadata_args, account, "tenant-123")
|
||||
|
||||
def test_metadata_service_update_with_none_name_crashes(self) -> None:
|
||||
"""Test that MetadataService.update_metadata_name crashes when name is None."""
|
||||
@ -45,7 +45,9 @@ class TestMetadataNullableBug:
|
||||
none_name = cast(str, None)
|
||||
# This should crash with TypeError when calling len(None)
|
||||
with pytest.raises(TypeError, match="object of type 'NoneType' has no len"):
|
||||
MetadataService.update_metadata_name("dataset-123", "metadata-456", none_name, account, "tenant-123")
|
||||
MetadataService.update_metadata_name(
|
||||
Mock(), "dataset-123", "metadata-456", none_name, account, "tenant-123"
|
||||
)
|
||||
|
||||
def test_api_layer_now_uses_pydantic_validation(self) -> None:
|
||||
"""Verify that API layer relies on Pydantic validation instead of reqparse."""
|
||||
|
||||
@ -134,9 +134,14 @@ export type ComposerSavePayload = {
|
||||
agent_soul?: AgentSoulConfig | null
|
||||
binding?: ComposerBindingPayload | null
|
||||
client_revision_id?: string | null
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: AgentIconType | null
|
||||
idempotency_key?: string | null
|
||||
new_agent_name?: string | null
|
||||
node_job?: WorkflowNodeJobConfig | null
|
||||
role?: string | null
|
||||
save_strategy: ComposerSaveStrategy
|
||||
soul_lock?: ComposerSoulLockPayload
|
||||
variant: ComposerVariant
|
||||
@ -536,6 +541,8 @@ export type ComposerBindingPayload = {
|
||||
current_snapshot_id?: string | null
|
||||
}
|
||||
|
||||
export type AgentIconType = 'emoji' | 'image' | 'link'
|
||||
|
||||
export type WorkflowNodeJobConfig = {
|
||||
declared_outputs?: Array<DeclaredOutputConfig>
|
||||
human_contacts?: Array<AgentHumanContactConfig>
|
||||
@ -876,8 +883,6 @@ export type LlmMode = 'chat' | 'completion'
|
||||
|
||||
export type AgentKind = 'dify_agent'
|
||||
|
||||
export type AgentIconType = 'emoji' | 'image' | 'link'
|
||||
|
||||
export type AgentPublishedReferenceResponse = {
|
||||
app_icon?: string | null
|
||||
app_icon_background?: string | null
|
||||
|
||||
@ -282,6 +282,13 @@ export const zComposerBindingPayload = z.object({
|
||||
current_snapshot_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AgentIconType
|
||||
*
|
||||
* Supported icon storage formats for Agent roster entries.
|
||||
*/
|
||||
export const zAgentIconType = z.enum(['emoji', 'image', 'link'])
|
||||
|
||||
/**
|
||||
* ComposerSoulLockPayload
|
||||
*/
|
||||
@ -830,13 +837,6 @@ export const zAgentAppDetailWithSite = z.object({
|
||||
*/
|
||||
export const zAgentKind = z.enum(['dify_agent'])
|
||||
|
||||
/**
|
||||
* AgentIconType
|
||||
*
|
||||
* Supported icon storage formats for Agent roster entries.
|
||||
*/
|
||||
export const zAgentIconType = z.enum(['emoji', 'image', 'link'])
|
||||
|
||||
/**
|
||||
* AgentPublishedReferenceResponse
|
||||
*/
|
||||
@ -1876,9 +1876,14 @@ export const zComposerSavePayload = z.object({
|
||||
agent_soul: zAgentSoulConfig.nullish(),
|
||||
binding: zComposerBindingPayload.nullish(),
|
||||
client_revision_id: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
icon: z.string().max(255).nullish(),
|
||||
icon_background: z.string().max(255).nullish(),
|
||||
icon_type: zAgentIconType.nullish(),
|
||||
idempotency_key: z.string().nullish(),
|
||||
new_agent_name: z.string().min(1).max(255).nullish(),
|
||||
node_job: zWorkflowNodeJobConfig.nullish(),
|
||||
role: z.string().max(255).nullish(),
|
||||
save_strategy: zComposerSaveStrategy,
|
||||
soul_lock: zComposerSoulLockPayload.optional(),
|
||||
variant: zComposerVariant,
|
||||
|
||||
@ -392,6 +392,9 @@ import {
|
||||
zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunBody,
|
||||
zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunPath,
|
||||
zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunResponse,
|
||||
zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterBody,
|
||||
zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterPath,
|
||||
zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterResponse,
|
||||
zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactBody,
|
||||
zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactPath,
|
||||
zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactResponse,
|
||||
@ -3479,6 +3482,26 @@ export const candidates = {
|
||||
}
|
||||
|
||||
export const post51 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRoster',
|
||||
path: '/apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/copy-from-roster',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
body: zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterBody,
|
||||
params: zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterPath,
|
||||
}),
|
||||
)
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterResponse)
|
||||
|
||||
export const copyFromRoster = {
|
||||
post: post51,
|
||||
}
|
||||
|
||||
export const post52 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -3495,10 +3518,10 @@ export const post51 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactResponse)
|
||||
|
||||
export const impact = {
|
||||
post: post51,
|
||||
post: post52,
|
||||
}
|
||||
|
||||
export const post52 = oc
|
||||
export const post53 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -3515,10 +3538,10 @@ export const post52 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerSaveToRosterResponse)
|
||||
|
||||
export const saveToRoster = {
|
||||
post: post52,
|
||||
post: post53,
|
||||
}
|
||||
|
||||
export const post53 = oc
|
||||
export const post54 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -3535,7 +3558,7 @@ export const post53 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerValidateResponse)
|
||||
|
||||
export const validate = {
|
||||
post: post53,
|
||||
post: post54,
|
||||
}
|
||||
|
||||
export const get62 = oc
|
||||
@ -3569,6 +3592,7 @@ export const agentComposer = {
|
||||
get: get62,
|
||||
put: put4,
|
||||
candidates,
|
||||
copyFromRoster,
|
||||
impact,
|
||||
saveToRoster,
|
||||
validate,
|
||||
@ -3598,7 +3622,7 @@ export const lastRun = {
|
||||
*
|
||||
* Run draft workflow node
|
||||
*/
|
||||
export const post54 = oc
|
||||
export const post55 = oc
|
||||
.route({
|
||||
description: 'Run draft workflow node',
|
||||
inputStructure: 'detailed',
|
||||
@ -3617,7 +3641,7 @@ export const post54 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdRunResponse)
|
||||
|
||||
export const run8 = {
|
||||
post: post54,
|
||||
post: post55,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3625,7 +3649,7 @@ export const run8 = {
|
||||
*
|
||||
* Poll for trigger events and execute single node when event arrives
|
||||
*/
|
||||
export const post55 = oc
|
||||
export const post56 = oc
|
||||
.route({
|
||||
description: 'Poll for trigger events and execute single node when event arrives',
|
||||
inputStructure: 'detailed',
|
||||
@ -3639,7 +3663,7 @@ export const post55 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdTriggerRunResponse)
|
||||
|
||||
export const run9 = {
|
||||
post: post55,
|
||||
post: post56,
|
||||
}
|
||||
|
||||
export const trigger = {
|
||||
@ -3699,7 +3723,7 @@ export const nodes7 = {
|
||||
*
|
||||
* Run draft workflow
|
||||
*/
|
||||
export const post56 = oc
|
||||
export const post57 = oc
|
||||
.route({
|
||||
description: 'Run draft workflow',
|
||||
inputStructure: 'detailed',
|
||||
@ -3718,7 +3742,7 @@ export const post56 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftRunResponse)
|
||||
|
||||
export const run10 = {
|
||||
post: post56,
|
||||
post: post57,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3840,7 +3864,7 @@ export const systemVariables = {
|
||||
*
|
||||
* Poll for trigger events and execute full workflow when event arrives
|
||||
*/
|
||||
export const post57 = oc
|
||||
export const post58 = oc
|
||||
.route({
|
||||
description: 'Poll for trigger events and execute full workflow when event arrives',
|
||||
inputStructure: 'detailed',
|
||||
@ -3859,7 +3883,7 @@ export const post57 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftTriggerRunResponse)
|
||||
|
||||
export const run11 = {
|
||||
post: post57,
|
||||
post: post58,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3867,7 +3891,7 @@ export const run11 = {
|
||||
*
|
||||
* Full workflow debug when the start node is a trigger
|
||||
*/
|
||||
export const post58 = oc
|
||||
export const post59 = oc
|
||||
.route({
|
||||
description: 'Full workflow debug when the start node is a trigger',
|
||||
inputStructure: 'detailed',
|
||||
@ -3886,7 +3910,7 @@ export const post58 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftTriggerRunAllResponse)
|
||||
|
||||
export const runAll = {
|
||||
post: post58,
|
||||
post: post59,
|
||||
}
|
||||
|
||||
export const trigger2 = {
|
||||
@ -4039,7 +4063,7 @@ export const get72 = oc
|
||||
*
|
||||
* Sync draft workflow configuration
|
||||
*/
|
||||
export const post59 = oc
|
||||
export const post60 = oc
|
||||
.route({
|
||||
description: 'Sync draft workflow configuration',
|
||||
inputStructure: 'detailed',
|
||||
@ -4059,7 +4083,7 @@ export const post59 = oc
|
||||
|
||||
export const draft2 = {
|
||||
get: get72,
|
||||
post: post59,
|
||||
post: post60,
|
||||
conversationVariables: conversationVariables2,
|
||||
environmentVariables,
|
||||
features,
|
||||
@ -4095,7 +4119,7 @@ export const get73 = oc
|
||||
/**
|
||||
* Publish workflow
|
||||
*/
|
||||
export const post60 = oc
|
||||
export const post61 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -4114,7 +4138,7 @@ export const post60 = oc
|
||||
|
||||
export const publish = {
|
||||
get: get73,
|
||||
post: post60,
|
||||
post: post61,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4251,7 +4275,7 @@ export const triggers2 = {
|
||||
/**
|
||||
* Restore a published workflow version into the draft workflow
|
||||
*/
|
||||
export const post61 = oc
|
||||
export const post62 = oc
|
||||
.route({
|
||||
description: 'Restore a published workflow version into the draft workflow',
|
||||
inputStructure: 'detailed',
|
||||
@ -4264,7 +4288,7 @@ export const post61 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsByWorkflowIdRestoreResponse)
|
||||
|
||||
export const restore = {
|
||||
post: post61,
|
||||
post: post62,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4489,7 +4513,7 @@ export const get81 = oc
|
||||
*
|
||||
* Create a new API key for an app
|
||||
*/
|
||||
export const post62 = oc
|
||||
export const post63 = oc
|
||||
.route({
|
||||
description: 'Create a new API key for an app',
|
||||
inputStructure: 'detailed',
|
||||
@ -4505,7 +4529,7 @@ export const post62 = oc
|
||||
|
||||
export const apiKeys = {
|
||||
get: get81,
|
||||
post: post62,
|
||||
post: post63,
|
||||
byApiKeyId,
|
||||
}
|
||||
|
||||
@ -4563,7 +4587,7 @@ export const get83 = oc
|
||||
*
|
||||
* Create a new application
|
||||
*/
|
||||
export const post63 = oc
|
||||
export const post64 = oc
|
||||
.route({
|
||||
description: 'Create a new application',
|
||||
inputStructure: 'detailed',
|
||||
@ -4579,7 +4603,7 @@ export const post63 = oc
|
||||
|
||||
export const apps = {
|
||||
get: get83,
|
||||
post: post63,
|
||||
post: post64,
|
||||
imports,
|
||||
starred,
|
||||
workflows,
|
||||
|
||||
@ -986,9 +986,14 @@ export type ComposerSavePayload = {
|
||||
agent_soul?: AgentSoulConfig | null
|
||||
binding?: ComposerBindingPayload | null
|
||||
client_revision_id?: string | null
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: AgentIconType | null
|
||||
idempotency_key?: string | null
|
||||
new_agent_name?: string | null
|
||||
node_job?: WorkflowNodeJobConfig | null
|
||||
role?: string | null
|
||||
save_strategy: ComposerSaveStrategy
|
||||
soul_lock?: ComposerSoulLockPayload
|
||||
variant: ComposerVariant
|
||||
@ -1003,6 +1008,12 @@ export type AgentComposerCandidatesResponse = {
|
||||
variant: ComposerVariant
|
||||
}
|
||||
|
||||
export type WorkflowComposerCopyFromRosterPayload = {
|
||||
idempotency_key?: string | null
|
||||
source_agent_id: string
|
||||
source_snapshot_id?: string | null
|
||||
}
|
||||
|
||||
export type AgentComposerImpactResponse = {
|
||||
bindings?: Array<AgentComposerImpactBindingResponse>
|
||||
current_snapshot_id?: string | null
|
||||
@ -1873,6 +1884,8 @@ export type ComposerBindingPayload = {
|
||||
current_snapshot_id?: string | null
|
||||
}
|
||||
|
||||
export type AgentIconType = 'emoji' | 'image' | 'link'
|
||||
|
||||
export type ComposerSoulLockPayload = {
|
||||
locked?: boolean
|
||||
unlocked_from_version_id?: string | null
|
||||
@ -5415,6 +5428,23 @@ export type GetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCandidatesResp
|
||||
export type GetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCandidatesResponse
|
||||
= GetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCandidatesResponses[keyof GetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCandidatesResponses]
|
||||
|
||||
export type PostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterData = {
|
||||
body: WorkflowComposerCopyFromRosterPayload
|
||||
path: {
|
||||
app_id: string
|
||||
node_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/copy-from-roster'
|
||||
}
|
||||
|
||||
export type PostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterResponses = {
|
||||
200: WorkflowAgentComposerResponse
|
||||
}
|
||||
|
||||
export type PostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterResponse
|
||||
= PostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterResponses[keyof PostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterResponses]
|
||||
|
||||
export type PostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactData = {
|
||||
body: ComposerSavePayload
|
||||
path: {
|
||||
|
||||
@ -642,6 +642,15 @@ export const zHumanInputDeliveryTestPayload = z.object({
|
||||
*/
|
||||
export const zEmptyObjectResponse = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* WorkflowComposerCopyFromRosterPayload
|
||||
*/
|
||||
export const zWorkflowComposerCopyFromRosterPayload = z.object({
|
||||
idempotency_key: z.string().max(255).nullish(),
|
||||
source_agent_id: z.string().min(1).max(255),
|
||||
source_snapshot_id: z.string().max(255).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* DraftWorkflowNodeRunPayload
|
||||
*/
|
||||
@ -1835,6 +1844,13 @@ export const zComposerBindingPayload = z.object({
|
||||
current_snapshot_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AgentIconType
|
||||
*
|
||||
* Supported icon storage formats for Agent roster entries.
|
||||
*/
|
||||
export const zAgentIconType = z.enum(['emoji', 'image', 'link'])
|
||||
|
||||
/**
|
||||
* ComposerSoulLockPayload
|
||||
*/
|
||||
@ -3336,9 +3352,14 @@ export const zComposerSavePayload = z.object({
|
||||
agent_soul: zAgentSoulConfig.nullish(),
|
||||
binding: zComposerBindingPayload.nullish(),
|
||||
client_revision_id: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
icon: z.string().max(255).nullish(),
|
||||
icon_background: z.string().max(255).nullish(),
|
||||
icon_type: zAgentIconType.nullish(),
|
||||
idempotency_key: z.string().nullish(),
|
||||
new_agent_name: z.string().min(1).max(255).nullish(),
|
||||
node_job: zWorkflowNodeJobConfig.nullish(),
|
||||
role: z.string().max(255).nullish(),
|
||||
save_strategy: zComposerSaveStrategy,
|
||||
soul_lock: zComposerSoulLockPayload.optional(),
|
||||
variant: zComposerVariant,
|
||||
@ -5342,6 +5363,20 @@ export const zGetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCandidatesPa
|
||||
export const zGetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCandidatesResponse
|
||||
= zAgentComposerCandidatesResponse
|
||||
|
||||
export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterBody
|
||||
= zWorkflowComposerCopyFromRosterPayload
|
||||
|
||||
export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterPath = z.object({
|
||||
app_id: z.uuid(),
|
||||
node_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Workflow roster agent copied to inline agent
|
||||
*/
|
||||
export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCopyFromRosterResponse
|
||||
= zWorkflowAgentComposerResponse
|
||||
|
||||
export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactBody
|
||||
= zComposerSavePayload
|
||||
|
||||
|
||||
@ -380,12 +380,8 @@ export const listRollbackTargets = oc
|
||||
)
|
||||
.output(zDeploymentServiceListRollbackTargetsResponse)
|
||||
|
||||
/**
|
||||
* CancelDeployment cancels the in-flight deployment on the environment.
|
||||
*/
|
||||
export const cancelDeployment = oc
|
||||
.route({
|
||||
description: 'CancelDeployment cancels the in-flight deployment on the environment.',
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'DeploymentService_CancelDeployment',
|
||||
@ -607,13 +603,8 @@ export const releaseService = {
|
||||
precheckRelease,
|
||||
}
|
||||
|
||||
/**
|
||||
* ListEnvironments returns only the environments the current user can
|
||||
* deploy to.
|
||||
*/
|
||||
export const listEnvironments = oc
|
||||
.route({
|
||||
description: 'ListEnvironments returns only the environments the current user can\n deploy to.',
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
operationId: 'EnvironmentService_ListEnvironments',
|
||||
|
||||
@ -13,13 +13,13 @@ export const AccessMode = {
|
||||
|
||||
export type AccessMode = (typeof AccessMode)[keyof typeof AccessMode]
|
||||
|
||||
export const SubjectType = {
|
||||
SUBJECT_TYPE_UNSPECIFIED: 'SUBJECT_TYPE_UNSPECIFIED',
|
||||
SUBJECT_TYPE_ACCOUNT: 'SUBJECT_TYPE_ACCOUNT',
|
||||
SUBJECT_TYPE_GROUP: 'SUBJECT_TYPE_GROUP',
|
||||
export const AccessSubjectType = {
|
||||
ACCESS_SUBJECT_TYPE_UNSPECIFIED: 'ACCESS_SUBJECT_TYPE_UNSPECIFIED',
|
||||
ACCESS_SUBJECT_TYPE_ACCOUNT: 'ACCESS_SUBJECT_TYPE_ACCOUNT',
|
||||
ACCESS_SUBJECT_TYPE_GROUP: 'ACCESS_SUBJECT_TYPE_GROUP',
|
||||
} as const
|
||||
|
||||
export type SubjectType = (typeof SubjectType)[keyof typeof SubjectType]
|
||||
export type AccessSubjectType = (typeof AccessSubjectType)[keyof typeof AccessSubjectType]
|
||||
|
||||
export const AppRunnerLogStatus = {
|
||||
APP_RUNNER_LOG_STATUS_UNSPECIFIED: 'APP_RUNNER_LOG_STATUS_UNSPECIFIED',
|
||||
@ -295,7 +295,7 @@ export type AccessPolicy = {
|
||||
}
|
||||
|
||||
export type AccessSubject = {
|
||||
subjectType: SubjectType
|
||||
subjectType: AccessSubjectType
|
||||
subjectId: string
|
||||
}
|
||||
|
||||
@ -598,7 +598,6 @@ export type Environment = {
|
||||
status: EnvironmentStatus
|
||||
statusMessage: string
|
||||
lastError?: Error
|
||||
apiServer?: string
|
||||
namespace?: string
|
||||
managedBy?: string
|
||||
runtimeEndpoint?: string
|
||||
@ -741,9 +740,6 @@ export type GetReleaseResponse = {
|
||||
|
||||
export type K8sEnvironmentConfig = {
|
||||
namespace?: string
|
||||
apiServer?: string
|
||||
caBundle?: string
|
||||
bearerToken?: string
|
||||
}
|
||||
|
||||
export type ListApiKeysResponse = {
|
||||
@ -832,6 +828,7 @@ export type PrecheckReleaseResponse = {
|
||||
canCreate: boolean
|
||||
matchedRelease?: ReleaseContentMatch
|
||||
unsupportedNodes: Array<UnsupportedDslNode>
|
||||
unsupportedToolProviders: Array<UnsupportedToolProvider>
|
||||
}
|
||||
|
||||
export type PromoteRequest = {
|
||||
@ -998,6 +995,14 @@ export type UnsupportedDslNode = {
|
||||
type: string
|
||||
}
|
||||
|
||||
export type UnsupportedToolProvider = {
|
||||
nodeId: string
|
||||
providerType: string
|
||||
providerId?: string
|
||||
providerName?: string
|
||||
toolName?: string
|
||||
}
|
||||
|
||||
export type UpdateAccessChannelsRequest = {
|
||||
appInstanceId?: string
|
||||
webAppEnabled?: boolean
|
||||
@ -1362,7 +1367,6 @@ export type InfoConfigReply = {
|
||||
Branding?: BrandingInfo
|
||||
WebAppAuth?: WebAppAuthInfo
|
||||
PluginInstallationPermission?: PluginInstallationPermissionInfo
|
||||
EnableAppDeploy?: boolean
|
||||
}
|
||||
|
||||
export type InnerAdmission = {
|
||||
@ -1458,6 +1462,19 @@ export type IsUserAllowedToAccessWebAppRes = {
|
||||
result?: boolean
|
||||
}
|
||||
|
||||
export type IssueMcpTokenReply = {
|
||||
token?: string
|
||||
expiresAt?: string
|
||||
tokenType?: string
|
||||
}
|
||||
|
||||
export type IssueMcpTokenReq = {
|
||||
userId?: string
|
||||
tenantId?: string
|
||||
appId?: string
|
||||
audience?: string
|
||||
}
|
||||
|
||||
export type JoinWorkspaceReply = {
|
||||
message?: string
|
||||
}
|
||||
@ -1466,6 +1483,7 @@ export type JoinWorkspaceReq = {
|
||||
id?: string
|
||||
email?: string
|
||||
role?: string
|
||||
rbacRole?: string
|
||||
}
|
||||
|
||||
export type LicenseInfo = {
|
||||
@ -1667,12 +1685,9 @@ export type PluginInstallationSettingsReply = {
|
||||
|
||||
export type RbacRole = {
|
||||
id?: string
|
||||
type?: string
|
||||
name?: string
|
||||
description?: string
|
||||
isBuiltin?: boolean
|
||||
category?: string
|
||||
permissionKeys?: Array<string>
|
||||
permissions?: Array<string>
|
||||
}
|
||||
|
||||
export type ResetMemberPasswordReply = {
|
||||
@ -1813,7 +1828,7 @@ export type SetDefaultWorkspaceReq = {
|
||||
|
||||
export type Subject = {
|
||||
subjectId?: string
|
||||
subjectType?: SubjectType
|
||||
subjectType?: string
|
||||
accountData?: SubjectAccountData
|
||||
groupData?: SubjectGroupData
|
||||
}
|
||||
|
||||
@ -9,10 +9,10 @@ export const zAccessMode = z.enum([
|
||||
'ACCESS_MODE_PRIVATE_ALL',
|
||||
])
|
||||
|
||||
export const zSubjectType = z.enum([
|
||||
'SUBJECT_TYPE_UNSPECIFIED',
|
||||
'SUBJECT_TYPE_ACCOUNT',
|
||||
'SUBJECT_TYPE_GROUP',
|
||||
export const zAccessSubjectType = z.enum([
|
||||
'ACCESS_SUBJECT_TYPE_UNSPECIFIED',
|
||||
'ACCESS_SUBJECT_TYPE_ACCOUNT',
|
||||
'ACCESS_SUBJECT_TYPE_GROUP',
|
||||
])
|
||||
|
||||
export const zAppRunnerLogStatus = z.enum([
|
||||
@ -203,7 +203,7 @@ export const zLimitStatus = z.enum([
|
||||
])
|
||||
|
||||
export const zAccessSubject = z.object({
|
||||
subjectType: zSubjectType,
|
||||
subjectType: zAccessSubjectType,
|
||||
subjectId: z.string(),
|
||||
})
|
||||
|
||||
@ -254,10 +254,6 @@ export const zAppInstance = z.object({
|
||||
updatedAt: z.iso.datetime(),
|
||||
})
|
||||
|
||||
/**
|
||||
* BootstrapAssignment is one runtime_instance assignment in a runner's startup
|
||||
* baseline.
|
||||
*/
|
||||
export const zBootstrapAssignment = z.object({
|
||||
appId: z.string().optional(),
|
||||
environmentId: z.string().optional(),
|
||||
@ -322,10 +318,6 @@ export const zCreateReleaseRequest = z.object({
|
||||
sourceAppId: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* CredentialCandidate is one tenant-visible credential a frontend may
|
||||
* pick for a credential slot. It carries no secret.
|
||||
*/
|
||||
export const zCredentialCandidate = z.object({
|
||||
credentialId: z.string(),
|
||||
providerId: z.string(),
|
||||
@ -334,20 +326,12 @@ export const zCredentialCandidate = z.object({
|
||||
fromEnterprise: z.boolean(),
|
||||
})
|
||||
|
||||
/**
|
||||
* CredentialSelectionInput is one deploy-time plugin-credential
|
||||
* selection: a shared credential id chosen for a required DSL slot.
|
||||
*/
|
||||
export const zCredentialSelectionInput = z.object({
|
||||
providerId: z.string(),
|
||||
category: zPluginCategory.optional(),
|
||||
credentialId: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* CredentialSlot is one model/tool plugin-credential requirement a
|
||||
* Release's DSL declares, paired with the candidates selectable for it.
|
||||
*/
|
||||
export const zCredentialSlot = z.object({
|
||||
providerId: z.string(),
|
||||
category: zPluginCategory,
|
||||
@ -406,10 +390,6 @@ export const zEnvironmentDeploymentRecord = z.object({
|
||||
finalizedAt: z.iso.datetime().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Error is the package-wide failure shape, carried wherever an operation or
|
||||
* resource reports an error.
|
||||
*/
|
||||
export const zError = z.object({
|
||||
code: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
@ -445,7 +425,6 @@ export const zEnvironment = z.object({
|
||||
status: zEnvironmentStatus,
|
||||
statusMessage: z.string(),
|
||||
lastError: zError.optional(),
|
||||
apiServer: z.string().optional(),
|
||||
namespace: z.string().optional(),
|
||||
managedBy: z.string().optional(),
|
||||
runtimeEndpoint: z.string().optional(),
|
||||
@ -523,9 +502,6 @@ export const zGetEnvironmentResponse = z.object({
|
||||
|
||||
export const zK8sEnvironmentConfig = z.object({
|
||||
namespace: z.string().optional(),
|
||||
apiServer: z.string().optional(),
|
||||
caBundle: z.string().optional(),
|
||||
bearerToken: z.string().optional(),
|
||||
})
|
||||
|
||||
export const zCreateEnvironmentRequest = z.object({
|
||||
@ -571,9 +547,6 @@ export const zDeployRequest = z.object({
|
||||
expectedDslDigest: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Operator is who triggered the run (the "END USER OR ACCOUNT" column).
|
||||
*/
|
||||
export const zOperator = z.object({
|
||||
type: zOperatorType,
|
||||
id: z.string(),
|
||||
@ -620,10 +593,6 @@ export const zPromoteRequest = z.object({
|
||||
idempotencyKey: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ReleaseContentMatch identifies an existing release whose DSL content is
|
||||
* identical to the checked content.
|
||||
*/
|
||||
export const zReleaseContentMatch = z.object({
|
||||
releaseId: z.string(),
|
||||
displayName: z.string(),
|
||||
@ -638,11 +607,6 @@ export const zReleaseEnvironmentAction = z.object({
|
||||
currentReleaseId: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ReleaseEnvironmentDeployment is an environment where the release is the
|
||||
* active deployment, paired with that environment's runtime status so the
|
||||
* version history can show running vs failed vs deploying.
|
||||
*/
|
||||
export const zReleaseEnvironmentDeployment = z.object({
|
||||
environment: zEnvironment,
|
||||
status: zRuntimeInstanceStatus,
|
||||
@ -663,10 +627,6 @@ export const zReportRuntimeAssignmentStatusResponse = z.object({
|
||||
stale: z.boolean().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* RequiredSlot is an input requirement extracted from a Release's
|
||||
* DSL.
|
||||
*/
|
||||
export const zRequiredSlot = z.object({
|
||||
type: zSlotType,
|
||||
providerId: z.string(),
|
||||
@ -715,10 +675,6 @@ export const zDeployResponse = z.object({
|
||||
deployment: zDeployment,
|
||||
})
|
||||
|
||||
/**
|
||||
* EnvironmentAppInstance is one app instance as seen from a single environment:
|
||||
* its current release, runtime status, and derived last error in THIS env.
|
||||
*/
|
||||
export const zEnvironmentAppInstance = z.object({
|
||||
appInstance: zAppInstance.optional(),
|
||||
currentRelease: zRelease.optional(),
|
||||
@ -759,10 +715,6 @@ export const zComputeReleaseDeploymentViewResponse = z.object({
|
||||
options: zDeploymentOptions.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* EnvironmentDeploymentHistoryItem is one deployment row in an environment's
|
||||
* history, with a thin reference to the owning app instance.
|
||||
*/
|
||||
export const zEnvironmentDeploymentHistoryItem = z.object({
|
||||
deployment: zDeployment.optional(),
|
||||
appInstanceId: z.string().optional(),
|
||||
@ -904,20 +856,25 @@ export const zUndeployResponse = z.object({
|
||||
deployment: zDeployment,
|
||||
})
|
||||
|
||||
/**
|
||||
* UnsupportedDslNode identifies a workflow node whose type the app runner
|
||||
* cannot execute.
|
||||
*/
|
||||
export const zUnsupportedDslNode = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
|
||||
export const zUnsupportedToolProvider = z.object({
|
||||
nodeId: z.string(),
|
||||
providerType: z.string(),
|
||||
providerId: z.string().optional(),
|
||||
providerName: z.string().optional(),
|
||||
toolName: z.string().optional(),
|
||||
})
|
||||
|
||||
export const zPrecheckReleaseResponse = z.object({
|
||||
gateCommitId: z.string(),
|
||||
canCreate: z.boolean(),
|
||||
matchedRelease: zReleaseContentMatch.optional(),
|
||||
unsupportedNodes: z.array(zUnsupportedDslNode),
|
||||
unsupportedToolProviders: z.array(zUnsupportedToolProvider),
|
||||
})
|
||||
|
||||
export const zUpdateAccessChannelsRequest = z.object({
|
||||
@ -1302,6 +1259,19 @@ export const zIsUserAllowedToAccessWebAppRes = z.object({
|
||||
result: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const zIssueMcpTokenReply = z.object({
|
||||
token: z.string().optional(),
|
||||
expiresAt: z.string().optional(),
|
||||
tokenType: z.string().optional(),
|
||||
})
|
||||
|
||||
export const zIssueMcpTokenReq = z.object({
|
||||
userId: z.string().optional(),
|
||||
tenantId: z.string().optional(),
|
||||
appId: z.string().optional(),
|
||||
audience: z.string().optional(),
|
||||
})
|
||||
|
||||
export const zJoinWorkspaceReply = z.object({
|
||||
message: z.string().optional(),
|
||||
})
|
||||
@ -1313,6 +1283,7 @@ export const zJoinWorkspaceReq = z.object({
|
||||
id: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
rbacRole: z.string().optional(),
|
||||
})
|
||||
|
||||
export const zLimitConfig = z.object({
|
||||
@ -1494,12 +1465,9 @@ export const zPluginInstallationSettingsReply = z.object({
|
||||
|
||||
export const zRbacRole = z.object({
|
||||
id: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
isBuiltin: z.boolean().optional(),
|
||||
category: z.string().optional(),
|
||||
permissionKeys: z.array(z.string()).optional(),
|
||||
permissions: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const zGetMemberRbacRolesReply = z.object({
|
||||
@ -1778,7 +1746,7 @@ export const zGetWebAppWhitelistSubjectsRes = z.object({
|
||||
*/
|
||||
export const zSubject = z.object({
|
||||
subjectId: z.string().optional(),
|
||||
subjectType: zSubjectType.optional(),
|
||||
subjectType: z.string().optional(),
|
||||
accountData: zSubjectAccountData.optional(),
|
||||
groupData: zSubjectGroupData.optional(),
|
||||
})
|
||||
@ -2104,7 +2072,6 @@ export const zInfoConfigReply = z.object({
|
||||
Branding: zBrandingInfo.optional(),
|
||||
WebAppAuth: zWebAppAuthInfo.optional(),
|
||||
PluginInstallationPermission: zPluginInstallationPermissionInfo.optional(),
|
||||
EnableAppDeploy: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const zWebOAuth2LoginReply = z.object({
|
||||
|
||||
@ -116,3 +116,4 @@ NEXT_PUBLIC_ENABLE_CHANGE_EMAIL=true
|
||||
NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED=true
|
||||
NEXT_PUBLIC_ENABLE_TRIAL_APP=true
|
||||
NEXT_PUBLIC_ENABLE_EXPLORE_BANNER=true
|
||||
NEXT_PUBLIC_RBAC_ENABLED=false
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
describe('env runtime transport', () => {
|
||||
const originalAgentV2Env = process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||
const originalRbacEnv = process.env.NEXT_PUBLIC_RBAC_ENABLED
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -7,7 +8,9 @@ describe('env runtime transport', () => {
|
||||
vi.doUnmock('../utils/client')
|
||||
document.body.removeAttribute('data-enable-agent-v2')
|
||||
document.body.removeAttribute('data-enable-agent-v-2')
|
||||
document.body.removeAttribute('data-rbac-enabled')
|
||||
delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||
delete process.env.NEXT_PUBLIC_RBAC_ENABLED
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
@ -15,6 +18,11 @@ describe('env runtime transport', () => {
|
||||
delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||
else
|
||||
process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = originalAgentV2Env
|
||||
|
||||
if (originalRbacEnv === undefined)
|
||||
delete process.env.NEXT_PUBLIC_RBAC_ENABLED
|
||||
else
|
||||
process.env.NEXT_PUBLIC_RBAC_ENABLED = originalRbacEnv
|
||||
})
|
||||
|
||||
it('should read NEXT_PUBLIC_ENABLE_AGENT_V2 from the browser runtime dataset key', async () => {
|
||||
@ -25,6 +33,14 @@ describe('env runtime transport', () => {
|
||||
expect(env.NEXT_PUBLIC_ENABLE_AGENT_V2).toBe(true)
|
||||
})
|
||||
|
||||
it('should read NEXT_PUBLIC_RBAC_ENABLED from the browser runtime dataset key', async () => {
|
||||
document.body.setAttribute('data-rbac-enabled', 'true')
|
||||
|
||||
const { env } = await import('../env')
|
||||
|
||||
expect(env.NEXT_PUBLIC_RBAC_ENABLED).toBe(true)
|
||||
})
|
||||
|
||||
it('should emit the Agent v2 runtime dataset attribute from getDatasetMap on the server', async () => {
|
||||
process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = 'true'
|
||||
|
||||
@ -39,4 +55,18 @@ describe('env runtime transport', () => {
|
||||
expect(datasetMap['data-enable-agent-v2']).toBe(true)
|
||||
expect(datasetMap['data-enable-agent-v-2']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should emit the RBAC runtime dataset attribute from getDatasetMap on the server', async () => {
|
||||
process.env.NEXT_PUBLIC_RBAC_ENABLED = 'true'
|
||||
|
||||
vi.doMock('../utils/client', () => ({
|
||||
isClient: false,
|
||||
isServer: true,
|
||||
}))
|
||||
|
||||
const { getDatasetMap } = await import('../env')
|
||||
const datasetMap = getDatasetMap()
|
||||
|
||||
expect(datasetMap['data-rbac-enabled']).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models/access-control'
|
||||
import type { App } from '@/types/app'
|
||||
import { SubjectType as EnterpriseSubjectType } from '@dify/contracts/enterprise/types.gen'
|
||||
import { AccessSubjectType as EnterpriseSubjectType } from '@dify/contracts/enterprise/types.gen'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
@ -375,8 +375,8 @@ describe('AccessControl', () => {
|
||||
appId: app.id,
|
||||
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
subjects: [
|
||||
{ subjectId: baseGroup.id, subjectType: EnterpriseSubjectType.SUBJECT_TYPE_GROUP },
|
||||
{ subjectId: baseMember.id, subjectType: EnterpriseSubjectType.SUBJECT_TYPE_ACCOUNT },
|
||||
{ subjectId: baseGroup.id, subjectType: EnterpriseSubjectType.ACCESS_SUBJECT_TYPE_GROUP },
|
||||
{ subjectId: baseMember.id, subjectType: EnterpriseSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { Subject as EnterpriseSubject } from '@dify/contracts/enterprise/types.gen'
|
||||
import type { App } from '@/types/app'
|
||||
import { SubjectType as EnterpriseSubjectType } from '@dify/contracts/enterprise/types.gen'
|
||||
import { AccessSubjectType as EnterpriseSubjectType } from '@dify/contracts/enterprise/types.gen'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -94,12 +94,12 @@ function AccessControlForm({
|
||||
if (currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS) {
|
||||
const subjects: Pick<EnterpriseSubject, 'subjectId' | 'subjectType'>[] = []
|
||||
specificGroups.forEach((group) => {
|
||||
subjects.push({ subjectId: group.id, subjectType: EnterpriseSubjectType.SUBJECT_TYPE_GROUP })
|
||||
subjects.push({ subjectId: group.id, subjectType: EnterpriseSubjectType.ACCESS_SUBJECT_TYPE_GROUP })
|
||||
})
|
||||
specificMembers.forEach((member) => {
|
||||
subjects.push({
|
||||
subjectId: member.id,
|
||||
subjectType: EnterpriseSubjectType.SUBJECT_TYPE_ACCOUNT,
|
||||
subjectType: EnterpriseSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT,
|
||||
})
|
||||
})
|
||||
submitData.subjects = subjects
|
||||
|
||||
@ -42,6 +42,7 @@ export NEXT_PUBLIC_ENABLE_CHANGE_EMAIL=${NEXT_PUBLIC_ENABLE_CHANGE_EMAIL:-${ENAB
|
||||
export NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED=${NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED:-${CREATORS_PLATFORM_FEATURES_ENABLED}}
|
||||
export NEXT_PUBLIC_ENABLE_TRIAL_APP=${NEXT_PUBLIC_ENABLE_TRIAL_APP:-${ENABLE_TRIAL_APP}}
|
||||
export NEXT_PUBLIC_ENABLE_EXPLORE_BANNER=${NEXT_PUBLIC_ENABLE_EXPLORE_BANNER:-${ENABLE_EXPLORE_BANNER}}
|
||||
export NEXT_PUBLIC_RBAC_ENABLED=${NEXT_PUBLIC_RBAC_ENABLED:-${RBAC_ENABLED}}
|
||||
|
||||
export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS}
|
||||
export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST}
|
||||
|
||||
@ -87,6 +87,7 @@ const clientSchema = {
|
||||
NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED: coercedBoolean.default(true),
|
||||
NEXT_PUBLIC_ENABLE_TRIAL_APP: coercedBoolean.default(true),
|
||||
NEXT_PUBLIC_ENABLE_EXPLORE_BANNER: coercedBoolean.default(true),
|
||||
NEXT_PUBLIC_RBAC_ENABLED: coercedBoolean.default(false),
|
||||
|
||||
/**
|
||||
* Enable inline LaTeX rendering with single dollar signs ($...$)
|
||||
@ -216,6 +217,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED: isServer ? process.env.NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED : getRuntimeEnvFromBody('creatorsPlatformFeaturesEnabled'),
|
||||
NEXT_PUBLIC_ENABLE_TRIAL_APP: isServer ? process.env.NEXT_PUBLIC_ENABLE_TRIAL_APP : getRuntimeEnvFromBody('enableTrialApp'),
|
||||
NEXT_PUBLIC_ENABLE_EXPLORE_BANNER: isServer ? process.env.NEXT_PUBLIC_ENABLE_EXPLORE_BANNER : getRuntimeEnvFromBody('enableExploreBanner'),
|
||||
NEXT_PUBLIC_RBAC_ENABLED: isServer ? process.env.NEXT_PUBLIC_RBAC_ENABLED : getRuntimeEnvFromBody('rbacEnabled'),
|
||||
|
||||
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX: isServer ? process.env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX : getRuntimeEnvFromBody('enableSingleDollarLatex'),
|
||||
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL : getRuntimeEnvFromBody('enableWebsiteFirecrawl'),
|
||||
|
||||
@ -123,6 +123,14 @@ async function loadState() {
|
||||
return await import('../index')
|
||||
}
|
||||
|
||||
function workflowDsl() {
|
||||
return [
|
||||
'app:',
|
||||
' mode: workflow',
|
||||
' name: Imported guide',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
describe('create deployment guide state', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -215,4 +223,24 @@ describe('create deployment guide state', () => {
|
||||
expect(store.get(state.releaseNameAtom)).toBe('Initial Release')
|
||||
expect(store.get(state.stepAtom)).toBe('release')
|
||||
})
|
||||
|
||||
it('should read selected DSL file content through the file content query', async () => {
|
||||
const state = await loadState()
|
||||
const store = createStore()
|
||||
const text = vi.fn().mockResolvedValue(workflowDsl())
|
||||
const file = new File([], 'workflow.yml', { type: 'text/yaml' })
|
||||
Object.defineProperty(file, 'text', { value: text })
|
||||
|
||||
store.set(state.selectDslFileAtom, file)
|
||||
mockQueryResults.current.set('createGuideDslFileContent', {
|
||||
data: workflowDsl(),
|
||||
isSuccess: true,
|
||||
})
|
||||
|
||||
expect(text).not.toHaveBeenCalled()
|
||||
expect(store.get(state.dslFileAtom)).toBe(file)
|
||||
expect(store.get(state.dslDefaultAppNameAtom)).toBe('Imported guide')
|
||||
expect(store.get(state.isReadingDslAtom)).toBe(false)
|
||||
expect(store.get(state.dslReadErrorAtom)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,7 +10,7 @@ import type { RuntimeCredentialBindingSelections } from '@/features/deployments/
|
||||
import type { UnsupportedDslNode } from '@/features/deployments/shared/domain/error'
|
||||
import type { App } from '@/types/app'
|
||||
import { EnvVarValueSource as ApiEnvVarValueSource } from '@dify/contracts/enterprise/types.gen'
|
||||
import { keepPreviousData, skipToken } from '@tanstack/react-query'
|
||||
import { keepPreviousData, queryOptions, skipToken } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithInfiniteQuery, atomWithMutation, atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { envVarBindingSlotFromContract, envVarBindingValueType } from '@/features/deployments/components/env-var-bindings-utils'
|
||||
@ -139,10 +139,41 @@ export const selectedAppAtom = atom<WorkflowSourceApp | undefined>(undefined)
|
||||
|
||||
// DSL primitives and derived state
|
||||
export const dslFileAtom = atom<File | undefined>(undefined)
|
||||
const dslContentAtom = atom('')
|
||||
export const isReadingDslAtom = atom(false)
|
||||
export const dslReadErrorAtom = atom(false)
|
||||
const dslReadTokenAtom = atom(0)
|
||||
const dslFileReadVersionAtom = atom(0)
|
||||
|
||||
const dslFileContentQueryAtom = atomWithQuery((get) => {
|
||||
const file = get(dslFileAtom)
|
||||
const fileReadVersion = get(dslFileReadVersionAtom)
|
||||
|
||||
return queryOptions({
|
||||
queryKey: [
|
||||
'createGuideDslFileContent',
|
||||
fileReadVersion,
|
||||
file,
|
||||
file?.name ?? '',
|
||||
file?.size ?? 0,
|
||||
file?.lastModified ?? 0,
|
||||
],
|
||||
queryFn: async () => file ? await file.text() : '',
|
||||
enabled: Boolean(file),
|
||||
retry: false,
|
||||
})
|
||||
})
|
||||
|
||||
const dslContentAtom = atom((get) => {
|
||||
return get(dslFileContentQueryAtom).data ?? ''
|
||||
})
|
||||
|
||||
export const isReadingDslAtom = atom((get) => {
|
||||
const file = get(dslFileAtom)
|
||||
const dslFileContentQuery = get(dslFileContentQueryAtom)
|
||||
|
||||
return Boolean(file && (dslFileContentQuery.isLoading || dslFileContentQuery.isFetching))
|
||||
})
|
||||
|
||||
export const dslReadErrorAtom = atom((get) => {
|
||||
return Boolean(get(dslFileAtom) && get(dslFileContentQueryAtom).isError)
|
||||
})
|
||||
|
||||
export const dslDefaultAppNameAtom = atom((get) => {
|
||||
const dslContent = get(dslContentAtom)
|
||||
@ -470,42 +501,14 @@ export const continueFromSourceAtom = atom(null, (get, set, {
|
||||
})
|
||||
|
||||
// DSL actions
|
||||
export const selectDslFileAtom = atom(null, async (get, set, dslFile?: File) => {
|
||||
export const selectDslFileAtom = atom(null, (get, set, dslFile?: File) => {
|
||||
set(selectedEnvironmentIdAtom, '')
|
||||
set(manualBindingSelectionsAtom, {})
|
||||
set(envVarValuesAtom, {})
|
||||
set(submissionUnsupportedDslNodesAtom, [])
|
||||
|
||||
// Token guard prevents a slow read from an older file from overwriting the newest selection.
|
||||
const dslReadToken = get(dslReadTokenAtom) + 1
|
||||
set(dslReadTokenAtom, dslReadToken)
|
||||
set(dslFileReadVersionAtom, get(dslFileReadVersionAtom) + 1)
|
||||
set(dslFileAtom, dslFile)
|
||||
set(dslContentAtom, '')
|
||||
set(isReadingDslAtom, Boolean(dslFile))
|
||||
set(dslReadErrorAtom, false)
|
||||
|
||||
if (!dslFile)
|
||||
return
|
||||
|
||||
try {
|
||||
const content = await dslFile.text()
|
||||
if (get(dslReadTokenAtom) !== dslReadToken)
|
||||
return
|
||||
|
||||
set(dslContentAtom, content)
|
||||
set(dslReadErrorAtom, false)
|
||||
}
|
||||
catch {
|
||||
if (get(dslReadTokenAtom) !== dslReadToken)
|
||||
return
|
||||
|
||||
set(dslContentAtom, '')
|
||||
set(dslReadErrorAtom, true)
|
||||
}
|
||||
finally {
|
||||
if (get(dslReadTokenAtom) === dslReadToken)
|
||||
set(isReadingDslAtom, false)
|
||||
}
|
||||
})
|
||||
|
||||
// Release derived state and actions
|
||||
@ -914,10 +917,7 @@ export const createDeploymentGuideScopedAtoms = [
|
||||
sourceSearchTextAtom,
|
||||
selectedAppAtom,
|
||||
dslFileAtom,
|
||||
dslContentAtom,
|
||||
isReadingDslAtom,
|
||||
dslReadErrorAtom,
|
||||
dslReadTokenAtom,
|
||||
dslFileReadVersionAtom,
|
||||
instanceNameAtom,
|
||||
instanceDescriptionAtom,
|
||||
releaseNameAtom,
|
||||
|
||||
@ -1,7 +1,33 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { SourceStepContent } from '../source-step'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const sourceAppsQuery = {
|
||||
data: { pages: [{ data: [] }] },
|
||||
hasNextPage: false,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
isPlaceholderData: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
}
|
||||
|
||||
return {
|
||||
sourceAppsQuery,
|
||||
useInfiniteScroll: vi.fn(() => ({
|
||||
rootEl: null,
|
||||
rootRef: vi.fn(),
|
||||
sentinelEl: null,
|
||||
sentinelRef: vi.fn(),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/features/deployments/shared/hooks/use-infinite-scroll', () => ({
|
||||
useInfiniteScroll: mocks.useInfiniteScroll,
|
||||
}))
|
||||
|
||||
vi.mock('@/features/deployments/create-guide/state', async () => {
|
||||
const { atom } = await import('jotai')
|
||||
const methodAtom = atom<'bindApp' | 'importDsl'>('bindApp')
|
||||
@ -22,15 +48,7 @@ vi.mock('@/features/deployments/create-guide/state', async () => {
|
||||
}),
|
||||
selectSourceAppAtom: emptyActionAtom,
|
||||
setSourceSearchTextAtom: emptyActionAtom,
|
||||
sourceAppsQueryAtom: atom({
|
||||
data: { pages: [{ data: [] }] },
|
||||
hasNextPage: false,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
isPlaceholderData: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
}),
|
||||
sourceAppsQueryAtom: atom(mocks.sourceAppsQuery),
|
||||
sourceCanGoNextAtom: atom(false),
|
||||
sourceSearchTextAtom: atom(''),
|
||||
unsupportedDslNodesAtom: atom([]),
|
||||
@ -38,6 +56,19 @@ vi.mock('@/features/deployments/create-guide/state', async () => {
|
||||
})
|
||||
|
||||
describe('SourceStepContent', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
Object.assign(mocks.sourceAppsQuery, {
|
||||
data: { pages: [{ data: [] }] },
|
||||
hasNextPage: false,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
isPlaceholderData: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
})
|
||||
})
|
||||
|
||||
it('should hide the import DSL option when deployment DSL import is disabled', () => {
|
||||
render(<SourceStepContent />)
|
||||
|
||||
@ -46,4 +77,29 @@ describe('SourceStepContent', () => {
|
||||
expect(screen.queryByText(/createGuide\.methods\.importDsl\.description/)).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox', { name: /createGuide\.source\.sourceApp/ })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use infinite scroll to load more source apps', () => {
|
||||
Object.assign(mocks.sourceAppsQuery, {
|
||||
data: {
|
||||
pages: [{
|
||||
data: [{
|
||||
id: 'app-1',
|
||||
name: 'Workflow App',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
hasNextPage: true,
|
||||
})
|
||||
|
||||
render(<SourceStepContent />)
|
||||
|
||||
expect(mocks.useInfiniteScroll).toHaveBeenCalledWith(
|
||||
mocks.sourceAppsQuery,
|
||||
expect.objectContaining({
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
threshold: 0.1,
|
||||
}),
|
||||
)
|
||||
expect(screen.queryByRole('button', { name: /createModal\.loadMoreApps/ })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
unsupportedDslNodesAtom,
|
||||
} from '@/features/deployments/create-guide/state'
|
||||
import { isDeploymentDslImportEnabled } from '@/features/deployments/shared/domain/feature-flags'
|
||||
import { useInfiniteScroll } from '@/features/deployments/shared/hooks/use-infinite-scroll'
|
||||
import { StepShell } from './layout'
|
||||
|
||||
const sourceAppSkeletonKeys = ['first-source-app', 'second-source-app', 'third-source-app']
|
||||
@ -187,9 +188,14 @@ function SourceAppList() {
|
||||
const sourceAppsQuery = useAtomValue(sourceAppsQueryAtom)
|
||||
const sourceApps = (sourceAppsQuery.data?.pages.flatMap(page => page.data) ?? []) as WorkflowSourceApp[]
|
||||
const sourceAppsLoading = sourceAppsQuery.isLoading || sourceAppsQuery.isPlaceholderData || (sourceAppsQuery.isFetching && sourceApps.length === 0)
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement>(sourceAppsQuery, {
|
||||
enabled: !sourceAppsLoading,
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
threshold: 0.1,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto rounded-lg border border-divider-subtle bg-background-default">
|
||||
<div ref={rootRef} className="min-h-0 flex-1 overflow-y-auto rounded-lg border border-divider-subtle bg-background-default">
|
||||
{sourceAppsLoading
|
||||
? <SourceAppSkeleton />
|
||||
: sourceApps.length === 0
|
||||
@ -208,20 +214,12 @@ function SourceAppList() {
|
||||
onSelect={() => selectSourceApp(app)}
|
||||
/>
|
||||
))}
|
||||
{sourceAppsQuery.hasNextPage && (
|
||||
<div className="flex justify-center border-t border-divider-subtle px-3 py-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
disabled={sourceAppsQuery.isFetchingNextPage}
|
||||
onClick={() => {
|
||||
void sourceAppsQuery.fetchNextPage()
|
||||
}}
|
||||
>
|
||||
{sourceAppsQuery.isFetchingNextPage ? t('createModal.loadingApps') : t('createModal.loadMoreApps')}
|
||||
</Button>
|
||||
{sourceAppsQuery.isFetchingNextPage && (
|
||||
<div className="border-t border-divider-subtle px-3 py-2 text-center system-xs-regular text-text-tertiary">
|
||||
{t('createModal.loadingApps')}
|
||||
</div>
|
||||
)}
|
||||
{sourceAppsQuery.hasNextPage && <div ref={sentinelRef} aria-hidden="true" className="h-px" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,51 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { SourceAppPicker } from '../source-app-picker'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const sourceAppsQuery = {
|
||||
data: {
|
||||
pages: [{
|
||||
data: [{
|
||||
id: 'app-1',
|
||||
name: 'Workflow App',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
error: null,
|
||||
fetchNextPage: vi.fn(),
|
||||
hasNextPage: true,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
}
|
||||
|
||||
return {
|
||||
sourceAppsQuery,
|
||||
useInfiniteScroll: vi.fn(() => ({
|
||||
rootEl: null,
|
||||
rootRef: vi.fn(),
|
||||
sentinelEl: null,
|
||||
sentinelRef: vi.fn(),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/features/deployments/create-release/state', async () => {
|
||||
const { atom } = await import('jotai')
|
||||
|
||||
return {
|
||||
createReleaseSourceAppSearchTextAtom: atom(''),
|
||||
createReleaseSourceAppsQueryAtom: atom(mocks.sourceAppsQuery),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/features/deployments/shared/hooks/use-infinite-scroll', () => ({
|
||||
useInfiniteScroll: mocks.useInfiniteScroll,
|
||||
}))
|
||||
|
||||
function renderSourceAppPicker(disabled: boolean) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@ -24,10 +67,59 @@ function renderSourceAppPicker(disabled: boolean) {
|
||||
}
|
||||
|
||||
describe('SourceAppPicker', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
Object.assign(mocks.sourceAppsQuery, {
|
||||
data: {
|
||||
pages: [{
|
||||
data: [{
|
||||
id: 'app-1',
|
||||
name: 'Workflow App',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
error: null,
|
||||
fetchNextPage: vi.fn(),
|
||||
hasNextPage: true,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable the switch control when disabled', () => {
|
||||
renderSourceAppPicker(true)
|
||||
|
||||
expect(screen.getByText('Workflow 1')).toBeInTheDocument()
|
||||
expect(screen.getByRole('combobox', { name: 'deployments.versions.sourceAppOption' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should use infinite scroll to load more apps when the picker is open', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderSourceAppPicker(false)
|
||||
|
||||
expect(mocks.useInfiniteScroll).toHaveBeenCalledWith(
|
||||
mocks.sourceAppsQuery,
|
||||
expect.objectContaining({
|
||||
enabled: false,
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
threshold: 0.1,
|
||||
}),
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('combobox', { name: 'deployments.versions.sourceAppOption' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.useInfiniteScroll).toHaveBeenLastCalledWith(
|
||||
mocks.sourceAppsQuery,
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
threshold: 0.1,
|
||||
}),
|
||||
)
|
||||
})
|
||||
expect(screen.queryByRole('button', { name: /createModal\.loadMoreApps/ })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import type { SourceAppPickerValue } from '../state'
|
||||
import type { App } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Combobox,
|
||||
@ -19,6 +18,7 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
|
||||
import { useInfiniteScroll } from '@/features/deployments/shared/hooks/use-infinite-scroll'
|
||||
import { TitleTooltip } from '../../components/title-tooltip'
|
||||
import {
|
||||
createReleaseSourceAppSearchTextAtom,
|
||||
@ -134,13 +134,18 @@ export function SourceAppPicker({ value, onChange, disabled = false }: {
|
||||
const [isShow, setIsShow] = useState(false)
|
||||
const searchText = useAtomValue(createReleaseSourceAppSearchTextAtom)
|
||||
const setSearchText = useSetAtom(createReleaseSourceAppSearchTextAtom)
|
||||
const sourceAppsQuery = useAtomValue(createReleaseSourceAppsQueryAtom)
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
} = useAtomValue(createReleaseSourceAppsQueryAtom)
|
||||
} = sourceAppsQuery
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement>(sourceAppsQuery, {
|
||||
enabled: isShow && !disabled,
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
threshold: 0.1,
|
||||
})
|
||||
|
||||
const apps = data?.pages.flatMap(page => page.data) ?? []
|
||||
|
||||
@ -202,7 +207,7 @@ export function SourceAppPicker({ value, onChange, disabled = false }: {
|
||||
/>
|
||||
</ComboboxInputGroup>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-1">
|
||||
<div ref={rootRef} className="min-h-0 flex-1 overflow-y-auto p-1">
|
||||
{(isLoading || isFetchingNextPage) && apps.length === 0 && <SourceAppPickerSkeleton />}
|
||||
<ComboboxList className="max-h-none p-0">
|
||||
{(app: App) => (
|
||||
@ -214,20 +219,12 @@ export function SourceAppPicker({ value, onChange, disabled = false }: {
|
||||
{t('createModal.appSearchEmpty')}
|
||||
</ComboboxEmpty>
|
||||
)}
|
||||
{hasNextPage && (
|
||||
<div className="flex justify-center px-3 py-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
disabled={isFetchingNextPage}
|
||||
onClick={() => {
|
||||
void fetchNextPage()
|
||||
}}
|
||||
>
|
||||
{isFetchingNextPage ? t('createModal.loadingApps') : t('createModal.loadMoreApps')}
|
||||
</Button>
|
||||
{isFetchingNextPage && apps.length > 0 && (
|
||||
<div className="px-3 py-2 text-center system-xs-regular text-text-tertiary">
|
||||
{t('createModal.loadingApps')}
|
||||
</div>
|
||||
)}
|
||||
{hasNextPage && <div ref={sentinelRef} aria-hidden="true" className="h-px" />}
|
||||
</div>
|
||||
</div>
|
||||
</ComboboxContent>
|
||||
|
||||
@ -4,7 +4,7 @@ import type { EnvironmentDeployment } from '@dify/contracts/enterprise/types.gen
|
||||
import { RuntimeInstanceStatus } from '@dify/contracts/enterprise/types.gen'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { openDeployDrawerAtom } from '../../deploy-drawer/state'
|
||||
@ -24,11 +24,9 @@ export function DeploymentRowActions({ appInstanceId, envId, row }: {
|
||||
const undeployDeployment = useMutation(consoleQuery.enterprise.deploymentService.undeploy.mutationOptions())
|
||||
const [showUndeployConfirm, setShowUndeployConfirm] = useState(false)
|
||||
const [showErrorDetail, setShowErrorDetail] = useState(false)
|
||||
const [isUndeploying, setIsUndeploying] = useState(false)
|
||||
const undeployInFlightRef = useRef(false)
|
||||
const isUndeployed = isUndeployedDeploymentRow(row)
|
||||
const status = row.status
|
||||
const isUndeployRequesting = undeployDeployment.isPending || isUndeploying
|
||||
const isUndeployRequesting = undeployDeployment.isPending
|
||||
const undeployActionDisabled = isUndeployRequesting
|
||||
const isDeploymentInProgress = isRuntimeDeploymentInProgress(status)
|
||||
const isDeployFailed = status === RuntimeInstanceStatus.RUNTIME_INSTANCE_STATUS_FAILED
|
||||
@ -43,11 +41,9 @@ export function DeploymentRowActions({ appInstanceId, envId, row }: {
|
||||
}
|
||||
|
||||
function handleUndeploy() {
|
||||
if (undeployInFlightRef.current)
|
||||
if (isUndeployRequesting)
|
||||
return
|
||||
|
||||
undeployInFlightRef.current = true
|
||||
setIsUndeploying(true)
|
||||
undeployDeployment.mutate(
|
||||
{
|
||||
params: { appInstanceId, environmentId: envId },
|
||||
@ -59,8 +55,6 @@ export function DeploymentRowActions({ appInstanceId, envId, row }: {
|
||||
},
|
||||
{
|
||||
onSettled: () => {
|
||||
undeployInFlightRef.current = false
|
||||
setIsUndeploying(false)
|
||||
setShowUndeployConfirm(false)
|
||||
},
|
||||
},
|
||||
|
||||
@ -2,7 +2,7 @@ import type {
|
||||
AccessPolicy,
|
||||
Subject,
|
||||
} from '@dify/contracts/enterprise/types.gen'
|
||||
import { AccessMode, SubjectType } from '@dify/contracts/enterprise/types.gen'
|
||||
import { AccessMode, AccessSubjectType } from '@dify/contracts/enterprise/types.gen'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { AccessMode as AppAccessMode } from '@/models/access-control'
|
||||
import {
|
||||
@ -56,7 +56,7 @@ describe('access policy mode mapping', () => {
|
||||
describe('access policy subject conversion', () => {
|
||||
it('should normalize resolved group and account subjects', () => {
|
||||
expect(normalizeResolvedSubject({
|
||||
subjectType: SubjectType.SUBJECT_TYPE_GROUP,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP,
|
||||
groupData: {
|
||||
id: 'group-1',
|
||||
name: 'Admins',
|
||||
@ -64,53 +64,53 @@ describe('access policy subject conversion', () => {
|
||||
},
|
||||
})).toEqual({
|
||||
id: 'group-1',
|
||||
subjectType: SubjectType.SUBJECT_TYPE_GROUP,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP,
|
||||
name: 'Admins',
|
||||
memberCount: 3,
|
||||
})
|
||||
|
||||
expect(normalizeResolvedSubject({
|
||||
subjectType: SubjectType.SUBJECT_TYPE_ACCOUNT,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT,
|
||||
accountData: {
|
||||
id: 'account-1',
|
||||
email: 'member@example.com',
|
||||
},
|
||||
})).toEqual({
|
||||
id: 'account-1',
|
||||
subjectType: SubjectType.SUBJECT_TYPE_ACCOUNT,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT,
|
||||
name: 'member@example.com',
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore unsupported subjects and subjects without ids', () => {
|
||||
expect(normalizeResolvedSubject({ subjectType: SubjectType.SUBJECT_TYPE_GROUP })).toBeUndefined()
|
||||
expect(normalizeResolvedSubject({ subjectType: SubjectType.SUBJECT_TYPE_ACCOUNT })).toBeUndefined()
|
||||
expect(normalizeResolvedSubject({ subjectType: SubjectType.SUBJECT_TYPE_UNSPECIFIED } as Subject)).toBeUndefined()
|
||||
expect(normalizeResolvedSubject({ subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP })).toBeUndefined()
|
||||
expect(normalizeResolvedSubject({ subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT })).toBeUndefined()
|
||||
expect(normalizeResolvedSubject({ subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_UNSPECIFIED } as Subject)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should preserve labels when reading selected subjects from policy', () => {
|
||||
expect(selectedSubjectsFromPolicy(policy({
|
||||
subjects: [
|
||||
{ subjectId: 'group-1', subjectType: SubjectType.SUBJECT_TYPE_GROUP },
|
||||
{ subjectId: 'account-1', subjectType: SubjectType.SUBJECT_TYPE_ACCOUNT },
|
||||
{ subjectId: 'group-1', subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP },
|
||||
{ subjectId: 'account-1', subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT },
|
||||
],
|
||||
}), [
|
||||
{
|
||||
id: 'group-1',
|
||||
subjectType: SubjectType.SUBJECT_TYPE_GROUP,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP,
|
||||
name: 'Admins',
|
||||
memberCount: 3,
|
||||
},
|
||||
])).toEqual([
|
||||
{
|
||||
id: 'group-1',
|
||||
subjectType: SubjectType.SUBJECT_TYPE_GROUP,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP,
|
||||
name: 'Admins',
|
||||
memberCount: 3,
|
||||
},
|
||||
{
|
||||
id: 'account-1',
|
||||
subjectType: SubjectType.SUBJECT_TYPE_ACCOUNT,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT,
|
||||
},
|
||||
])
|
||||
|
||||
@ -121,20 +121,20 @@ describe('access policy subject conversion', () => {
|
||||
const subjects = [
|
||||
{
|
||||
id: 'group-1',
|
||||
subjectType: SubjectType.SUBJECT_TYPE_GROUP,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP,
|
||||
name: 'Admins',
|
||||
memberCount: 3,
|
||||
},
|
||||
{
|
||||
id: 'account-1',
|
||||
subjectType: SubjectType.SUBJECT_TYPE_ACCOUNT,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT,
|
||||
name: 'Member',
|
||||
},
|
||||
]
|
||||
|
||||
expect(policySubjects(subjects)).toEqual([
|
||||
{ subjectId: 'group-1', subjectType: SubjectType.SUBJECT_TYPE_GROUP },
|
||||
{ subjectId: 'account-1', subjectType: SubjectType.SUBJECT_TYPE_ACCOUNT },
|
||||
{ subjectId: 'group-1', subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP },
|
||||
{ subjectId: 'account-1', subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT },
|
||||
])
|
||||
|
||||
const selection = accessControlSelectionFromSubjects(subjects)
|
||||
|
||||
@ -3,16 +3,26 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ApiKeyGenerateMenu } from '../api-key-generate-menu'
|
||||
|
||||
const mockMutate = vi.fn()
|
||||
const mockUseMutation = vi.hoisted(() => vi.fn())
|
||||
const mockMutate = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
return {
|
||||
...actual,
|
||||
useMutation: (...args: unknown[]) => mockUseMutation(...args),
|
||||
}
|
||||
})
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useMutation: () => ({
|
||||
isPending: false,
|
||||
mutate: mockMutate,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
enterprise: {
|
||||
accessService: {
|
||||
createApiKey: {
|
||||
mutationOptions: () => ({ mutationKey: ['createApiKey'] }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
function createEnvironment(): Environment {
|
||||
return {
|
||||
@ -24,10 +34,6 @@ function createEnvironment(): Environment {
|
||||
describe('ApiKeyGenerateMenu', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseMutation.mockReturnValue({
|
||||
isPending: false,
|
||||
mutate: mockMutate,
|
||||
})
|
||||
})
|
||||
|
||||
it('should show the required name error when submitting an empty name', () => {
|
||||
|
||||
@ -3,15 +3,26 @@ import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AccessChannelsSection } from '../channels-section'
|
||||
|
||||
const mockUseMutation = vi.hoisted(() => vi.fn())
|
||||
const mockToggleAccessChannel = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
return {
|
||||
...actual,
|
||||
useMutation: (...args: unknown[]) => mockUseMutation(...args),
|
||||
}
|
||||
})
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useMutation: () => ({
|
||||
isPending: false,
|
||||
mutate: mockToggleAccessChannel,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
enterprise: {
|
||||
accessService: {
|
||||
updateAccessChannels: {
|
||||
mutationOptions: () => ({ mutationKey: ['updateAccessChannels'] }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
function createAccessChannels(): AccessChannels {
|
||||
return {
|
||||
@ -38,10 +49,6 @@ function createEndpoint(endpointUrl: string): AccessEndpoint {
|
||||
describe('AccessChannelsSection', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseMutation.mockReturnValue({
|
||||
isPending: false,
|
||||
mutate: vi.fn(),
|
||||
})
|
||||
})
|
||||
|
||||
it('should render channel descriptions when access channels are enabled', () => {
|
||||
|
||||
@ -1,22 +1,42 @@
|
||||
import type { AccessPolicy, Environment, EnvironmentAccessPolicy } from '@dify/contracts/enterprise/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import { AccessMode, SubjectType } from '@dify/contracts/enterprise/types.gen'
|
||||
import { AccessMode, AccessSubjectType } from '@dify/contracts/enterprise/types.gen'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { createStore, Provider as JotaiProvider } from 'jotai'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { EnvironmentPermissionRow } from '../permissions'
|
||||
import { AccessPermissionsSection } from '../permissions-section'
|
||||
|
||||
const mockMutate = vi.fn()
|
||||
const mockUseMutation = vi.hoisted(() => vi.fn())
|
||||
const mockMutate = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
return {
|
||||
...actual,
|
||||
useMutation: (...args: unknown[]) => mockUseMutation(...args),
|
||||
}
|
||||
})
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useInfiniteQuery: () => ({
|
||||
data: { pages: [] },
|
||||
fetchNextPage: vi.fn(),
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
}),
|
||||
useMutation: () => ({
|
||||
isPending: false,
|
||||
mutate: mockMutate,
|
||||
}),
|
||||
useQuery: () => ({
|
||||
data: undefined,
|
||||
isPending: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
enterprise: {
|
||||
accessService: {
|
||||
updateAccessPolicy: {
|
||||
mutationOptions: () => ({ mutationKey: ['updateAccessPolicy'] }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
function renderWithAtomStore(children: ReactNode) {
|
||||
return render(
|
||||
@ -53,11 +73,11 @@ function createSpecificAccessPolicy(): AccessPolicy {
|
||||
subjects: [
|
||||
{
|
||||
subjectId: 'group-1',
|
||||
subjectType: SubjectType.SUBJECT_TYPE_GROUP,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP,
|
||||
},
|
||||
{
|
||||
subjectId: 'member-1',
|
||||
subjectType: SubjectType.SUBJECT_TYPE_ACCOUNT,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT,
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -77,10 +97,6 @@ describe('EnvironmentPermissionRow', () => {
|
||||
mockMutate.mockImplementation((_variables: unknown, options?: { onError?: () => void }) => {
|
||||
options?.onError?.()
|
||||
})
|
||||
mockUseMutation.mockReturnValue({
|
||||
isPending: false,
|
||||
mutate: mockMutate,
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep the previous permission visible when updating the policy fails', () => {
|
||||
@ -100,6 +116,89 @@ describe('EnvironmentPermissionRow', () => {
|
||||
expect(screen.getByText('deployments.access.permission.organization')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show the updated policy after success', () => {
|
||||
mockMutate.mockImplementation((_variables: unknown, options?: { onSuccess?: () => void }) => {
|
||||
options?.onSuccess?.()
|
||||
})
|
||||
|
||||
renderWithAtomStore(
|
||||
<EnvironmentPermissionRow
|
||||
appInstanceId="app-instance-1"
|
||||
environment={createEnvironment()}
|
||||
summaryPolicy={createAccessPolicy()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /deployments\.access\.permissions\.editAriaLabel/ }))
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'app.accessControlDialog.accessItems.anyone' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
{
|
||||
params: {
|
||||
appInstanceId: 'app-instance-1',
|
||||
environmentId: 'environment-1',
|
||||
},
|
||||
body: {
|
||||
appInstanceId: 'app-instance-1',
|
||||
environmentId: 'environment-1',
|
||||
mode: AccessMode.ACCESS_MODE_PUBLIC,
|
||||
subjects: [],
|
||||
},
|
||||
},
|
||||
expect.objectContaining({
|
||||
onError: expect.any(Function),
|
||||
onSuccess: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
expect(screen.getByText('deployments.access.permission.anyone')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should submit specific subjects with the deployment access subject type', () => {
|
||||
mockMutate.mockImplementation((_variables: unknown, options?: { onSuccess?: () => void }) => {
|
||||
options?.onSuccess?.()
|
||||
})
|
||||
|
||||
renderWithAtomStore(
|
||||
<EnvironmentPermissionRow
|
||||
appInstanceId="app-instance-1"
|
||||
environment={createEnvironment()}
|
||||
summaryPolicy={createSpecificAccessPolicy()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /deployments\.access\.permissions\.editAriaLabel/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
{
|
||||
params: {
|
||||
appInstanceId: 'app-instance-1',
|
||||
environmentId: 'environment-1',
|
||||
},
|
||||
body: {
|
||||
appInstanceId: 'app-instance-1',
|
||||
environmentId: 'environment-1',
|
||||
mode: AccessMode.ACCESS_MODE_PRIVATE,
|
||||
subjects: [
|
||||
{
|
||||
subjectId: 'group-1',
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP,
|
||||
},
|
||||
{
|
||||
subjectId: 'member-1',
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
expect.objectContaining({
|
||||
onError: expect.any(Function),
|
||||
onSuccess: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should show specific subject counts in the access summary', () => {
|
||||
renderWithAtomStore(
|
||||
<EnvironmentPermissionRow
|
||||
@ -121,10 +220,6 @@ describe('EnvironmentPermissionRow', () => {
|
||||
describe('AccessPermissionsSection', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseMutation.mockReturnValue({
|
||||
isPending: false,
|
||||
mutate: mockMutate,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render permission rows without column headers', () => {
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
import type { Getter } from 'jotai'
|
||||
import { skipToken } from '@tanstack/react-query'
|
||||
import { atom, createStore } from 'jotai'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { setNextRouteStateAtom } from '@/app/components/next-route-state/atoms'
|
||||
|
||||
type QueryOptions = {
|
||||
enabled?: boolean
|
||||
input?: unknown
|
||||
queryKey?: readonly unknown[]
|
||||
}
|
||||
|
||||
vi.mock('jotai-tanstack-query', () => ({
|
||||
atomWithQuery: (createOptions: (get: Getter) => QueryOptions) => atom(get => ({
|
||||
...createOptions(get),
|
||||
data: undefined,
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
enterprise: {
|
||||
accessService: {
|
||||
getAccessSettings: {
|
||||
queryOptions: (options: QueryOptions) => ({
|
||||
...options,
|
||||
queryKey: ['getAccessSettings', options.input],
|
||||
}),
|
||||
},
|
||||
getDeveloperApiSettings: {
|
||||
queryOptions: (options: QueryOptions) => ({
|
||||
...options,
|
||||
queryKey: ['getDeveloperApiSettings', options.input],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
async function loadState() {
|
||||
return await import('../state')
|
||||
}
|
||||
|
||||
function setDeploymentRoute(store: ReturnType<typeof createStore>, appInstanceId = 'app-instance-1') {
|
||||
store.set(setNextRouteStateAtom, {
|
||||
pathname: `/deployments/${appInstanceId}/settings/access`,
|
||||
params: { appInstanceId },
|
||||
})
|
||||
}
|
||||
|
||||
describe('deployment access state', () => {
|
||||
it('should gate access queries until a route app instance exists', async () => {
|
||||
const state = await loadState()
|
||||
const store = createStore()
|
||||
|
||||
expect(store.get(state.accessSettingsQueryAtom)).toMatchObject({
|
||||
enabled: false,
|
||||
input: skipToken,
|
||||
})
|
||||
expect(store.get(state.developerApiSettingsQueryAtom)).toMatchObject({
|
||||
enabled: false,
|
||||
input: skipToken,
|
||||
})
|
||||
|
||||
setDeploymentRoute(store)
|
||||
|
||||
expect(store.get(state.accessSettingsQueryAtom)).toMatchObject({
|
||||
enabled: true,
|
||||
input: { params: { appInstanceId: 'app-instance-1' } },
|
||||
})
|
||||
expect(store.get(state.developerApiSettingsQueryAtom)).toMatchObject({
|
||||
enabled: true,
|
||||
input: { params: { appInstanceId: 'app-instance-1' } },
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -2,7 +2,7 @@ import type {
|
||||
AccessPolicy,
|
||||
AccessMode as AccessPolicyMode,
|
||||
AccessSubject,
|
||||
SubjectType as AccessSubjectType,
|
||||
AccessSubjectType as AccessSubjectTypeValue,
|
||||
Subject,
|
||||
} from '@dify/contracts/enterprise/types.gen'
|
||||
import type { AccessSubjectSelectionValue } from '@/app/components/app/app-access-control/access-subject-selector/types'
|
||||
@ -12,7 +12,7 @@ import type {
|
||||
} from '@/models/access-control'
|
||||
import {
|
||||
AccessMode,
|
||||
SubjectType,
|
||||
AccessSubjectType,
|
||||
} from '@dify/contracts/enterprise/types.gen'
|
||||
import { AccessMode as AppAccessMode } from '@/models/access-control'
|
||||
|
||||
@ -26,7 +26,7 @@ export const permissionIcon: Record<AccessPermissionKind, string> = {
|
||||
|
||||
export type SelectableAccessSubject = {
|
||||
id: string
|
||||
subjectType: AccessSubjectType
|
||||
subjectType: AccessSubjectTypeValue
|
||||
name?: string
|
||||
memberCount?: number
|
||||
}
|
||||
@ -64,27 +64,27 @@ export function appAccessModeToPermissionKey(mode: AppAccessMode): AccessPermiss
|
||||
}
|
||||
|
||||
export function normalizeResolvedSubject(subject: Subject): SelectableAccessSubject | undefined {
|
||||
if (subject.subjectType === SubjectType.SUBJECT_TYPE_GROUP) {
|
||||
if (subject.subjectType === AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP) {
|
||||
const id = subject.subjectId || subject.groupData?.id
|
||||
if (!id)
|
||||
return undefined
|
||||
|
||||
return {
|
||||
id,
|
||||
subjectType: SubjectType.SUBJECT_TYPE_GROUP,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP,
|
||||
name: subject.groupData?.name,
|
||||
memberCount: subject.groupData?.groupSize,
|
||||
}
|
||||
}
|
||||
|
||||
if (subject.subjectType === SubjectType.SUBJECT_TYPE_ACCOUNT) {
|
||||
if (subject.subjectType === AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT) {
|
||||
const id = subject.subjectId || subject.accountData?.id
|
||||
if (!id)
|
||||
return undefined
|
||||
|
||||
return {
|
||||
id,
|
||||
subjectType: SubjectType.SUBJECT_TYPE_ACCOUNT,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT,
|
||||
name: subject.accountData?.name || subject.accountData?.email,
|
||||
}
|
||||
}
|
||||
@ -144,10 +144,10 @@ function selectableSubjectToAccount(subject: SelectableAccessSubject): AccessCon
|
||||
export function accessControlSelectionFromSubjects(subjects: SelectableAccessSubject[]): AccessSubjectSelectionValue {
|
||||
return {
|
||||
groups: subjects
|
||||
.filter(subject => subject.subjectType === SubjectType.SUBJECT_TYPE_GROUP)
|
||||
.filter(subject => subject.subjectType === AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP)
|
||||
.map(selectableSubjectToGroup),
|
||||
members: subjects
|
||||
.filter(subject => subject.subjectType === SubjectType.SUBJECT_TYPE_ACCOUNT)
|
||||
.filter(subject => subject.subjectType === AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT)
|
||||
.map(selectableSubjectToAccount),
|
||||
}
|
||||
}
|
||||
@ -156,13 +156,13 @@ export function subjectsFromAccessControlSelection(value: AccessSubjectSelection
|
||||
return [
|
||||
...value.groups.map((group): SelectableAccessSubject => ({
|
||||
id: group.id,
|
||||
subjectType: SubjectType.SUBJECT_TYPE_GROUP,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP,
|
||||
name: group.name,
|
||||
memberCount: group.groupSize,
|
||||
})),
|
||||
...value.members.map((member): SelectableAccessSubject => ({
|
||||
id: member.id,
|
||||
subjectType: SubjectType.SUBJECT_TYPE_ACCOUNT,
|
||||
subjectType: AccessSubjectType.ACCESS_SUBJECT_TYPE_ACCOUNT,
|
||||
name: member.name || member.email,
|
||||
})),
|
||||
]
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
} from './access-policy'
|
||||
import type { AccessSubjectSelectionValue } from '@/app/components/app/app-access-control/access-subject-selector/types'
|
||||
import type { AccessControlDraft } from '@/app/components/app/app-access-control/store'
|
||||
import { SubjectType } from '@dify/contracts/enterprise/types.gen'
|
||||
import { AccessSubjectType } from '@dify/contracts/enterprise/types.gen'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AccessControlDialog } from '@/app/components/app/app-access-control/access-control-dialog'
|
||||
@ -35,7 +35,7 @@ export function PermissionSummaryButton({
|
||||
onClick: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const groupCount = subjects?.filter(subject => subject.subjectType === SubjectType.SUBJECT_TYPE_GROUP).length ?? 0
|
||||
const groupCount = subjects?.filter(subject => subject.subjectType === AccessSubjectType.ACCESS_SUBJECT_TYPE_GROUP).length ?? 0
|
||||
const memberCount = (subjects?.length ?? 0) - groupCount
|
||||
const countLabels = [
|
||||
...(groupCount > 0 ? [t('access.members.groupCount', { count: groupCount })] : []),
|
||||
|
||||
@ -4,19 +4,37 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { DeployReleaseMenu } from '../deploy-release-menu'
|
||||
|
||||
const mockUseMutation = vi.hoisted(() => vi.fn())
|
||||
const mockDeleteRelease = vi.fn()
|
||||
const mockDeleteRelease = vi.hoisted(() => vi.fn())
|
||||
const mockExportReleaseDsl = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dropdown-menu', () => import('@/__mocks__/base-ui-dropdown-menu'))
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useMutation: (...args: unknown[]) => mockUseMutation(...args),
|
||||
useMutation: (options: { mutationKey?: readonly unknown[] }) => {
|
||||
if (options.mutationKey?.[0] === 'deployments')
|
||||
return { isPending: false, mutate: mockExportReleaseDsl }
|
||||
|
||||
return { isPending: false, mutate: mockDeleteRelease }
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
enterprise: {
|
||||
releaseService: {
|
||||
deleteRelease: {
|
||||
mutationOptions: () => ({ mutationKey: ['deleteRelease'] }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../state', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../state')>()
|
||||
const { atom } = await import('jotai')
|
||||
@ -78,10 +96,6 @@ function appInstanceResult() {
|
||||
describe('DeployReleaseMenu', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseMutation.mockReturnValue({
|
||||
isPending: false,
|
||||
mutate: mockDeleteRelease,
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable release deletion when deployment usage cannot be checked', () => {
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { mutationOptions, useMutation } from '@tanstack/react-query'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -32,6 +32,12 @@ import {
|
||||
setDeployReleaseMenuOpenAtom,
|
||||
} from './state'
|
||||
|
||||
type ExportReleaseDslInput = {
|
||||
release: Release
|
||||
releaseId: string
|
||||
appInstanceName?: string
|
||||
}
|
||||
|
||||
export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows, onDeleted }: {
|
||||
appInstanceId: string
|
||||
releaseId: string
|
||||
@ -44,11 +50,14 @@ export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows, onDel
|
||||
const setDeployReleaseMenuOpen = useSetAtom(setDeployReleaseMenuOpenAtom)
|
||||
const [showEditDialog, setShowEditDialog] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [isExportingDsl, setIsExportingDsl] = useState(false)
|
||||
const open = openReleaseMenuId === releaseId
|
||||
const environmentDeploymentsQuery = useAtomValue(deployReleaseMenuEnvironmentDeploymentsQueryAtom)
|
||||
const appInstanceQuery = useAtomValue(deployReleaseMenuAppInstanceQueryAtom)
|
||||
const deleteRelease = useMutation(consoleQuery.enterprise.releaseService.deleteRelease.mutationOptions())
|
||||
const exportReleaseDslMutation = useMutation(mutationOptions({
|
||||
mutationKey: ['deployments', 'release-dsl-export'],
|
||||
mutationFn: (input: ExportReleaseDslInput) => exportReleaseDsl(input),
|
||||
}))
|
||||
|
||||
const environments = (environmentDeploymentsQuery.data?.environmentDeployments ?? [])
|
||||
.map(row => row.environment)
|
||||
@ -59,12 +68,14 @@ export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows, onDel
|
||||
if (!targetRelease)
|
||||
return null
|
||||
|
||||
const targetReleaseName = targetRelease.displayName
|
||||
const release = targetRelease
|
||||
const targetReleaseName = release.displayName
|
||||
const deleteUsageCount = releaseUsageCount(releaseId, deploymentRows)
|
||||
const isCheckingDeleteUsage = open && environmentDeploymentsQuery.isLoading
|
||||
const hasDeleteUsageCheckFailed = open && environmentDeploymentsQuery.isError
|
||||
const isReleaseInUse = deleteUsageCount > 0
|
||||
const isDeletingRelease = deleteRelease.isPending
|
||||
const isExportingDsl = exportReleaseDslMutation.isPending
|
||||
const deleteDisabledReason = isCheckingDeleteUsage
|
||||
? t('versions.disabledReason.checkingDeployments')
|
||||
: hasDeleteUsageCheckFailed
|
||||
@ -78,21 +89,21 @@ export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows, onDel
|
||||
setDeployReleaseMenuOpen({ releaseId, open: nextOpen })
|
||||
}
|
||||
|
||||
const handleExportDsl = async () => {
|
||||
function handleExportDsl() {
|
||||
if (isExportingDsl)
|
||||
return
|
||||
|
||||
setIsExportingDsl(true)
|
||||
try {
|
||||
await exportReleaseDsl({ release: targetRelease, releaseId, appInstanceName })
|
||||
handleOpenChange(false)
|
||||
}
|
||||
catch {
|
||||
toast.error(t('versions.exportDslFailed'))
|
||||
}
|
||||
finally {
|
||||
setIsExportingDsl(false)
|
||||
}
|
||||
exportReleaseDslMutation.mutate(
|
||||
{ release, releaseId, appInstanceName },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleOpenChange(false)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('versions.exportDslFailed'))
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function handleDeleteRelease() {
|
||||
@ -123,7 +134,7 @@ export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows, onDel
|
||||
environmentDeployments: environmentDeploymentsQuery.data?.environmentDeployments ?? [],
|
||||
releaseRows,
|
||||
releaseId,
|
||||
targetRelease,
|
||||
targetRelease: release,
|
||||
t,
|
||||
})
|
||||
|
||||
@ -224,14 +235,14 @@ export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows, onDel
|
||||
</DropdownMenu>
|
||||
|
||||
<EditReleaseDialog
|
||||
release={targetRelease}
|
||||
release={release}
|
||||
open={showEditDialog}
|
||||
onOpenChange={setShowEditDialog}
|
||||
/>
|
||||
|
||||
<DeleteReleaseDialog
|
||||
open={showDeleteConfirm}
|
||||
release={targetRelease}
|
||||
release={release}
|
||||
isDeleting={isDeletingRelease}
|
||||
onOpenChange={setShowDeleteConfirm}
|
||||
onConfirm={handleDeleteRelease}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import type { ListReleaseSummariesResponse } from '@dify/contracts/enterprise/types.gen'
|
||||
import { keepPreviousData, skipToken } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||
@ -11,7 +10,7 @@ import { RELEASE_HISTORY_PAGE_SIZE } from '../../shared/domain/pagination'
|
||||
export const releaseHistoryCurrentPageAtom = atom(0)
|
||||
export const deployReleaseMenuOpenReleaseIdAtom = atom<string | undefined>(undefined)
|
||||
|
||||
export const releaseHistoryQueryAtom = atomWithQuery<ListReleaseSummariesResponse>((get) => {
|
||||
export const releaseHistoryQueryAtom = atomWithQuery((get) => {
|
||||
const appInstanceId = get(deploymentRouteAppInstanceIdAtom)
|
||||
const currentPage = get(releaseHistoryCurrentPageAtom)
|
||||
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { ListAppInstanceSummariesResponse } from '@dify/contracts/enterprise/types.gen'
|
||||
import type { InfiniteData, QueryKey } from '@tanstack/react-query'
|
||||
import type { ReactNode } from 'react'
|
||||
import { keepPreviousData } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithInfiniteQuery, atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { atomWithInfiniteQuery } from 'jotai-tanstack-query'
|
||||
import { useHydrateAtoms } from 'jotai/utils'
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
@ -34,21 +32,7 @@ export function DeploymentsListStateBoundary({ children }: {
|
||||
return children
|
||||
}
|
||||
|
||||
function listDeploymentStatusPollingInterval(data?: InfiniteData<ListAppInstanceSummariesResponse>) {
|
||||
const rows = data?.pages?.flatMap(page =>
|
||||
page.appInstanceSummaries.flatMap(summary => summary.environmentDeployments),
|
||||
) ?? []
|
||||
|
||||
return deploymentStatusPollingInterval(rows)
|
||||
}
|
||||
|
||||
export const deploymentsListQueryAtom = atomWithInfiniteQuery<
|
||||
ListAppInstanceSummariesResponse,
|
||||
Error,
|
||||
InfiniteData<ListAppInstanceSummariesResponse>,
|
||||
QueryKey,
|
||||
number
|
||||
>((get) => {
|
||||
export const deploymentsListQueryAtom = atomWithInfiniteQuery((get) => {
|
||||
const queryKeywords = get(deploymentsListKeywordsAtom).trim()
|
||||
const queryEnvironmentId = get(deploymentsListEnvironmentIdAtom) ?? undefined
|
||||
|
||||
@ -64,7 +48,13 @@ export const deploymentsListQueryAtom = atomWithInfiniteQuery<
|
||||
getNextPageParam: lastPage => getNextPageParamFromPagination(lastPage.pagination),
|
||||
initialPageParam: 1,
|
||||
placeholderData: keepPreviousData,
|
||||
refetchInterval: query => listDeploymentStatusPollingInterval(query.state.data),
|
||||
refetchInterval: (query) => {
|
||||
const rows = query.state.data?.pages.flatMap(page =>
|
||||
page.appInstanceSummaries.flatMap(summary => summary.environmentDeployments),
|
||||
) ?? []
|
||||
|
||||
return deploymentStatusPollingInterval(rows)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -88,16 +78,3 @@ export const deploymentsListShowEmptyStateAtom = atom((get) => {
|
||||
export const deploymentsListHasFilterAtom = atom((get) => {
|
||||
return Boolean(get(deploymentsListKeywordsAtom).trim() || get(deploymentsListEnvironmentIdAtom))
|
||||
})
|
||||
|
||||
export const environmentsFilterQueryAtom = atomWithQuery(() =>
|
||||
consoleQuery.enterprise.environmentService.listEnvironments.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
// The filter lists every deployable environment; environment count is
|
||||
// capped well below the 100-per-page maximum.
|
||||
pageNumber: 1,
|
||||
resultsPerPage: 100,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@ -8,14 +8,12 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
envFilterQueryState,
|
||||
environmentsFilterQueryAtom,
|
||||
} from '../state'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { envFilterQueryState } from '../state'
|
||||
|
||||
type EnvironmentFilterOption = {
|
||||
value: string | null
|
||||
@ -33,7 +31,16 @@ export function EnvironmentFilter({ className }: {
|
||||
const { t } = useTranslation('deployments')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [envFilter, setEnvFilter] = useQueryState('env', envFilterQueryState)
|
||||
const environmentsQuery = useAtomValue(environmentsFilterQueryAtom)
|
||||
const environmentsQuery = useQuery(consoleQuery.enterprise.environmentService.listEnvironments.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
// The filter lists every deployable environment; environment count is
|
||||
// capped well below the 100-per-page maximum.
|
||||
pageNumber: 1,
|
||||
resultsPerPage: 100,
|
||||
},
|
||||
},
|
||||
}))
|
||||
const environmentOptions: EnvironmentFilterOption[] = environmentsQuery.data?.environments
|
||||
?.map(environment => ({
|
||||
value: environment.id,
|
||||
|
||||
@ -6,11 +6,11 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { debounce, useQueryState } from 'nuqs'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StudioListHeader } from '@/app/components/apps/studio-list-header'
|
||||
import { SkeletonRectangle } from '@/app/components/base/skeleton'
|
||||
import { DeploymentEmptyState, DeploymentStateMessage } from '../../components/empty-state'
|
||||
import { useInfiniteScroll } from '../../shared/hooks/use-infinite-scroll'
|
||||
import {
|
||||
deploymentsListHasFilterAtom,
|
||||
deploymentsListQueryAtom,
|
||||
@ -157,48 +157,22 @@ function DeploymentsListControls() {
|
||||
|
||||
export function DeploymentsListShell() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const deploymentsListQuery = useAtomValue(deploymentsListQueryAtom)
|
||||
const appInstanceSummaries = useAtomValue(deploymentsListRowsAtom)
|
||||
const showSkeleton = useAtomValue(deploymentsListShowSkeletonAtom)
|
||||
const showEmptyState = useAtomValue(deploymentsListShowEmptyStateAtom)
|
||||
const {
|
||||
error,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isError,
|
||||
isFetchingNextPage,
|
||||
isLoading,
|
||||
} = deploymentsListQuery
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasNextPage || isLoading || isFetchingNextPage || error)
|
||||
return
|
||||
|
||||
const anchor = anchorRef.current
|
||||
const container = containerRef.current
|
||||
if (!anchor || !container)
|
||||
return
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0]?.isIntersecting)
|
||||
void fetchNextPage()
|
||||
}, {
|
||||
root: container,
|
||||
rootMargin: '160px',
|
||||
threshold: 0.1,
|
||||
})
|
||||
|
||||
observer.observe(anchor)
|
||||
return () => observer.disconnect()
|
||||
}, [error, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading])
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement>(deploymentsListQuery)
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body">
|
||||
<div ref={rootRef} className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body">
|
||||
<DeploymentsListControls />
|
||||
<div className={cn(
|
||||
'relative grid grow grid-cols-[repeat(auto-fill,minmax(min(100%,20rem),1fr))] content-start gap-4 px-8 pt-2',
|
||||
'relative grid grow grid-cols-[repeat(auto-fill,minmax(min(100%,20rem),1fr))] content-start gap-4 px-8 pt-2 pb-8',
|
||||
showEmptyState && 'overflow-hidden',
|
||||
)}
|
||||
>
|
||||
@ -215,10 +189,8 @@ export function DeploymentsListShell() {
|
||||
/>
|
||||
))}
|
||||
{isFetchingNextPage && <DeploymentsListSkeleton />}
|
||||
<div ref={sentinelRef} aria-hidden="true" className="col-span-full h-px" />
|
||||
</div>
|
||||
|
||||
<div ref={anchorRef} className="h-0" />
|
||||
<div className="py-4" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,9 +3,7 @@
|
||||
import type {
|
||||
AppInstance,
|
||||
GetAppInstanceResponse,
|
||||
ListAppInstancesResponse,
|
||||
} from '@dify/contracts/enterprise/types.gen'
|
||||
import type { InfiniteData, QueryKey } from '@tanstack/react-query'
|
||||
import type { NavItem } from '@/app/components/header/nav/nav-selector'
|
||||
import { keepPreviousData, skipToken } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
@ -64,13 +62,7 @@ const deploymentsNavCurrentInstanceQueryAtom = atomWithQuery((get) => {
|
||||
})
|
||||
})
|
||||
|
||||
export const deploymentsNavListQueryAtom = atomWithInfiniteQuery<
|
||||
ListAppInstancesResponse,
|
||||
Error,
|
||||
InfiniteData<ListAppInstancesResponse>,
|
||||
QueryKey,
|
||||
number
|
||||
>((get) => {
|
||||
export const deploymentsNavListQueryAtom = atomWithInfiniteQuery((get) => {
|
||||
const isActive = get(deploymentsRouteActiveAtom)
|
||||
|
||||
return consoleQuery.enterprise.appInstanceService.listAppInstances.infiniteOptions({
|
||||
|
||||
@ -0,0 +1,199 @@
|
||||
import type { InfiniteScrollQueryResult, UseInfiniteScrollOptions } from '../use-infinite-scroll'
|
||||
import { act, render } from '@testing-library/react'
|
||||
import { createElement } from 'react'
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useInfiniteScroll } from '../use-infinite-scroll'
|
||||
|
||||
let intersectionCallback: IntersectionObserverCallback | undefined
|
||||
let intersectionOptions: IntersectionObserverInit | undefined
|
||||
const observe = vi.fn()
|
||||
const disconnect = vi.fn()
|
||||
const unobserve = vi.fn()
|
||||
const originalIntersectionObserver = globalThis.IntersectionObserver
|
||||
|
||||
class MockIntersectionObserver implements IntersectionObserver {
|
||||
readonly root: Element | Document | null
|
||||
readonly rootMargin: string
|
||||
readonly scrollMargin = ''
|
||||
readonly thresholds: ReadonlyArray<number>
|
||||
|
||||
constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
|
||||
intersectionCallback = callback
|
||||
intersectionOptions = options
|
||||
this.root = options?.root ?? null
|
||||
this.rootMargin = options?.rootMargin ?? ''
|
||||
this.thresholds = Array.isArray(options?.threshold)
|
||||
? options.threshold
|
||||
: [options?.threshold ?? 0]
|
||||
}
|
||||
|
||||
observe = observe
|
||||
unobserve = unobserve
|
||||
disconnect = disconnect
|
||||
takeRecords = () => []
|
||||
}
|
||||
|
||||
function TestInfiniteScroll({
|
||||
options,
|
||||
query,
|
||||
}: {
|
||||
options?: UseInfiniteScrollOptions
|
||||
query: InfiniteScrollQueryResult
|
||||
}) {
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement, HTMLDivElement>(query, options)
|
||||
|
||||
return createElement(
|
||||
'div',
|
||||
{ 'ref': rootRef, 'data-testid': 'scroll-root' },
|
||||
createElement('div', { 'ref': sentinelRef, 'data-testid': 'scroll-sentinel' }),
|
||||
)
|
||||
}
|
||||
|
||||
function createInfiniteScrollQuery(overrides: Partial<InfiniteScrollQueryResult> = {}): InfiniteScrollQueryResult {
|
||||
return {
|
||||
error: null,
|
||||
fetchNextPage: vi.fn(() => Promise.resolve()),
|
||||
hasNextPage: true,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function renderInfiniteScroll(
|
||||
query: InfiniteScrollQueryResult,
|
||||
options?: UseInfiniteScrollOptions,
|
||||
) {
|
||||
const view = render(createElement(TestInfiniteScroll, { options, query }))
|
||||
|
||||
return {
|
||||
...view,
|
||||
root: view.getByTestId('scroll-root'),
|
||||
sentinel: view.getByTestId('scroll-sentinel'),
|
||||
}
|
||||
}
|
||||
|
||||
function triggerIntersection(isIntersecting: boolean) {
|
||||
if (!intersectionCallback)
|
||||
throw new Error('Expected IntersectionObserver callback to be registered')
|
||||
|
||||
intersectionCallback([
|
||||
{ isIntersecting } as IntersectionObserverEntry,
|
||||
], {} as IntersectionObserver)
|
||||
}
|
||||
|
||||
describe('useInfiniteScroll', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
intersectionCallback = undefined
|
||||
intersectionOptions = undefined
|
||||
globalThis.IntersectionObserver = MockIntersectionObserver as unknown as typeof IntersectionObserver
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
globalThis.IntersectionObserver = originalIntersectionObserver
|
||||
})
|
||||
|
||||
// The hook owns both refs and wires the sentinel to the scroll container root.
|
||||
it('should observe the sentinel with the scroll root', () => {
|
||||
const query = createInfiniteScrollQuery()
|
||||
const { root, sentinel } = renderInfiniteScroll(query)
|
||||
|
||||
expect(observe).toHaveBeenCalledWith(sentinel)
|
||||
expect(intersectionOptions?.root).toBe(root)
|
||||
expect(intersectionOptions?.rootMargin).toBe('0px 0px 300px 0px')
|
||||
expect(intersectionOptions?.threshold).toBe(0)
|
||||
})
|
||||
|
||||
// Pagination starts when the sentinel enters the configured observer area.
|
||||
it('should load more when the sentinel intersects', () => {
|
||||
const query = createInfiniteScrollQuery()
|
||||
renderInfiniteScroll(query)
|
||||
|
||||
triggerIntersection(true)
|
||||
|
||||
expect(query.fetchNextPage).toHaveBeenCalledTimes(1)
|
||||
expect(query.fetchNextPage).toHaveBeenCalledWith({ cancelRefetch: false })
|
||||
})
|
||||
|
||||
// Non-intersecting observer entries should not advance the query.
|
||||
it('should not load more when the sentinel is outside the observer area', () => {
|
||||
const query = createInfiniteScrollQuery()
|
||||
renderInfiniteScroll(query)
|
||||
|
||||
triggerIntersection(false)
|
||||
|
||||
expect(query.fetchNextPage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Query state should gate observer registration so stale or duplicate loads do not fire.
|
||||
it.each([
|
||||
['has no next page', { hasNextPage: false }],
|
||||
['is loading', { isLoading: true }],
|
||||
['is fetching the next page', { isFetchingNextPage: true }],
|
||||
['is fetching and guarded', { isFetching: true }],
|
||||
['has an error', { error: new Error('load failed') }],
|
||||
] as const)('should not observe when the query %s', (_label, overrides) => {
|
||||
const query = createInfiniteScrollQuery(overrides)
|
||||
|
||||
renderInfiniteScroll(query)
|
||||
|
||||
expect(observe).not.toHaveBeenCalled()
|
||||
expect(query.fetchNextPage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Options are passed through to IntersectionObserver and fetchNextPage.
|
||||
it('should use custom observer and fetch options', () => {
|
||||
const query = createInfiniteScrollQuery()
|
||||
renderInfiniteScroll(query, {
|
||||
cancelRefetch: true,
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
threshold: 0.1,
|
||||
})
|
||||
|
||||
triggerIntersection(true)
|
||||
|
||||
expect(intersectionOptions?.rootMargin).toBe('0px 0px 160px 0px')
|
||||
expect(intersectionOptions?.threshold).toBe(0.1)
|
||||
expect(query.fetchNextPage).toHaveBeenCalledWith({ cancelRefetch: true })
|
||||
})
|
||||
|
||||
// Window mode observes against the viewport instead of the local scroll container.
|
||||
it('should use the viewport root when useWindow is enabled', () => {
|
||||
const query = createInfiniteScrollQuery()
|
||||
renderInfiniteScroll(query, { useWindow: true })
|
||||
|
||||
expect(intersectionOptions?.root).toBeNull()
|
||||
})
|
||||
|
||||
// The local lock avoids repeated calls while TanStack Query is still resolving fetchNextPage.
|
||||
it('should not request another page while the previous request is pending', async () => {
|
||||
let resolveFetch: (value?: unknown) => void = () => undefined
|
||||
const fetchNextPage = vi.fn(() => new Promise((resolve) => {
|
||||
resolveFetch = resolve
|
||||
}))
|
||||
const query = createInfiniteScrollQuery({ fetchNextPage })
|
||||
renderInfiniteScroll(query)
|
||||
|
||||
triggerIntersection(true)
|
||||
triggerIntersection(true)
|
||||
|
||||
expect(fetchNextPage).toHaveBeenCalledTimes(1)
|
||||
|
||||
await act(async () => {
|
||||
resolveFetch()
|
||||
await Promise.resolve()
|
||||
})
|
||||
})
|
||||
|
||||
// Cleanup matters when the list unmounts or query state recreates the observer.
|
||||
it('should disconnect the observer on unmount', () => {
|
||||
const query = createInfiniteScrollQuery()
|
||||
const { unmount } = renderInfiniteScroll(query)
|
||||
|
||||
unmount()
|
||||
|
||||
expect(disconnect).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
147
web/features/deployments/shared/hooks/use-infinite-scroll.ts
Normal file
147
web/features/deployments/shared/hooks/use-infinite-scroll.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import type { RefCallback } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
type FetchNextPageOptions = {
|
||||
cancelRefetch?: boolean
|
||||
}
|
||||
|
||||
export type InfiniteScrollQueryResult = {
|
||||
error?: unknown
|
||||
fetchNextPage: (options?: FetchNextPageOptions) => Promise<unknown> | unknown
|
||||
hasNextPage?: boolean
|
||||
isFetching?: boolean
|
||||
isFetchingNextPage: boolean
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export type UseInfiniteScrollOptions = {
|
||||
cancelRefetch?: boolean
|
||||
enabled?: boolean
|
||||
guardOnFetching?: boolean
|
||||
rootMargin?: string
|
||||
threshold?: number | number[]
|
||||
useWindow?: boolean
|
||||
}
|
||||
|
||||
type UseInfiniteScrollResult<TRoot extends Element, TTarget extends Element> = {
|
||||
rootEl: TRoot | null
|
||||
rootRef: RefCallback<TRoot>
|
||||
sentinelEl: TTarget | null
|
||||
sentinelRef: RefCallback<TTarget>
|
||||
}
|
||||
|
||||
export function useInfiniteScroll<
|
||||
TRoot extends Element = HTMLDivElement,
|
||||
TTarget extends Element = HTMLDivElement,
|
||||
>(
|
||||
query: InfiniteScrollQueryResult,
|
||||
options: UseInfiniteScrollOptions = {},
|
||||
): UseInfiniteScrollResult<TRoot, TTarget> {
|
||||
const {
|
||||
cancelRefetch = false,
|
||||
enabled = true,
|
||||
guardOnFetching = true,
|
||||
rootMargin = '0px 0px 300px 0px',
|
||||
threshold = 0,
|
||||
useWindow = false,
|
||||
} = options
|
||||
|
||||
const [rootEl, setRootEl] = useState<TRoot | null>(null)
|
||||
const [sentinelEl, setSentinelEl] = useState<TTarget | null>(null)
|
||||
const loadingLockRef = useRef(false)
|
||||
|
||||
const latestRef = useRef({
|
||||
cancelRefetch,
|
||||
enabled,
|
||||
error: query.error,
|
||||
fetchNextPage: query.fetchNextPage,
|
||||
guardOnFetching,
|
||||
hasNextPage: Boolean(query.hasNextPage),
|
||||
isFetching: query.isFetching ?? false,
|
||||
isFetchingNextPage: query.isFetchingNextPage,
|
||||
isLoading: query.isLoading ?? false,
|
||||
})
|
||||
|
||||
latestRef.current = {
|
||||
cancelRefetch,
|
||||
enabled,
|
||||
error: query.error,
|
||||
fetchNextPage: query.fetchNextPage,
|
||||
guardOnFetching,
|
||||
hasNextPage: Boolean(query.hasNextPage),
|
||||
isFetching: query.isFetching ?? false,
|
||||
isFetchingNextPage: query.isFetchingNextPage,
|
||||
isLoading: query.isLoading ?? false,
|
||||
}
|
||||
|
||||
const rootRef = useCallback((node: TRoot | null) => {
|
||||
setRootEl(node)
|
||||
}, [])
|
||||
|
||||
const sentinelRef = useCallback((node: TTarget | null) => {
|
||||
setSentinelEl(node)
|
||||
}, [])
|
||||
|
||||
const canLoad = enabled
|
||||
&& Boolean(query.hasNextPage)
|
||||
&& !query.isFetchingNextPage
|
||||
&& !(query.isLoading ?? false)
|
||||
&& !query.error
|
||||
&& !(guardOnFetching && (query.isFetching ?? false))
|
||||
|
||||
useEffect(() => {
|
||||
if (!canLoad)
|
||||
return
|
||||
|
||||
if (!sentinelEl)
|
||||
return
|
||||
|
||||
if (!useWindow && !rootEl)
|
||||
return
|
||||
|
||||
if (typeof IntersectionObserver === 'undefined')
|
||||
return
|
||||
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
const latest = latestRef.current
|
||||
|
||||
if (!entry?.isIntersecting)
|
||||
return
|
||||
|
||||
if (!latest.enabled
|
||||
|| !latest.hasNextPage
|
||||
|| latest.isLoading
|
||||
|| latest.isFetchingNextPage
|
||||
|| latest.error
|
||||
|| (latest.guardOnFetching && latest.isFetching)
|
||||
|| loadingLockRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
loadingLockRef.current = true
|
||||
|
||||
const nextPage = latest.fetchNextPage({
|
||||
cancelRefetch: latest.cancelRefetch,
|
||||
})
|
||||
|
||||
void Promise.resolve(nextPage).finally(() => {
|
||||
loadingLockRef.current = false
|
||||
})
|
||||
}, {
|
||||
root: useWindow ? null : rootEl,
|
||||
rootMargin,
|
||||
threshold,
|
||||
})
|
||||
|
||||
observer.observe(sentinelEl)
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [canLoad, rootEl, rootMargin, sentinelEl, threshold, useWindow])
|
||||
|
||||
return {
|
||||
rootEl,
|
||||
rootRef,
|
||||
sentinelEl,
|
||||
sentinelRef,
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,7 @@ const defaultCloudEnv = {
|
||||
NEXT_PUBLIC_ENABLE_SOCIAL_OAUTH_LOGIN: true,
|
||||
NEXT_PUBLIC_ENABLE_TRIAL_APP: true,
|
||||
NEXT_PUBLIC_IS_EMAIL_SETUP: true,
|
||||
NEXT_PUBLIC_RBAC_ENABLED: false,
|
||||
}
|
||||
|
||||
const queryKey = ['console', 'systemFeatures'] as const
|
||||
@ -139,6 +140,7 @@ describe('systemFeaturesQueryOptions', () => {
|
||||
enable_email_password_login: false,
|
||||
enable_social_oauth_login: true,
|
||||
enable_trial_app: true,
|
||||
rbac_enabled: false,
|
||||
})
|
||||
})
|
||||
|
||||
@ -151,6 +153,7 @@ describe('systemFeaturesQueryOptions', () => {
|
||||
NEXT_PUBLIC_ENABLE_COLLABORATION_MODE: true,
|
||||
NEXT_PUBLIC_ALLOW_REGISTER: false,
|
||||
NEXT_PUBLIC_ENABLE_EXPLORE_BANNER: false,
|
||||
NEXT_PUBLIC_RBAC_ENABLED: true,
|
||||
},
|
||||
})
|
||||
|
||||
@ -163,6 +166,7 @@ describe('systemFeaturesQueryOptions', () => {
|
||||
enable_collaboration_mode: true,
|
||||
is_allow_register: false,
|
||||
enable_explore_banner: false,
|
||||
rbac_enabled: true,
|
||||
branding: {
|
||||
enabled: false,
|
||||
application_title: '',
|
||||
|
||||
@ -101,4 +101,5 @@ export const cloudSystemFeatures = {
|
||||
enable_creators_platform: env.NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED,
|
||||
enable_trial_app: env.NEXT_PUBLIC_ENABLE_TRIAL_APP,
|
||||
enable_explore_banner: env.NEXT_PUBLIC_ENABLE_EXPLORE_BANNER,
|
||||
rbac_enabled: env.NEXT_PUBLIC_RBAC_ENABLED,
|
||||
} satisfies GetSystemFeaturesResponse
|
||||
|
||||
51
web/i18n/ar-TN/permission-keys.json
Normal file
51
web/i18n/ar-TN/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "إدارة إعدادات امتداد API",
|
||||
"app.access_config": "تكوين أذونات الوصول إلى التطبيق",
|
||||
"app.acl.access_config": "تكوين أذونات الوصول إلى التطبيق",
|
||||
"app.acl.delete": "حذف التطبيق",
|
||||
"app.acl.edit": "تعديل التطبيق وتنسيقه",
|
||||
"app.acl.import_export_dsl": "استيراد / تصدير DSL",
|
||||
"app.acl.monitor": "المراقبة والعمليات",
|
||||
"app.acl.preview": "معاينة التطبيق",
|
||||
"app.acl.release_and_version": "نشر التطبيق وإدارة الإصدارات",
|
||||
"app.acl.test_and_run": "اختبار التطبيق واستخدامه",
|
||||
"app.acl.view_layout": "صفحة التنسيق للقراءة فقط",
|
||||
"app.create_and_management": "إنشاء التطبيقات وإدارة التطبيقات التي أنشأتها",
|
||||
"app.tag.manage": "إدارة وسوم التطبيق",
|
||||
"app_library.access": "الوصول إلى مكتبة التطبيقات",
|
||||
"billing.manage": "تغيير خطط الاشتراك",
|
||||
"billing.subscription.manage": "إدارة الفوترة والاشتراكات في بوابة الفوترة",
|
||||
"billing.view": "الوصول إلى إعدادات الفوترة",
|
||||
"credential.create": "إضافة بيانات الاعتماد",
|
||||
"credential.manage": "تعديل بيانات الاعتماد وحذفها",
|
||||
"credential.use": "عرض بيانات الاعتماد واستخدامها",
|
||||
"customization.manage": "إدارة التخصيص",
|
||||
"data_source.manage": "إدارة تكوين مصدر البيانات",
|
||||
"dataset.access_config": "تكوين أذونات الوصول إلى قاعدة المعرفة",
|
||||
"dataset.acl.access_config": "تكوين أذونات الوصول إلى قاعدة المعرفة",
|
||||
"dataset.acl.delete": "حذف قاعدة المعرفة",
|
||||
"dataset.acl.delete_file": "حذف ملفات قاعدة المعرفة",
|
||||
"dataset.acl.document_download": "تنزيل المستندات",
|
||||
"dataset.acl.edit": "تعديل قاعدة المعرفة",
|
||||
"dataset.acl.import_export_dsl": "استيراد / تصدير DSL لخط أنابيب المعرفة",
|
||||
"dataset.acl.pipeline_release": "نشر خط أنابيب المعرفة وإدارة الإصدارات",
|
||||
"dataset.acl.pipeline_test": "اختبار خط الأنابيب",
|
||||
"dataset.acl.preview": "معاينة قاعدة المعرفة",
|
||||
"dataset.acl.readonly": "قاعدة المعرفة للقراءة فقط",
|
||||
"dataset.acl.retrieval_recall": "استرجاع قاعدة المعرفة",
|
||||
"dataset.acl.use": "إضافة مستندات إلى قاعدة المعرفة",
|
||||
"dataset.api_key.manage": "إدارة مفاتيح API لقاعدة المعرفة",
|
||||
"dataset.create_and_management": "إنشاء قواعد المعرفة وإدارة قواعد المعرفة التي أنشأتها",
|
||||
"dataset.external.connect": "الاتصال بقواعد المعرفة الخارجية",
|
||||
"dataset.tag.manage": "إدارة وسوم قاعدة المعرفة",
|
||||
"mcp.manage": "إدارة MCP",
|
||||
"plugin.debug": "تصحيح الإضافات",
|
||||
"plugin.install": "تثبيت الإضافات وتحديثها",
|
||||
"plugin.manage": "إدارة الإضافات",
|
||||
"plugin.plugin_preferences": "إدارة تفضيلات الإضافات",
|
||||
"snippets.create_and_modify": "إنشاء المقتطفات وتعديلها",
|
||||
"snippets.management": "إدارة المقتطفات",
|
||||
"tool.manage": "إدارة الأدوات",
|
||||
"workspace.member.manage": "إدارة الأعضاء",
|
||||
"workspace.role.manage": "إدارة أذونات الأدوار وقواعد الوصول إلى الموارد"
|
||||
}
|
||||
105
web/i18n/ar-TN/permission.json
Normal file
105
web/i18n/ar-TN/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "الإجراءات",
|
||||
"accessRule.addMemberAria": "إضافة {{name}}",
|
||||
"accessRule.addMembersTitle": "إضافة أعضاء",
|
||||
"accessRule.allPermittedMembers": "جميع الأعضاء الذين لديهم أذونات الأدوار",
|
||||
"accessRule.allPermittedMembersDescription": "يمكن للأعضاء الذين لديهم أذونات أدوار مطابقة الوصول إلى هذا المورد.",
|
||||
"accessRule.appDescription": "تحكم في من يُفتح له هذا التطبيق. لا يزال الأعضاء بحاجة إلى أذونات الأدوار لعرضه أو تشغيله.",
|
||||
"accessRule.appTitle": "قواعد الوصول إلى التطبيق",
|
||||
"accessRule.changeOpenScopeDescription": "سيؤدي تغيير نطاق الفتح إلى إعادة تعيين جميع إعدادات الأذونات الفردية لهذا المورد. ستحتاج إلى إضافة أذونات خاصة بالأعضاء مرة أخرى بعد التبديل.",
|
||||
"accessRule.changeOpenScopeTitle": "تغيير نطاق فتح المورد؟",
|
||||
"accessRule.collapseSection": "طي {{title}}",
|
||||
"accessRule.copied": "تم نسخ قاعدة الوصول بنجاح",
|
||||
"accessRule.created": "تم إنشاء قاعدة الوصول بنجاح",
|
||||
"accessRule.datasetDescription": "تحكم في من تُفتح له قاعدة المعرفة هذه. لا يزال الأعضاء بحاجة إلى أذونات الأدوار لعرضها أو تشغيلها.",
|
||||
"accessRule.datasetTitle": "قواعد الوصول إلى قاعدة المعرفة",
|
||||
"accessRule.defaultPermission": "حسب أذونات الأدوار",
|
||||
"accessRule.deleteDescription": "سيتم حذف قاعدة الوصول هذه نهائيًا وإزالتها من قائمة تفويض المورد.",
|
||||
"accessRule.deleteTitle": "حذف \"{{name}}\"؟",
|
||||
"accessRule.deleted": "تم حذف قاعدة الوصول بنجاح",
|
||||
"accessRule.exceptionPermissionFor": "إذن استثنائي لـ {{name}}",
|
||||
"accessRule.expandSection": "توسيع {{title}}",
|
||||
"accessRule.individualPermissionSettings": "إعدادات الأذونات الفردية",
|
||||
"accessRule.individualPermissionSettingsTip": "عيّن استثناءات الأذونات لمتعاونين أو مجموعات محددة. تتجاوز هذه الإعدادات مستوى الوصول الافتراضي.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} مقفل",
|
||||
"accessRule.lockedSummary_other": "· {{count}} مقفل",
|
||||
"accessRule.maintainer": "مشرف الصيانة",
|
||||
"accessRule.member": "عضو",
|
||||
"accessRule.newPermissionSet": "مجموعة أذونات جديدة",
|
||||
"accessRule.noAvailableMembers": "لا يوجد أعضاء متاحون للإضافة",
|
||||
"accessRule.noDescription": "لا يوجد وصف",
|
||||
"accessRule.noRoles": "لا توجد أدوار",
|
||||
"accessRule.noRules": "لا توجد قواعد وصول",
|
||||
"accessRule.noUserAccessSettings": "لا توجد إعدادات أذونات فردية",
|
||||
"accessRule.permission": "الإذن",
|
||||
"accessRule.resourceOpenScope": "نطاق فتح المورد",
|
||||
"accessRule.resourceOpenScopeDescription": "اختر من يُفتح له هذا المورد. لا تزال أذونات الأدوار تحدد ما يمكن لكل عضو فعله.",
|
||||
"accessRule.specificMembersOnly": "أعضاء محددون فقط",
|
||||
"accessRule.specificMembersOnlyDescription": "يمكن للأعضاء المحددين فقط الوصول إلى هذا المورد.",
|
||||
"accessRule.summary_one": "{{count}} مجموعة أذونات",
|
||||
"accessRule.summary_other": "{{count}} مجموعات أذونات",
|
||||
"accessRule.updated": "تم تحديث قاعدة الوصول بنجاح",
|
||||
"common.duplicateAction": "تكرار",
|
||||
"group.app": "التطبيقات",
|
||||
"group.app_acl": "أذونات الوصول إلى التطبيق",
|
||||
"group.billing": "الفوترة",
|
||||
"group.credential": "بيانات الاعتماد",
|
||||
"group.dataset": "قواعد المعرفة",
|
||||
"group.dataset_acl": "أذونات الوصول إلى قاعدة المعرفة",
|
||||
"group.integration": "التكاملات",
|
||||
"group.plugin": "الإضافات",
|
||||
"group.tool_mcp": "الأدوات و MCP",
|
||||
"group.workspace": "مساحة العمل",
|
||||
"permissionList.clearAll": "مسح الكل",
|
||||
"permissionList.collapseGroup": "طي المجموعة",
|
||||
"permissionList.expandGroup": "توسيع المجموعة",
|
||||
"permissionList.noPermissionsFound": "لم يتم العثور على أذونات",
|
||||
"permissionList.selectAll": "تحديد الكل",
|
||||
"permissionSet.descriptionLabel": "الوصف",
|
||||
"permissionSet.descriptionPlaceholder": "صف ما تمنحه مجموعة الأذونات هذه",
|
||||
"permissionSet.learnMore": "تعرّف على المزيد حول الأذونات",
|
||||
"permissionSet.modal.create.app.description": "أنشئ مجموعة أذونات تطبيق يمكن الرجوع إليها في قواعد الوصول للتفويض السريع.",
|
||||
"permissionSet.modal.create.app.title": "إنشاء مجموعة أذونات التطبيق",
|
||||
"permissionSet.modal.create.dataset.description": "أنشئ مجموعة أذونات قاعدة معرفة يمكن الرجوع إليها في قواعد الوصول للتفويض السريع.",
|
||||
"permissionSet.modal.create.dataset.title": "إنشاء مجموعة أذونات قاعدة المعرفة",
|
||||
"permissionSet.modal.edit.app.description": "عدّل الاسم والوصف والأذونات الممنوحة لمجموعة الأذونات هذه.",
|
||||
"permissionSet.modal.edit.app.title": "تعديل مجموعة أذونات التطبيق",
|
||||
"permissionSet.modal.edit.dataset.description": "عدّل الاسم والوصف والأذونات الممنوحة لمجموعة الأذونات هذه.",
|
||||
"permissionSet.modal.edit.dataset.title": "تعديل مجموعة أذونات قاعدة المعرفة",
|
||||
"permissionSet.modal.view.app.description": "اعرض الاسم والوصف والأذونات الممنوحة لمجموعة الأذونات هذه.",
|
||||
"permissionSet.modal.view.app.title": "عرض مجموعة أذونات التطبيق",
|
||||
"permissionSet.modal.view.dataset.description": "اعرض الاسم والوصف والأذونات الممنوحة لمجموعة الأذونات هذه.",
|
||||
"permissionSet.modal.view.dataset.title": "عرض مجموعة أذونات قاعدة المعرفة",
|
||||
"permissionSet.nameLabel": "اسم مجموعة الأذونات",
|
||||
"permissionSet.namePlaceholder": "مثال: يمكنه تصدير DSL",
|
||||
"permissionSet.permissions": "الأذونات",
|
||||
"role.addRole": "إنشاء الأدوار",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" مُعيَّن لعضو {{count}}. هل تريد أن تتضمن نسخة الدور الجديد العضو نفسه؟",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" مُعيَّن لـ {{count}} أعضاء. هل تريد أن تتضمن نسخة الدور الجديد الأعضاء أنفسهم؟",
|
||||
"role.copyMembersLoading": "جارٍ تحميل تعيينات الأعضاء...",
|
||||
"role.copyMembersTitle": "نسخ تعيينات الأعضاء؟",
|
||||
"role.created": "تم إنشاء الدور بنجاح",
|
||||
"role.deleteDescription": "سيتم حذف هذا الدور نهائيًا وإزالته من أي أعضاء أو قواعد وصول تستخدمه.",
|
||||
"role.deleteTitle": "حذف \"{{name}}\"؟",
|
||||
"role.deleted": "تم حذف الدور بنجاح",
|
||||
"role.duplicated": "تم تكرار الدور بنجاح",
|
||||
"role.groups.builtin": "أدوار النظام",
|
||||
"role.groups.custom": "الأدوار المخصصة",
|
||||
"role.loading": "جارٍ تحميل الأدوار...",
|
||||
"role.modal.create.description": "إنشاء دور وتعيين الأذونات",
|
||||
"role.modal.create.title": "إنشاء دور",
|
||||
"role.modal.descriptionLabel": "الوصف",
|
||||
"role.modal.descriptionPlaceholder": "صف ما هذا الدور مسؤول عنه",
|
||||
"role.modal.edit.description": "تعديل تفاصيل الدور والأذونات",
|
||||
"role.modal.edit.title": "تعديل الدور",
|
||||
"role.modal.nameLabel": "اسم الدور",
|
||||
"role.modal.namePlaceholder": "مثال: قائد التسويق",
|
||||
"role.modal.view.description": "عرض تفاصيل الدور والأذونات",
|
||||
"role.modal.view.title": "عرض الدور",
|
||||
"role.noDescription": "لا يوجد وصف",
|
||||
"role.noMatchingRoles": "لا توجد أدوار مطابقة",
|
||||
"role.searchPlaceholder": "البحث في الأدوار...",
|
||||
"role.updated": "تم تحديث الدور بنجاح",
|
||||
"role.workspaceRoles.description": "أنشئ أدوارًا وحدد ما يمكن لكل دور فعله في مساحة العمل هذه.",
|
||||
"role.workspaceRoles.title": "أدوار مساحة العمل"
|
||||
}
|
||||
51
web/i18n/de-DE/permission-keys.json
Normal file
51
web/i18n/de-DE/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "API-Erweiterungskonfiguration verwalten",
|
||||
"app.access_config": "App-Zugriffsberechtigungen konfigurieren",
|
||||
"app.acl.access_config": "App-Zugriffsberechtigungen konfigurieren",
|
||||
"app.acl.delete": "App löschen",
|
||||
"app.acl.edit": "App bearbeiten und orchestrieren",
|
||||
"app.acl.import_export_dsl": "DSL importieren / exportieren",
|
||||
"app.acl.monitor": "Überwachung und Betrieb",
|
||||
"app.acl.preview": "App-Vorschau",
|
||||
"app.acl.release_and_version": "App-Veröffentlichung und Versionsverwaltung",
|
||||
"app.acl.test_and_run": "App testen und verwenden",
|
||||
"app.acl.view_layout": "Schreibgeschützte Orchestrierungsseite",
|
||||
"app.create_and_management": "Apps erstellen und von Ihnen erstellte Apps verwalten",
|
||||
"app.tag.manage": "App-Tags verwalten",
|
||||
"app_library.access": "Auf App-Bibliothek zugreifen",
|
||||
"billing.manage": "Abonnementpläne ändern",
|
||||
"billing.subscription.manage": "Abrechnung und Abonnements im Abrechnungsportal verwalten",
|
||||
"billing.view": "Auf Abrechnungseinstellungen zugreifen",
|
||||
"credential.create": "Anmeldeinformationen hinzufügen",
|
||||
"credential.manage": "Anmeldeinformationen bearbeiten und löschen",
|
||||
"credential.use": "Anmeldeinformationen anzeigen und verwenden",
|
||||
"customization.manage": "Anpassung verwalten",
|
||||
"data_source.manage": "Datenquellenkonfiguration verwalten",
|
||||
"dataset.access_config": "Zugriffsberechtigungen der Wissensdatenbank konfigurieren",
|
||||
"dataset.acl.access_config": "Zugriffsberechtigungen der Wissensdatenbank konfigurieren",
|
||||
"dataset.acl.delete": "Wissensdatenbank löschen",
|
||||
"dataset.acl.delete_file": "Wissensdatenbankdateien löschen",
|
||||
"dataset.acl.document_download": "Dokumente herunterladen",
|
||||
"dataset.acl.edit": "Wissensdatenbank bearbeiten",
|
||||
"dataset.acl.import_export_dsl": "Knowledge-Pipeline-DSL importieren / exportieren",
|
||||
"dataset.acl.pipeline_release": "Knowledge-Pipeline-Veröffentlichung und Versionsverwaltung",
|
||||
"dataset.acl.pipeline_test": "Pipeline-Tests",
|
||||
"dataset.acl.preview": "Wissensdatenbank-Vorschau",
|
||||
"dataset.acl.readonly": "Schreibgeschützte Wissensdatenbank",
|
||||
"dataset.acl.retrieval_recall": "Wissensdatenbank-Abruf",
|
||||
"dataset.acl.use": "Dokumente zur Wissensdatenbank hinzufügen",
|
||||
"dataset.api_key.manage": "API-Schlüssel der Wissensdatenbank verwalten",
|
||||
"dataset.create_and_management": "Wissensdatenbanken erstellen und von Ihnen erstellte Wissensdatenbanken verwalten",
|
||||
"dataset.external.connect": "Externe Wissensdatenbanken verbinden",
|
||||
"dataset.tag.manage": "Wissensdatenbank-Tags verwalten",
|
||||
"mcp.manage": "MCP verwalten",
|
||||
"plugin.debug": "Plugins debuggen",
|
||||
"plugin.install": "Plugins installieren und aktualisieren",
|
||||
"plugin.manage": "Plugins verwalten",
|
||||
"plugin.plugin_preferences": "Plugin-Einstellungen verwalten",
|
||||
"snippets.create_and_modify": "Snippets erstellen und ändern",
|
||||
"snippets.management": "Snippets verwalten",
|
||||
"tool.manage": "Tools verwalten",
|
||||
"workspace.member.manage": "Mitglieder verwalten",
|
||||
"workspace.role.manage": "Rollenberechtigungen und Ressourcenzugriffsregeln verwalten"
|
||||
}
|
||||
105
web/i18n/de-DE/permission.json
Normal file
105
web/i18n/de-DE/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Aktionen",
|
||||
"accessRule.addMemberAria": "{{name}} hinzufügen",
|
||||
"accessRule.addMembersTitle": "Mitglieder hinzufügen",
|
||||
"accessRule.allPermittedMembers": "Alle Mitglieder mit Rollenberechtigungen",
|
||||
"accessRule.allPermittedMembersDescription": "Mitglieder mit passenden Rollenberechtigungen können auf diese Ressource zugreifen.",
|
||||
"accessRule.appDescription": "Steuern Sie, für wen diese App geöffnet ist. Mitglieder benötigen dennoch Rollenberechtigungen, um sie anzuzeigen oder zu bedienen.",
|
||||
"accessRule.appTitle": "App-Zugriffsregeln",
|
||||
"accessRule.changeOpenScopeDescription": "Das Ändern des Freigabebereichs setzt alle individuellen Berechtigungseinstellungen für diese Ressource zurück. Nach dem Wechsel müssen Sie mitgliederspezifische Berechtigungen erneut hinzufügen.",
|
||||
"accessRule.changeOpenScopeTitle": "Freigabebereich der Ressource ändern?",
|
||||
"accessRule.collapseSection": "{{title}} einklappen",
|
||||
"accessRule.copied": "Zugriffsregel erfolgreich kopiert",
|
||||
"accessRule.created": "Zugriffsregel erfolgreich erstellt",
|
||||
"accessRule.datasetDescription": "Steuern Sie, für wen diese Wissensdatenbank geöffnet ist. Mitglieder benötigen dennoch Rollenberechtigungen, um sie anzuzeigen oder zu bedienen.",
|
||||
"accessRule.datasetTitle": "Zugriffsregeln der Wissensdatenbank",
|
||||
"accessRule.defaultPermission": "Nach Rollenberechtigungen",
|
||||
"accessRule.deleteDescription": "Diese Zugriffsregel wird dauerhaft gelöscht und aus der Ressourcen-Autorisierungsliste entfernt.",
|
||||
"accessRule.deleteTitle": "\"{{name}}\" löschen?",
|
||||
"accessRule.deleted": "Zugriffsregel erfolgreich gelöscht",
|
||||
"accessRule.exceptionPermissionFor": "Ausnahmeberechtigung für {{name}}",
|
||||
"accessRule.expandSection": "{{title}} ausklappen",
|
||||
"accessRule.individualPermissionSettings": "Individuelle Berechtigungseinstellungen",
|
||||
"accessRule.individualPermissionSettingsTip": "Legen Sie Berechtigungsausnahmen für bestimmte Mitarbeiter oder Gruppen fest. Diese Einstellungen überschreiben die Standardzugriffsstufe.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} gesperrt",
|
||||
"accessRule.lockedSummary_other": "· {{count}} gesperrt",
|
||||
"accessRule.maintainer": "Betreuer",
|
||||
"accessRule.member": "Mitglied",
|
||||
"accessRule.newPermissionSet": "Neuer Berechtigungssatz",
|
||||
"accessRule.noAvailableMembers": "Keine Mitglieder zum Hinzufügen verfügbar",
|
||||
"accessRule.noDescription": "Keine Beschreibung",
|
||||
"accessRule.noRoles": "Keine Rollen",
|
||||
"accessRule.noRules": "Keine Zugriffsregeln",
|
||||
"accessRule.noUserAccessSettings": "Keine individuellen Berechtigungseinstellungen",
|
||||
"accessRule.permission": "Berechtigung",
|
||||
"accessRule.resourceOpenScope": "Freigabebereich der Ressource",
|
||||
"accessRule.resourceOpenScopeDescription": "Wählen Sie, für wen diese Ressource geöffnet ist. Rollenberechtigungen bestimmen weiterhin, was jedes Mitglied tun kann.",
|
||||
"accessRule.specificMembersOnly": "Nur bestimmte Mitglieder",
|
||||
"accessRule.specificMembersOnlyDescription": "Nur ausgewählte Mitglieder können auf diese Ressource zugreifen.",
|
||||
"accessRule.summary_one": "{{count}} Berechtigungssatz",
|
||||
"accessRule.summary_other": "{{count}} Berechtigungssätze",
|
||||
"accessRule.updated": "Zugriffsregel erfolgreich aktualisiert",
|
||||
"common.duplicateAction": "Duplizieren",
|
||||
"group.app": "Anwendungen",
|
||||
"group.app_acl": "App-Zugriffsberechtigungen",
|
||||
"group.billing": "Abrechnung",
|
||||
"group.credential": "Anmeldeinformationen",
|
||||
"group.dataset": "Wissensdatenbanken",
|
||||
"group.dataset_acl": "Zugriffsberechtigungen der Wissensdatenbank",
|
||||
"group.integration": "Integrationen",
|
||||
"group.plugin": "Plugins",
|
||||
"group.tool_mcp": "Tools und MCP",
|
||||
"group.workspace": "Arbeitsbereich",
|
||||
"permissionList.clearAll": "Alle löschen",
|
||||
"permissionList.collapseGroup": "Gruppe einklappen",
|
||||
"permissionList.expandGroup": "Gruppe ausklappen",
|
||||
"permissionList.noPermissionsFound": "Keine Berechtigungen gefunden",
|
||||
"permissionList.selectAll": "Alle auswählen",
|
||||
"permissionSet.descriptionLabel": "Beschreibung",
|
||||
"permissionSet.descriptionPlaceholder": "Beschreiben Sie, was dieser Berechtigungssatz gewährt",
|
||||
"permissionSet.learnMore": "Mehr über Berechtigungen erfahren",
|
||||
"permissionSet.modal.create.app.description": "Erstellen Sie einen App-Berechtigungssatz, der in Zugriffsregeln zur schnellen Autorisierung referenziert werden kann.",
|
||||
"permissionSet.modal.create.app.title": "App-Berechtigungssatz erstellen",
|
||||
"permissionSet.modal.create.dataset.description": "Erstellen Sie einen Wissensdatenbank-Berechtigungssatz, der in Zugriffsregeln zur schnellen Autorisierung referenziert werden kann.",
|
||||
"permissionSet.modal.create.dataset.title": "Wissensdatenbank-Berechtigungssatz erstellen",
|
||||
"permissionSet.modal.edit.app.description": "Ändern Sie den Namen, die Beschreibung und die für diesen Berechtigungssatz gewährten Berechtigungen.",
|
||||
"permissionSet.modal.edit.app.title": "App-Berechtigungssatz bearbeiten",
|
||||
"permissionSet.modal.edit.dataset.description": "Ändern Sie den Namen, die Beschreibung und die für diesen Berechtigungssatz gewährten Berechtigungen.",
|
||||
"permissionSet.modal.edit.dataset.title": "Wissensdatenbank-Berechtigungssatz bearbeiten",
|
||||
"permissionSet.modal.view.app.description": "Sehen Sie den Namen, die Beschreibung und die für diesen Berechtigungssatz gewährten Berechtigungen.",
|
||||
"permissionSet.modal.view.app.title": "App-Berechtigungssatz anzeigen",
|
||||
"permissionSet.modal.view.dataset.description": "Sehen Sie den Namen, die Beschreibung und die für diesen Berechtigungssatz gewährten Berechtigungen.",
|
||||
"permissionSet.modal.view.dataset.title": "Wissensdatenbank-Berechtigungssatz anzeigen",
|
||||
"permissionSet.nameLabel": "Name des Berechtigungssatzes",
|
||||
"permissionSet.namePlaceholder": "z. B. Kann DSL exportieren",
|
||||
"permissionSet.permissions": "Berechtigungen",
|
||||
"role.addRole": "Rollen erstellen",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" ist {{count}} Mitglied zugewiesen. Möchten Sie, dass die neue Rollenkopie dasselbe Mitglied enthält?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" ist {{count}} Mitgliedern zugewiesen. Möchten Sie, dass die neue Rollenkopie dieselben Mitglieder enthält?",
|
||||
"role.copyMembersLoading": "Mitgliederzuweisungen werden geladen...",
|
||||
"role.copyMembersTitle": "Mitgliederzuweisungen kopieren?",
|
||||
"role.created": "Rolle erfolgreich erstellt",
|
||||
"role.deleteDescription": "Diese Rolle wird dauerhaft gelöscht und von allen Mitgliedern oder Zugriffsregeln entfernt, die sie verwenden.",
|
||||
"role.deleteTitle": "\"{{name}}\" löschen?",
|
||||
"role.deleted": "Rolle erfolgreich gelöscht",
|
||||
"role.duplicated": "Rolle erfolgreich dupliziert",
|
||||
"role.groups.builtin": "Systemrollen",
|
||||
"role.groups.custom": "Benutzerdefinierte Rollen",
|
||||
"role.loading": "Rollen werden geladen...",
|
||||
"role.modal.create.description": "Erstellen Sie eine Rolle und weisen Sie Berechtigungen zu",
|
||||
"role.modal.create.title": "Rolle erstellen",
|
||||
"role.modal.descriptionLabel": "Beschreibung",
|
||||
"role.modal.descriptionPlaceholder": "Beschreiben Sie, wofür diese Rolle zuständig ist",
|
||||
"role.modal.edit.description": "Rollendetails und Berechtigungen bearbeiten",
|
||||
"role.modal.edit.title": "Rolle bearbeiten",
|
||||
"role.modal.nameLabel": "Rollenname",
|
||||
"role.modal.namePlaceholder": "z. B. Marketing-Leitung",
|
||||
"role.modal.view.description": "Rollendetails und Berechtigungen anzeigen",
|
||||
"role.modal.view.title": "Rolle anzeigen",
|
||||
"role.noDescription": "Keine Beschreibung",
|
||||
"role.noMatchingRoles": "Keine passenden Rollen",
|
||||
"role.searchPlaceholder": "Rollen suchen...",
|
||||
"role.updated": "Rolle erfolgreich aktualisiert",
|
||||
"role.workspaceRoles.description": "Erstellen Sie Rollen und definieren Sie, was jede Rolle in diesem Arbeitsbereich tun kann.",
|
||||
"role.workspaceRoles.title": "Arbeitsbereichsrollen"
|
||||
}
|
||||
51
web/i18n/es-ES/permission-keys.json
Normal file
51
web/i18n/es-ES/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "Gestionar la configuración de la extensión de API",
|
||||
"app.access_config": "Configurar los permisos de acceso de la app",
|
||||
"app.acl.access_config": "Configurar los permisos de acceso de la app",
|
||||
"app.acl.delete": "Eliminar app",
|
||||
"app.acl.edit": "Editar y orquestar la app",
|
||||
"app.acl.import_export_dsl": "Importar / exportar DSL",
|
||||
"app.acl.monitor": "Supervisión y operaciones",
|
||||
"app.acl.preview": "Previsualizar app",
|
||||
"app.acl.release_and_version": "Publicación de la app y gestión de versiones",
|
||||
"app.acl.test_and_run": "Probar y usar la app",
|
||||
"app.acl.view_layout": "Página de orquestación de solo lectura",
|
||||
"app.create_and_management": "Crear apps y gestionar las apps que has creado",
|
||||
"app.tag.manage": "Gestionar etiquetas de apps",
|
||||
"app_library.access": "Acceder a la Biblioteca de apps",
|
||||
"billing.manage": "Cambiar planes de suscripción",
|
||||
"billing.subscription.manage": "Gestionar la facturación y las suscripciones en el portal de facturación",
|
||||
"billing.view": "Acceder a la configuración de facturación",
|
||||
"credential.create": "Añadir credenciales",
|
||||
"credential.manage": "Editar y eliminar credenciales",
|
||||
"credential.use": "Ver y usar credenciales",
|
||||
"customization.manage": "Gestionar la personalización",
|
||||
"data_source.manage": "Gestionar la configuración de la fuente de datos",
|
||||
"dataset.access_config": "Configurar los permisos de acceso de la base de conocimiento",
|
||||
"dataset.acl.access_config": "Configurar los permisos de acceso de la base de conocimiento",
|
||||
"dataset.acl.delete": "Eliminar base de conocimiento",
|
||||
"dataset.acl.delete_file": "Eliminar archivos de la base de conocimiento",
|
||||
"dataset.acl.document_download": "Descargar documentos",
|
||||
"dataset.acl.edit": "Editar base de conocimiento",
|
||||
"dataset.acl.import_export_dsl": "Importar / exportar el DSL del pipeline de conocimiento",
|
||||
"dataset.acl.pipeline_release": "Publicación del pipeline de conocimiento y gestión de versiones",
|
||||
"dataset.acl.pipeline_test": "Pruebas del pipeline",
|
||||
"dataset.acl.preview": "Previsualizar base de conocimiento",
|
||||
"dataset.acl.readonly": "Base de conocimiento de solo lectura",
|
||||
"dataset.acl.retrieval_recall": "Recuperación de la base de conocimiento",
|
||||
"dataset.acl.use": "Añadir documentos a la base de conocimiento",
|
||||
"dataset.api_key.manage": "Gestionar las claves de API de la base de conocimiento",
|
||||
"dataset.create_and_management": "Crear bases de conocimiento y gestionar las bases de conocimiento que has creado",
|
||||
"dataset.external.connect": "Conectar bases de conocimiento externas",
|
||||
"dataset.tag.manage": "Gestionar etiquetas de la base de conocimiento",
|
||||
"mcp.manage": "Gestionar MCP",
|
||||
"plugin.debug": "Depurar plugins",
|
||||
"plugin.install": "Instalar y actualizar plugins",
|
||||
"plugin.manage": "Gestionar plugins",
|
||||
"plugin.plugin_preferences": "Gestionar las preferencias de plugins",
|
||||
"snippets.create_and_modify": "Crear y modificar fragmentos",
|
||||
"snippets.management": "Gestionar fragmentos",
|
||||
"tool.manage": "Gestionar herramientas",
|
||||
"workspace.member.manage": "Gestionar miembros",
|
||||
"workspace.role.manage": "Gestionar permisos de roles y reglas de acceso a recursos"
|
||||
}
|
||||
105
web/i18n/es-ES/permission.json
Normal file
105
web/i18n/es-ES/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Acciones",
|
||||
"accessRule.addMemberAria": "Añadir {{name}}",
|
||||
"accessRule.addMembersTitle": "Añadir miembros",
|
||||
"accessRule.allPermittedMembers": "Todos los miembros con permisos de rol",
|
||||
"accessRule.allPermittedMembersDescription": "Los miembros con permisos de rol coincidentes pueden acceder a este recurso.",
|
||||
"accessRule.appDescription": "Controla a quién está abierta esta app. Los miembros todavía necesitan permisos de rol para verla u operarla.",
|
||||
"accessRule.appTitle": "Reglas de acceso de la app",
|
||||
"accessRule.changeOpenScopeDescription": "Cambiar el ámbito de apertura restablecerá todos los ajustes de permisos individuales para este recurso. Tendrás que volver a añadir los permisos específicos de cada miembro después de cambiarlo.",
|
||||
"accessRule.changeOpenScopeTitle": "¿Cambiar el ámbito de apertura del recurso?",
|
||||
"accessRule.collapseSection": "Contraer {{title}}",
|
||||
"accessRule.copied": "Regla de acceso copiada correctamente",
|
||||
"accessRule.created": "Regla de acceso creada correctamente",
|
||||
"accessRule.datasetDescription": "Controla a quién está abierta esta base de conocimiento. Los miembros todavía necesitan permisos de rol para verla u operarla.",
|
||||
"accessRule.datasetTitle": "Reglas de acceso de la base de conocimiento",
|
||||
"accessRule.defaultPermission": "Por permisos de rol",
|
||||
"accessRule.deleteDescription": "Esta regla de acceso se eliminará de forma permanente y se quitará de la lista de autorización del recurso.",
|
||||
"accessRule.deleteTitle": "¿Eliminar \"{{name}}\"?",
|
||||
"accessRule.deleted": "Regla de acceso eliminada correctamente",
|
||||
"accessRule.exceptionPermissionFor": "Permiso de excepción para {{name}}",
|
||||
"accessRule.expandSection": "Expandir {{title}}",
|
||||
"accessRule.individualPermissionSettings": "Ajustes de permisos individuales",
|
||||
"accessRule.individualPermissionSettingsTip": "Establece excepciones de permisos para colaboradores o grupos específicos. Estos ajustes anulan el nivel de acceso predeterminado.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} bloqueado",
|
||||
"accessRule.lockedSummary_other": "· {{count}} bloqueados",
|
||||
"accessRule.maintainer": "Mantenedor",
|
||||
"accessRule.member": "Miembro",
|
||||
"accessRule.newPermissionSet": "Nuevo conjunto de permisos",
|
||||
"accessRule.noAvailableMembers": "No hay miembros disponibles para añadir",
|
||||
"accessRule.noDescription": "Sin descripción",
|
||||
"accessRule.noRoles": "Sin roles",
|
||||
"accessRule.noRules": "Sin reglas de acceso",
|
||||
"accessRule.noUserAccessSettings": "Sin ajustes de permisos individuales",
|
||||
"accessRule.permission": "Permiso",
|
||||
"accessRule.resourceOpenScope": "Ámbito de apertura del recurso",
|
||||
"accessRule.resourceOpenScopeDescription": "Elige a quién está abierto este recurso. Los permisos de rol siguen decidiendo lo que puede hacer cada miembro.",
|
||||
"accessRule.specificMembersOnly": "Solo miembros específicos",
|
||||
"accessRule.specificMembersOnlyDescription": "Solo los miembros seleccionados pueden acceder a este recurso.",
|
||||
"accessRule.summary_one": "{{count}} conjunto de permisos",
|
||||
"accessRule.summary_other": "{{count}} conjuntos de permisos",
|
||||
"accessRule.updated": "Regla de acceso actualizada correctamente",
|
||||
"common.duplicateAction": "Duplicar",
|
||||
"group.app": "Aplicaciones",
|
||||
"group.app_acl": "Permisos de acceso de la app",
|
||||
"group.billing": "Facturación",
|
||||
"group.credential": "Credenciales",
|
||||
"group.dataset": "Bases de conocimiento",
|
||||
"group.dataset_acl": "Permisos de acceso de la base de conocimiento",
|
||||
"group.integration": "Integraciones",
|
||||
"group.plugin": "Plugins",
|
||||
"group.tool_mcp": "Herramientas y MCP",
|
||||
"group.workspace": "Espacio de trabajo",
|
||||
"permissionList.clearAll": "Borrar todo",
|
||||
"permissionList.collapseGroup": "Contraer grupo",
|
||||
"permissionList.expandGroup": "Expandir grupo",
|
||||
"permissionList.noPermissionsFound": "No se encontraron permisos",
|
||||
"permissionList.selectAll": "Seleccionar todo",
|
||||
"permissionSet.descriptionLabel": "Descripción",
|
||||
"permissionSet.descriptionPlaceholder": "Describe qué concede este conjunto de permisos",
|
||||
"permissionSet.learnMore": "Más información sobre los permisos",
|
||||
"permissionSet.modal.create.app.description": "Crea un conjunto de permisos de app que se pueda referenciar en las reglas de acceso para una autorización rápida.",
|
||||
"permissionSet.modal.create.app.title": "Crear conjunto de permisos de app",
|
||||
"permissionSet.modal.create.dataset.description": "Crea un conjunto de permisos de base de conocimiento que se pueda referenciar en las reglas de acceso para una autorización rápida.",
|
||||
"permissionSet.modal.create.dataset.title": "Crear conjunto de permisos de base de conocimiento",
|
||||
"permissionSet.modal.edit.app.description": "Modifica el nombre, la descripción y los permisos concedidos para este conjunto de permisos.",
|
||||
"permissionSet.modal.edit.app.title": "Editar conjunto de permisos de app",
|
||||
"permissionSet.modal.edit.dataset.description": "Modifica el nombre, la descripción y los permisos concedidos para este conjunto de permisos.",
|
||||
"permissionSet.modal.edit.dataset.title": "Editar conjunto de permisos de base de conocimiento",
|
||||
"permissionSet.modal.view.app.description": "Consulta el nombre, la descripción y los permisos concedidos para este conjunto de permisos.",
|
||||
"permissionSet.modal.view.app.title": "Ver conjunto de permisos de app",
|
||||
"permissionSet.modal.view.dataset.description": "Consulta el nombre, la descripción y los permisos concedidos para este conjunto de permisos.",
|
||||
"permissionSet.modal.view.dataset.title": "Ver conjunto de permisos de base de conocimiento",
|
||||
"permissionSet.nameLabel": "Nombre del conjunto de permisos",
|
||||
"permissionSet.namePlaceholder": "p. ej. Puede exportar DSL",
|
||||
"permissionSet.permissions": "Permisos",
|
||||
"role.addRole": "Crear roles",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" está asignado a {{count}} miembro. ¿Quieres que la nueva copia del rol incluya el mismo miembro?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" está asignado a {{count}} miembros. ¿Quieres que la nueva copia del rol incluya los mismos miembros?",
|
||||
"role.copyMembersLoading": "Cargando asignaciones de miembros...",
|
||||
"role.copyMembersTitle": "¿Copiar asignaciones de miembros?",
|
||||
"role.created": "Rol creado correctamente",
|
||||
"role.deleteDescription": "Este rol se eliminará de forma permanente y se quitará de cualquier miembro o regla de acceso que lo use.",
|
||||
"role.deleteTitle": "¿Eliminar \"{{name}}\"?",
|
||||
"role.deleted": "Rol eliminado correctamente",
|
||||
"role.duplicated": "Rol duplicado correctamente",
|
||||
"role.groups.builtin": "Roles del sistema",
|
||||
"role.groups.custom": "Roles personalizados",
|
||||
"role.loading": "Cargando roles...",
|
||||
"role.modal.create.description": "Crea un rol y asigna permisos",
|
||||
"role.modal.create.title": "Crear rol",
|
||||
"role.modal.descriptionLabel": "Descripción",
|
||||
"role.modal.descriptionPlaceholder": "Describe de qué es responsable este rol",
|
||||
"role.modal.edit.description": "Editar los detalles y permisos del rol",
|
||||
"role.modal.edit.title": "Editar rol",
|
||||
"role.modal.nameLabel": "Nombre del rol",
|
||||
"role.modal.namePlaceholder": "p. ej. Responsable de marketing",
|
||||
"role.modal.view.description": "Ver los detalles y permisos del rol",
|
||||
"role.modal.view.title": "Ver rol",
|
||||
"role.noDescription": "Sin descripción",
|
||||
"role.noMatchingRoles": "No hay roles coincidentes",
|
||||
"role.searchPlaceholder": "Buscar roles...",
|
||||
"role.updated": "Rol actualizado correctamente",
|
||||
"role.workspaceRoles.description": "Crea roles y define lo que cada rol puede hacer en este espacio de trabajo.",
|
||||
"role.workspaceRoles.title": "Roles del espacio de trabajo"
|
||||
}
|
||||
51
web/i18n/fa-IR/permission-keys.json
Normal file
51
web/i18n/fa-IR/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "مدیریت پیکربندی افزونه API",
|
||||
"app.access_config": "پیکربندی مجوزهای دسترسی برنامه",
|
||||
"app.acl.access_config": "پیکربندی مجوزهای دسترسی برنامه",
|
||||
"app.acl.delete": "حذف برنامه",
|
||||
"app.acl.edit": "ویرایش و هماهنگسازی برنامه",
|
||||
"app.acl.import_export_dsl": "وارد کردن / صادر کردن DSL",
|
||||
"app.acl.monitor": "نظارت و عملیات",
|
||||
"app.acl.preview": "پیشنمایش برنامه",
|
||||
"app.acl.release_and_version": "انتشار برنامه و مدیریت نسخه",
|
||||
"app.acl.test_and_run": "آزمایش و استفاده از برنامه",
|
||||
"app.acl.view_layout": "صفحه هماهنگسازی فقطخواندنی",
|
||||
"app.create_and_management": "ایجاد برنامهها و مدیریت برنامههایی که ایجاد کردهاید",
|
||||
"app.tag.manage": "مدیریت برچسبهای برنامه",
|
||||
"app_library.access": "دسترسی به کتابخانه برنامه",
|
||||
"billing.manage": "تغییر طرحهای اشتراک",
|
||||
"billing.subscription.manage": "مدیریت صورتحساب و اشتراکها در پورتال صورتحساب",
|
||||
"billing.view": "دسترسی به تنظیمات صورتحساب",
|
||||
"credential.create": "افزودن اعتبارنامهها",
|
||||
"credential.manage": "ویرایش و حذف اعتبارنامهها",
|
||||
"credential.use": "مشاهده و استفاده از اعتبارنامهها",
|
||||
"customization.manage": "مدیریت سفارشیسازی",
|
||||
"data_source.manage": "مدیریت پیکربندی منبع داده",
|
||||
"dataset.access_config": "پیکربندی مجوزهای دسترسی پایگاه دانش",
|
||||
"dataset.acl.access_config": "پیکربندی مجوزهای دسترسی پایگاه دانش",
|
||||
"dataset.acl.delete": "حذف پایگاه دانش",
|
||||
"dataset.acl.delete_file": "حذف فایلهای پایگاه دانش",
|
||||
"dataset.acl.document_download": "دانلود اسناد",
|
||||
"dataset.acl.edit": "ویرایش پایگاه دانش",
|
||||
"dataset.acl.import_export_dsl": "وارد کردن / صادر کردن DSL خط لوله دانش",
|
||||
"dataset.acl.pipeline_release": "انتشار خط لوله دانش و مدیریت نسخه",
|
||||
"dataset.acl.pipeline_test": "آزمایش خط لوله",
|
||||
"dataset.acl.preview": "پیشنمایش پایگاه دانش",
|
||||
"dataset.acl.readonly": "پایگاه دانش فقطخواندنی",
|
||||
"dataset.acl.retrieval_recall": "بازیابی پایگاه دانش",
|
||||
"dataset.acl.use": "افزودن اسناد به پایگاه دانش",
|
||||
"dataset.api_key.manage": "مدیریت کلیدهای API پایگاه دانش",
|
||||
"dataset.create_and_management": "ایجاد پایگاههای دانش و مدیریت پایگاههای دانشی که ایجاد کردهاید",
|
||||
"dataset.external.connect": "اتصال به پایگاههای دانش خارجی",
|
||||
"dataset.tag.manage": "مدیریت برچسبهای پایگاه دانش",
|
||||
"mcp.manage": "مدیریت MCP",
|
||||
"plugin.debug": "اشکالزدایی افزونهها",
|
||||
"plugin.install": "نصب و بهروزرسانی افزونهها",
|
||||
"plugin.manage": "مدیریت افزونهها",
|
||||
"plugin.plugin_preferences": "مدیریت ترجیحات افزونه",
|
||||
"snippets.create_and_modify": "ایجاد و اصلاح قطعهکدها",
|
||||
"snippets.management": "مدیریت قطعهکدها",
|
||||
"tool.manage": "مدیریت ابزارها",
|
||||
"workspace.member.manage": "مدیریت اعضا",
|
||||
"workspace.role.manage": "مدیریت مجوزهای نقش و قوانین دسترسی به منابع"
|
||||
}
|
||||
105
web/i18n/fa-IR/permission.json
Normal file
105
web/i18n/fa-IR/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "اقدامات",
|
||||
"accessRule.addMemberAria": "افزودن {{name}}",
|
||||
"accessRule.addMembersTitle": "افزودن اعضا",
|
||||
"accessRule.allPermittedMembers": "همه اعضای دارای مجوزهای نقش",
|
||||
"accessRule.allPermittedMembersDescription": "اعضای دارای مجوزهای نقش منطبق میتوانند به این منبع دسترسی داشته باشند.",
|
||||
"accessRule.appDescription": "کنترل کنید این برنامه برای چه کسانی باز است. اعضا همچنان برای مشاهده یا کار با آن به مجوزهای نقش نیاز دارند.",
|
||||
"accessRule.appTitle": "قوانین دسترسی برنامه",
|
||||
"accessRule.changeOpenScopeDescription": "تغییر دامنه دسترسی، همه تنظیمات مجوز فردی این منبع را بازنشانی میکند. پس از تغییر، باید مجوزهای خاص اعضا را دوباره اضافه کنید.",
|
||||
"accessRule.changeOpenScopeTitle": "دامنه دسترسی منبع تغییر کند؟",
|
||||
"accessRule.collapseSection": "جمع کردن {{title}}",
|
||||
"accessRule.copied": "قانون دسترسی با موفقیت کپی شد",
|
||||
"accessRule.created": "قانون دسترسی با موفقیت ایجاد شد",
|
||||
"accessRule.datasetDescription": "کنترل کنید این پایگاه دانش برای چه کسانی باز است. اعضا همچنان برای مشاهده یا کار با آن به مجوزهای نقش نیاز دارند.",
|
||||
"accessRule.datasetTitle": "قوانین دسترسی پایگاه دانش",
|
||||
"accessRule.defaultPermission": "بر اساس مجوزهای نقش",
|
||||
"accessRule.deleteDescription": "این قانون دسترسی به طور دائمی حذف شده و از فهرست مجوزدهی منبع برداشته میشود.",
|
||||
"accessRule.deleteTitle": "حذف «{{name}}»؟",
|
||||
"accessRule.deleted": "قانون دسترسی با موفقیت حذف شد",
|
||||
"accessRule.exceptionPermissionFor": "مجوز استثنا برای {{name}}",
|
||||
"accessRule.expandSection": "گسترش {{title}}",
|
||||
"accessRule.individualPermissionSettings": "تنظیمات مجوز فردی",
|
||||
"accessRule.individualPermissionSettingsTip": "استثناهای مجوز را برای همکاران یا گروههای خاص تنظیم کنید. این تنظیمات سطح دسترسی پیشفرض را لغو میکنند.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} قفل شده",
|
||||
"accessRule.lockedSummary_other": "· {{count}} قفل شده",
|
||||
"accessRule.maintainer": "نگهدارنده",
|
||||
"accessRule.member": "عضو",
|
||||
"accessRule.newPermissionSet": "مجموعه مجوز جدید",
|
||||
"accessRule.noAvailableMembers": "هیچ عضوی برای افزودن در دسترس نیست",
|
||||
"accessRule.noDescription": "بدون توضیحات",
|
||||
"accessRule.noRoles": "بدون نقش",
|
||||
"accessRule.noRules": "بدون قانون دسترسی",
|
||||
"accessRule.noUserAccessSettings": "بدون تنظیمات مجوز فردی",
|
||||
"accessRule.permission": "مجوز",
|
||||
"accessRule.resourceOpenScope": "دامنه دسترسی منبع",
|
||||
"accessRule.resourceOpenScopeDescription": "انتخاب کنید این منبع برای چه کسانی باز است. مجوزهای نقش همچنان تعیین میکنند هر عضو چه کاری میتواند انجام دهد.",
|
||||
"accessRule.specificMembersOnly": "فقط اعضای خاص",
|
||||
"accessRule.specificMembersOnlyDescription": "فقط اعضای انتخابشده میتوانند به این منبع دسترسی داشته باشند.",
|
||||
"accessRule.summary_one": "{{count}} مجموعه مجوز",
|
||||
"accessRule.summary_other": "{{count}} مجموعه مجوز",
|
||||
"accessRule.updated": "قانون دسترسی با موفقیت بهروزرسانی شد",
|
||||
"common.duplicateAction": "تکثیر",
|
||||
"group.app": "برنامهها",
|
||||
"group.app_acl": "مجوزهای دسترسی برنامه",
|
||||
"group.billing": "صورتحساب",
|
||||
"group.credential": "اعتبارنامهها",
|
||||
"group.dataset": "پایگاههای دانش",
|
||||
"group.dataset_acl": "مجوزهای دسترسی پایگاه دانش",
|
||||
"group.integration": "یکپارچهسازیها",
|
||||
"group.plugin": "افزونهها",
|
||||
"group.tool_mcp": "ابزارها و MCP",
|
||||
"group.workspace": "فضای کاری",
|
||||
"permissionList.clearAll": "پاک کردن همه",
|
||||
"permissionList.collapseGroup": "جمع کردن گروه",
|
||||
"permissionList.expandGroup": "گسترش گروه",
|
||||
"permissionList.noPermissionsFound": "هیچ مجوزی یافت نشد",
|
||||
"permissionList.selectAll": "انتخاب همه",
|
||||
"permissionSet.descriptionLabel": "توضیحات",
|
||||
"permissionSet.descriptionPlaceholder": "توضیح دهید این مجموعه مجوز چه چیزی را اعطا میکند",
|
||||
"permissionSet.learnMore": "درباره مجوزها بیشتر بدانید",
|
||||
"permissionSet.modal.create.app.description": "یک مجموعه مجوز برنامه ایجاد کنید که بتوان برای مجوزدهی سریع در قوانین دسترسی به آن ارجاع داد.",
|
||||
"permissionSet.modal.create.app.title": "ایجاد مجموعه مجوز برنامه",
|
||||
"permissionSet.modal.create.dataset.description": "یک مجموعه مجوز پایگاه دانش ایجاد کنید که بتوان برای مجوزدهی سریع در قوانین دسترسی به آن ارجاع داد.",
|
||||
"permissionSet.modal.create.dataset.title": "ایجاد مجموعه مجوز پایگاه دانش",
|
||||
"permissionSet.modal.edit.app.description": "نام، توضیحات و مجوزهای اعطاشده برای این مجموعه مجوز را اصلاح کنید.",
|
||||
"permissionSet.modal.edit.app.title": "ویرایش مجموعه مجوز برنامه",
|
||||
"permissionSet.modal.edit.dataset.description": "نام، توضیحات و مجوزهای اعطاشده برای این مجموعه مجوز را اصلاح کنید.",
|
||||
"permissionSet.modal.edit.dataset.title": "ویرایش مجموعه مجوز پایگاه دانش",
|
||||
"permissionSet.modal.view.app.description": "نام، توضیحات و مجوزهای اعطاشده برای این مجموعه مجوز را مشاهده کنید.",
|
||||
"permissionSet.modal.view.app.title": "مشاهده مجموعه مجوز برنامه",
|
||||
"permissionSet.modal.view.dataset.description": "نام، توضیحات و مجوزهای اعطاشده برای این مجموعه مجوز را مشاهده کنید.",
|
||||
"permissionSet.modal.view.dataset.title": "مشاهده مجموعه مجوز پایگاه دانش",
|
||||
"permissionSet.nameLabel": "نام مجموعه مجوز",
|
||||
"permissionSet.namePlaceholder": "مثلاً میتواند DSL را صادر کند",
|
||||
"permissionSet.permissions": "مجوزها",
|
||||
"role.addRole": "ایجاد نقشها",
|
||||
"role.copyMembersDescription_one": "«{{name}}» به {{count}} عضو اختصاص داده شده است. آیا میخواهید نسخه نقش جدید همان عضو را در بر بگیرد؟",
|
||||
"role.copyMembersDescription_other": "«{{name}}» به {{count}} عضو اختصاص داده شده است. آیا میخواهید نسخه نقش جدید همان اعضا را در بر بگیرد؟",
|
||||
"role.copyMembersLoading": "در حال بارگذاری تخصیصهای اعضا...",
|
||||
"role.copyMembersTitle": "تخصیصهای اعضا کپی شود؟",
|
||||
"role.created": "نقش با موفقیت ایجاد شد",
|
||||
"role.deleteDescription": "این نقش به طور دائمی حذف شده و از هر عضو یا قانون دسترسی که از آن استفاده میکند برداشته میشود.",
|
||||
"role.deleteTitle": "حذف «{{name}}»؟",
|
||||
"role.deleted": "نقش با موفقیت حذف شد",
|
||||
"role.duplicated": "نقش با موفقیت تکثیر شد",
|
||||
"role.groups.builtin": "نقشهای سیستمی",
|
||||
"role.groups.custom": "نقشهای سفارشی",
|
||||
"role.loading": "در حال بارگذاری نقشها...",
|
||||
"role.modal.create.description": "یک نقش ایجاد کنید و مجوزها را اختصاص دهید",
|
||||
"role.modal.create.title": "ایجاد نقش",
|
||||
"role.modal.descriptionLabel": "توضیحات",
|
||||
"role.modal.descriptionPlaceholder": "توضیح دهید این نقش مسئول چه چیزی است",
|
||||
"role.modal.edit.description": "ویرایش جزئیات و مجوزهای نقش",
|
||||
"role.modal.edit.title": "ویرایش نقش",
|
||||
"role.modal.nameLabel": "نام نقش",
|
||||
"role.modal.namePlaceholder": "مثلاً سرپرست بازاریابی",
|
||||
"role.modal.view.description": "مشاهده جزئیات و مجوزهای نقش",
|
||||
"role.modal.view.title": "مشاهده نقش",
|
||||
"role.noDescription": "بدون توضیحات",
|
||||
"role.noMatchingRoles": "هیچ نقش منطبقی یافت نشد",
|
||||
"role.searchPlaceholder": "جستجوی نقشها...",
|
||||
"role.updated": "نقش با موفقیت بهروزرسانی شد",
|
||||
"role.workspaceRoles.description": "نقشها را ایجاد کنید و تعریف کنید هر نقش در این فضای کاری چه کاری میتواند انجام دهد.",
|
||||
"role.workspaceRoles.title": "نقشهای فضای کاری"
|
||||
}
|
||||
51
web/i18n/fr-FR/permission-keys.json
Normal file
51
web/i18n/fr-FR/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "Gérer la configuration de l'extension API",
|
||||
"app.access_config": "Configurer les autorisations d'accès à l'application",
|
||||
"app.acl.access_config": "Configurer les autorisations d'accès à l'application",
|
||||
"app.acl.delete": "Supprimer l'application",
|
||||
"app.acl.edit": "Modifier et orchestrer l'application",
|
||||
"app.acl.import_export_dsl": "Importer / exporter le DSL",
|
||||
"app.acl.monitor": "Surveillance et exploitation",
|
||||
"app.acl.preview": "Prévisualiser l'application",
|
||||
"app.acl.release_and_version": "Publication de l'application et gestion des versions",
|
||||
"app.acl.test_and_run": "Tester et utiliser l'application",
|
||||
"app.acl.view_layout": "Page d'orchestration en lecture seule",
|
||||
"app.create_and_management": "Créer des applications et gérer les applications que vous avez créées",
|
||||
"app.tag.manage": "Gérer les étiquettes d'applications",
|
||||
"app_library.access": "Accéder à la bibliothèque d'applications",
|
||||
"billing.manage": "Modifier les forfaits d'abonnement",
|
||||
"billing.subscription.manage": "Gérer la facturation et les abonnements dans le portail de facturation",
|
||||
"billing.view": "Accéder aux paramètres de facturation",
|
||||
"credential.create": "Ajouter des identifiants",
|
||||
"credential.manage": "Modifier et supprimer des identifiants",
|
||||
"credential.use": "Afficher et utiliser des identifiants",
|
||||
"customization.manage": "Gérer la personnalisation",
|
||||
"data_source.manage": "Gérer la configuration des sources de données",
|
||||
"dataset.access_config": "Configurer les autorisations d'accès à la base de connaissances",
|
||||
"dataset.acl.access_config": "Configurer les autorisations d'accès à la base de connaissances",
|
||||
"dataset.acl.delete": "Supprimer la base de connaissances",
|
||||
"dataset.acl.delete_file": "Supprimer les fichiers de la base de connaissances",
|
||||
"dataset.acl.document_download": "Télécharger des documents",
|
||||
"dataset.acl.edit": "Modifier la base de connaissances",
|
||||
"dataset.acl.import_export_dsl": "Importer / exporter le DSL du pipeline de connaissances",
|
||||
"dataset.acl.pipeline_release": "Publication du pipeline de connaissances et gestion des versions",
|
||||
"dataset.acl.pipeline_test": "Test du pipeline",
|
||||
"dataset.acl.preview": "Prévisualiser la base de connaissances",
|
||||
"dataset.acl.readonly": "Base de connaissances en lecture seule",
|
||||
"dataset.acl.retrieval_recall": "Récupération dans la base de connaissances",
|
||||
"dataset.acl.use": "Ajouter des documents à la base de connaissances",
|
||||
"dataset.api_key.manage": "Gérer les clés API de la base de connaissances",
|
||||
"dataset.create_and_management": "Créer des bases de connaissances et gérer les bases de connaissances que vous avez créées",
|
||||
"dataset.external.connect": "Connecter des bases de connaissances externes",
|
||||
"dataset.tag.manage": "Gérer les étiquettes de bases de connaissances",
|
||||
"mcp.manage": "Gérer MCP",
|
||||
"plugin.debug": "Déboguer les plugins",
|
||||
"plugin.install": "Installer et mettre à jour les plugins",
|
||||
"plugin.manage": "Gérer les plugins",
|
||||
"plugin.plugin_preferences": "Gérer les préférences des plugins",
|
||||
"snippets.create_and_modify": "Créer et modifier des extraits",
|
||||
"snippets.management": "Gérer les extraits",
|
||||
"tool.manage": "Gérer les outils",
|
||||
"workspace.member.manage": "Gérer les membres",
|
||||
"workspace.role.manage": "Gérer les autorisations de rôles et les règles d'accès aux ressources"
|
||||
}
|
||||
105
web/i18n/fr-FR/permission.json
Normal file
105
web/i18n/fr-FR/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Actions",
|
||||
"accessRule.addMemberAria": "Ajouter {{name}}",
|
||||
"accessRule.addMembersTitle": "Ajouter des membres",
|
||||
"accessRule.allPermittedMembers": "Tous les membres ayant les autorisations de rôle",
|
||||
"accessRule.allPermittedMembersDescription": "Les membres disposant d'autorisations de rôle correspondantes peuvent accéder à cette ressource.",
|
||||
"accessRule.appDescription": "Contrôlez à qui cette application est ouverte. Les membres ont toujours besoin d'autorisations de rôle pour la consulter ou l'utiliser.",
|
||||
"accessRule.appTitle": "Règles d'accès à l'application",
|
||||
"accessRule.changeOpenScopeDescription": "Modifier la portée d'ouverture réinitialisera tous les paramètres d'autorisation individuels de cette ressource. Vous devrez ajouter à nouveau les autorisations spécifiques aux membres après le changement.",
|
||||
"accessRule.changeOpenScopeTitle": "Modifier la portée d'ouverture de la ressource ?",
|
||||
"accessRule.collapseSection": "Réduire {{title}}",
|
||||
"accessRule.copied": "Règle d'accès copiée avec succès",
|
||||
"accessRule.created": "Règle d'accès créée avec succès",
|
||||
"accessRule.datasetDescription": "Contrôlez à qui cette base de connaissances est ouverte. Les membres ont toujours besoin d'autorisations de rôle pour la consulter ou l'utiliser.",
|
||||
"accessRule.datasetTitle": "Règles d'accès à la base de connaissances",
|
||||
"accessRule.defaultPermission": "Selon les autorisations de rôle",
|
||||
"accessRule.deleteDescription": "Cette règle d'accès sera définitivement supprimée et retirée de la liste d'autorisation de la ressource.",
|
||||
"accessRule.deleteTitle": "Supprimer \"{{name}}\" ?",
|
||||
"accessRule.deleted": "Règle d'accès supprimée avec succès",
|
||||
"accessRule.exceptionPermissionFor": "Autorisation d'exception pour {{name}}",
|
||||
"accessRule.expandSection": "Développer {{title}}",
|
||||
"accessRule.individualPermissionSettings": "Paramètres d'autorisation individuels",
|
||||
"accessRule.individualPermissionSettingsTip": "Définissez des exceptions d'autorisation pour des collaborateurs ou des groupes spécifiques. Ces paramètres remplacent le niveau d'accès par défaut.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} verrouillé",
|
||||
"accessRule.lockedSummary_other": "· {{count}} verrouillés",
|
||||
"accessRule.maintainer": "Mainteneur",
|
||||
"accessRule.member": "Membre",
|
||||
"accessRule.newPermissionSet": "Nouvel ensemble d'autorisations",
|
||||
"accessRule.noAvailableMembers": "Aucun membre disponible à ajouter",
|
||||
"accessRule.noDescription": "Aucune description",
|
||||
"accessRule.noRoles": "Aucun rôle",
|
||||
"accessRule.noRules": "Aucune règle d'accès",
|
||||
"accessRule.noUserAccessSettings": "Aucun paramètre d'autorisation individuel",
|
||||
"accessRule.permission": "Autorisation",
|
||||
"accessRule.resourceOpenScope": "Portée d'ouverture de la ressource",
|
||||
"accessRule.resourceOpenScopeDescription": "Choisissez à qui cette ressource est ouverte. Les autorisations de rôle déterminent toujours ce que chaque membre peut faire.",
|
||||
"accessRule.specificMembersOnly": "Membres spécifiques uniquement",
|
||||
"accessRule.specificMembersOnlyDescription": "Seuls les membres sélectionnés peuvent accéder à cette ressource.",
|
||||
"accessRule.summary_one": "{{count}} ensemble d'autorisations",
|
||||
"accessRule.summary_other": "{{count}} ensembles d'autorisations",
|
||||
"accessRule.updated": "Règle d'accès mise à jour avec succès",
|
||||
"common.duplicateAction": "Dupliquer",
|
||||
"group.app": "Applications",
|
||||
"group.app_acl": "Autorisations d'accès à l'application",
|
||||
"group.billing": "Facturation",
|
||||
"group.credential": "Identifiants",
|
||||
"group.dataset": "Bases de connaissances",
|
||||
"group.dataset_acl": "Autorisations d'accès à la base de connaissances",
|
||||
"group.integration": "Intégrations",
|
||||
"group.plugin": "Plugins",
|
||||
"group.tool_mcp": "Outils et MCP",
|
||||
"group.workspace": "Espace de travail",
|
||||
"permissionList.clearAll": "Tout effacer",
|
||||
"permissionList.collapseGroup": "Réduire le groupe",
|
||||
"permissionList.expandGroup": "Développer le groupe",
|
||||
"permissionList.noPermissionsFound": "Aucune autorisation trouvée",
|
||||
"permissionList.selectAll": "Tout sélectionner",
|
||||
"permissionSet.descriptionLabel": "Description",
|
||||
"permissionSet.descriptionPlaceholder": "Décrivez ce que cet ensemble d'autorisations accorde",
|
||||
"permissionSet.learnMore": "En savoir plus sur les autorisations",
|
||||
"permissionSet.modal.create.app.description": "Créez un ensemble d'autorisations d'application qui peut être référencé dans les règles d'accès pour une autorisation rapide.",
|
||||
"permissionSet.modal.create.app.title": "Créer un ensemble d'autorisations d'application",
|
||||
"permissionSet.modal.create.dataset.description": "Créez un ensemble d'autorisations de base de connaissances qui peut être référencé dans les règles d'accès pour une autorisation rapide.",
|
||||
"permissionSet.modal.create.dataset.title": "Créer un ensemble d'autorisations de base de connaissances",
|
||||
"permissionSet.modal.edit.app.description": "Modifiez le nom, la description et les autorisations accordées pour cet ensemble d'autorisations.",
|
||||
"permissionSet.modal.edit.app.title": "Modifier l'ensemble d'autorisations d'application",
|
||||
"permissionSet.modal.edit.dataset.description": "Modifiez le nom, la description et les autorisations accordées pour cet ensemble d'autorisations.",
|
||||
"permissionSet.modal.edit.dataset.title": "Modifier l'ensemble d'autorisations de base de connaissances",
|
||||
"permissionSet.modal.view.app.description": "Affichez le nom, la description et les autorisations accordées pour cet ensemble d'autorisations.",
|
||||
"permissionSet.modal.view.app.title": "Afficher l'ensemble d'autorisations d'application",
|
||||
"permissionSet.modal.view.dataset.description": "Affichez le nom, la description et les autorisations accordées pour cet ensemble d'autorisations.",
|
||||
"permissionSet.modal.view.dataset.title": "Afficher l'ensemble d'autorisations de base de connaissances",
|
||||
"permissionSet.nameLabel": "Nom de l'ensemble d'autorisations",
|
||||
"permissionSet.namePlaceholder": "ex. Peut exporter le DSL",
|
||||
"permissionSet.permissions": "Autorisations",
|
||||
"role.addRole": "Créer des rôles",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" est attribué à {{count}} membre. Voulez-vous que la copie du nouveau rôle inclue le même membre ?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" est attribué à {{count}} membres. Voulez-vous que la copie du nouveau rôle inclue les mêmes membres ?",
|
||||
"role.copyMembersLoading": "Chargement des attributions de membres...",
|
||||
"role.copyMembersTitle": "Copier les attributions de membres ?",
|
||||
"role.created": "Rôle créé avec succès",
|
||||
"role.deleteDescription": "Ce rôle sera définitivement supprimé et retiré de tous les membres ou règles d'accès qui l'utilisent.",
|
||||
"role.deleteTitle": "Supprimer \"{{name}}\" ?",
|
||||
"role.deleted": "Rôle supprimé avec succès",
|
||||
"role.duplicated": "Rôle dupliqué avec succès",
|
||||
"role.groups.builtin": "Rôles système",
|
||||
"role.groups.custom": "Rôles personnalisés",
|
||||
"role.loading": "Chargement des rôles...",
|
||||
"role.modal.create.description": "Créer un rôle et attribuer des autorisations",
|
||||
"role.modal.create.title": "Créer un rôle",
|
||||
"role.modal.descriptionLabel": "Description",
|
||||
"role.modal.descriptionPlaceholder": "Décrivez de quoi ce rôle est responsable",
|
||||
"role.modal.edit.description": "Modifier les détails et les autorisations du rôle",
|
||||
"role.modal.edit.title": "Modifier le rôle",
|
||||
"role.modal.nameLabel": "Nom du rôle",
|
||||
"role.modal.namePlaceholder": "ex. Responsable marketing",
|
||||
"role.modal.view.description": "Afficher les détails et les autorisations du rôle",
|
||||
"role.modal.view.title": "Afficher le rôle",
|
||||
"role.noDescription": "Aucune description",
|
||||
"role.noMatchingRoles": "Aucun rôle correspondant",
|
||||
"role.searchPlaceholder": "Rechercher des rôles...",
|
||||
"role.updated": "Rôle mis à jour avec succès",
|
||||
"role.workspaceRoles.description": "Créez des rôles et définissez ce que chaque rôle peut faire dans cet espace de travail.",
|
||||
"role.workspaceRoles.title": "Rôles de l'espace de travail"
|
||||
}
|
||||
51
web/i18n/hi-IN/permission-keys.json
Normal file
51
web/i18n/hi-IN/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "API एक्सटेंशन कॉन्फ़िगरेशन प्रबंधित करें",
|
||||
"app.access_config": "ऐप एक्सेस अनुमतियाँ कॉन्फ़िगर करें",
|
||||
"app.acl.access_config": "ऐप एक्सेस अनुमतियाँ कॉन्फ़िगर करें",
|
||||
"app.acl.delete": "ऐप हटाएं",
|
||||
"app.acl.edit": "ऐप संपादित और ऑर्केस्ट्रेट करें",
|
||||
"app.acl.import_export_dsl": "DSL आयात / निर्यात करें",
|
||||
"app.acl.monitor": "निगरानी और संचालन",
|
||||
"app.acl.preview": "ऐप पूर्वावलोकन करें",
|
||||
"app.acl.release_and_version": "ऐप प्रकाशन और संस्करण प्रबंधन",
|
||||
"app.acl.test_and_run": "ऐप परीक्षण और उपयोग करें",
|
||||
"app.acl.view_layout": "केवल-पढ़ने योग्य ऑर्केस्ट्रेशन पृष्ठ",
|
||||
"app.create_and_management": "ऐप्स बनाएं और अपने बनाए गए ऐप्स प्रबंधित करें",
|
||||
"app.tag.manage": "ऐप टैग प्रबंधित करें",
|
||||
"app_library.access": "ऐप लाइब्रेरी तक पहुंचें",
|
||||
"billing.manage": "सदस्यता योजनाएं बदलें",
|
||||
"billing.subscription.manage": "बिलिंग पोर्टल में बिलिंग और सदस्यताएं प्रबंधित करें",
|
||||
"billing.view": "बिलिंग सेटिंग्स तक पहुंचें",
|
||||
"credential.create": "क्रेडेंशियल जोड़ें",
|
||||
"credential.manage": "क्रेडेंशियल संपादित और हटाएं",
|
||||
"credential.use": "क्रेडेंशियल देखें और उपयोग करें",
|
||||
"customization.manage": "अनुकूलन प्रबंधित करें",
|
||||
"data_source.manage": "डेटा स्रोत कॉन्फ़िगरेशन प्रबंधित करें",
|
||||
"dataset.access_config": "ज्ञान आधार एक्सेस अनुमतियाँ कॉन्फ़िगर करें",
|
||||
"dataset.acl.access_config": "ज्ञान आधार एक्सेस अनुमतियाँ कॉन्फ़िगर करें",
|
||||
"dataset.acl.delete": "ज्ञान आधार हटाएं",
|
||||
"dataset.acl.delete_file": "ज्ञान आधार फ़ाइलें हटाएं",
|
||||
"dataset.acl.document_download": "दस्तावेज़ डाउनलोड करें",
|
||||
"dataset.acl.edit": "ज्ञान आधार संपादित करें",
|
||||
"dataset.acl.import_export_dsl": "ज्ञान पाइपलाइन DSL आयात / निर्यात करें",
|
||||
"dataset.acl.pipeline_release": "ज्ञान पाइपलाइन प्रकाशन और संस्करण प्रबंधन",
|
||||
"dataset.acl.pipeline_test": "पाइपलाइन परीक्षण",
|
||||
"dataset.acl.preview": "ज्ञान आधार पूर्वावलोकन करें",
|
||||
"dataset.acl.readonly": "केवल-पढ़ने योग्य ज्ञान आधार",
|
||||
"dataset.acl.retrieval_recall": "ज्ञान आधार पुनर्प्राप्ति",
|
||||
"dataset.acl.use": "ज्ञान आधार में दस्तावेज़ जोड़ें",
|
||||
"dataset.api_key.manage": "ज्ञान आधार API कुंजियाँ प्रबंधित करें",
|
||||
"dataset.create_and_management": "ज्ञान आधार बनाएं और अपने बनाए गए ज्ञान आधार प्रबंधित करें",
|
||||
"dataset.external.connect": "बाहरी ज्ञान आधार कनेक्ट करें",
|
||||
"dataset.tag.manage": "ज्ञान आधार टैग प्रबंधित करें",
|
||||
"mcp.manage": "MCP प्रबंधित करें",
|
||||
"plugin.debug": "प्लगइन्स डिबग करें",
|
||||
"plugin.install": "प्लगइन्स इंस्टॉल और अपडेट करें",
|
||||
"plugin.manage": "प्लगइन्स प्रबंधित करें",
|
||||
"plugin.plugin_preferences": "प्लगइन प्राथमिकताएं प्रबंधित करें",
|
||||
"snippets.create_and_modify": "स्निपेट बनाएं और संशोधित करें",
|
||||
"snippets.management": "स्निपेट प्रबंधित करें",
|
||||
"tool.manage": "टूल प्रबंधित करें",
|
||||
"workspace.member.manage": "सदस्य प्रबंधित करें",
|
||||
"workspace.role.manage": "भूमिका अनुमतियाँ और संसाधन एक्सेस नियम प्रबंधित करें"
|
||||
}
|
||||
105
web/i18n/hi-IN/permission.json
Normal file
105
web/i18n/hi-IN/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "क्रियाएं",
|
||||
"accessRule.addMemberAria": "{{name}} जोड़ें",
|
||||
"accessRule.addMembersTitle": "सदस्य जोड़ें",
|
||||
"accessRule.allPermittedMembers": "भूमिका अनुमतियों वाले सभी सदस्य",
|
||||
"accessRule.allPermittedMembersDescription": "मिलती-जुलती भूमिका अनुमतियों वाले सदस्य इस संसाधन तक पहुंच सकते हैं।",
|
||||
"accessRule.appDescription": "नियंत्रित करें कि यह ऐप किसके लिए खुला है। सदस्यों को इसे देखने या संचालित करने के लिए अभी भी भूमिका अनुमतियों की आवश्यकता होती है।",
|
||||
"accessRule.appTitle": "ऐप एक्सेस नियम",
|
||||
"accessRule.changeOpenScopeDescription": "खुले दायरे को बदलने से इस संसाधन के लिए सभी व्यक्तिगत अनुमति सेटिंग्स रीसेट हो जाएंगी। स्विच करने के बाद आपको सदस्य-विशिष्ट अनुमतियां फिर से जोड़नी होंगी।",
|
||||
"accessRule.changeOpenScopeTitle": "संसाधन का खुला दायरा बदलें?",
|
||||
"accessRule.collapseSection": "{{title}} संक्षिप्त करें",
|
||||
"accessRule.copied": "एक्सेस नियम सफलतापूर्वक कॉपी किया गया",
|
||||
"accessRule.created": "एक्सेस नियम सफलतापूर्वक बनाया गया",
|
||||
"accessRule.datasetDescription": "नियंत्रित करें कि यह ज्ञान आधार किसके लिए खुला है। सदस्यों को इसे देखने या संचालित करने के लिए अभी भी भूमिका अनुमतियों की आवश्यकता होती है।",
|
||||
"accessRule.datasetTitle": "ज्ञान आधार एक्सेस नियम",
|
||||
"accessRule.defaultPermission": "भूमिका अनुमतियों के अनुसार",
|
||||
"accessRule.deleteDescription": "यह एक्सेस नियम स्थायी रूप से हटा दिया जाएगा और संसाधन प्राधिकरण सूची से हटा दिया जाएगा।",
|
||||
"accessRule.deleteTitle": "\"{{name}}\" हटाएं?",
|
||||
"accessRule.deleted": "एक्सेस नियम सफलतापूर्वक हटाया गया",
|
||||
"accessRule.exceptionPermissionFor": "{{name}} के लिए अपवाद अनुमति",
|
||||
"accessRule.expandSection": "{{title}} विस्तृत करें",
|
||||
"accessRule.individualPermissionSettings": "व्यक्तिगत अनुमति सेटिंग्स",
|
||||
"accessRule.individualPermissionSettingsTip": "विशिष्ट सहयोगियों या समूहों के लिए अनुमति अपवाद सेट करें। ये सेटिंग्स डिफ़ॉल्ट एक्सेस स्तर को ओवरराइड करती हैं।",
|
||||
"accessRule.lockedSummary_one": "· {{count}} लॉक किया गया",
|
||||
"accessRule.lockedSummary_other": "· {{count}} लॉक किए गए",
|
||||
"accessRule.maintainer": "रखरखावकर्ता",
|
||||
"accessRule.member": "सदस्य",
|
||||
"accessRule.newPermissionSet": "नया अनुमति सेट",
|
||||
"accessRule.noAvailableMembers": "जोड़ने के लिए कोई सदस्य उपलब्ध नहीं",
|
||||
"accessRule.noDescription": "कोई विवरण नहीं",
|
||||
"accessRule.noRoles": "कोई भूमिका नहीं",
|
||||
"accessRule.noRules": "कोई एक्सेस नियम नहीं",
|
||||
"accessRule.noUserAccessSettings": "कोई व्यक्तिगत अनुमति सेटिंग्स नहीं",
|
||||
"accessRule.permission": "अनुमति",
|
||||
"accessRule.resourceOpenScope": "संसाधन का खुला दायरा",
|
||||
"accessRule.resourceOpenScopeDescription": "चुनें कि यह संसाधन किसके लिए खुला है। भूमिका अनुमतियां अभी भी तय करती हैं कि प्रत्येक सदस्य क्या कर सकता है।",
|
||||
"accessRule.specificMembersOnly": "केवल विशिष्ट सदस्य",
|
||||
"accessRule.specificMembersOnlyDescription": "केवल चयनित सदस्य ही इस संसाधन तक पहुंच सकते हैं।",
|
||||
"accessRule.summary_one": "{{count}} अनुमति सेट",
|
||||
"accessRule.summary_other": "{{count}} अनुमति सेट",
|
||||
"accessRule.updated": "एक्सेस नियम सफलतापूर्वक अपडेट किया गया",
|
||||
"common.duplicateAction": "डुप्लिकेट करें",
|
||||
"group.app": "अनुप्रयोग",
|
||||
"group.app_acl": "ऐप एक्सेस अनुमतियाँ",
|
||||
"group.billing": "बिलिंग",
|
||||
"group.credential": "क्रेडेंशियल",
|
||||
"group.dataset": "ज्ञान आधार",
|
||||
"group.dataset_acl": "ज्ञान आधार एक्सेस अनुमतियाँ",
|
||||
"group.integration": "एकीकरण",
|
||||
"group.plugin": "प्लगइन्स",
|
||||
"group.tool_mcp": "टूल और MCP",
|
||||
"group.workspace": "कार्यक्षेत्र",
|
||||
"permissionList.clearAll": "सभी साफ़ करें",
|
||||
"permissionList.collapseGroup": "समूह संक्षिप्त करें",
|
||||
"permissionList.expandGroup": "समूह विस्तृत करें",
|
||||
"permissionList.noPermissionsFound": "कोई अनुमति नहीं मिली",
|
||||
"permissionList.selectAll": "सभी चुनें",
|
||||
"permissionSet.descriptionLabel": "विवरण",
|
||||
"permissionSet.descriptionPlaceholder": "वर्णन करें कि यह अनुमति सेट क्या प्रदान करता है",
|
||||
"permissionSet.learnMore": "अनुमतियों के बारे में अधिक जानें",
|
||||
"permissionSet.modal.create.app.description": "एक ऐप अनुमति सेट बनाएं जिसे त्वरित प्राधिकरण के लिए एक्सेस नियमों में संदर्भित किया जा सके।",
|
||||
"permissionSet.modal.create.app.title": "ऐप अनुमति सेट बनाएं",
|
||||
"permissionSet.modal.create.dataset.description": "एक ज्ञान आधार अनुमति सेट बनाएं जिसे त्वरित प्राधिकरण के लिए एक्सेस नियमों में संदर्भित किया जा सके।",
|
||||
"permissionSet.modal.create.dataset.title": "ज्ञान आधार अनुमति सेट बनाएं",
|
||||
"permissionSet.modal.edit.app.description": "इस अनुमति सेट के लिए नाम, विवरण और प्रदान की गई अनुमतियों को संशोधित करें।",
|
||||
"permissionSet.modal.edit.app.title": "ऐप अनुमति सेट संपादित करें",
|
||||
"permissionSet.modal.edit.dataset.description": "इस अनुमति सेट के लिए नाम, विवरण और प्रदान की गई अनुमतियों को संशोधित करें।",
|
||||
"permissionSet.modal.edit.dataset.title": "ज्ञान आधार अनुमति सेट संपादित करें",
|
||||
"permissionSet.modal.view.app.description": "इस अनुमति सेट के लिए नाम, विवरण और प्रदान की गई अनुमतियों को देखें।",
|
||||
"permissionSet.modal.view.app.title": "ऐप अनुमति सेट देखें",
|
||||
"permissionSet.modal.view.dataset.description": "इस अनुमति सेट के लिए नाम, विवरण और प्रदान की गई अनुमतियों को देखें।",
|
||||
"permissionSet.modal.view.dataset.title": "ज्ञान आधार अनुमति सेट देखें",
|
||||
"permissionSet.nameLabel": "अनुमति सेट नाम",
|
||||
"permissionSet.namePlaceholder": "उदा. DSL निर्यात कर सकते हैं",
|
||||
"permissionSet.permissions": "अनुमतियां",
|
||||
"role.addRole": "भूमिकाएं बनाएं",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" {{count}} सदस्य को सौंपी गई है। क्या आप चाहते हैं कि नई भूमिका प्रति में वही सदस्य शामिल हो?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" {{count}} सदस्यों को सौंपी गई है। क्या आप चाहते हैं कि नई भूमिका प्रति में वही सदस्य शामिल हों?",
|
||||
"role.copyMembersLoading": "सदस्य असाइनमेंट लोड हो रहे हैं...",
|
||||
"role.copyMembersTitle": "सदस्य असाइनमेंट कॉपी करें?",
|
||||
"role.created": "भूमिका सफलतापूर्वक बनाई गई",
|
||||
"role.deleteDescription": "यह भूमिका स्थायी रूप से हटा दी जाएगी और इसका उपयोग करने वाले किसी भी सदस्य या एक्सेस नियम से हटा दी जाएगी।",
|
||||
"role.deleteTitle": "\"{{name}}\" हटाएं?",
|
||||
"role.deleted": "भूमिका सफलतापूर्वक हटाई गई",
|
||||
"role.duplicated": "भूमिका सफलतापूर्वक डुप्लिकेट की गई",
|
||||
"role.groups.builtin": "सिस्टम भूमिकाएं",
|
||||
"role.groups.custom": "कस्टम भूमिकाएं",
|
||||
"role.loading": "भूमिकाएं लोड हो रही हैं...",
|
||||
"role.modal.create.description": "एक भूमिका बनाएं और अनुमतियां असाइन करें",
|
||||
"role.modal.create.title": "भूमिका बनाएं",
|
||||
"role.modal.descriptionLabel": "विवरण",
|
||||
"role.modal.descriptionPlaceholder": "वर्णन करें कि यह भूमिका किसके लिए जिम्मेदार है",
|
||||
"role.modal.edit.description": "भूमिका विवरण और अनुमतियां संपादित करें",
|
||||
"role.modal.edit.title": "भूमिका संपादित करें",
|
||||
"role.modal.nameLabel": "भूमिका नाम",
|
||||
"role.modal.namePlaceholder": "उदा. मार्केटिंग लीड",
|
||||
"role.modal.view.description": "भूमिका विवरण और अनुमतियां देखें",
|
||||
"role.modal.view.title": "भूमिका देखें",
|
||||
"role.noDescription": "कोई विवरण नहीं",
|
||||
"role.noMatchingRoles": "कोई मिलती-जुलती भूमिका नहीं",
|
||||
"role.searchPlaceholder": "भूमिकाएं खोजें...",
|
||||
"role.updated": "भूमिका सफलतापूर्वक अपडेट की गई",
|
||||
"role.workspaceRoles.description": "भूमिकाएं बनाएं और परिभाषित करें कि प्रत्येक भूमिका इस कार्यक्षेत्र में क्या कर सकती है।",
|
||||
"role.workspaceRoles.title": "कार्यक्षेत्र भूमिकाएं"
|
||||
}
|
||||
51
web/i18n/id-ID/permission-keys.json
Normal file
51
web/i18n/id-ID/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "Kelola konfigurasi ekstensi API",
|
||||
"app.access_config": "Konfigurasikan izin akses aplikasi",
|
||||
"app.acl.access_config": "Konfigurasikan izin akses aplikasi",
|
||||
"app.acl.delete": "Hapus aplikasi",
|
||||
"app.acl.edit": "Edit dan orkestrasikan aplikasi",
|
||||
"app.acl.import_export_dsl": "Impor / ekspor DSL",
|
||||
"app.acl.monitor": "Pemantauan dan operasi",
|
||||
"app.acl.preview": "Pratinjau aplikasi",
|
||||
"app.acl.release_and_version": "Penerbitan aplikasi dan manajemen versi",
|
||||
"app.acl.test_and_run": "Uji dan gunakan aplikasi",
|
||||
"app.acl.view_layout": "Halaman orkestrasi hanya-baca",
|
||||
"app.create_and_management": "Buat aplikasi dan kelola aplikasi yang Anda buat",
|
||||
"app.tag.manage": "Kelola tag aplikasi",
|
||||
"app_library.access": "Akses Pustaka Aplikasi",
|
||||
"billing.manage": "Ubah paket langganan",
|
||||
"billing.subscription.manage": "Kelola penagihan dan langganan di portal penagihan",
|
||||
"billing.view": "Akses pengaturan Penagihan",
|
||||
"credential.create": "Tambahkan kredensial",
|
||||
"credential.manage": "Edit dan hapus kredensial",
|
||||
"credential.use": "Lihat dan gunakan kredensial",
|
||||
"customization.manage": "Kelola penyesuaian",
|
||||
"data_source.manage": "Kelola konfigurasi sumber data",
|
||||
"dataset.access_config": "Konfigurasikan izin akses basis pengetahuan",
|
||||
"dataset.acl.access_config": "Konfigurasikan izin akses basis pengetahuan",
|
||||
"dataset.acl.delete": "Hapus basis pengetahuan",
|
||||
"dataset.acl.delete_file": "Hapus berkas basis pengetahuan",
|
||||
"dataset.acl.document_download": "Unduh dokumen",
|
||||
"dataset.acl.edit": "Edit basis pengetahuan",
|
||||
"dataset.acl.import_export_dsl": "Impor / ekspor DSL knowledge pipeline",
|
||||
"dataset.acl.pipeline_release": "Penerbitan knowledge pipeline dan manajemen versi",
|
||||
"dataset.acl.pipeline_test": "Pengujian pipeline",
|
||||
"dataset.acl.preview": "Pratinjau basis pengetahuan",
|
||||
"dataset.acl.readonly": "Basis pengetahuan hanya-baca",
|
||||
"dataset.acl.retrieval_recall": "Pengambilan basis pengetahuan",
|
||||
"dataset.acl.use": "Tambahkan dokumen ke basis pengetahuan",
|
||||
"dataset.api_key.manage": "Kelola kunci API basis pengetahuan",
|
||||
"dataset.create_and_management": "Buat basis pengetahuan dan kelola basis pengetahuan yang Anda buat",
|
||||
"dataset.external.connect": "Hubungkan basis pengetahuan eksternal",
|
||||
"dataset.tag.manage": "Kelola tag basis pengetahuan",
|
||||
"mcp.manage": "Kelola MCP",
|
||||
"plugin.debug": "Debug plugin",
|
||||
"plugin.install": "Instal dan perbarui plugin",
|
||||
"plugin.manage": "Kelola plugin",
|
||||
"plugin.plugin_preferences": "Kelola preferensi plugin",
|
||||
"snippets.create_and_modify": "Buat dan ubah snippet",
|
||||
"snippets.management": "Kelola snippet",
|
||||
"tool.manage": "Kelola alat",
|
||||
"workspace.member.manage": "Kelola anggota",
|
||||
"workspace.role.manage": "Kelola izin peran dan aturan akses sumber daya"
|
||||
}
|
||||
105
web/i18n/id-ID/permission.json
Normal file
105
web/i18n/id-ID/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Tindakan",
|
||||
"accessRule.addMemberAria": "Tambahkan {{name}}",
|
||||
"accessRule.addMembersTitle": "Tambahkan anggota",
|
||||
"accessRule.allPermittedMembers": "Semua anggota dengan izin peran",
|
||||
"accessRule.allPermittedMembersDescription": "Anggota dengan izin peran yang cocok dapat mengakses sumber daya ini.",
|
||||
"accessRule.appDescription": "Kontrol siapa yang dapat mengakses aplikasi ini. Anggota tetap memerlukan izin peran untuk melihat atau mengoperasikannya.",
|
||||
"accessRule.appTitle": "Aturan Akses Aplikasi",
|
||||
"accessRule.changeOpenScopeDescription": "Mengubah cakupan akses akan mengatur ulang semua pengaturan izin individu untuk sumber daya ini. Anda perlu menambahkan kembali izin khusus anggota setelah beralih.",
|
||||
"accessRule.changeOpenScopeTitle": "Ubah cakupan akses sumber daya?",
|
||||
"accessRule.collapseSection": "Ciutkan {{title}}",
|
||||
"accessRule.copied": "Aturan akses berhasil disalin",
|
||||
"accessRule.created": "Aturan akses berhasil dibuat",
|
||||
"accessRule.datasetDescription": "Kontrol siapa yang dapat mengakses basis pengetahuan ini. Anggota tetap memerlukan izin peran untuk melihat atau mengoperasikannya.",
|
||||
"accessRule.datasetTitle": "Aturan Akses Basis Pengetahuan",
|
||||
"accessRule.defaultPermission": "Berdasarkan izin peran",
|
||||
"accessRule.deleteDescription": "Aturan akses ini akan dihapus secara permanen dan dihapus dari daftar otorisasi sumber daya.",
|
||||
"accessRule.deleteTitle": "Hapus \"{{name}}\"?",
|
||||
"accessRule.deleted": "Aturan akses berhasil dihapus",
|
||||
"accessRule.exceptionPermissionFor": "Izin pengecualian untuk {{name}}",
|
||||
"accessRule.expandSection": "Perluas {{title}}",
|
||||
"accessRule.individualPermissionSettings": "Pengaturan izin individu",
|
||||
"accessRule.individualPermissionSettingsTip": "Tetapkan pengecualian izin untuk kolaborator atau grup tertentu. Pengaturan ini menggantikan tingkat akses default.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} terkunci",
|
||||
"accessRule.lockedSummary_other": "· {{count}} terkunci",
|
||||
"accessRule.maintainer": "Pengelola",
|
||||
"accessRule.member": "Anggota",
|
||||
"accessRule.newPermissionSet": "Set izin baru",
|
||||
"accessRule.noAvailableMembers": "Tidak ada anggota yang tersedia untuk ditambahkan",
|
||||
"accessRule.noDescription": "Tidak ada deskripsi",
|
||||
"accessRule.noRoles": "Tidak ada peran",
|
||||
"accessRule.noRules": "Tidak ada aturan akses",
|
||||
"accessRule.noUserAccessSettings": "Tidak ada pengaturan izin individu",
|
||||
"accessRule.permission": "Izin",
|
||||
"accessRule.resourceOpenScope": "Cakupan akses sumber daya",
|
||||
"accessRule.resourceOpenScopeDescription": "Pilih siapa yang dapat mengakses sumber daya ini. Izin peran tetap menentukan apa yang dapat dilakukan setiap anggota.",
|
||||
"accessRule.specificMembersOnly": "Hanya anggota tertentu",
|
||||
"accessRule.specificMembersOnlyDescription": "Hanya anggota yang dipilih yang dapat mengakses sumber daya ini.",
|
||||
"accessRule.summary_one": "{{count}} set izin",
|
||||
"accessRule.summary_other": "{{count}} set izin",
|
||||
"accessRule.updated": "Aturan akses berhasil diperbarui",
|
||||
"common.duplicateAction": "Duplikat",
|
||||
"group.app": "Aplikasi",
|
||||
"group.app_acl": "Izin akses aplikasi",
|
||||
"group.billing": "Penagihan",
|
||||
"group.credential": "Kredensial",
|
||||
"group.dataset": "Basis pengetahuan",
|
||||
"group.dataset_acl": "Izin akses basis pengetahuan",
|
||||
"group.integration": "Integrasi",
|
||||
"group.plugin": "Plugin",
|
||||
"group.tool_mcp": "Alat dan MCP",
|
||||
"group.workspace": "Ruang kerja",
|
||||
"permissionList.clearAll": "Hapus semua",
|
||||
"permissionList.collapseGroup": "Ciutkan grup",
|
||||
"permissionList.expandGroup": "Perluas grup",
|
||||
"permissionList.noPermissionsFound": "Tidak ada izin yang ditemukan",
|
||||
"permissionList.selectAll": "Pilih semua",
|
||||
"permissionSet.descriptionLabel": "Deskripsi",
|
||||
"permissionSet.descriptionPlaceholder": "Jelaskan apa yang diberikan oleh set izin ini",
|
||||
"permissionSet.learnMore": "Pelajari lebih lanjut tentang izin",
|
||||
"permissionSet.modal.create.app.description": "Buat set izin aplikasi yang dapat dirujuk dalam aturan akses untuk otorisasi cepat.",
|
||||
"permissionSet.modal.create.app.title": "Buat set izin Aplikasi",
|
||||
"permissionSet.modal.create.dataset.description": "Buat set izin basis pengetahuan yang dapat dirujuk dalam aturan akses untuk otorisasi cepat.",
|
||||
"permissionSet.modal.create.dataset.title": "Buat set izin Basis Pengetahuan",
|
||||
"permissionSet.modal.edit.app.description": "Ubah nama, deskripsi, dan izin yang diberikan untuk set izin ini.",
|
||||
"permissionSet.modal.edit.app.title": "Edit set izin Aplikasi",
|
||||
"permissionSet.modal.edit.dataset.description": "Ubah nama, deskripsi, dan izin yang diberikan untuk set izin ini.",
|
||||
"permissionSet.modal.edit.dataset.title": "Edit set izin Basis Pengetahuan",
|
||||
"permissionSet.modal.view.app.description": "Lihat nama, deskripsi, dan izin yang diberikan untuk set izin ini.",
|
||||
"permissionSet.modal.view.app.title": "Lihat set izin Aplikasi",
|
||||
"permissionSet.modal.view.dataset.description": "Lihat nama, deskripsi, dan izin yang diberikan untuk set izin ini.",
|
||||
"permissionSet.modal.view.dataset.title": "Lihat set izin Basis Pengetahuan",
|
||||
"permissionSet.nameLabel": "Nama set izin",
|
||||
"permissionSet.namePlaceholder": "mis. Dapat mengekspor DSL",
|
||||
"permissionSet.permissions": "Izin",
|
||||
"role.addRole": "Buat peran",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" ditetapkan ke {{count}} anggota. Apakah Anda ingin salinan peran baru menyertakan anggota yang sama?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" ditetapkan ke {{count}} anggota. Apakah Anda ingin salinan peran baru menyertakan anggota yang sama?",
|
||||
"role.copyMembersLoading": "Memuat penetapan anggota...",
|
||||
"role.copyMembersTitle": "Salin penetapan anggota?",
|
||||
"role.created": "Peran berhasil dibuat",
|
||||
"role.deleteDescription": "Peran ini akan dihapus secara permanen dan dihapus dari anggota atau aturan akses mana pun yang menggunakannya.",
|
||||
"role.deleteTitle": "Hapus \"{{name}}\"?",
|
||||
"role.deleted": "Peran berhasil dihapus",
|
||||
"role.duplicated": "Peran berhasil diduplikasi",
|
||||
"role.groups.builtin": "Peran Sistem",
|
||||
"role.groups.custom": "Peran Kustom",
|
||||
"role.loading": "Memuat peran...",
|
||||
"role.modal.create.description": "Buat peran dan tetapkan izin",
|
||||
"role.modal.create.title": "Buat Peran",
|
||||
"role.modal.descriptionLabel": "Deskripsi",
|
||||
"role.modal.descriptionPlaceholder": "Jelaskan tanggung jawab peran ini",
|
||||
"role.modal.edit.description": "Edit detail dan izin peran",
|
||||
"role.modal.edit.title": "Edit Peran",
|
||||
"role.modal.nameLabel": "Nama peran",
|
||||
"role.modal.namePlaceholder": "mis. Pemimpin Pemasaran",
|
||||
"role.modal.view.description": "Lihat detail dan izin peran",
|
||||
"role.modal.view.title": "Lihat Peran",
|
||||
"role.noDescription": "Tidak ada deskripsi",
|
||||
"role.noMatchingRoles": "Tidak ada peran yang cocok",
|
||||
"role.searchPlaceholder": "Cari peran...",
|
||||
"role.updated": "Peran berhasil diperbarui",
|
||||
"role.workspaceRoles.description": "Buat peran dan tentukan apa yang dapat dilakukan setiap peran di ruang kerja ini.",
|
||||
"role.workspaceRoles.title": "Peran Ruang Kerja"
|
||||
}
|
||||
51
web/i18n/it-IT/permission-keys.json
Normal file
51
web/i18n/it-IT/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "Gestisci la configurazione delle estensioni API",
|
||||
"app.access_config": "Configura i permessi di accesso all'app",
|
||||
"app.acl.access_config": "Configura i permessi di accesso all'app",
|
||||
"app.acl.delete": "Elimina app",
|
||||
"app.acl.edit": "Modifica e orchestra l'app",
|
||||
"app.acl.import_export_dsl": "Importa / esporta DSL",
|
||||
"app.acl.monitor": "Monitoraggio e operazioni",
|
||||
"app.acl.preview": "Anteprima app",
|
||||
"app.acl.release_and_version": "Pubblicazione dell'app e gestione delle versioni",
|
||||
"app.acl.test_and_run": "Testa e usa l'app",
|
||||
"app.acl.view_layout": "Pagina di orchestrazione in sola lettura",
|
||||
"app.create_and_management": "Crea app e gestisci le app che hai creato",
|
||||
"app.tag.manage": "Gestisci i tag delle app",
|
||||
"app_library.access": "Accedi alla Libreria delle app",
|
||||
"billing.manage": "Cambia i piani di abbonamento",
|
||||
"billing.subscription.manage": "Gestisci fatturazione e abbonamenti nel portale di fatturazione",
|
||||
"billing.view": "Accedi alle impostazioni di fatturazione",
|
||||
"credential.create": "Aggiungi credenziali",
|
||||
"credential.manage": "Modifica ed elimina credenziali",
|
||||
"credential.use": "Visualizza e usa credenziali",
|
||||
"customization.manage": "Gestisci personalizzazione",
|
||||
"data_source.manage": "Gestisci la configurazione delle origini dati",
|
||||
"dataset.access_config": "Configura i permessi di accesso alla knowledge base",
|
||||
"dataset.acl.access_config": "Configura i permessi di accesso alla knowledge base",
|
||||
"dataset.acl.delete": "Elimina knowledge base",
|
||||
"dataset.acl.delete_file": "Elimina i file della knowledge base",
|
||||
"dataset.acl.document_download": "Scarica documenti",
|
||||
"dataset.acl.edit": "Modifica knowledge base",
|
||||
"dataset.acl.import_export_dsl": "Importa / esporta DSL della knowledge pipeline",
|
||||
"dataset.acl.pipeline_release": "Pubblicazione della knowledge pipeline e gestione delle versioni",
|
||||
"dataset.acl.pipeline_test": "Test della pipeline",
|
||||
"dataset.acl.preview": "Anteprima knowledge base",
|
||||
"dataset.acl.readonly": "Knowledge base in sola lettura",
|
||||
"dataset.acl.retrieval_recall": "Recupero dalla knowledge base",
|
||||
"dataset.acl.use": "Aggiungi documenti alla knowledge base",
|
||||
"dataset.api_key.manage": "Gestisci le chiavi API della knowledge base",
|
||||
"dataset.create_and_management": "Crea knowledge base e gestisci le knowledge base che hai creato",
|
||||
"dataset.external.connect": "Connetti knowledge base esterne",
|
||||
"dataset.tag.manage": "Gestisci i tag della knowledge base",
|
||||
"mcp.manage": "Gestisci MCP",
|
||||
"plugin.debug": "Esegui il debug dei plugin",
|
||||
"plugin.install": "Installa e aggiorna plugin",
|
||||
"plugin.manage": "Gestisci plugin",
|
||||
"plugin.plugin_preferences": "Gestisci le preferenze dei plugin",
|
||||
"snippets.create_and_modify": "Crea e modifica snippet",
|
||||
"snippets.management": "Gestisci snippet",
|
||||
"tool.manage": "Gestisci strumenti",
|
||||
"workspace.member.manage": "Gestisci i membri",
|
||||
"workspace.role.manage": "Gestisci i permessi dei ruoli e le regole di accesso alle risorse"
|
||||
}
|
||||
105
web/i18n/it-IT/permission.json
Normal file
105
web/i18n/it-IT/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Azioni",
|
||||
"accessRule.addMemberAria": "Aggiungi {{name}}",
|
||||
"accessRule.addMembersTitle": "Aggiungi membri",
|
||||
"accessRule.allPermittedMembers": "Tutti i membri con permessi di ruolo",
|
||||
"accessRule.allPermittedMembersDescription": "I membri con permessi di ruolo corrispondenti possono accedere a questa risorsa.",
|
||||
"accessRule.appDescription": "Controlla a chi è aperta questa app. I membri necessitano comunque dei permessi di ruolo per visualizzarla o utilizzarla.",
|
||||
"accessRule.appTitle": "Regole di accesso all'app",
|
||||
"accessRule.changeOpenScopeDescription": "La modifica dell'ambito di apertura reimposterà tutte le impostazioni dei permessi individuali per questa risorsa. Dovrai aggiungere nuovamente i permessi specifici dei membri dopo il cambio.",
|
||||
"accessRule.changeOpenScopeTitle": "Modificare l'ambito di apertura della risorsa?",
|
||||
"accessRule.collapseSection": "Comprimi {{title}}",
|
||||
"accessRule.copied": "Regola di accesso copiata con successo",
|
||||
"accessRule.created": "Regola di accesso creata con successo",
|
||||
"accessRule.datasetDescription": "Controlla a chi è aperta questa knowledge base. I membri necessitano comunque dei permessi di ruolo per visualizzarla o utilizzarla.",
|
||||
"accessRule.datasetTitle": "Regole di accesso alla knowledge base",
|
||||
"accessRule.defaultPermission": "In base ai permessi di ruolo",
|
||||
"accessRule.deleteDescription": "Questa regola di accesso verrà eliminata definitivamente e rimossa dall'elenco delle autorizzazioni della risorsa.",
|
||||
"accessRule.deleteTitle": "Eliminare \"{{name}}\"?",
|
||||
"accessRule.deleted": "Regola di accesso eliminata con successo",
|
||||
"accessRule.exceptionPermissionFor": "Permesso di eccezione per {{name}}",
|
||||
"accessRule.expandSection": "Espandi {{title}}",
|
||||
"accessRule.individualPermissionSettings": "Impostazioni dei permessi individuali",
|
||||
"accessRule.individualPermissionSettingsTip": "Imposta eccezioni ai permessi per collaboratori o gruppi specifici. Queste impostazioni hanno la precedenza sul livello di accesso predefinito.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} bloccato",
|
||||
"accessRule.lockedSummary_other": "· {{count}} bloccati",
|
||||
"accessRule.maintainer": "Manutentore",
|
||||
"accessRule.member": "Membro",
|
||||
"accessRule.newPermissionSet": "Nuovo set di permessi",
|
||||
"accessRule.noAvailableMembers": "Nessun membro disponibile da aggiungere",
|
||||
"accessRule.noDescription": "Nessuna descrizione",
|
||||
"accessRule.noRoles": "Nessun ruolo",
|
||||
"accessRule.noRules": "Nessuna regola di accesso",
|
||||
"accessRule.noUserAccessSettings": "Nessuna impostazione dei permessi individuali",
|
||||
"accessRule.permission": "Permesso",
|
||||
"accessRule.resourceOpenScope": "Ambito di apertura della risorsa",
|
||||
"accessRule.resourceOpenScopeDescription": "Scegli a chi è aperta questa risorsa. I permessi di ruolo determinano comunque cosa può fare ciascun membro.",
|
||||
"accessRule.specificMembersOnly": "Solo membri specifici",
|
||||
"accessRule.specificMembersOnlyDescription": "Solo i membri selezionati possono accedere a questa risorsa.",
|
||||
"accessRule.summary_one": "{{count}} set di permessi",
|
||||
"accessRule.summary_other": "{{count}} set di permessi",
|
||||
"accessRule.updated": "Regola di accesso aggiornata con successo",
|
||||
"common.duplicateAction": "Duplica",
|
||||
"group.app": "Applicazioni",
|
||||
"group.app_acl": "Permessi di accesso all'app",
|
||||
"group.billing": "Fatturazione",
|
||||
"group.credential": "Credenziali",
|
||||
"group.dataset": "Knowledge base",
|
||||
"group.dataset_acl": "Permessi di accesso alla knowledge base",
|
||||
"group.integration": "Integrazioni",
|
||||
"group.plugin": "Plugin",
|
||||
"group.tool_mcp": "Strumenti e MCP",
|
||||
"group.workspace": "Workspace",
|
||||
"permissionList.clearAll": "Cancella tutto",
|
||||
"permissionList.collapseGroup": "Comprimi gruppo",
|
||||
"permissionList.expandGroup": "Espandi gruppo",
|
||||
"permissionList.noPermissionsFound": "Nessun permesso trovato",
|
||||
"permissionList.selectAll": "Seleziona tutto",
|
||||
"permissionSet.descriptionLabel": "Descrizione",
|
||||
"permissionSet.descriptionPlaceholder": "Descrivi cosa concede questo set di permessi",
|
||||
"permissionSet.learnMore": "Scopri di più sui permessi",
|
||||
"permissionSet.modal.create.app.description": "Crea un set di permessi per app che può essere referenziato nelle regole di accesso per un'autorizzazione rapida.",
|
||||
"permissionSet.modal.create.app.title": "Crea set di permessi per app",
|
||||
"permissionSet.modal.create.dataset.description": "Crea un set di permessi per knowledge base che può essere referenziato nelle regole di accesso per un'autorizzazione rapida.",
|
||||
"permissionSet.modal.create.dataset.title": "Crea set di permessi per knowledge base",
|
||||
"permissionSet.modal.edit.app.description": "Modifica il nome, la descrizione e i permessi concessi per questo set di permessi.",
|
||||
"permissionSet.modal.edit.app.title": "Modifica set di permessi per app",
|
||||
"permissionSet.modal.edit.dataset.description": "Modifica il nome, la descrizione e i permessi concessi per questo set di permessi.",
|
||||
"permissionSet.modal.edit.dataset.title": "Modifica set di permessi per knowledge base",
|
||||
"permissionSet.modal.view.app.description": "Visualizza il nome, la descrizione e i permessi concessi per questo set di permessi.",
|
||||
"permissionSet.modal.view.app.title": "Visualizza set di permessi per app",
|
||||
"permissionSet.modal.view.dataset.description": "Visualizza il nome, la descrizione e i permessi concessi per questo set di permessi.",
|
||||
"permissionSet.modal.view.dataset.title": "Visualizza set di permessi per knowledge base",
|
||||
"permissionSet.nameLabel": "Nome del set di permessi",
|
||||
"permissionSet.namePlaceholder": "es. Può esportare DSL",
|
||||
"permissionSet.permissions": "Permessi",
|
||||
"role.addRole": "Crea ruoli",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" è assegnato a {{count}} membro. Vuoi che la copia del nuovo ruolo includa lo stesso membro?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" è assegnato a {{count}} membri. Vuoi che la copia del nuovo ruolo includa gli stessi membri?",
|
||||
"role.copyMembersLoading": "Caricamento delle assegnazioni dei membri...",
|
||||
"role.copyMembersTitle": "Copiare le assegnazioni dei membri?",
|
||||
"role.created": "Ruolo creato con successo",
|
||||
"role.deleteDescription": "Questo ruolo verrà eliminato definitivamente e rimosso da tutti i membri o le regole di accesso che lo utilizzano.",
|
||||
"role.deleteTitle": "Eliminare \"{{name}}\"?",
|
||||
"role.deleted": "Ruolo eliminato con successo",
|
||||
"role.duplicated": "Ruolo duplicato con successo",
|
||||
"role.groups.builtin": "Ruoli di sistema",
|
||||
"role.groups.custom": "Ruoli personalizzati",
|
||||
"role.loading": "Caricamento dei ruoli...",
|
||||
"role.modal.create.description": "Crea un ruolo e assegna i permessi",
|
||||
"role.modal.create.title": "Crea ruolo",
|
||||
"role.modal.descriptionLabel": "Descrizione",
|
||||
"role.modal.descriptionPlaceholder": "Descrivi di cosa è responsabile questo ruolo",
|
||||
"role.modal.edit.description": "Modifica i dettagli e i permessi del ruolo",
|
||||
"role.modal.edit.title": "Modifica ruolo",
|
||||
"role.modal.nameLabel": "Nome del ruolo",
|
||||
"role.modal.namePlaceholder": "es. Responsabile Marketing",
|
||||
"role.modal.view.description": "Visualizza i dettagli e i permessi del ruolo",
|
||||
"role.modal.view.title": "Visualizza ruolo",
|
||||
"role.noDescription": "Nessuna descrizione",
|
||||
"role.noMatchingRoles": "Nessun ruolo corrispondente",
|
||||
"role.searchPlaceholder": "Cerca ruoli...",
|
||||
"role.updated": "Ruolo aggiornato con successo",
|
||||
"role.workspaceRoles.description": "Crea ruoli e definisci cosa può fare ciascun ruolo in questo workspace.",
|
||||
"role.workspaceRoles.title": "Ruoli del workspace"
|
||||
}
|
||||
51
web/i18n/ko-KR/permission-keys.json
Normal file
51
web/i18n/ko-KR/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "API 확장 구성 관리",
|
||||
"app.access_config": "앱 접근 권한 구성",
|
||||
"app.acl.access_config": "앱 접근 권한 구성",
|
||||
"app.acl.delete": "앱 삭제",
|
||||
"app.acl.edit": "앱 편집 및 오케스트레이션",
|
||||
"app.acl.import_export_dsl": "DSL 가져오기 / 내보내기",
|
||||
"app.acl.monitor": "모니터링 및 운영",
|
||||
"app.acl.preview": "앱 미리보기",
|
||||
"app.acl.release_and_version": "앱 게시 및 버전 관리",
|
||||
"app.acl.test_and_run": "앱 테스트 및 사용",
|
||||
"app.acl.view_layout": "읽기 전용 오케스트레이션 페이지",
|
||||
"app.create_and_management": "앱 생성 및 직접 만든 앱 관리",
|
||||
"app.tag.manage": "앱 태그 관리",
|
||||
"app_library.access": "앱 라이브러리 접근",
|
||||
"billing.manage": "구독 플랜 변경",
|
||||
"billing.subscription.manage": "결제 포털에서 결제 및 구독 관리",
|
||||
"billing.view": "결제 설정 접근",
|
||||
"credential.create": "자격 증명 추가",
|
||||
"credential.manage": "자격 증명 편집 및 삭제",
|
||||
"credential.use": "자격 증명 보기 및 사용",
|
||||
"customization.manage": "사용자 정의 관리",
|
||||
"data_source.manage": "데이터 소스 구성 관리",
|
||||
"dataset.access_config": "지식 베이스 접근 권한 구성",
|
||||
"dataset.acl.access_config": "지식 베이스 접근 권한 구성",
|
||||
"dataset.acl.delete": "지식 베이스 삭제",
|
||||
"dataset.acl.delete_file": "지식 베이스 파일 삭제",
|
||||
"dataset.acl.document_download": "문서 다운로드",
|
||||
"dataset.acl.edit": "지식 베이스 편집",
|
||||
"dataset.acl.import_export_dsl": "지식 파이프라인 DSL 가져오기 / 내보내기",
|
||||
"dataset.acl.pipeline_release": "지식 파이프라인 게시 및 버전 관리",
|
||||
"dataset.acl.pipeline_test": "파이프라인 테스트",
|
||||
"dataset.acl.preview": "지식 베이스 미리보기",
|
||||
"dataset.acl.readonly": "읽기 전용 지식 베이스",
|
||||
"dataset.acl.retrieval_recall": "지식 베이스 검색",
|
||||
"dataset.acl.use": "지식 베이스에 문서 추가",
|
||||
"dataset.api_key.manage": "지식 베이스 API 키 관리",
|
||||
"dataset.create_and_management": "지식 베이스 생성 및 직접 만든 지식 베이스 관리",
|
||||
"dataset.external.connect": "외부 지식 베이스 연결",
|
||||
"dataset.tag.manage": "지식 베이스 태그 관리",
|
||||
"mcp.manage": "MCP 관리",
|
||||
"plugin.debug": "플러그인 디버그",
|
||||
"plugin.install": "플러그인 설치 및 업데이트",
|
||||
"plugin.manage": "플러그인 관리",
|
||||
"plugin.plugin_preferences": "플러그인 환경설정 관리",
|
||||
"snippets.create_and_modify": "스니펫 생성 및 수정",
|
||||
"snippets.management": "스니펫 관리",
|
||||
"tool.manage": "도구 관리",
|
||||
"workspace.member.manage": "멤버 관리",
|
||||
"workspace.role.manage": "역할 권한 및 리소스 접근 규칙 관리"
|
||||
}
|
||||
105
web/i18n/ko-KR/permission.json
Normal file
105
web/i18n/ko-KR/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "작업",
|
||||
"accessRule.addMemberAria": "{{name}} 추가",
|
||||
"accessRule.addMembersTitle": "멤버 추가",
|
||||
"accessRule.allPermittedMembers": "역할 권한이 있는 모든 멤버",
|
||||
"accessRule.allPermittedMembersDescription": "일치하는 역할 권한이 있는 멤버가 이 리소스에 접근할 수 있습니다.",
|
||||
"accessRule.appDescription": "이 앱을 누구에게 공개할지 제어합니다. 멤버가 앱을 보거나 조작하려면 여전히 역할 권한이 필요합니다.",
|
||||
"accessRule.appTitle": "앱 접근 규칙",
|
||||
"accessRule.changeOpenScopeDescription": "공개 범위를 변경하면 이 리소스의 모든 개별 권한 설정이 초기화됩니다. 전환 후 멤버별 권한을 다시 추가해야 합니다.",
|
||||
"accessRule.changeOpenScopeTitle": "리소스 공개 범위를 변경하시겠습니까?",
|
||||
"accessRule.collapseSection": "{{title}} 접기",
|
||||
"accessRule.copied": "접근 규칙이 성공적으로 복사되었습니다",
|
||||
"accessRule.created": "접근 규칙이 성공적으로 생성되었습니다",
|
||||
"accessRule.datasetDescription": "이 지식 베이스를 누구에게 공개할지 제어합니다. 멤버가 지식 베이스를 보거나 조작하려면 여전히 역할 권한이 필요합니다.",
|
||||
"accessRule.datasetTitle": "지식 베이스 접근 규칙",
|
||||
"accessRule.defaultPermission": "역할 권한 기준",
|
||||
"accessRule.deleteDescription": "이 접근 규칙은 영구적으로 삭제되며 리소스 권한 부여 목록에서 제거됩니다.",
|
||||
"accessRule.deleteTitle": "\"{{name}}\"을(를) 삭제하시겠습니까?",
|
||||
"accessRule.deleted": "접근 규칙이 성공적으로 삭제되었습니다",
|
||||
"accessRule.exceptionPermissionFor": "{{name}}에 대한 예외 권한",
|
||||
"accessRule.expandSection": "{{title}} 펼치기",
|
||||
"accessRule.individualPermissionSettings": "개별 권한 설정",
|
||||
"accessRule.individualPermissionSettingsTip": "특정 협업자 또는 그룹에 대한 권한 예외를 설정합니다. 이 설정은 기본 접근 수준을 재정의합니다.",
|
||||
"accessRule.lockedSummary_one": "· {{count}}개 잠김",
|
||||
"accessRule.lockedSummary_other": "· {{count}}개 잠김",
|
||||
"accessRule.maintainer": "관리자",
|
||||
"accessRule.member": "멤버",
|
||||
"accessRule.newPermissionSet": "새 권한 집합",
|
||||
"accessRule.noAvailableMembers": "추가할 수 있는 멤버가 없습니다",
|
||||
"accessRule.noDescription": "설명 없음",
|
||||
"accessRule.noRoles": "역할 없음",
|
||||
"accessRule.noRules": "접근 규칙 없음",
|
||||
"accessRule.noUserAccessSettings": "개별 권한 설정 없음",
|
||||
"accessRule.permission": "권한",
|
||||
"accessRule.resourceOpenScope": "리소스 공개 범위",
|
||||
"accessRule.resourceOpenScopeDescription": "이 리소스를 누구에게 공개할지 선택합니다. 각 멤버가 할 수 있는 작업은 여전히 역할 권한이 결정합니다.",
|
||||
"accessRule.specificMembersOnly": "특정 멤버만",
|
||||
"accessRule.specificMembersOnlyDescription": "선택한 멤버만 이 리소스에 접근할 수 있습니다.",
|
||||
"accessRule.summary_one": "{{count}}개 권한 집합",
|
||||
"accessRule.summary_other": "{{count}}개 권한 집합",
|
||||
"accessRule.updated": "접근 규칙이 성공적으로 업데이트되었습니다",
|
||||
"common.duplicateAction": "복제",
|
||||
"group.app": "애플리케이션",
|
||||
"group.app_acl": "앱 접근 권한",
|
||||
"group.billing": "결제",
|
||||
"group.credential": "자격 증명",
|
||||
"group.dataset": "지식 베이스",
|
||||
"group.dataset_acl": "지식 베이스 접근 권한",
|
||||
"group.integration": "통합",
|
||||
"group.plugin": "플러그인",
|
||||
"group.tool_mcp": "도구 및 MCP",
|
||||
"group.workspace": "작업 공간",
|
||||
"permissionList.clearAll": "모두 지우기",
|
||||
"permissionList.collapseGroup": "그룹 접기",
|
||||
"permissionList.expandGroup": "그룹 펼치기",
|
||||
"permissionList.noPermissionsFound": "권한을 찾을 수 없습니다",
|
||||
"permissionList.selectAll": "모두 선택",
|
||||
"permissionSet.descriptionLabel": "설명",
|
||||
"permissionSet.descriptionPlaceholder": "이 권한 집합이 부여하는 내용을 설명하세요",
|
||||
"permissionSet.learnMore": "권한에 대해 자세히 알아보기",
|
||||
"permissionSet.modal.create.app.description": "빠른 권한 부여를 위해 접근 규칙에서 참조할 수 있는 앱 권한 집합을 생성합니다.",
|
||||
"permissionSet.modal.create.app.title": "앱 권한 집합 생성",
|
||||
"permissionSet.modal.create.dataset.description": "빠른 권한 부여를 위해 접근 규칙에서 참조할 수 있는 지식 베이스 권한 집합을 생성합니다.",
|
||||
"permissionSet.modal.create.dataset.title": "지식 베이스 권한 집합 생성",
|
||||
"permissionSet.modal.edit.app.description": "이 권한 집합의 이름, 설명 및 부여된 권한을 수정합니다.",
|
||||
"permissionSet.modal.edit.app.title": "앱 권한 집합 편집",
|
||||
"permissionSet.modal.edit.dataset.description": "이 권한 집합의 이름, 설명 및 부여된 권한을 수정합니다.",
|
||||
"permissionSet.modal.edit.dataset.title": "지식 베이스 권한 집합 편집",
|
||||
"permissionSet.modal.view.app.description": "이 권한 집합의 이름, 설명 및 부여된 권한을 봅니다.",
|
||||
"permissionSet.modal.view.app.title": "앱 권한 집합 보기",
|
||||
"permissionSet.modal.view.dataset.description": "이 권한 집합의 이름, 설명 및 부여된 권한을 봅니다.",
|
||||
"permissionSet.modal.view.dataset.title": "지식 베이스 권한 집합 보기",
|
||||
"permissionSet.nameLabel": "권한 집합 이름",
|
||||
"permissionSet.namePlaceholder": "예: DSL 내보내기 가능",
|
||||
"permissionSet.permissions": "권한",
|
||||
"role.addRole": "역할 생성",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\"이(가) {{count}}명의 멤버에게 할당되어 있습니다. 새 역할 복사본에 동일한 멤버를 포함하시겠습니까?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\"이(가) {{count}}명의 멤버에게 할당되어 있습니다. 새 역할 복사본에 동일한 멤버를 포함하시겠습니까?",
|
||||
"role.copyMembersLoading": "멤버 할당을 불러오는 중...",
|
||||
"role.copyMembersTitle": "멤버 할당을 복사하시겠습니까?",
|
||||
"role.created": "역할이 성공적으로 생성되었습니다",
|
||||
"role.deleteDescription": "이 역할은 영구적으로 삭제되며 이를 사용하는 모든 멤버 또는 접근 규칙에서 제거됩니다.",
|
||||
"role.deleteTitle": "\"{{name}}\"을(를) 삭제하시겠습니까?",
|
||||
"role.deleted": "역할이 성공적으로 삭제되었습니다",
|
||||
"role.duplicated": "역할이 성공적으로 복제되었습니다",
|
||||
"role.groups.builtin": "시스템 역할",
|
||||
"role.groups.custom": "사용자 정의 역할",
|
||||
"role.loading": "역할을 불러오는 중...",
|
||||
"role.modal.create.description": "역할을 생성하고 권한을 할당합니다",
|
||||
"role.modal.create.title": "역할 생성",
|
||||
"role.modal.descriptionLabel": "설명",
|
||||
"role.modal.descriptionPlaceholder": "이 역할이 담당하는 내용을 설명하세요",
|
||||
"role.modal.edit.description": "역할 세부 정보 및 권한 편집",
|
||||
"role.modal.edit.title": "역할 편집",
|
||||
"role.modal.nameLabel": "역할 이름",
|
||||
"role.modal.namePlaceholder": "예: 마케팅 리드",
|
||||
"role.modal.view.description": "역할 세부 정보 및 권한 보기",
|
||||
"role.modal.view.title": "역할 보기",
|
||||
"role.noDescription": "설명 없음",
|
||||
"role.noMatchingRoles": "일치하는 역할 없음",
|
||||
"role.searchPlaceholder": "역할 검색...",
|
||||
"role.updated": "역할이 성공적으로 업데이트되었습니다",
|
||||
"role.workspaceRoles.description": "역할을 생성하고 각 역할이 이 작업 공간에서 할 수 있는 작업을 정의합니다.",
|
||||
"role.workspaceRoles.title": "작업 공간 역할"
|
||||
}
|
||||
51
web/i18n/nl-NL/permission-keys.json
Normal file
51
web/i18n/nl-NL/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "API-extensieconfiguratie beheren",
|
||||
"app.access_config": "Toegangsrechten voor app configureren",
|
||||
"app.acl.access_config": "Toegangsrechten voor app configureren",
|
||||
"app.acl.delete": "App verwijderen",
|
||||
"app.acl.edit": "App bewerken en orkestreren",
|
||||
"app.acl.import_export_dsl": "DSL importeren / exporteren",
|
||||
"app.acl.monitor": "Monitoring en bewerkingen",
|
||||
"app.acl.preview": "App voorbeeld bekijken",
|
||||
"app.acl.release_and_version": "App publiceren en versiebeheer",
|
||||
"app.acl.test_and_run": "App testen en gebruiken",
|
||||
"app.acl.view_layout": "Alleen-lezen orkestratiepagina",
|
||||
"app.create_and_management": "Apps maken en door jou gemaakte apps beheren",
|
||||
"app.tag.manage": "App-tags beheren",
|
||||
"app_library.access": "Toegang tot App-bibliotheek",
|
||||
"billing.manage": "Abonnementen wijzigen",
|
||||
"billing.subscription.manage": "Facturering en abonnementen beheren in het factureringsportaal",
|
||||
"billing.view": "Toegang tot factureringsinstellingen",
|
||||
"credential.create": "Inloggegevens toevoegen",
|
||||
"credential.manage": "Inloggegevens bewerken en verwijderen",
|
||||
"credential.use": "Inloggegevens bekijken en gebruiken",
|
||||
"customization.manage": "Aanpassingen beheren",
|
||||
"data_source.manage": "Configuratie van gegevensbron beheren",
|
||||
"dataset.access_config": "Toegangsrechten voor kennisbank configureren",
|
||||
"dataset.acl.access_config": "Toegangsrechten voor kennisbank configureren",
|
||||
"dataset.acl.delete": "Kennisbank verwijderen",
|
||||
"dataset.acl.delete_file": "Kennisbankbestanden verwijderen",
|
||||
"dataset.acl.document_download": "Documenten downloaden",
|
||||
"dataset.acl.edit": "Kennisbank bewerken",
|
||||
"dataset.acl.import_export_dsl": "DSL van kennispijplijn importeren / exporteren",
|
||||
"dataset.acl.pipeline_release": "Kennispijplijn publiceren en versiebeheer",
|
||||
"dataset.acl.pipeline_test": "Pijplijn testen",
|
||||
"dataset.acl.preview": "Kennisbank voorbeeld bekijken",
|
||||
"dataset.acl.readonly": "Alleen-lezen kennisbank",
|
||||
"dataset.acl.retrieval_recall": "Kennisbank ophalen",
|
||||
"dataset.acl.use": "Documenten toevoegen aan kennisbank",
|
||||
"dataset.api_key.manage": "API-sleutels van kennisbank beheren",
|
||||
"dataset.create_and_management": "Kennisbanken maken en door jou gemaakte kennisbanken beheren",
|
||||
"dataset.external.connect": "Externe kennisbanken verbinden",
|
||||
"dataset.tag.manage": "Kennisbank-tags beheren",
|
||||
"mcp.manage": "MCP beheren",
|
||||
"plugin.debug": "Plug-ins debuggen",
|
||||
"plugin.install": "Plug-ins installeren en bijwerken",
|
||||
"plugin.manage": "Plug-ins beheren",
|
||||
"plugin.plugin_preferences": "Plug-invoorkeuren beheren",
|
||||
"snippets.create_and_modify": "Snippets maken en wijzigen",
|
||||
"snippets.management": "Snippets beheren",
|
||||
"tool.manage": "Tools beheren",
|
||||
"workspace.member.manage": "Leden beheren",
|
||||
"workspace.role.manage": "Rolrechten en regels voor resourcetoegang beheren"
|
||||
}
|
||||
105
web/i18n/nl-NL/permission.json
Normal file
105
web/i18n/nl-NL/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Acties",
|
||||
"accessRule.addMemberAria": "{{name}} toevoegen",
|
||||
"accessRule.addMembersTitle": "Leden toevoegen",
|
||||
"accessRule.allPermittedMembers": "Alle leden met rolrechten",
|
||||
"accessRule.allPermittedMembersDescription": "Leden met overeenkomende rolrechten hebben toegang tot deze resource.",
|
||||
"accessRule.appDescription": "Bepaal voor wie deze app toegankelijk is. Leden hebben nog steeds rolrechten nodig om deze te bekijken of te bedienen.",
|
||||
"accessRule.appTitle": "Toegangsregels voor app",
|
||||
"accessRule.changeOpenScopeDescription": "Het wijzigen van het toegangsbereik stelt alle individuele rechteninstellingen voor deze resource opnieuw in. Je moet ledenspecifieke rechten opnieuw toevoegen na het wisselen.",
|
||||
"accessRule.changeOpenScopeTitle": "Toegangsbereik van resource wijzigen?",
|
||||
"accessRule.collapseSection": "{{title}} samenvouwen",
|
||||
"accessRule.copied": "Toegangsregel succesvol gekopieerd",
|
||||
"accessRule.created": "Toegangsregel succesvol gemaakt",
|
||||
"accessRule.datasetDescription": "Bepaal voor wie deze kennisbank toegankelijk is. Leden hebben nog steeds rolrechten nodig om deze te bekijken of te bedienen.",
|
||||
"accessRule.datasetTitle": "Toegangsregels voor kennisbank",
|
||||
"accessRule.defaultPermission": "Op basis van rolrechten",
|
||||
"accessRule.deleteDescription": "Deze toegangsregel wordt permanent verwijderd en uit de autorisatielijst van de resource gehaald.",
|
||||
"accessRule.deleteTitle": "\"{{name}}\" verwijderen?",
|
||||
"accessRule.deleted": "Toegangsregel succesvol verwijderd",
|
||||
"accessRule.exceptionPermissionFor": "Uitzonderingsrecht voor {{name}}",
|
||||
"accessRule.expandSection": "{{title}} uitvouwen",
|
||||
"accessRule.individualPermissionSettings": "Individuele rechteninstellingen",
|
||||
"accessRule.individualPermissionSettingsTip": "Stel rechtenuitzonderingen in voor specifieke samenwerkers of groepen. Deze instellingen overschrijven het standaard toegangsniveau.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} vergrendeld",
|
||||
"accessRule.lockedSummary_other": "· {{count}} vergrendeld",
|
||||
"accessRule.maintainer": "Beheerder",
|
||||
"accessRule.member": "Lid",
|
||||
"accessRule.newPermissionSet": "Nieuwe rechtenset",
|
||||
"accessRule.noAvailableMembers": "Geen leden beschikbaar om toe te voegen",
|
||||
"accessRule.noDescription": "Geen beschrijving",
|
||||
"accessRule.noRoles": "Geen rollen",
|
||||
"accessRule.noRules": "Geen toegangsregels",
|
||||
"accessRule.noUserAccessSettings": "Geen individuele rechteninstellingen",
|
||||
"accessRule.permission": "Recht",
|
||||
"accessRule.resourceOpenScope": "Toegangsbereik van resource",
|
||||
"accessRule.resourceOpenScopeDescription": "Kies voor wie deze resource toegankelijk is. Rolrechten bepalen nog steeds wat elk lid kan doen.",
|
||||
"accessRule.specificMembersOnly": "Alleen specifieke leden",
|
||||
"accessRule.specificMembersOnlyDescription": "Alleen geselecteerde leden hebben toegang tot deze resource.",
|
||||
"accessRule.summary_one": "{{count}} rechtenset",
|
||||
"accessRule.summary_other": "{{count}} rechtensets",
|
||||
"accessRule.updated": "Toegangsregel succesvol bijgewerkt",
|
||||
"common.duplicateAction": "Dupliceren",
|
||||
"group.app": "Applicaties",
|
||||
"group.app_acl": "Toegangsrechten voor app",
|
||||
"group.billing": "Facturering",
|
||||
"group.credential": "Inloggegevens",
|
||||
"group.dataset": "Kennisbanken",
|
||||
"group.dataset_acl": "Toegangsrechten voor kennisbank",
|
||||
"group.integration": "Integraties",
|
||||
"group.plugin": "Plug-ins",
|
||||
"group.tool_mcp": "Tools en MCP",
|
||||
"group.workspace": "Werkruimte",
|
||||
"permissionList.clearAll": "Alles wissen",
|
||||
"permissionList.collapseGroup": "Groep samenvouwen",
|
||||
"permissionList.expandGroup": "Groep uitvouwen",
|
||||
"permissionList.noPermissionsFound": "Geen rechten gevonden",
|
||||
"permissionList.selectAll": "Alles selecteren",
|
||||
"permissionSet.descriptionLabel": "Beschrijving",
|
||||
"permissionSet.descriptionPlaceholder": "Beschrijf wat deze rechtenset verleent",
|
||||
"permissionSet.learnMore": "Meer informatie over rechten",
|
||||
"permissionSet.modal.create.app.description": "Maak een app-rechtenset die in toegangsregels kan worden gebruikt voor snelle autorisatie.",
|
||||
"permissionSet.modal.create.app.title": "App-rechtenset maken",
|
||||
"permissionSet.modal.create.dataset.description": "Maak een kennisbank-rechtenset die in toegangsregels kan worden gebruikt voor snelle autorisatie.",
|
||||
"permissionSet.modal.create.dataset.title": "Kennisbank-rechtenset maken",
|
||||
"permissionSet.modal.edit.app.description": "Wijzig de naam, beschrijving en verleende rechten voor deze rechtenset.",
|
||||
"permissionSet.modal.edit.app.title": "App-rechtenset bewerken",
|
||||
"permissionSet.modal.edit.dataset.description": "Wijzig de naam, beschrijving en verleende rechten voor deze rechtenset.",
|
||||
"permissionSet.modal.edit.dataset.title": "Kennisbank-rechtenset bewerken",
|
||||
"permissionSet.modal.view.app.description": "Bekijk de naam, beschrijving en verleende rechten voor deze rechtenset.",
|
||||
"permissionSet.modal.view.app.title": "App-rechtenset bekijken",
|
||||
"permissionSet.modal.view.dataset.description": "Bekijk de naam, beschrijving en verleende rechten voor deze rechtenset.",
|
||||
"permissionSet.modal.view.dataset.title": "Kennisbank-rechtenset bekijken",
|
||||
"permissionSet.nameLabel": "Naam van rechtenset",
|
||||
"permissionSet.namePlaceholder": "bijv. Kan DSL exporteren",
|
||||
"permissionSet.permissions": "Rechten",
|
||||
"role.addRole": "Rollen maken",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" is toegewezen aan {{count}} lid. Wil je dat de nieuwe rolkopie hetzelfde lid bevat?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" is toegewezen aan {{count}} leden. Wil je dat de nieuwe rolkopie dezelfde leden bevat?",
|
||||
"role.copyMembersLoading": "Ledentoewijzingen laden...",
|
||||
"role.copyMembersTitle": "Ledentoewijzingen kopiëren?",
|
||||
"role.created": "Rol succesvol gemaakt",
|
||||
"role.deleteDescription": "Deze rol wordt permanent verwijderd en uit alle leden of toegangsregels die deze gebruiken gehaald.",
|
||||
"role.deleteTitle": "\"{{name}}\" verwijderen?",
|
||||
"role.deleted": "Rol succesvol verwijderd",
|
||||
"role.duplicated": "Rol succesvol gedupliceerd",
|
||||
"role.groups.builtin": "Systeemrollen",
|
||||
"role.groups.custom": "Aangepaste rollen",
|
||||
"role.loading": "Rollen laden...",
|
||||
"role.modal.create.description": "Maak een rol en wijs rechten toe",
|
||||
"role.modal.create.title": "Rol maken",
|
||||
"role.modal.descriptionLabel": "Beschrijving",
|
||||
"role.modal.descriptionPlaceholder": "Beschrijf waar deze rol verantwoordelijk voor is",
|
||||
"role.modal.edit.description": "Roldetails en rechten bewerken",
|
||||
"role.modal.edit.title": "Rol bewerken",
|
||||
"role.modal.nameLabel": "Rolnaam",
|
||||
"role.modal.namePlaceholder": "bijv. Marketingleider",
|
||||
"role.modal.view.description": "Roldetails en rechten bekijken",
|
||||
"role.modal.view.title": "Rol bekijken",
|
||||
"role.noDescription": "Geen beschrijving",
|
||||
"role.noMatchingRoles": "Geen overeenkomende rollen",
|
||||
"role.searchPlaceholder": "Rollen zoeken...",
|
||||
"role.updated": "Rol succesvol bijgewerkt",
|
||||
"role.workspaceRoles.description": "Maak rollen en bepaal wat elke rol in deze werkruimte kan doen.",
|
||||
"role.workspaceRoles.title": "Werkruimterollen"
|
||||
}
|
||||
51
web/i18n/pl-PL/permission-keys.json
Normal file
51
web/i18n/pl-PL/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "Zarządzaj konfiguracją rozszerzenia API",
|
||||
"app.access_config": "Konfiguruj uprawnienia dostępu do aplikacji",
|
||||
"app.acl.access_config": "Konfiguruj uprawnienia dostępu do aplikacji",
|
||||
"app.acl.delete": "Usuń aplikację",
|
||||
"app.acl.edit": "Edytuj i orkiestruj aplikację",
|
||||
"app.acl.import_export_dsl": "Importuj / eksportuj DSL",
|
||||
"app.acl.monitor": "Monitorowanie i operacje",
|
||||
"app.acl.preview": "Podgląd aplikacji",
|
||||
"app.acl.release_and_version": "Publikowanie aplikacji i zarządzanie wersjami",
|
||||
"app.acl.test_and_run": "Testuj i używaj aplikacji",
|
||||
"app.acl.view_layout": "Strona orkiestracji tylko do odczytu",
|
||||
"app.create_and_management": "Twórz aplikacje i zarządzaj utworzonymi przez siebie aplikacjami",
|
||||
"app.tag.manage": "Zarządzaj tagami aplikacji",
|
||||
"app_library.access": "Dostęp do Biblioteki aplikacji",
|
||||
"billing.manage": "Zmień plany subskrypcji",
|
||||
"billing.subscription.manage": "Zarządzaj rozliczeniami i subskrypcjami w portalu rozliczeniowym",
|
||||
"billing.view": "Dostęp do ustawień rozliczeń",
|
||||
"credential.create": "Dodaj poświadczenia",
|
||||
"credential.manage": "Edytuj i usuwaj poświadczenia",
|
||||
"credential.use": "Przeglądaj i używaj poświadczeń",
|
||||
"customization.manage": "Zarządzaj dostosowywaniem",
|
||||
"data_source.manage": "Zarządzaj konfiguracją źródła danych",
|
||||
"dataset.access_config": "Konfiguruj uprawnienia dostępu do bazy wiedzy",
|
||||
"dataset.acl.access_config": "Konfiguruj uprawnienia dostępu do bazy wiedzy",
|
||||
"dataset.acl.delete": "Usuń bazę wiedzy",
|
||||
"dataset.acl.delete_file": "Usuń pliki bazy wiedzy",
|
||||
"dataset.acl.document_download": "Pobierz dokumenty",
|
||||
"dataset.acl.edit": "Edytuj bazę wiedzy",
|
||||
"dataset.acl.import_export_dsl": "Importuj / eksportuj DSL potoku wiedzy",
|
||||
"dataset.acl.pipeline_release": "Publikowanie potoku wiedzy i zarządzanie wersjami",
|
||||
"dataset.acl.pipeline_test": "Testowanie potoku",
|
||||
"dataset.acl.preview": "Podgląd bazy wiedzy",
|
||||
"dataset.acl.readonly": "Baza wiedzy tylko do odczytu",
|
||||
"dataset.acl.retrieval_recall": "Wyszukiwanie w bazie wiedzy",
|
||||
"dataset.acl.use": "Dodaj dokumenty do bazy wiedzy",
|
||||
"dataset.api_key.manage": "Zarządzaj kluczami API bazy wiedzy",
|
||||
"dataset.create_and_management": "Twórz bazy wiedzy i zarządzaj utworzonymi przez siebie bazami wiedzy",
|
||||
"dataset.external.connect": "Połącz zewnętrzne bazy wiedzy",
|
||||
"dataset.tag.manage": "Zarządzaj tagami bazy wiedzy",
|
||||
"mcp.manage": "Zarządzaj MCP",
|
||||
"plugin.debug": "Debuguj wtyczki",
|
||||
"plugin.install": "Instaluj i aktualizuj wtyczki",
|
||||
"plugin.manage": "Zarządzaj wtyczkami",
|
||||
"plugin.plugin_preferences": "Zarządzaj preferencjami wtyczek",
|
||||
"snippets.create_and_modify": "Twórz i modyfikuj fragmenty kodu",
|
||||
"snippets.management": "Zarządzaj fragmentami kodu",
|
||||
"tool.manage": "Zarządzaj narzędziami",
|
||||
"workspace.member.manage": "Zarządzaj członkami",
|
||||
"workspace.role.manage": "Zarządzaj uprawnieniami ról i regułami dostępu do zasobów"
|
||||
}
|
||||
105
web/i18n/pl-PL/permission.json
Normal file
105
web/i18n/pl-PL/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Akcje",
|
||||
"accessRule.addMemberAria": "Dodaj {{name}}",
|
||||
"accessRule.addMembersTitle": "Dodaj członków",
|
||||
"accessRule.allPermittedMembers": "Wszyscy członkowie z uprawnieniami ról",
|
||||
"accessRule.allPermittedMembersDescription": "Członkowie z pasującymi uprawnieniami ról mogą uzyskać dostęp do tego zasobu.",
|
||||
"accessRule.appDescription": "Kontroluj, dla kogo ta aplikacja jest otwarta. Członkowie nadal potrzebują uprawnień ról, aby ją przeglądać lub obsługiwać.",
|
||||
"accessRule.appTitle": "Reguły dostępu do aplikacji",
|
||||
"accessRule.changeOpenScopeDescription": "Zmiana zakresu otwarcia zresetuje wszystkie indywidualne ustawienia uprawnień dla tego zasobu. Po zmianie będziesz musiał ponownie dodać uprawnienia specyficzne dla członków.",
|
||||
"accessRule.changeOpenScopeTitle": "Zmienić zakres otwarcia zasobu?",
|
||||
"accessRule.collapseSection": "Zwiń {{title}}",
|
||||
"accessRule.copied": "Pomyślnie skopiowano regułę dostępu",
|
||||
"accessRule.created": "Pomyślnie utworzono regułę dostępu",
|
||||
"accessRule.datasetDescription": "Kontroluj, dla kogo ta baza wiedzy jest otwarta. Członkowie nadal potrzebują uprawnień ról, aby ją przeglądać lub obsługiwać.",
|
||||
"accessRule.datasetTitle": "Reguły dostępu do bazy wiedzy",
|
||||
"accessRule.defaultPermission": "Według uprawnień ról",
|
||||
"accessRule.deleteDescription": "Ta reguła dostępu zostanie trwale usunięta i wykreślona z listy autoryzacji zasobu.",
|
||||
"accessRule.deleteTitle": "Usunąć \"{{name}}\"?",
|
||||
"accessRule.deleted": "Pomyślnie usunięto regułę dostępu",
|
||||
"accessRule.exceptionPermissionFor": "Wyjątkowe uprawnienie dla {{name}}",
|
||||
"accessRule.expandSection": "Rozwiń {{title}}",
|
||||
"accessRule.individualPermissionSettings": "Indywidualne ustawienia uprawnień",
|
||||
"accessRule.individualPermissionSettingsTip": "Ustaw wyjątki uprawnień dla określonych współpracowników lub grup. Te ustawienia zastępują domyślny poziom dostępu.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} zablokowany",
|
||||
"accessRule.lockedSummary_other": "· {{count}} zablokowanych",
|
||||
"accessRule.maintainer": "Opiekun",
|
||||
"accessRule.member": "Członek",
|
||||
"accessRule.newPermissionSet": "Nowy zestaw uprawnień",
|
||||
"accessRule.noAvailableMembers": "Brak członków dostępnych do dodania",
|
||||
"accessRule.noDescription": "Brak opisu",
|
||||
"accessRule.noRoles": "Brak ról",
|
||||
"accessRule.noRules": "Brak reguł dostępu",
|
||||
"accessRule.noUserAccessSettings": "Brak indywidualnych ustawień uprawnień",
|
||||
"accessRule.permission": "Uprawnienie",
|
||||
"accessRule.resourceOpenScope": "Zakres otwarcia zasobu",
|
||||
"accessRule.resourceOpenScopeDescription": "Wybierz, dla kogo ten zasób jest otwarty. Uprawnienia ról nadal decydują o tym, co każdy członek może robić.",
|
||||
"accessRule.specificMembersOnly": "Tylko określeni członkowie",
|
||||
"accessRule.specificMembersOnlyDescription": "Tylko wybrani członkowie mogą uzyskać dostęp do tego zasobu.",
|
||||
"accessRule.summary_one": "{{count}} zestaw uprawnień",
|
||||
"accessRule.summary_other": "{{count}} zestawów uprawnień",
|
||||
"accessRule.updated": "Pomyślnie zaktualizowano regułę dostępu",
|
||||
"common.duplicateAction": "Duplikuj",
|
||||
"group.app": "Aplikacje",
|
||||
"group.app_acl": "Uprawnienia dostępu do aplikacji",
|
||||
"group.billing": "Rozliczenia",
|
||||
"group.credential": "Poświadczenia",
|
||||
"group.dataset": "Bazy wiedzy",
|
||||
"group.dataset_acl": "Uprawnienia dostępu do bazy wiedzy",
|
||||
"group.integration": "Integracje",
|
||||
"group.plugin": "Wtyczki",
|
||||
"group.tool_mcp": "Narzędzia i MCP",
|
||||
"group.workspace": "Przestrzeń robocza",
|
||||
"permissionList.clearAll": "Wyczyść wszystko",
|
||||
"permissionList.collapseGroup": "Zwiń grupę",
|
||||
"permissionList.expandGroup": "Rozwiń grupę",
|
||||
"permissionList.noPermissionsFound": "Nie znaleziono uprawnień",
|
||||
"permissionList.selectAll": "Zaznacz wszystko",
|
||||
"permissionSet.descriptionLabel": "Opis",
|
||||
"permissionSet.descriptionPlaceholder": "Opisz, co przyznaje ten zestaw uprawnień",
|
||||
"permissionSet.learnMore": "Dowiedz się więcej o uprawnieniach",
|
||||
"permissionSet.modal.create.app.description": "Utwórz zestaw uprawnień aplikacji, do którego można się odwoływać w regułach dostępu w celu szybkiej autoryzacji.",
|
||||
"permissionSet.modal.create.app.title": "Utwórz zestaw uprawnień aplikacji",
|
||||
"permissionSet.modal.create.dataset.description": "Utwórz zestaw uprawnień bazy wiedzy, do którego można się odwoływać w regułach dostępu w celu szybkiej autoryzacji.",
|
||||
"permissionSet.modal.create.dataset.title": "Utwórz zestaw uprawnień bazy wiedzy",
|
||||
"permissionSet.modal.edit.app.description": "Zmodyfikuj nazwę, opis i uprawnienia przyznane dla tego zestawu uprawnień.",
|
||||
"permissionSet.modal.edit.app.title": "Edytuj zestaw uprawnień aplikacji",
|
||||
"permissionSet.modal.edit.dataset.description": "Zmodyfikuj nazwę, opis i uprawnienia przyznane dla tego zestawu uprawnień.",
|
||||
"permissionSet.modal.edit.dataset.title": "Edytuj zestaw uprawnień bazy wiedzy",
|
||||
"permissionSet.modal.view.app.description": "Wyświetl nazwę, opis i uprawnienia przyznane dla tego zestawu uprawnień.",
|
||||
"permissionSet.modal.view.app.title": "Wyświetl zestaw uprawnień aplikacji",
|
||||
"permissionSet.modal.view.dataset.description": "Wyświetl nazwę, opis i uprawnienia przyznane dla tego zestawu uprawnień.",
|
||||
"permissionSet.modal.view.dataset.title": "Wyświetl zestaw uprawnień bazy wiedzy",
|
||||
"permissionSet.nameLabel": "Nazwa zestawu uprawnień",
|
||||
"permissionSet.namePlaceholder": "np. Może eksportować DSL",
|
||||
"permissionSet.permissions": "Uprawnienia",
|
||||
"role.addRole": "Twórz role",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" jest przypisana do {{count}} członka. Czy chcesz, aby nowa kopia roli zawierała tego samego członka?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" jest przypisana do {{count}} członków. Czy chcesz, aby nowa kopia roli zawierała tych samych członków?",
|
||||
"role.copyMembersLoading": "Ładowanie przypisań członków...",
|
||||
"role.copyMembersTitle": "Skopiować przypisania członków?",
|
||||
"role.created": "Pomyślnie utworzono rolę",
|
||||
"role.deleteDescription": "Ta rola zostanie trwale usunięta i wykreślona ze wszystkich członków lub reguł dostępu, które jej używają.",
|
||||
"role.deleteTitle": "Usunąć \"{{name}}\"?",
|
||||
"role.deleted": "Pomyślnie usunięto rolę",
|
||||
"role.duplicated": "Pomyślnie zduplikowano rolę",
|
||||
"role.groups.builtin": "Role systemowe",
|
||||
"role.groups.custom": "Role niestandardowe",
|
||||
"role.loading": "Ładowanie ról...",
|
||||
"role.modal.create.description": "Utwórz rolę i przypisz uprawnienia",
|
||||
"role.modal.create.title": "Utwórz rolę",
|
||||
"role.modal.descriptionLabel": "Opis",
|
||||
"role.modal.descriptionPlaceholder": "Opisz, za co odpowiada ta rola",
|
||||
"role.modal.edit.description": "Edytuj szczegóły i uprawnienia roli",
|
||||
"role.modal.edit.title": "Edytuj rolę",
|
||||
"role.modal.nameLabel": "Nazwa roli",
|
||||
"role.modal.namePlaceholder": "np. Lider marketingu",
|
||||
"role.modal.view.description": "Wyświetl szczegóły i uprawnienia roli",
|
||||
"role.modal.view.title": "Wyświetl rolę",
|
||||
"role.noDescription": "Brak opisu",
|
||||
"role.noMatchingRoles": "Brak pasujących ról",
|
||||
"role.searchPlaceholder": "Szukaj ról...",
|
||||
"role.updated": "Pomyślnie zaktualizowano rolę",
|
||||
"role.workspaceRoles.description": "Twórz role i definiuj, co każda rola może robić w tej przestrzeni roboczej.",
|
||||
"role.workspaceRoles.title": "Role przestrzeni roboczej"
|
||||
}
|
||||
51
web/i18n/pt-BR/permission-keys.json
Normal file
51
web/i18n/pt-BR/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "Gerenciar configuração de extensão de API",
|
||||
"app.access_config": "Configurar permissões de acesso ao aplicativo",
|
||||
"app.acl.access_config": "Configurar permissões de acesso ao aplicativo",
|
||||
"app.acl.delete": "Excluir aplicativo",
|
||||
"app.acl.edit": "Editar e orquestrar aplicativo",
|
||||
"app.acl.import_export_dsl": "Importar / exportar DSL",
|
||||
"app.acl.monitor": "Monitoramento e operações",
|
||||
"app.acl.preview": "Visualizar aplicativo",
|
||||
"app.acl.release_and_version": "Publicação de aplicativo e gerenciamento de versões",
|
||||
"app.acl.test_and_run": "Testar e usar aplicativo",
|
||||
"app.acl.view_layout": "Página de orquestração somente leitura",
|
||||
"app.create_and_management": "Criar aplicativos e gerenciar os aplicativos que você criou",
|
||||
"app.tag.manage": "Gerenciar tags de aplicativo",
|
||||
"app_library.access": "Acessar a Biblioteca de Aplicativos",
|
||||
"billing.manage": "Alterar planos de assinatura",
|
||||
"billing.subscription.manage": "Gerenciar faturamento e assinaturas no portal de faturamento",
|
||||
"billing.view": "Acessar configurações de Faturamento",
|
||||
"credential.create": "Adicionar credenciais",
|
||||
"credential.manage": "Editar e excluir credenciais",
|
||||
"credential.use": "Visualizar e usar credenciais",
|
||||
"customization.manage": "Gerenciar personalização",
|
||||
"data_source.manage": "Gerenciar configuração de fonte de dados",
|
||||
"dataset.access_config": "Configurar permissões de acesso ao Conhecimento",
|
||||
"dataset.acl.access_config": "Configurar permissões de acesso ao Conhecimento",
|
||||
"dataset.acl.delete": "Excluir Conhecimento",
|
||||
"dataset.acl.delete_file": "Excluir arquivos do Conhecimento",
|
||||
"dataset.acl.document_download": "Baixar documentos",
|
||||
"dataset.acl.edit": "Editar Conhecimento",
|
||||
"dataset.acl.import_export_dsl": "Importar / exportar DSL de pipeline de conhecimento",
|
||||
"dataset.acl.pipeline_release": "Publicação de pipeline de conhecimento e gerenciamento de versões",
|
||||
"dataset.acl.pipeline_test": "Teste de pipeline",
|
||||
"dataset.acl.preview": "Visualizar Conhecimento",
|
||||
"dataset.acl.readonly": "Conhecimento somente leitura",
|
||||
"dataset.acl.retrieval_recall": "Recuperação do Conhecimento",
|
||||
"dataset.acl.use": "Adicionar documentos ao Conhecimento",
|
||||
"dataset.api_key.manage": "Gerenciar chaves de API do Conhecimento",
|
||||
"dataset.create_and_management": "Criar Conhecimentos e gerenciar os Conhecimentos que você criou",
|
||||
"dataset.external.connect": "Conectar Conhecimentos externos",
|
||||
"dataset.tag.manage": "Gerenciar tags do Conhecimento",
|
||||
"mcp.manage": "Gerenciar MCP",
|
||||
"plugin.debug": "Depurar plug-ins",
|
||||
"plugin.install": "Instalar e atualizar plug-ins",
|
||||
"plugin.manage": "Gerenciar plug-ins",
|
||||
"plugin.plugin_preferences": "Gerenciar preferências de plug-ins",
|
||||
"snippets.create_and_modify": "Criar e modificar snippets",
|
||||
"snippets.management": "Gerenciar snippets",
|
||||
"tool.manage": "Gerenciar ferramentas",
|
||||
"workspace.member.manage": "Gerenciar membros",
|
||||
"workspace.role.manage": "Gerenciar permissões de função e regras de acesso a recursos"
|
||||
}
|
||||
105
web/i18n/pt-BR/permission.json
Normal file
105
web/i18n/pt-BR/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Ações",
|
||||
"accessRule.addMemberAria": "Adicionar {{name}}",
|
||||
"accessRule.addMembersTitle": "Adicionar membros",
|
||||
"accessRule.allPermittedMembers": "Todos os membros com permissões de função",
|
||||
"accessRule.allPermittedMembersDescription": "Membros com permissões de função correspondentes podem acessar este recurso.",
|
||||
"accessRule.appDescription": "Controle para quem este aplicativo está aberto. Os membros ainda precisam de permissões de função para visualizá-lo ou operá-lo.",
|
||||
"accessRule.appTitle": "Regras de Acesso ao Aplicativo",
|
||||
"accessRule.changeOpenScopeDescription": "Alterar o escopo de abertura redefinirá todas as configurações de permissão individuais para este recurso. Você precisará adicionar permissões específicas de membro novamente após a alteração.",
|
||||
"accessRule.changeOpenScopeTitle": "Alterar escopo de abertura do recurso?",
|
||||
"accessRule.collapseSection": "Recolher {{title}}",
|
||||
"accessRule.copied": "Regra de acesso copiada com sucesso",
|
||||
"accessRule.created": "Regra de acesso criada com sucesso",
|
||||
"accessRule.datasetDescription": "Controle para quem este Conhecimento está aberto. Os membros ainda precisam de permissões de função para visualizá-lo ou operá-lo.",
|
||||
"accessRule.datasetTitle": "Regras de Acesso ao Conhecimento",
|
||||
"accessRule.defaultPermission": "Por permissões de função",
|
||||
"accessRule.deleteDescription": "Esta regra de acesso será excluída permanentemente e removida da lista de autorização do recurso.",
|
||||
"accessRule.deleteTitle": "Excluir \"{{name}}\"?",
|
||||
"accessRule.deleted": "Regra de acesso excluída com sucesso",
|
||||
"accessRule.exceptionPermissionFor": "Permissão de exceção para {{name}}",
|
||||
"accessRule.expandSection": "Expandir {{title}}",
|
||||
"accessRule.individualPermissionSettings": "Configurações de permissão individuais",
|
||||
"accessRule.individualPermissionSettingsTip": "Defina exceções de permissão para colaboradores ou grupos específicos. Essas configurações substituem o nível de acesso padrão.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} bloqueado",
|
||||
"accessRule.lockedSummary_other": "· {{count}} bloqueados",
|
||||
"accessRule.maintainer": "Mantenedor",
|
||||
"accessRule.member": "Membro",
|
||||
"accessRule.newPermissionSet": "Novo conjunto de permissões",
|
||||
"accessRule.noAvailableMembers": "Nenhum membro disponível para adicionar",
|
||||
"accessRule.noDescription": "Sem descrição",
|
||||
"accessRule.noRoles": "Sem funções",
|
||||
"accessRule.noRules": "Sem regras de acesso",
|
||||
"accessRule.noUserAccessSettings": "Sem configurações de permissão individuais",
|
||||
"accessRule.permission": "Permissão",
|
||||
"accessRule.resourceOpenScope": "Escopo de abertura do recurso",
|
||||
"accessRule.resourceOpenScopeDescription": "Escolha para quem este recurso está aberto. As permissões de função ainda determinam o que cada membro pode fazer.",
|
||||
"accessRule.specificMembersOnly": "Apenas membros específicos",
|
||||
"accessRule.specificMembersOnlyDescription": "Apenas os membros selecionados podem acessar este recurso.",
|
||||
"accessRule.summary_one": "{{count}} conjunto de permissões",
|
||||
"accessRule.summary_other": "{{count}} conjuntos de permissões",
|
||||
"accessRule.updated": "Regra de acesso atualizada com sucesso",
|
||||
"common.duplicateAction": "Duplicar",
|
||||
"group.app": "Aplicativos",
|
||||
"group.app_acl": "Permissões de acesso ao aplicativo",
|
||||
"group.billing": "Faturamento",
|
||||
"group.credential": "Credenciais",
|
||||
"group.dataset": "Conhecimentos",
|
||||
"group.dataset_acl": "Permissões de acesso ao Conhecimento",
|
||||
"group.integration": "Integrações",
|
||||
"group.plugin": "Plug-ins",
|
||||
"group.tool_mcp": "Ferramentas e MCP",
|
||||
"group.workspace": "Espaço de trabalho",
|
||||
"permissionList.clearAll": "Limpar tudo",
|
||||
"permissionList.collapseGroup": "Recolher grupo",
|
||||
"permissionList.expandGroup": "Expandir grupo",
|
||||
"permissionList.noPermissionsFound": "Nenhuma permissão encontrada",
|
||||
"permissionList.selectAll": "Selecionar tudo",
|
||||
"permissionSet.descriptionLabel": "Descrição",
|
||||
"permissionSet.descriptionPlaceholder": "Descreva o que este conjunto de permissões concede",
|
||||
"permissionSet.learnMore": "Saiba mais sobre permissões",
|
||||
"permissionSet.modal.create.app.description": "Crie um conjunto de permissões de aplicativo que pode ser referenciado em regras de acesso para autorização rápida.",
|
||||
"permissionSet.modal.create.app.title": "Criar conjunto de permissões de Aplicativo",
|
||||
"permissionSet.modal.create.dataset.description": "Crie um conjunto de permissões de Conhecimento que pode ser referenciado em regras de acesso para autorização rápida.",
|
||||
"permissionSet.modal.create.dataset.title": "Criar conjunto de permissões de Conhecimento",
|
||||
"permissionSet.modal.edit.app.description": "Modifique o nome, a descrição e as permissões concedidas para este conjunto de permissões.",
|
||||
"permissionSet.modal.edit.app.title": "Editar conjunto de permissões de Aplicativo",
|
||||
"permissionSet.modal.edit.dataset.description": "Modifique o nome, a descrição e as permissões concedidas para este conjunto de permissões.",
|
||||
"permissionSet.modal.edit.dataset.title": "Editar conjunto de permissões de Conhecimento",
|
||||
"permissionSet.modal.view.app.description": "Visualize o nome, a descrição e as permissões concedidas para este conjunto de permissões.",
|
||||
"permissionSet.modal.view.app.title": "Visualizar conjunto de permissões de Aplicativo",
|
||||
"permissionSet.modal.view.dataset.description": "Visualize o nome, a descrição e as permissões concedidas para este conjunto de permissões.",
|
||||
"permissionSet.modal.view.dataset.title": "Visualizar conjunto de permissões de Conhecimento",
|
||||
"permissionSet.nameLabel": "Nome do conjunto de permissões",
|
||||
"permissionSet.namePlaceholder": "ex.: Pode exportar DSL",
|
||||
"permissionSet.permissions": "Permissões",
|
||||
"role.addRole": "Criar funções",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" está atribuída a {{count}} membro. Você deseja que a cópia da nova função inclua o mesmo membro?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" está atribuída a {{count}} membros. Você deseja que a cópia da nova função inclua os mesmos membros?",
|
||||
"role.copyMembersLoading": "Carregando atribuições de membros...",
|
||||
"role.copyMembersTitle": "Copiar atribuições de membros?",
|
||||
"role.created": "Função criada com sucesso",
|
||||
"role.deleteDescription": "Esta função será excluída permanentemente e removida de quaisquer membros ou regras de acesso que a utilizem.",
|
||||
"role.deleteTitle": "Excluir \"{{name}}\"?",
|
||||
"role.deleted": "Função excluída com sucesso",
|
||||
"role.duplicated": "Função duplicada com sucesso",
|
||||
"role.groups.builtin": "Funções do Sistema",
|
||||
"role.groups.custom": "Funções Personalizadas",
|
||||
"role.loading": "Carregando funções...",
|
||||
"role.modal.create.description": "Crie uma função e atribua permissões",
|
||||
"role.modal.create.title": "Criar Função",
|
||||
"role.modal.descriptionLabel": "Descrição",
|
||||
"role.modal.descriptionPlaceholder": "Descreva pelo que esta função é responsável",
|
||||
"role.modal.edit.description": "Editar detalhes e permissões da função",
|
||||
"role.modal.edit.title": "Editar Função",
|
||||
"role.modal.nameLabel": "Nome da função",
|
||||
"role.modal.namePlaceholder": "ex.: Líder de Marketing",
|
||||
"role.modal.view.description": "Visualizar detalhes e permissões da função",
|
||||
"role.modal.view.title": "Visualizar Função",
|
||||
"role.noDescription": "Sem descrição",
|
||||
"role.noMatchingRoles": "Nenhuma função correspondente",
|
||||
"role.searchPlaceholder": "Pesquisar funções...",
|
||||
"role.updated": "Função atualizada com sucesso",
|
||||
"role.workspaceRoles.description": "Crie funções e defina o que cada função pode fazer neste espaço de trabalho.",
|
||||
"role.workspaceRoles.title": "Funções do Espaço de Trabalho"
|
||||
}
|
||||
51
web/i18n/ro-RO/permission-keys.json
Normal file
51
web/i18n/ro-RO/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "Gestionează configurația extensiei API",
|
||||
"app.access_config": "Configurează permisiunile de acces ale aplicației",
|
||||
"app.acl.access_config": "Configurează permisiunile de acces ale aplicației",
|
||||
"app.acl.delete": "Șterge aplicația",
|
||||
"app.acl.edit": "Editează și orchestrează aplicația",
|
||||
"app.acl.import_export_dsl": "Importă / exportă DSL",
|
||||
"app.acl.monitor": "Monitorizare și operațiuni",
|
||||
"app.acl.preview": "Previzualizează aplicația",
|
||||
"app.acl.release_and_version": "Publicarea aplicației și gestionarea versiunilor",
|
||||
"app.acl.test_and_run": "Testează și utilizează aplicația",
|
||||
"app.acl.view_layout": "Pagină de orchestrare doar pentru citire",
|
||||
"app.create_and_management": "Creează aplicații și gestionează aplicațiile pe care le-ai creat",
|
||||
"app.tag.manage": "Gestionează etichetele aplicațiilor",
|
||||
"app_library.access": "Accesează Biblioteca de aplicații",
|
||||
"billing.manage": "Schimbă planurile de abonament",
|
||||
"billing.subscription.manage": "Gestionează facturarea și abonamentele în portalul de facturare",
|
||||
"billing.view": "Accesează setările de Facturare",
|
||||
"credential.create": "Adaugă acreditive",
|
||||
"credential.manage": "Editează și șterge acreditive",
|
||||
"credential.use": "Vizualizează și utilizează acreditive",
|
||||
"customization.manage": "Gestionează personalizarea",
|
||||
"data_source.manage": "Gestionează configurația sursei de date",
|
||||
"dataset.access_config": "Configurează permisiunile de acces ale bazei de cunoștințe",
|
||||
"dataset.acl.access_config": "Configurează permisiunile de acces ale bazei de cunoștințe",
|
||||
"dataset.acl.delete": "Șterge baza de cunoștințe",
|
||||
"dataset.acl.delete_file": "Șterge fișierele bazei de cunoștințe",
|
||||
"dataset.acl.document_download": "Descarcă documente",
|
||||
"dataset.acl.edit": "Editează baza de cunoștințe",
|
||||
"dataset.acl.import_export_dsl": "Importă / exportă DSL-ul pipeline-ului de cunoștințe",
|
||||
"dataset.acl.pipeline_release": "Publicarea pipeline-ului de cunoștințe și gestionarea versiunilor",
|
||||
"dataset.acl.pipeline_test": "Testarea pipeline-ului",
|
||||
"dataset.acl.preview": "Previzualizează baza de cunoștințe",
|
||||
"dataset.acl.readonly": "Bază de cunoștințe doar pentru citire",
|
||||
"dataset.acl.retrieval_recall": "Recuperare din baza de cunoștințe",
|
||||
"dataset.acl.use": "Adaugă documente în baza de cunoștințe",
|
||||
"dataset.api_key.manage": "Gestionează cheile API ale bazei de cunoștințe",
|
||||
"dataset.create_and_management": "Creează baze de cunoștințe și gestionează bazele de cunoștințe pe care le-ai creat",
|
||||
"dataset.external.connect": "Conectează baze de cunoștințe externe",
|
||||
"dataset.tag.manage": "Gestionează etichetele bazei de cunoștințe",
|
||||
"mcp.manage": "Gestionează MCP",
|
||||
"plugin.debug": "Depanează plugin-uri",
|
||||
"plugin.install": "Instalează și actualizează plugin-uri",
|
||||
"plugin.manage": "Gestionează plugin-uri",
|
||||
"plugin.plugin_preferences": "Gestionează preferințele plugin-urilor",
|
||||
"snippets.create_and_modify": "Creează și modifică fragmente",
|
||||
"snippets.management": "Gestionează fragmente",
|
||||
"tool.manage": "Gestionează instrumente",
|
||||
"workspace.member.manage": "Gestionează membrii",
|
||||
"workspace.role.manage": "Gestionează permisiunile rolurilor și regulile de acces la resurse"
|
||||
}
|
||||
105
web/i18n/ro-RO/permission.json
Normal file
105
web/i18n/ro-RO/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Acțiuni",
|
||||
"accessRule.addMemberAria": "Adaugă {{name}}",
|
||||
"accessRule.addMembersTitle": "Adaugă membri",
|
||||
"accessRule.allPermittedMembers": "Toți membrii cu permisiuni de rol",
|
||||
"accessRule.allPermittedMembersDescription": "Membrii cu permisiuni de rol corespunzătoare pot accesa această resursă.",
|
||||
"accessRule.appDescription": "Controlează cui îi este deschisă această aplicație. Membrii au în continuare nevoie de permisiuni de rol pentru a o vizualiza sau opera.",
|
||||
"accessRule.appTitle": "Reguli de acces ale aplicației",
|
||||
"accessRule.changeOpenScopeDescription": "Modificarea domeniului de deschidere va reseta toate setările individuale de permisiuni pentru această resursă. Va trebui să adaugi din nou permisiunile specifice membrilor după comutare.",
|
||||
"accessRule.changeOpenScopeTitle": "Schimbi domeniul de deschidere al resursei?",
|
||||
"accessRule.collapseSection": "Restrânge {{title}}",
|
||||
"accessRule.copied": "Regula de acces a fost copiată cu succes",
|
||||
"accessRule.created": "Regula de acces a fost creată cu succes",
|
||||
"accessRule.datasetDescription": "Controlează cui îi este deschisă această bază de cunoștințe. Membrii au în continuare nevoie de permisiuni de rol pentru a o vizualiza sau opera.",
|
||||
"accessRule.datasetTitle": "Reguli de acces ale bazei de cunoștințe",
|
||||
"accessRule.defaultPermission": "După permisiunile de rol",
|
||||
"accessRule.deleteDescription": "Această regulă de acces va fi ștearsă definitiv și eliminată din lista de autorizare a resursei.",
|
||||
"accessRule.deleteTitle": "Ștergi \"{{name}}\"?",
|
||||
"accessRule.deleted": "Regula de acces a fost ștearsă cu succes",
|
||||
"accessRule.exceptionPermissionFor": "Permisiune de excepție pentru {{name}}",
|
||||
"accessRule.expandSection": "Extinde {{title}}",
|
||||
"accessRule.individualPermissionSettings": "Setări individuale de permisiuni",
|
||||
"accessRule.individualPermissionSettingsTip": "Setează excepții de permisiuni pentru colaboratori sau grupuri specifice. Aceste setări înlocuiesc nivelul de acces implicit.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} blocat",
|
||||
"accessRule.lockedSummary_other": "· {{count}} blocate",
|
||||
"accessRule.maintainer": "Întreținător",
|
||||
"accessRule.member": "Membru",
|
||||
"accessRule.newPermissionSet": "Set nou de permisiuni",
|
||||
"accessRule.noAvailableMembers": "Niciun membru disponibil pentru adăugare",
|
||||
"accessRule.noDescription": "Fără descriere",
|
||||
"accessRule.noRoles": "Fără roluri",
|
||||
"accessRule.noRules": "Fără reguli de acces",
|
||||
"accessRule.noUserAccessSettings": "Fără setări individuale de permisiuni",
|
||||
"accessRule.permission": "Permisiune",
|
||||
"accessRule.resourceOpenScope": "Domeniul de deschidere al resursei",
|
||||
"accessRule.resourceOpenScopeDescription": "Alege cui îi este deschisă această resursă. Permisiunile de rol decid în continuare ce poate face fiecare membru.",
|
||||
"accessRule.specificMembersOnly": "Doar membri specifici",
|
||||
"accessRule.specificMembersOnlyDescription": "Doar membrii selectați pot accesa această resursă.",
|
||||
"accessRule.summary_one": "{{count}} set de permisiuni",
|
||||
"accessRule.summary_other": "{{count}} seturi de permisiuni",
|
||||
"accessRule.updated": "Regula de acces a fost actualizată cu succes",
|
||||
"common.duplicateAction": "Duplică",
|
||||
"group.app": "Aplicații",
|
||||
"group.app_acl": "Permisiuni de acces ale aplicației",
|
||||
"group.billing": "Facturare",
|
||||
"group.credential": "Acreditive",
|
||||
"group.dataset": "Baze de cunoștințe",
|
||||
"group.dataset_acl": "Permisiuni de acces ale bazei de cunoștințe",
|
||||
"group.integration": "Integrări",
|
||||
"group.plugin": "Plugin-uri",
|
||||
"group.tool_mcp": "Instrumente și MCP",
|
||||
"group.workspace": "Spațiu de lucru",
|
||||
"permissionList.clearAll": "Șterge tot",
|
||||
"permissionList.collapseGroup": "Restrânge grupul",
|
||||
"permissionList.expandGroup": "Extinde grupul",
|
||||
"permissionList.noPermissionsFound": "Nu au fost găsite permisiuni",
|
||||
"permissionList.selectAll": "Selectează tot",
|
||||
"permissionSet.descriptionLabel": "Descriere",
|
||||
"permissionSet.descriptionPlaceholder": "Descrie ce acordă acest set de permisiuni",
|
||||
"permissionSet.learnMore": "Află mai multe despre permisiuni",
|
||||
"permissionSet.modal.create.app.description": "Creează un set de permisiuni pentru aplicație care poate fi referențiat în regulile de acces pentru autorizare rapidă.",
|
||||
"permissionSet.modal.create.app.title": "Creează set de permisiuni pentru aplicație",
|
||||
"permissionSet.modal.create.dataset.description": "Creează un set de permisiuni pentru baza de cunoștințe care poate fi referențiat în regulile de acces pentru autorizare rapidă.",
|
||||
"permissionSet.modal.create.dataset.title": "Creează set de permisiuni pentru baza de cunoștințe",
|
||||
"permissionSet.modal.edit.app.description": "Modifică numele, descrierea și permisiunile acordate pentru acest set de permisiuni.",
|
||||
"permissionSet.modal.edit.app.title": "Editează set de permisiuni pentru aplicație",
|
||||
"permissionSet.modal.edit.dataset.description": "Modifică numele, descrierea și permisiunile acordate pentru acest set de permisiuni.",
|
||||
"permissionSet.modal.edit.dataset.title": "Editează set de permisiuni pentru baza de cunoștințe",
|
||||
"permissionSet.modal.view.app.description": "Vizualizează numele, descrierea și permisiunile acordate pentru acest set de permisiuni.",
|
||||
"permissionSet.modal.view.app.title": "Vizualizează set de permisiuni pentru aplicație",
|
||||
"permissionSet.modal.view.dataset.description": "Vizualizează numele, descrierea și permisiunile acordate pentru acest set de permisiuni.",
|
||||
"permissionSet.modal.view.dataset.title": "Vizualizează set de permisiuni pentru baza de cunoștințe",
|
||||
"permissionSet.nameLabel": "Numele setului de permisiuni",
|
||||
"permissionSet.namePlaceholder": "de ex. Poate exporta DSL",
|
||||
"permissionSet.permissions": "Permisiuni",
|
||||
"role.addRole": "Creează roluri",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" este atribuit unui {{count}} membru. Dorești ca noua copie a rolului să includă același membru?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" este atribuit la {{count}} membri. Dorești ca noua copie a rolului să includă aceiași membri?",
|
||||
"role.copyMembersLoading": "Se încarcă atribuirile de membri...",
|
||||
"role.copyMembersTitle": "Copiezi atribuirile de membri?",
|
||||
"role.created": "Rolul a fost creat cu succes",
|
||||
"role.deleteDescription": "Acest rol va fi șters definitiv și eliminat de la orice membri sau reguli de acces care îl utilizează.",
|
||||
"role.deleteTitle": "Ștergi \"{{name}}\"?",
|
||||
"role.deleted": "Rolul a fost șters cu succes",
|
||||
"role.duplicated": "Rolul a fost duplicat cu succes",
|
||||
"role.groups.builtin": "Roluri de sistem",
|
||||
"role.groups.custom": "Roluri personalizate",
|
||||
"role.loading": "Se încarcă rolurile...",
|
||||
"role.modal.create.description": "Creează un rol și atribuie permisiuni",
|
||||
"role.modal.create.title": "Creează rol",
|
||||
"role.modal.descriptionLabel": "Descriere",
|
||||
"role.modal.descriptionPlaceholder": "Descrie de ce este responsabil acest rol",
|
||||
"role.modal.edit.description": "Editează detaliile și permisiunile rolului",
|
||||
"role.modal.edit.title": "Editează rol",
|
||||
"role.modal.nameLabel": "Numele rolului",
|
||||
"role.modal.namePlaceholder": "de ex. Lider de marketing",
|
||||
"role.modal.view.description": "Vizualizează detaliile și permisiunile rolului",
|
||||
"role.modal.view.title": "Vizualizează rol",
|
||||
"role.noDescription": "Fără descriere",
|
||||
"role.noMatchingRoles": "Niciun rol corespunzător",
|
||||
"role.searchPlaceholder": "Caută roluri...",
|
||||
"role.updated": "Rolul a fost actualizat cu succes",
|
||||
"role.workspaceRoles.description": "Creează roluri și definește ce poate face fiecare rol în acest spațiu de lucru.",
|
||||
"role.workspaceRoles.title": "Roluri ale spațiului de lucru"
|
||||
}
|
||||
51
web/i18n/ru-RU/permission-keys.json
Normal file
51
web/i18n/ru-RU/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "Управление конфигурацией API-расширений",
|
||||
"app.access_config": "Настройка прав доступа к приложению",
|
||||
"app.acl.access_config": "Настройка прав доступа к приложению",
|
||||
"app.acl.delete": "Удаление приложения",
|
||||
"app.acl.edit": "Редактирование и оркестрация приложения",
|
||||
"app.acl.import_export_dsl": "Импорт / экспорт DSL",
|
||||
"app.acl.monitor": "Мониторинг и эксплуатация",
|
||||
"app.acl.preview": "Предпросмотр приложения",
|
||||
"app.acl.release_and_version": "Публикация приложения и управление версиями",
|
||||
"app.acl.test_and_run": "Тестирование и использование приложения",
|
||||
"app.acl.view_layout": "Страница оркестрации только для чтения",
|
||||
"app.create_and_management": "Создание приложений и управление созданными вами приложениями",
|
||||
"app.tag.manage": "Управление тегами приложений",
|
||||
"app_library.access": "Доступ к библиотеке приложений",
|
||||
"billing.manage": "Изменение тарифных планов",
|
||||
"billing.subscription.manage": "Управление оплатой и подписками в биллинг-портале",
|
||||
"billing.view": "Доступ к настройкам биллинга",
|
||||
"credential.create": "Добавление учетных данных",
|
||||
"credential.manage": "Редактирование и удаление учетных данных",
|
||||
"credential.use": "Просмотр и использование учетных данных",
|
||||
"customization.manage": "Управление кастомизацией",
|
||||
"data_source.manage": "Управление конфигурацией источников данных",
|
||||
"dataset.access_config": "Настройка прав доступа к базе знаний",
|
||||
"dataset.acl.access_config": "Настройка прав доступа к базе знаний",
|
||||
"dataset.acl.delete": "Удаление базы знаний",
|
||||
"dataset.acl.delete_file": "Удаление файлов базы знаний",
|
||||
"dataset.acl.document_download": "Скачивание документов",
|
||||
"dataset.acl.edit": "Редактирование базы знаний",
|
||||
"dataset.acl.import_export_dsl": "Импорт / экспорт DSL конвейера знаний",
|
||||
"dataset.acl.pipeline_release": "Публикация конвейера знаний и управление версиями",
|
||||
"dataset.acl.pipeline_test": "Тестирование конвейера",
|
||||
"dataset.acl.preview": "Предпросмотр базы знаний",
|
||||
"dataset.acl.readonly": "База знаний только для чтения",
|
||||
"dataset.acl.retrieval_recall": "Извлечение из базы знаний",
|
||||
"dataset.acl.use": "Добавление документов в базу знаний",
|
||||
"dataset.api_key.manage": "Управление API-ключами базы знаний",
|
||||
"dataset.create_and_management": "Создание баз знаний и управление созданными вами базами знаний",
|
||||
"dataset.external.connect": "Подключение внешних баз знаний",
|
||||
"dataset.tag.manage": "Управление тегами баз знаний",
|
||||
"mcp.manage": "Управление MCP",
|
||||
"plugin.debug": "Отладка плагинов",
|
||||
"plugin.install": "Установка и обновление плагинов",
|
||||
"plugin.manage": "Управление плагинами",
|
||||
"plugin.plugin_preferences": "Управление настройками плагинов",
|
||||
"snippets.create_and_modify": "Создание и изменение сниппетов",
|
||||
"snippets.management": "Управление сниппетами",
|
||||
"tool.manage": "Управление инструментами",
|
||||
"workspace.member.manage": "Управление участниками",
|
||||
"workspace.role.manage": "Управление правами ролей и правилами доступа к ресурсам"
|
||||
}
|
||||
105
web/i18n/ru-RU/permission.json
Normal file
105
web/i18n/ru-RU/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Действия",
|
||||
"accessRule.addMemberAria": "Добавить {{name}}",
|
||||
"accessRule.addMembersTitle": "Добавить участников",
|
||||
"accessRule.allPermittedMembers": "Все участники с правами роли",
|
||||
"accessRule.allPermittedMembersDescription": "Участники с соответствующими правами роли могут получить доступ к этому ресурсу.",
|
||||
"accessRule.appDescription": "Управляйте тем, кому открыто это приложение. Участникам по-прежнему нужны права роли для просмотра или работы с ним.",
|
||||
"accessRule.appTitle": "Правила доступа к приложению",
|
||||
"accessRule.changeOpenScopeDescription": "Изменение области открытости сбросит все индивидуальные настройки прав для этого ресурса. После переключения вам потребуется снова добавить права для отдельных участников.",
|
||||
"accessRule.changeOpenScopeTitle": "Изменить область открытости ресурса?",
|
||||
"accessRule.collapseSection": "Свернуть {{title}}",
|
||||
"accessRule.copied": "Правило доступа успешно скопировано",
|
||||
"accessRule.created": "Правило доступа успешно создано",
|
||||
"accessRule.datasetDescription": "Управляйте тем, кому открыта эта база знаний. Участникам по-прежнему нужны права роли для просмотра или работы с ней.",
|
||||
"accessRule.datasetTitle": "Правила доступа к базе знаний",
|
||||
"accessRule.defaultPermission": "По правам роли",
|
||||
"accessRule.deleteDescription": "Это правило доступа будет безвозвратно удалено и исключено из списка авторизации ресурса.",
|
||||
"accessRule.deleteTitle": "Удалить \"{{name}}\"?",
|
||||
"accessRule.deleted": "Правило доступа успешно удалено",
|
||||
"accessRule.exceptionPermissionFor": "Исключение из прав для {{name}}",
|
||||
"accessRule.expandSection": "Развернуть {{title}}",
|
||||
"accessRule.individualPermissionSettings": "Индивидуальные настройки прав",
|
||||
"accessRule.individualPermissionSettingsTip": "Задайте исключения из прав для определенных участников или групп. Эти настройки переопределяют уровень доступа по умолчанию.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} заблокирован",
|
||||
"accessRule.lockedSummary_other": "· {{count}} заблокировано",
|
||||
"accessRule.maintainer": "Сопровождающий",
|
||||
"accessRule.member": "Участник",
|
||||
"accessRule.newPermissionSet": "Новый набор прав",
|
||||
"accessRule.noAvailableMembers": "Нет участников для добавления",
|
||||
"accessRule.noDescription": "Без описания",
|
||||
"accessRule.noRoles": "Нет ролей",
|
||||
"accessRule.noRules": "Нет правил доступа",
|
||||
"accessRule.noUserAccessSettings": "Нет индивидуальных настроек прав",
|
||||
"accessRule.permission": "Право",
|
||||
"accessRule.resourceOpenScope": "Область открытости ресурса",
|
||||
"accessRule.resourceOpenScopeDescription": "Выберите, кому открыт этот ресурс. Права роли по-прежнему определяют, что может делать каждый участник.",
|
||||
"accessRule.specificMembersOnly": "Только определенные участники",
|
||||
"accessRule.specificMembersOnlyDescription": "Доступ к этому ресурсу могут получить только выбранные участники.",
|
||||
"accessRule.summary_one": "{{count}} набор прав",
|
||||
"accessRule.summary_other": "{{count}} наборов прав",
|
||||
"accessRule.updated": "Правило доступа успешно обновлено",
|
||||
"common.duplicateAction": "Дублировать",
|
||||
"group.app": "Приложения",
|
||||
"group.app_acl": "Права доступа к приложению",
|
||||
"group.billing": "Биллинг",
|
||||
"group.credential": "Учетные данные",
|
||||
"group.dataset": "Базы знаний",
|
||||
"group.dataset_acl": "Права доступа к базе знаний",
|
||||
"group.integration": "Интеграции",
|
||||
"group.plugin": "Плагины",
|
||||
"group.tool_mcp": "Инструменты и MCP",
|
||||
"group.workspace": "Рабочее пространство",
|
||||
"permissionList.clearAll": "Очистить все",
|
||||
"permissionList.collapseGroup": "Свернуть группу",
|
||||
"permissionList.expandGroup": "Развернуть группу",
|
||||
"permissionList.noPermissionsFound": "Права не найдены",
|
||||
"permissionList.selectAll": "Выбрать все",
|
||||
"permissionSet.descriptionLabel": "Описание",
|
||||
"permissionSet.descriptionPlaceholder": "Опишите, что предоставляет этот набор прав",
|
||||
"permissionSet.learnMore": "Подробнее о правах",
|
||||
"permissionSet.modal.create.app.description": "Создайте набор прав приложения, на который можно ссылаться в правилах доступа для быстрой авторизации.",
|
||||
"permissionSet.modal.create.app.title": "Создать набор прав приложения",
|
||||
"permissionSet.modal.create.dataset.description": "Создайте набор прав базы знаний, на который можно ссылаться в правилах доступа для быстрой авторизации.",
|
||||
"permissionSet.modal.create.dataset.title": "Создать набор прав базы знаний",
|
||||
"permissionSet.modal.edit.app.description": "Измените имя, описание и предоставляемые права для этого набора прав.",
|
||||
"permissionSet.modal.edit.app.title": "Редактировать набор прав приложения",
|
||||
"permissionSet.modal.edit.dataset.description": "Измените имя, описание и предоставляемые права для этого набора прав.",
|
||||
"permissionSet.modal.edit.dataset.title": "Редактировать набор прав базы знаний",
|
||||
"permissionSet.modal.view.app.description": "Просмотрите имя, описание и предоставляемые права для этого набора прав.",
|
||||
"permissionSet.modal.view.app.title": "Просмотр набора прав приложения",
|
||||
"permissionSet.modal.view.dataset.description": "Просмотрите имя, описание и предоставляемые права для этого набора прав.",
|
||||
"permissionSet.modal.view.dataset.title": "Просмотр набора прав базы знаний",
|
||||
"permissionSet.nameLabel": "Имя набора прав",
|
||||
"permissionSet.namePlaceholder": "напр. Может экспортировать DSL",
|
||||
"permissionSet.permissions": "Права",
|
||||
"role.addRole": "Создать роли",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" назначена {{count}} участнику. Хотите, чтобы копия новой роли включала того же участника?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" назначена {{count}} участникам. Хотите, чтобы копия новой роли включала тех же участников?",
|
||||
"role.copyMembersLoading": "Загрузка назначений участников...",
|
||||
"role.copyMembersTitle": "Скопировать назначения участников?",
|
||||
"role.created": "Роль успешно создана",
|
||||
"role.deleteDescription": "Эта роль будет безвозвратно удалена и исключена из всех участников и правил доступа, которые ее используют.",
|
||||
"role.deleteTitle": "Удалить \"{{name}}\"?",
|
||||
"role.deleted": "Роль успешно удалена",
|
||||
"role.duplicated": "Роль успешно продублирована",
|
||||
"role.groups.builtin": "Системные роли",
|
||||
"role.groups.custom": "Пользовательские роли",
|
||||
"role.loading": "Загрузка ролей...",
|
||||
"role.modal.create.description": "Создайте роль и назначьте права",
|
||||
"role.modal.create.title": "Создать роль",
|
||||
"role.modal.descriptionLabel": "Описание",
|
||||
"role.modal.descriptionPlaceholder": "Опишите, за что отвечает эта роль",
|
||||
"role.modal.edit.description": "Редактировать данные роли и права",
|
||||
"role.modal.edit.title": "Редактировать роль",
|
||||
"role.modal.nameLabel": "Имя роли",
|
||||
"role.modal.namePlaceholder": "напр. Руководитель маркетинга",
|
||||
"role.modal.view.description": "Просмотр данных роли и прав",
|
||||
"role.modal.view.title": "Просмотр роли",
|
||||
"role.noDescription": "Без описания",
|
||||
"role.noMatchingRoles": "Нет подходящих ролей",
|
||||
"role.searchPlaceholder": "Поиск ролей...",
|
||||
"role.updated": "Роль успешно обновлена",
|
||||
"role.workspaceRoles.description": "Создавайте роли и определяйте, что каждая роль может делать в этом рабочем пространстве.",
|
||||
"role.workspaceRoles.title": "Роли рабочего пространства"
|
||||
}
|
||||
51
web/i18n/sl-SI/permission-keys.json
Normal file
51
web/i18n/sl-SI/permission-keys.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"api_extension.manage": "Upravljanje konfiguracije razširitve API",
|
||||
"app.access_config": "Konfiguracija dovoljenj za dostop do aplikacije",
|
||||
"app.acl.access_config": "Konfiguracija dovoljenj za dostop do aplikacije",
|
||||
"app.acl.delete": "Izbriši aplikacijo",
|
||||
"app.acl.edit": "Uredi in orkestriraj aplikacijo",
|
||||
"app.acl.import_export_dsl": "Uvozi / izvozi DSL",
|
||||
"app.acl.monitor": "Spremljanje in operacije",
|
||||
"app.acl.preview": "Predogled aplikacije",
|
||||
"app.acl.release_and_version": "Objavljanje aplikacije in upravljanje različic",
|
||||
"app.acl.test_and_run": "Preizkusi in uporabi aplikacijo",
|
||||
"app.acl.view_layout": "Stran za orkestracijo samo za branje",
|
||||
"app.create_and_management": "Ustvarjanje aplikacij in upravljanje aplikacij, ki ste jih ustvarili",
|
||||
"app.tag.manage": "Upravljanje oznak aplikacij",
|
||||
"app_library.access": "Dostop do knjižnice aplikacij",
|
||||
"billing.manage": "Spremeni naročniške načrte",
|
||||
"billing.subscription.manage": "Upravljanje obračunavanja in naročnin v portalu za obračunavanje",
|
||||
"billing.view": "Dostop do nastavitev obračunavanja",
|
||||
"credential.create": "Dodaj poverilnice",
|
||||
"credential.manage": "Uredi in izbriši poverilnice",
|
||||
"credential.use": "Ogled in uporaba poverilnic",
|
||||
"customization.manage": "Upravljanje prilagoditev",
|
||||
"data_source.manage": "Upravljanje konfiguracije vira podatkov",
|
||||
"dataset.access_config": "Konfiguracija dovoljenj za dostop do baze znanja",
|
||||
"dataset.acl.access_config": "Konfiguracija dovoljenj za dostop do baze znanja",
|
||||
"dataset.acl.delete": "Izbriši bazo znanja",
|
||||
"dataset.acl.delete_file": "Izbriši datoteke baze znanja",
|
||||
"dataset.acl.document_download": "Prenesi dokumente",
|
||||
"dataset.acl.edit": "Uredi bazo znanja",
|
||||
"dataset.acl.import_export_dsl": "Uvozi / izvozi DSL cevovoda znanja",
|
||||
"dataset.acl.pipeline_release": "Objavljanje cevovoda znanja in upravljanje različic",
|
||||
"dataset.acl.pipeline_test": "Preizkušanje cevovoda",
|
||||
"dataset.acl.preview": "Predogled baze znanja",
|
||||
"dataset.acl.readonly": "Baza znanja samo za branje",
|
||||
"dataset.acl.retrieval_recall": "Pridobivanje iz baze znanja",
|
||||
"dataset.acl.use": "Dodaj dokumente v bazo znanja",
|
||||
"dataset.api_key.manage": "Upravljanje API ključev baze znanja",
|
||||
"dataset.create_and_management": "Ustvarjanje baz znanja in upravljanje baz znanja, ki ste jih ustvarili",
|
||||
"dataset.external.connect": "Poveži zunanje baze znanja",
|
||||
"dataset.tag.manage": "Upravljanje oznak baze znanja",
|
||||
"mcp.manage": "Upravljanje MCP",
|
||||
"plugin.debug": "Razhroščevanje vtičnikov",
|
||||
"plugin.install": "Namesti in posodobi vtičnike",
|
||||
"plugin.manage": "Upravljanje vtičnikov",
|
||||
"plugin.plugin_preferences": "Upravljanje nastavitev vtičnikov",
|
||||
"snippets.create_and_modify": "Ustvarjanje in spreminjanje izsekov",
|
||||
"snippets.management": "Upravljanje izsekov",
|
||||
"tool.manage": "Upravljanje orodij",
|
||||
"workspace.member.manage": "Upravljanje članov",
|
||||
"workspace.role.manage": "Upravljanje dovoljenj vlog in pravil za dostop do virov"
|
||||
}
|
||||
105
web/i18n/sl-SI/permission.json
Normal file
105
web/i18n/sl-SI/permission.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"accessRule.actions": "Dejanja",
|
||||
"accessRule.addMemberAria": "Dodaj {{name}}",
|
||||
"accessRule.addMembersTitle": "Dodaj člane",
|
||||
"accessRule.allPermittedMembers": "Vsi člani z dovoljenji vlog",
|
||||
"accessRule.allPermittedMembersDescription": "Člani z ustreznimi dovoljenji vlog lahko dostopajo do tega vira.",
|
||||
"accessRule.appDescription": "Nadzorujte, komu je ta aplikacija na voljo. Člani še vedno potrebujejo dovoljenja vlog za ogled ali upravljanje.",
|
||||
"accessRule.appTitle": "Pravila za dostop do aplikacije",
|
||||
"accessRule.changeOpenScopeDescription": "Sprememba obsega odprtosti bo ponastavila vse individualne nastavitve dovoljenj za ta vir. Po preklopu boste morali znova dodati dovoljenja, specifična za člane.",
|
||||
"accessRule.changeOpenScopeTitle": "Spremeni obseg odprtosti vira?",
|
||||
"accessRule.collapseSection": "Strni {{title}}",
|
||||
"accessRule.copied": "Pravilo za dostop uspešno kopirano",
|
||||
"accessRule.created": "Pravilo za dostop uspešno ustvarjeno",
|
||||
"accessRule.datasetDescription": "Nadzorujte, komu je ta baza znanja na voljo. Člani še vedno potrebujejo dovoljenja vlog za ogled ali upravljanje.",
|
||||
"accessRule.datasetTitle": "Pravila za dostop do baze znanja",
|
||||
"accessRule.defaultPermission": "Po dovoljenjih vlog",
|
||||
"accessRule.deleteDescription": "To pravilo za dostop bo trajno izbrisano in odstranjeno s seznama avtorizacij vira.",
|
||||
"accessRule.deleteTitle": "Izbrisati \"{{name}}\"?",
|
||||
"accessRule.deleted": "Pravilo za dostop uspešno izbrisano",
|
||||
"accessRule.exceptionPermissionFor": "Izjemno dovoljenje za {{name}}",
|
||||
"accessRule.expandSection": "Razširi {{title}}",
|
||||
"accessRule.individualPermissionSettings": "Individualne nastavitve dovoljenj",
|
||||
"accessRule.individualPermissionSettingsTip": "Nastavite izjeme dovoljenj za določene sodelavce ali skupine. Te nastavitve preglasijo privzeto raven dostopa.",
|
||||
"accessRule.lockedSummary_one": "· {{count}} zaklenjen",
|
||||
"accessRule.lockedSummary_other": "· {{count}} zaklenjenih",
|
||||
"accessRule.maintainer": "Vzdrževalec",
|
||||
"accessRule.member": "Član",
|
||||
"accessRule.newPermissionSet": "Nov nabor dovoljenj",
|
||||
"accessRule.noAvailableMembers": "Ni članov, ki bi jih bilo mogoče dodati",
|
||||
"accessRule.noDescription": "Brez opisa",
|
||||
"accessRule.noRoles": "Brez vlog",
|
||||
"accessRule.noRules": "Brez pravil za dostop",
|
||||
"accessRule.noUserAccessSettings": "Brez individualnih nastavitev dovoljenj",
|
||||
"accessRule.permission": "Dovoljenje",
|
||||
"accessRule.resourceOpenScope": "Obseg odprtosti vira",
|
||||
"accessRule.resourceOpenScopeDescription": "Izberite, komu je ta vir na voljo. Dovoljenja vlog še vedno določajo, kaj lahko vsak član počne.",
|
||||
"accessRule.specificMembersOnly": "Samo določeni člani",
|
||||
"accessRule.specificMembersOnlyDescription": "Do tega vira lahko dostopajo samo izbrani člani.",
|
||||
"accessRule.summary_one": "{{count}} nabor dovoljenj",
|
||||
"accessRule.summary_other": "{{count}} naborov dovoljenj",
|
||||
"accessRule.updated": "Pravilo za dostop uspešno posodobljeno",
|
||||
"common.duplicateAction": "Podvoji",
|
||||
"group.app": "Aplikacije",
|
||||
"group.app_acl": "Dovoljenja za dostop do aplikacije",
|
||||
"group.billing": "Obračunavanje",
|
||||
"group.credential": "Poverilnice",
|
||||
"group.dataset": "Baze znanja",
|
||||
"group.dataset_acl": "Dovoljenja za dostop do baze znanja",
|
||||
"group.integration": "Integracije",
|
||||
"group.plugin": "Vtičniki",
|
||||
"group.tool_mcp": "Orodja in MCP",
|
||||
"group.workspace": "Delovni prostor",
|
||||
"permissionList.clearAll": "Počisti vse",
|
||||
"permissionList.collapseGroup": "Strni skupino",
|
||||
"permissionList.expandGroup": "Razširi skupino",
|
||||
"permissionList.noPermissionsFound": "Ni najdenih dovoljenj",
|
||||
"permissionList.selectAll": "Izberi vse",
|
||||
"permissionSet.descriptionLabel": "Opis",
|
||||
"permissionSet.descriptionPlaceholder": "Opišite, kaj ta nabor dovoljenj podeljuje",
|
||||
"permissionSet.learnMore": "Več o dovoljenjih",
|
||||
"permissionSet.modal.create.app.description": "Ustvarite nabor dovoljenj za aplikacijo, na katerega se je mogoče sklicevati v pravilih za dostop za hitro avtorizacijo.",
|
||||
"permissionSet.modal.create.app.title": "Ustvari nabor dovoljenj za aplikacijo",
|
||||
"permissionSet.modal.create.dataset.description": "Ustvarite nabor dovoljenj za bazo znanja, na katerega se je mogoče sklicevati v pravilih za dostop za hitro avtorizacijo.",
|
||||
"permissionSet.modal.create.dataset.title": "Ustvari nabor dovoljenj za bazo znanja",
|
||||
"permissionSet.modal.edit.app.description": "Spremenite ime, opis in dovoljenja, podeljena za ta nabor dovoljenj.",
|
||||
"permissionSet.modal.edit.app.title": "Uredi nabor dovoljenj za aplikacijo",
|
||||
"permissionSet.modal.edit.dataset.description": "Spremenite ime, opis in dovoljenja, podeljena za ta nabor dovoljenj.",
|
||||
"permissionSet.modal.edit.dataset.title": "Uredi nabor dovoljenj za bazo znanja",
|
||||
"permissionSet.modal.view.app.description": "Oglejte si ime, opis in dovoljenja, podeljena za ta nabor dovoljenj.",
|
||||
"permissionSet.modal.view.app.title": "Ogled nabora dovoljenj za aplikacijo",
|
||||
"permissionSet.modal.view.dataset.description": "Oglejte si ime, opis in dovoljenja, podeljena za ta nabor dovoljenj.",
|
||||
"permissionSet.modal.view.dataset.title": "Ogled nabora dovoljenj za bazo znanja",
|
||||
"permissionSet.nameLabel": "Ime nabora dovoljenj",
|
||||
"permissionSet.namePlaceholder": "npr. Lahko izvozi DSL",
|
||||
"permissionSet.permissions": "Dovoljenja",
|
||||
"role.addRole": "Ustvari vloge",
|
||||
"role.copyMembersDescription_one": "\"{{name}}\" je dodeljen {{count}} članu. Ali želite, da nova kopija vloge vključuje istega člana?",
|
||||
"role.copyMembersDescription_other": "\"{{name}}\" je dodeljen {{count}} članom. Ali želite, da nova kopija vloge vključuje iste člane?",
|
||||
"role.copyMembersLoading": "Nalaganje dodelitev članov...",
|
||||
"role.copyMembersTitle": "Kopirati dodelitve članov?",
|
||||
"role.created": "Vloga uspešno ustvarjena",
|
||||
"role.deleteDescription": "Ta vloga bo trajno izbrisana in odstranjena iz vseh članov ali pravil za dostop, ki jo uporabljajo.",
|
||||
"role.deleteTitle": "Izbrisati \"{{name}}\"?",
|
||||
"role.deleted": "Vloga uspešno izbrisana",
|
||||
"role.duplicated": "Vloga uspešno podvojena",
|
||||
"role.groups.builtin": "Sistemske vloge",
|
||||
"role.groups.custom": "Vloge po meri",
|
||||
"role.loading": "Nalaganje vlog...",
|
||||
"role.modal.create.description": "Ustvarite vlogo in dodelite dovoljenja",
|
||||
"role.modal.create.title": "Ustvari vlogo",
|
||||
"role.modal.descriptionLabel": "Opis",
|
||||
"role.modal.descriptionPlaceholder": "Opišite, za kaj je ta vloga odgovorna",
|
||||
"role.modal.edit.description": "Uredi podrobnosti in dovoljenja vloge",
|
||||
"role.modal.edit.title": "Uredi vlogo",
|
||||
"role.modal.nameLabel": "Ime vloge",
|
||||
"role.modal.namePlaceholder": "npr. Vodja marketinga",
|
||||
"role.modal.view.description": "Ogled podrobnosti in dovoljenj vloge",
|
||||
"role.modal.view.title": "Ogled vloge",
|
||||
"role.noDescription": "Brez opisa",
|
||||
"role.noMatchingRoles": "Ni ujemajočih se vlog",
|
||||
"role.searchPlaceholder": "Išči vloge...",
|
||||
"role.updated": "Vloga uspešno posodobljena",
|
||||
"role.workspaceRoles.description": "Ustvarite vloge in določite, kaj lahko vsaka vloga počne v tem delovnem prostoru.",
|
||||
"role.workspaceRoles.title": "Vloge delovnega prostora"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user