Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/frontend/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/frontend/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/frontend/src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "名前",
Expand Down
5 changes: 5 additions & 0 deletions src/frontend/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/frontend/src/locales/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "名称",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Badge
variant={style.variant}
size="sm"
className="cursor-default"
title={t("toolsModal.accessHintTooltip")}
data-testid={`access-hint-${hint}`}
>
{t(style.labelKey)}
</Badge>
);
}
Original file line number Diff line number Diff line change
@@ -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(<AccessHintBadge hint="read_only" />);
expect(screen.getByTestId("access-hint-read_only")).toBeInTheDocument();
});

it("should label a writing tool", () => {
render(<AccessHintBadge hint="write" />);
expect(screen.getByTestId("access-hint-write")).toBeInTheDocument();
});

it("should label a destructive tool", () => {
render(<AccessHintBadge hint="destructive" />);
expect(screen.getByTestId("access-hint-destructive")).toBeInTheDocument();
});

it("should render nothing when the server declared no hint", () => {
const { container } = render(<AccessHintBadge hint={null} />);
expect(container).toBeEmptyDOMElement();
});

it("should render nothing when the field is absent", () => {
const { container } = render(<AccessHintBadge />);
expect(container).toBeEmptyDOMElement();
});

it("should render nothing for a value it does not recognize", () => {
const { container } = render(<AccessHintBadge hint="idempotent" />);
expect(container).toBeEmptyDOMElement();
});
});
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement & HTMLTextAreaElement>,
) => void;
};

jest.mock("@/components/common/genericIconComponent", () => ({
__esModule: true,
default: ({ name }: { name?: string }) => <span>{name}</span>,
}));

jest.mock("@/components/common/shadTooltipComponent", () => ({
__esModule: true,
default: ({ children }: Children) => <div>{children}</div>,
}));

jest.mock("@/components/ui/input", () => ({
Input: ({ value, onChange, placeholder }: InputLike) => (
<input value={value} onChange={onChange} placeholder={placeholder} />
),
}));

jest.mock("@/components/ui/button", () => ({
Button: ({ children }: Children) => <button type="button">{children}</button>,
}));

jest.mock("@/components/ui/textarea", () => ({
Textarea: ({ value, onChange }: InputLike) => (
<textarea value={value} onChange={onChange} />
),
}));

jest.mock("@/components/ui/separator", () => ({
Separator: () => <hr />,
}));

jest.mock("@/components/ui/sidebar", () => ({
Sidebar: ({ children }: Children) => <div>{children}</div>,
SidebarContent: ({ children }: Children) => <div>{children}</div>,
SidebarFooter: ({ children }: Children) => <div>{children}</div>,
SidebarGroup: ({ children }: Children) => <div>{children}</div>,
SidebarGroupContent: ({ children }: Children) => <div>{children}</div>,
useSidebar: () => ({ setOpen: jest.fn() }),
}));

// Report the column order the table was handed, so the assertions read the real
// columnDefs rather than ag-grid's rendered output.
jest.mock(
"@/components/core/parameterRenderComponent/components/tableComponent",
() => ({
__esModule: true,
default: React.forwardRef(
(props: { columnDefs: { field?: string }[] }, _ref: unknown) => (
<div data-testid="column-fields">
{props.columnDefs.map((col) => col.field).join(",")}
</div>
),
),
}),
);

jest.mock("@/utils/stringManipulation", () => ({
parseString: (str: string) => str,
sanitizeMcpName: (str: string) => str,
}));

const tool = (extra: Record<string, unknown> = {}) => ({
name: "fetch",
display_name: "Fetch",
description: "Fetch a page",
display_description: "Fetch a page",
status: true,
tags: ["fetch"],
readonly: false,
...extra,
});

const defaultProps = {
data: [],
setData: jest.fn(),
isAction: false,
placeholder: "Select tools",
open: true,
handleOnNewValue: jest.fn(),
};

const columnFields = () =>
(screen.getByTestId("column-fields").textContent ?? "").split(",");

describe("ToolsTable access column", () => {
it("should omit the column when no tool declares a hint", () => {
render(<ToolsTable {...defaultProps} rows={[tool(), tool()]} />);
expect(columnFields()).not.toContain("access_hint");
});

it("should show the column when a tool declares a hint", () => {
render(
<ToolsTable
{...defaultProps}
rows={[tool(), tool({ access_hint: "destructive" })]}
/>,
);
expect(columnFields()).toContain("access_hint");
});

it("should keep the column adjacent to the approval toggle", () => {
render(
<ToolsTable
{...defaultProps}
rows={[tool({ access_hint: "read_only" })]}
/>,
);
const fields = columnFields();
expect(fields.indexOf("approval_actions")).toBe(
fields.indexOf("access_hint") + 1,
);
});
});
Loading
Loading