diff --git a/src/frontend/src/locales/de.json b/src/frontend/src/locales/de.json index 41b8217607cf..a4fc09d333f1 100644 --- a/src/frontend/src/locales/de.json +++ b/src/frontend/src/locales/de.json @@ -2148,6 +2148,11 @@ "toolsModal.actionSlugHint": "Wird als Funktionsname verwendet, wenn dieser Ablauf für Kunden zugänglich gemacht wird.", "toolsModal.close": "Schließen", "toolsModal.columnApproval": "Erfordert Genehmigung", + "toolsModal.columnAccess": "Zugriff", + "toolsModal.accessReadOnly": "Nur Lesen", + "toolsModal.accessWrite": "Schreiben", + "toolsModal.accessDestructive": "Destruktiv", + "toolsModal.accessHintTooltip": "Verhaltenshinweis vom Server des Tools. Ein Hinweis, keine Garantie.", "toolsModal.columnDescription": "Beschreibung", "toolsModal.columnFlowName": "Name des Ablaufs", "toolsModal.columnName": "Ihren Namen", diff --git a/src/frontend/src/locales/en.json b/src/frontend/src/locales/en.json index 92f7c369184d..f9284b395b3e 100644 --- a/src/frontend/src/locales/en.json +++ b/src/frontend/src/locales/en.json @@ -169,6 +169,11 @@ "toolsModal.columnDescription": "Description", "toolsModal.columnSlug": "Slug", "toolsModal.columnApproval": "Requires Approval", + "toolsModal.columnAccess": "Access", + "toolsModal.accessReadOnly": "Read-only", + "toolsModal.accessWrite": "Write", + "toolsModal.accessDestructive": "Destructive", + "toolsModal.accessHintTooltip": "Behavior hint declared by the tool's server. A hint, not a guarantee.", "toolsModal.columnTool": "Tool", "toolsModal.labelSlug": "Slug", "toolsModal.labelToolName": "Tool name", diff --git a/src/frontend/src/locales/es.json b/src/frontend/src/locales/es.json index 0fedf26a1d5f..d31ded62ad9c 100644 --- a/src/frontend/src/locales/es.json +++ b/src/frontend/src/locales/es.json @@ -2148,6 +2148,11 @@ "toolsModal.actionSlugHint": "Se utiliza como nombre de la función cuando este flujo se pone a disposición de los clientes.", "toolsModal.close": "Cerrar", "toolsModal.columnApproval": "Requiere aprobación", + "toolsModal.columnAccess": "Acceso", + "toolsModal.accessReadOnly": "Solo lectura", + "toolsModal.accessWrite": "Escritura", + "toolsModal.accessDestructive": "Destructivo", + "toolsModal.accessHintTooltip": "Indicio de comportamiento declarado por el servidor de la herramienta. Es un indicio, no una garantía.", "toolsModal.columnDescription": "Descripción", "toolsModal.columnFlowName": "Nombre de flujo", "toolsModal.columnName": "Nombre", diff --git a/src/frontend/src/locales/fr.json b/src/frontend/src/locales/fr.json index 02a1135bfa9f..eb7ef8210cec 100644 --- a/src/frontend/src/locales/fr.json +++ b/src/frontend/src/locales/fr.json @@ -2148,6 +2148,11 @@ "toolsModal.actionSlugHint": "Utilisé comme nom de fonction lorsque ce flux est mis à la disposition des clients.", "toolsModal.close": "Fermer", "toolsModal.columnApproval": "Soumis à validation", + "toolsModal.columnAccess": "Accès", + "toolsModal.accessReadOnly": "Lecture seule", + "toolsModal.accessWrite": "Écriture", + "toolsModal.accessDestructive": "Destructif", + "toolsModal.accessHintTooltip": "Indication de comportement déclarée par le serveur de l'outil. Une indication, pas une garantie.", "toolsModal.columnDescription": "Descriptif", "toolsModal.columnFlowName": "Nom du flux", "toolsModal.columnName": "Nom", diff --git a/src/frontend/src/locales/ja.json b/src/frontend/src/locales/ja.json index a6a6f8a8aeb7..55f451c9f6c2 100644 --- a/src/frontend/src/locales/ja.json +++ b/src/frontend/src/locales/ja.json @@ -2148,6 +2148,11 @@ "toolsModal.actionSlugHint": "このフローをクライアントに公開する際の関数名として使用されます。", "toolsModal.close": "閉じる", "toolsModal.columnApproval": "承認が必要", + "toolsModal.columnAccess": "アクセス", + "toolsModal.accessReadOnly": "読み取り専用", + "toolsModal.accessWrite": "書き込み", + "toolsModal.accessDestructive": "破壊的", + "toolsModal.accessHintTooltip": "ツールのサーバーが宣言した動作のヒントです。保証ではありません。", "toolsModal.columnDescription": "説明", "toolsModal.columnFlowName": "フロー名", "toolsModal.columnName": "名前", diff --git a/src/frontend/src/locales/pt.json b/src/frontend/src/locales/pt.json index ddf5c12dc8b4..71bb2a67f58f 100644 --- a/src/frontend/src/locales/pt.json +++ b/src/frontend/src/locales/pt.json @@ -2148,6 +2148,11 @@ "toolsModal.actionSlugHint": "Usado como nome da função quando este fluxo é disponibilizado aos clientes.", "toolsModal.close": "Fechar", "toolsModal.columnApproval": "Requer aprovação", + "toolsModal.columnAccess": "Acesso", + "toolsModal.accessReadOnly": "Somente leitura", + "toolsModal.accessWrite": "Escrita", + "toolsModal.accessDestructive": "Destrutivo", + "toolsModal.accessHintTooltip": "Indicação de comportamento declarada pelo servidor da ferramenta. É uma indicação, não uma garantia.", "toolsModal.columnDescription": "Descrição", "toolsModal.columnFlowName": "Nome do fluxo", "toolsModal.columnName": "Nome", diff --git a/src/frontend/src/locales/zh-Hans.json b/src/frontend/src/locales/zh-Hans.json index 3788adc72b9b..558904526a7d 100644 --- a/src/frontend/src/locales/zh-Hans.json +++ b/src/frontend/src/locales/zh-Hans.json @@ -2148,6 +2148,11 @@ "toolsModal.actionSlugHint": "当此流程向客户端公开时,用作函数名称。", "toolsModal.close": "关闭", "toolsModal.columnApproval": "需要核准", + "toolsModal.columnAccess": "访问", + "toolsModal.accessReadOnly": "只读", + "toolsModal.accessWrite": "写入", + "toolsModal.accessDestructive": "破坏性", + "toolsModal.accessHintTooltip": "工具服务器声明的行为提示。仅为提示,并非保证。", "toolsModal.columnDescription": "描述", "toolsModal.columnFlowName": "流程名称", "toolsModal.columnName": "名称", diff --git a/src/frontend/src/modals/toolsModal/components/toolsTable/AccessHintBadge.tsx b/src/frontend/src/modals/toolsModal/components/toolsTable/AccessHintBadge.tsx new file mode 100644 index 000000000000..e588088f821d --- /dev/null +++ b/src/frontend/src/modals/toolsModal/components/toolsTable/AccessHintBadge.tsx @@ -0,0 +1,45 @@ +import { useTranslation } from "react-i18next"; +import type { BadgeProps } from "@/components/ui/badge"; +import { Badge } from "@/components/ui/badge"; + +// Values produced by `_tool_access_hint` in lfx/base/mcp/util.py from the MCP +// server's ToolAnnotations. A tool whose source declares nothing has no hint. +const HINT_STYLES: Record< + string, + { variant: BadgeProps["variant"]; labelKey: string } +> = { + read_only: { + variant: "secondaryStatic", + labelKey: "toolsModal.accessReadOnly", + }, + write: { variant: "outline", labelKey: "toolsModal.accessWrite" }, + destructive: { + variant: "errorStatic", + labelKey: "toolsModal.accessDestructive", + }, +}; + +export function AccessHintBadge({ + hint, +}: { + hint?: string | null; +}): JSX.Element | null { + const { t } = useTranslation(); + const style = hint ? HINT_STYLES[hint] : undefined; + + // No badge rather than an "unknown" one: most servers send no annotations, and a + // column of placeholders would read as a finding about the tool. + if (!style) return null; + + return ( + + {t(style.labelKey)} + + ); +} diff --git a/src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/accessHintBadge.test.tsx b/src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/accessHintBadge.test.tsx new file mode 100644 index 000000000000..13536794e266 --- /dev/null +++ b/src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/accessHintBadge.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from "@testing-library/react"; +import { AccessHintBadge } from "../AccessHintBadge"; + +describe("AccessHintBadge", () => { + it("should label a read-only tool", () => { + render(); + expect(screen.getByTestId("access-hint-read_only")).toBeInTheDocument(); + }); + + it("should label a writing tool", () => { + render(); + expect(screen.getByTestId("access-hint-write")).toBeInTheDocument(); + }); + + it("should label a destructive tool", () => { + render(); + expect(screen.getByTestId("access-hint-destructive")).toBeInTheDocument(); + }); + + it("should render nothing when the server declared no hint", () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it("should render nothing when the field is absent", () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it("should render nothing for a value it does not recognize", () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/toolsTable-accessColumn.test.tsx b/src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/toolsTable-accessColumn.test.tsx new file mode 100644 index 000000000000..6888a415ae92 --- /dev/null +++ b/src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/toolsTable-accessColumn.test.tsx @@ -0,0 +1,126 @@ +import { render, screen } from "@testing-library/react"; +import type { ReactNode } from "react"; +import React from "react"; +import ToolsTable from "../index"; + +type Children = { children?: ReactNode }; +type InputLike = { + value?: string; + placeholder?: string; + onChange?: ( + event: React.ChangeEvent, + ) => void; +}; + +jest.mock("@/components/common/genericIconComponent", () => ({ + __esModule: true, + default: ({ name }: { name?: string }) => {name}, +})); + +jest.mock("@/components/common/shadTooltipComponent", () => ({ + __esModule: true, + default: ({ children }: Children) =>
{children}
, +})); + +jest.mock("@/components/ui/input", () => ({ + Input: ({ value, onChange, placeholder }: InputLike) => ( + + ), +})); + +jest.mock("@/components/ui/button", () => ({ + Button: ({ children }: Children) => , +})); + +jest.mock("@/components/ui/textarea", () => ({ + Textarea: ({ value, onChange }: InputLike) => ( +