chore(ui): drop the antd dependency and its leftovers (#37574)
Some checks failed
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
CI Coverage / assert-ci-coverage (push) Waiting to run
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Publish basedpyright base counts / publish (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Unit Tests: Core Utilities / core-utils (push) Has been cancelled
Unit Tests: Enterprise, Google GenAI & Routing / enterprise-routing (push) Has been cancelled
Unit Tests: Integrations (Callbacks & Logging) / integrations (push) Has been cancelled
Unit Tests: LLM Provider Transformations / Vertex AI (push) Has been cancelled
Unit Tests: LLM Provider Transformations / All Other Providers (push) Has been cancelled
Unit Tests: MCP, Secrets, Containers & Misc / misc (push) Has been cancelled
Unit Tests: Proxy Auth & Key Management / proxy-auth (push) Has been cancelled
Unit Tests: Proxy Infrastructure / proxy-infra (push) Has been cancelled
Unit Tests: Responses, Caching & Types / responses-caching-types (push) Has been cancelled
Unit Tests: Proxy API Endpoints / proxy-endpoints (push) Has been cancelled
Unit Tests: Proxy API Endpoints / proxy-server (push) Has been cancelled

Nothing in the dashboard renders antd any more, so the package and the
scaffolding around it can go. This removes `antd` and
`@ant-design/cssinjs` from package.json, deletes the global StyleProvider
the root layout wrapped every page in, drops the `antd` cascade layer and
the z-index override that lifted Base UI popups over an antd Modal, and
retires the lint rules that policed antd imports and antd class selectors
in tests.

Fifteen test files still carried `vi.mock("antd", ...)` factories for
components that stopped importing antd during the migration. They were
inert, and they resolve the real module, so they would have broken the
moment the package left node_modules.

The compatibility shims keep their behaviour and lose the antd name:
`antdRules`/`antdRequired` become `validatorRules`/`requiredRule`,
`isAntdUrl` becomes `isValidUrl`, and `ABOVE_ANTD_MODAL` becomes
`NESTED_DIALOG_LAYER`. Comments that explain why a contract looks the way
it does still name antd, because that history is the reason.
This commit is contained in:
ryan-crabbe-berri 2026-08-19 20:44:27 -07:00 • committed by GitHub
parent 0b374541bb
commit 7b574b9df6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 105 additions and 1410 deletions

View file

@ -12,13 +12,13 @@ Most of the suite predates this split and is not yet classified, so an unsuffixe
Assert something the user could perceive, and assert it precisely enough that the test fails when the behaviour breaks. `eslint-plugin-testing-library` and `eslint-plugin-jest-dom` enforce the mechanical part of that. Two of the enabled rules exist because the failure they catch is silent rather than cosmetic: `await-async-queries` catches an unawaited `findBy*`, whose returned Promise is always truthy and makes the whole assertion vacuous, and `no-wait-for-side-effects` catches work inside a `waitFor` callback, which is retried on every poll. Prefer `findBy*` over `waitFor` wrapped around `getBy*`, and keep a `waitFor` callback to a single assertion
Do not trust `eslint --fix` for these two plugins. Fixing the suite in bulk produced seven distinct kinds of broken output. Four fail loudly: `no-wait-for-side-effects` and `no-wait-for-multiple-assertions` hoist a statement out of the `waitFor` callback while leaving the `const` it reads inside, `prefer-enabled-disabled` drops a closing paren when the subject carries a type assertion, `prefer-presence-queries` swaps in a query it never destructures, and `prefer-in-document` collapses `getAllBy*` to `getBy*` on a value still indexed as an array. Two fail quietly, which is worse: `prefer-checked` swaps the `checked` attribute for the `.checked` property, and antd radios set one without the other, and `prefer-to-have-text-content` wraps arbitrary strings in `new RegExp()` without escaping, so `toContain("100K+ requests")` becomes a pattern meaning "100 followed by one-or-more K". That last one compiles, lints clean, and keeps passing while no longer asserting what it says. Pass a plain string to `toHaveTextContent`, which is already a substring match. Run the fixer on a handful of files at a time and read the diff
Do not trust `eslint --fix` for these two plugins. Fixing the suite in bulk produced seven distinct kinds of broken output. Four fail loudly: `no-wait-for-side-effects` and `no-wait-for-multiple-assertions` hoist a statement out of the `waitFor` callback while leaving the `const` it reads inside, `prefer-enabled-disabled` drops a closing paren when the subject carries a type assertion, `prefer-presence-queries` swaps in a query it never destructures, and `prefer-in-document` collapses `getAllBy*` to `getBy*` on a value still indexed as an array. Two fail quietly, which is worse: `prefer-checked` swaps the `checked` attribute for the `.checked` property, and a radio can set one without the other, and `prefer-to-have-text-content` wraps arbitrary strings in `new RegExp()` without escaping, so `toContain("100K+ requests")` becomes a pattern meaning "100 followed by one-or-more K". That last one compiles, lints clean, and keeps passing while no longer asserting what it says. Pass a plain string to `toHaveTextContent`, which is already a substring match. Run the fixer on a handful of files at a time and read the diff
`jest-dom/prefer-to-have-value` stays off because its fixer is wrong here, not merely noisy. It matches any attribute whose name contains "value", so it rewrites `toHaveAttribute("aria-valuenow", n)` into `toHaveValue(n)`, and jest-dom's `toHaveValue` only supports form controls, so the assertion fails on the `role="meter"` elements the dashboard renders. Assert ARIA value attributes with `toHaveAttribute`
Reach for `fireEvent.change` rather than `user.type` when a test only needs a field to hold a value. `user.type` dispatches one event per character and re-renders the whole form each time, which is why a single form test could burn seven seconds. Keep `user.type` where the typing itself is the behaviour under test: an autocomplete that filters per keystroke, a debounce, a key handler, or any Base UI combobox, whose filter state is driven by real keyboard input and does not react to a raw change event
A test may reach for a component library's own CSS class only when that library exposes no role, label, title or ARIA state to query instead, and then the line carries a suppression naming the rule and the reason. Check first: antd icons render as `role="img"` with an `aria-label`, and antd `Form.Item` associates its label with the control, so both are reachable accessibly. When a label does not resolve, suspect the control rather than the test, since a custom wrapper that destructures props without spreading them drops the `id` antd injects and leaves the rendered label pointing at nothing
A test may reach for a component library's own CSS class only when that library exposes no role, label, title or ARIA state to query instead. Check first: the shadcn primitives forward roles and `aria-label`, and the shared form field associates its label with the control, so both are reachable accessibly. When nothing accessible identifies the element, prefer its `data-slot` attribute, which the primitives set deliberately and treat as stable. When a label does not resolve, suspect the control rather than the test, since a custom wrapper that destructures props without spreading them drops the `id` the field generates and leaves the rendered label pointing at nothing
Rules beyond the enabled set were measured against the whole suite and left off rather than recorded in a budget file, because a ceiling that permits a violation anywhere is worse than an honest gap. `no-node-access` and `no-container` are the ones worth revisiting first, since they catch the DOM archaeology the rules above only discourage. `prefer-implicit-assert` and `prefer-explicit-assert` contradict each other, so neither is enabled

View file

@ -1181,11 +1181,6 @@
"count": 1
}
},
"src/app/(dashboard)/users/_components/user_edit_view.test.tsx": {
"no-nested-ternary": {
"count": 1
}
},
"src/app/(dashboard)/users/_components/user_edit_view.tsx": {
"local/filename-pascal-case": {
"count": 1

View file

@ -57,15 +57,6 @@ const eslintConfig = [
message:
"@tremor/react is being phased out; build new UI with shadcn/ui primitives instead of adding tremor imports.",
},
{
group: ["antd", "antd/*"],
message:
"antd is being phased out; build new UI with shadcn/ui primitives instead of adding antd imports.",
},
{
group: ["@ant-design/icons", "@ant-design/icons/*"],
message: "@ant-design/icons is gone from the dashboard; use lucide-react instead.",
},
],
},
],
@ -94,7 +85,6 @@ const eslintConfig = [
files: ["src/**/*.test.{ts,tsx}", "tests/**/*.{ts,tsx}"],
plugins: { "testing-library": testingLibrary, "jest-dom": jestDom },
rules: {
"local/no-antd-class-selectors": "error",
"testing-library/await-async-queries": "error",
"testing-library/no-wait-for-multiple-assertions": "error",
"testing-library/no-wait-for-side-effects": "error",

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,6 @@
"gen:api": "node scripts/gen-api-types.mjs"
},
"dependencies": {
"@ant-design/cssinjs": "1.24.0",
"@anthropic-ai/sdk": "0.92.0",
"@base-ui/react": "^1.6.0",
"@headlessui/tailwindcss": "0.2.2",
@ -34,7 +33,6 @@
"@tanstack/react-query": "5.100.7",
"@tanstack/react-table": "8.21.3",
"@types/papaparse": "5.5.2",
"antd": "5.29.3",
"cva": "1.0.0-beta.4",
"date-fns": "^4.4.0",
"dayjs": "1.11.19",

View file

@ -2,7 +2,6 @@ import noLargeInlineObjectArg from "./no-large-inline-object-arg.mjs";
import noLongConditionChain from "./no-long-condition-chain.mjs";
import noComplexJsxArrow from "./no-complex-jsx-arrow.mjs";
import filenamePascalCase from "./filename-pascal-case.mjs";
import noAntdClassSelectors from "./no-antd-class-selectors.mjs";
const plugin = {
rules: {
@ -10,7 +9,6 @@ const plugin = {
"no-long-condition-chain": noLongConditionChain,
"no-complex-jsx-arrow": noComplexJsxArrow,
"filename-pascal-case": filenamePascalCase,
"no-antd-class-selectors": noAntdClassSelectors,
},
};

View file

@ -1,45 +0,0 @@
const SELECTOR_REFERENCE = /\.(?:ant|anticon)-[a-z0-9-]+/;
const BARE_CLASS_REFERENCE = /^(?:ant|anticon)-[a-z0-9-]+$/;
const CLASS_ASSERTION_CALLEES = new Set(["toHaveClass", "contains", "toContain"]);
const isClassAssertionArgument = (node) => {
const call = node.parent;
if (call?.type !== "CallExpression" || !call.arguments.includes(node)) return false;
const callee = call.callee;
return callee?.type === "MemberExpression" && CLASS_ASSERTION_CALLEES.has(callee.property?.name);
};
const rule = {
meta: {
type: "problem",
docs: {
description:
"Disallow locating or asserting on antd's internal CSS classes in tests; query by role, label or text instead.",
},
schema: [],
messages: {
antdClass:
'Test depends on antd internal class "{{value}}". Query by role, label or text (getByLabelText, getByRole("combobox"), getByTitle) so the test survives the shadcn migration.',
},
},
create(context) {
const report = (node, value) => {
if (typeof value !== "string") return;
const matches =
SELECTOR_REFERENCE.test(value) || (BARE_CLASS_REFERENCE.test(value) && isClassAssertionArgument(node));
if (!matches) return;
context.report({ node, messageId: "antdClass", data: { value } });
};
return {
Literal(node) {
report(node, node.value);
},
TemplateElement(node) {
report(node, node.value.cooked);
},
};
},
};
export default rule;

View file

@ -36,7 +36,7 @@ import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { isAntdUrl } from "@/lib/forms/antdUrl";
import { isValidUrl } from "@/lib/forms/urlValidation";
import { useZodForm } from "@/lib/forms/useZodForm";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
@ -51,7 +51,7 @@ const submitGuardrailSchema = z.object({
team_id: z.string().min(1, "Select a team"),
guardrail_name: z.string().min(1, "Enter a guardrail name"),
mode: z.string().min(1, "Select a mode"),
api_base: z.string().min(1, "Enter the API base URL").refine(isAntdUrl, "Must be a valid URL"),
api_base: z.string().min(1, "Enter the API base URL").refine(isValidUrl, "Must be a valid URL"),
extra_litellm_params: z.string().superRefine((value, ctx) => {
if (!value) return;
try {

View file

@ -4,7 +4,7 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ACTION_ITEMS } from "./action_options";
import { ABOVE_ANTD_MODAL } from "./dialog_layering";
import { NESTED_DIALOG_LAYER } from "./dialog_layering";
interface CustomPatternModalProps {
visible: boolean;
@ -31,7 +31,7 @@ const CustomPatternModal: React.FC<CustomPatternModalProps> = ({
}) => {
return (
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className={`max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px] ${ABOVE_ANTD_MODAL}`}>
<DialogContent className={`max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px] ${NESTED_DIALOG_LAYER}`}>
<DialogHeader>
<DialogTitle>Add custom regex pattern</DialogTitle>
</DialogHeader>

View file

@ -5,7 +5,7 @@ import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { ACTION_ITEMS } from "./action_options";
import { ABOVE_ANTD_MODAL } from "./dialog_layering";
import { NESTED_DIALOG_LAYER } from "./dialog_layering";
interface KeywordModalProps {
visible: boolean;
@ -32,7 +32,7 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
}) => {
return (
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className={`max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px] ${ABOVE_ANTD_MODAL}`}>
<DialogContent className={`max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px] ${NESTED_DIALOG_LAYER}`}>
<DialogHeader>
<DialogTitle>Add blocked keyword</DialogTitle>
</DialogHeader>

View file

@ -14,7 +14,7 @@ import {
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ACTION_ITEMS } from "./action_options";
import { ABOVE_ANTD_MODAL } from "./dialog_layering";
import { NESTED_DIALOG_LAYER } from "./dialog_layering";
interface PrebuiltPattern {
name: string;
@ -66,7 +66,7 @@ const PatternModal: React.FC<PatternModalProps> = ({
return (
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className={`max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px] ${ABOVE_ANTD_MODAL}`}>
<DialogContent className={`max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px] ${NESTED_DIALOG_LAYER}`}>
<DialogHeader>
<DialogTitle>Add prebuilt pattern</DialogTitle>
</DialogHeader>

View file

@ -1 +1 @@
export const ABOVE_ANTD_MODAL = "z-[1100]";
export const NESTED_DIALOG_LAYER = "z-[1100]";

View file

@ -3,7 +3,7 @@ import React from "react";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { MountedFormField } from "@/components/common_components/MountedFormField";
import { antdRequired } from "@/components/common_components/antdFormRules";
import { requiredRule } from "@/components/common_components/formRules";
import { PasswordInput } from "@/components/shared/PasswordInput";
import { Input } from "@/components/ui/input";
import { requiredWhenSiblingSet, textControl } from "./mcpFieldRules";
@ -39,7 +39,7 @@ const AwsSigV4Fields: React.FC = () => (
label={<FieldLabel label="AWS Region" tooltip="AWS region for SigV4 signing (e.g., us-east-1)" />}
name={["credentials", "aws_region_name"]}
required
rules={{ validate: { required: antdRequired("AWS region is required for SigV4 auth") } }}
rules={{ validate: { required: requiredRule("AWS region is required for SigV4 auth") } }}
>
{(control) => <Input {...textControl(control)} placeholder="us-east-1" className={fieldClassName} />}
</MountedFormField>

View file

@ -63,7 +63,7 @@ import {
useMountRegistry,
type MountedFormValues,
} from "@/components/common_components/MountedFormField";
import { antdRequired, antdRules } from "@/components/common_components/antdFormRules";
import { requiredRule, validatorRules } from "@/components/common_components/formRules";
import { allFieldsValue, mountedPaths, resetFields, setFieldsValue, singleBranchChange } from "./mcpFormStore";
import { numberControl, notOnlyWhitespace, selectControl, selectTriggerControl, textControl } from "./mcpFieldRules";
import mcpLogo from "../../../../../public/assets/logos/mcp_logo.png";
@ -657,7 +657,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
</span>
}
name="server_name"
rules={{ validate: antdRules({ validator: (_, value) => validateMCPServerName(value) }) }}
rules={{ validate: validatorRules({ validator: (_, value) => validateMCPServerName(value) }) }}
>
{(control) => (
<Input
@ -678,7 +678,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
</span>
}
name="alias"
rules={{ validate: antdRules({ validator: (_, value) => validateMCPServerName(value) }) }}
rules={{ validate: validatorRules({ validator: (_, value) => validateMCPServerName(value) }) }}
>
{(control) => (
<Input
@ -725,7 +725,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
label={<span className="text-sm font-medium text-gray-700">Transport Type</span>}
name="transport"
required
rules={{ validate: { required: antdRequired("Please select a transport type") } }}
rules={{ validate: { required: requiredRule("Please select a transport type") } }}
>
{(control) => (
<Select
@ -755,8 +755,8 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
required
rules={{
validate: {
required: antdRequired("Please enter a server URL"),
...antdRules({ validator: (_, value) => validateMCPServerUrl(value) }),
required: requiredRule("Please enter a server URL"),
...validatorRules({ validator: (_, value) => validateMCPServerUrl(value) }),
},
}}
>
@ -821,7 +821,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
label="Authentication"
name="auth_type"
required
rules={{ validate: { required: antdRequired("Please select an auth type") } }}
rules={{ validate: { required: requiredRule("Please select an auth type") } }}
>
{(control) => (
<Select {...selectControl<string>(control)} items={AUTH_TYPE_ITEMS}>

View file

@ -12,7 +12,7 @@ import {
useMountedName,
type MountedFormValues,
} from "@/components/common_components/MountedFormField";
import { antdRequired } from "@/components/common_components/antdFormRules";
import { requiredRule } from "@/components/common_components/formRules";
import { matchesPattern, selectControl, selectTriggerControl, textControl } from "./mcpFieldRules";
import { listControl } from "./mcpFormStore";
@ -80,7 +80,7 @@ const EnvVarsSection: React.FC = () => {
className="mb-0 flex-1"
rules={{
validate: {
required: antdRequired("Variable name is required"),
required: requiredRule("Variable name is required"),
pattern: matchesPattern(
VARIABLE_NAME_PATTERN,
"Use letters, digits, underscores; cannot start with a digit.",

View file

@ -3,7 +3,7 @@ import React from "react";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { MountedFormField } from "@/components/common_components/MountedFormField";
import { antdRequired } from "@/components/common_components/antdFormRules";
import { requiredRule } from "@/components/common_components/formRules";
import { MultiSelect } from "@/components/shared/MultiSelect";
import { PasswordInput } from "@/components/shared/PasswordInput";
import { Input } from "@/components/ui/input";
@ -30,7 +30,7 @@ const PRIVATE_KEY_PATH = ["credentials", "client_private_key"] as const;
const IdJagFormFields: React.FC<IdJagFormFieldsProps> = ({ isEditing = false }) => {
const placeholderSuffix = isEditing ? " (leave blank to keep existing)" : "";
const requiredWhenCreating = (message: string) =>
isEditing ? undefined : { validate: { required: antdRequired(message) } };
isEditing ? undefined : { validate: { required: requiredRule(message) } };
return (
<>

View file

@ -15,7 +15,7 @@ import {
type MountedFieldControlProps,
type MountedFormValues,
} from "@/components/common_components/MountedFormField";
import { antdRequired } from "@/components/common_components/antdFormRules";
import { requiredRule } from "@/components/common_components/formRules";
import { Field, FieldLabel } from "@/components/shared/form/field";
import { invertedSwitchControl, switchControl, tagsControl, textControl } from "./mcpFieldRules";
import { listControl } from "./mcpFormStore";
@ -64,7 +64,7 @@ const StaticHeadersFieldArray: React.FC = () => {
<MountedFormField
name={["static_headers", String(index), "header"]}
className="flex-1"
rules={{ validate: { required: antdRequired("Header name is required") } }}
rules={{ validate: { required: requiredRule("Header name is required") } }}
>
{(headerControl) => (
<ClearableInput
@ -77,7 +77,7 @@ const StaticHeadersFieldArray: React.FC = () => {
<MountedFormField
name={["static_headers", String(index), "value"]}
className="flex-1"
rules={{ validate: { required: antdRequired("Header value is required") } }}
rules={{ validate: { required: requiredRule("Header value is required") } }}
>
{(valueControl) => (
<ClearableInput control={valueControl} placeholder="Header value" clearLabel="Clear header value" />

View file

@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { OAUTH_FLOW } from "@/components/mcp_tools/types";
import { MountedFormField } from "@/components/common_components/MountedFormField";
import { antdRequired } from "@/components/common_components/antdFormRules";
import { requiredRule } from "@/components/common_components/formRules";
import TokenEndpointAuthMethodField from "./TokenEndpointAuthMethodField";
import {
numberControl,
@ -79,7 +79,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
}) => {
const placeholderSuffix = isEditing ? " (leave blank to keep existing)" : "";
const requiredWhenCreating = (message: string) =>
isEditing ? undefined : { validate: { required: antdRequired(message) } };
isEditing ? undefined : { validate: { required: requiredRule(message) } };
return (
<>

View file

@ -4,7 +4,7 @@ import { SimpleTooltip } from "@/components/ui/tooltip";
import { Input } from "@/components/ui/input";
import { AUTH_TYPE, OAUTH_FLOW } from "@/components/mcp_tools/types";
import { MountedFormField } from "@/components/common_components/MountedFormField";
import { antdRequired } from "@/components/common_components/antdFormRules";
import { requiredRule } from "@/components/common_components/formRules";
import OpenAPIQuickPicker, { OpenAPIRegistryEntry, OpenAPIKeyTool } from "./OpenAPIQuickPicker";
import { McpForm, resetFields, setFieldsValue } from "./mcpFormStore";
import { textControl } from "./mcpFieldRules";
@ -76,7 +76,7 @@ const OpenAPIFormSection: React.FC<OpenAPIFormSectionProps> = ({
}
name="spec_path"
required
rules={{ validate: { required: antdRequired("Please enter an OpenAPI spec URL") } }}
rules={{ validate: { required: requiredRule("Please enter an OpenAPI spec URL") } }}
>
{(control) => (
<Input

View file

@ -3,7 +3,7 @@ import React from "react";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { MountedFormField } from "@/components/common_components/MountedFormField";
import { antdRequired } from "@/components/common_components/antdFormRules";
import { requiredRule } from "@/components/common_components/formRules";
import { Textarea } from "@/components/ui/textarea";
import { parsesAsJson, textControl } from "./mcpFieldRules";
@ -46,7 +46,7 @@ const StdioConfiguration: React.FC<StdioConfigurationProps> = ({ isVisible, requ
required={required}
rules={{
validate: {
...(required ? { required: antdRequired("Please enter stdio configuration") } : {}),
...(required ? { required: requiredRule("Please enter stdio configuration") } : {}),
json: parsesAsJson("Please enter valid JSON"),
},
}}

View file

@ -6,7 +6,7 @@ import { SimpleTooltip } from "@/components/ui/tooltip";
import { useWatch } from "react-hook-form";
import { MountedFormField } from "@/components/common_components/MountedFormField";
import { antdRequired } from "@/components/common_components/antdFormRules";
import { requiredRule } from "@/components/common_components/formRules";
import { PasswordInput } from "@/components/shared/PasswordInput";
import { Input } from "@/components/ui/input";
import { selectControl, selectTriggerControl, tagsControl, textControl } from "./mcpFieldRules";
@ -35,7 +35,7 @@ const TokenExchangeFormFields: React.FC<TokenExchangeFormFieldsProps> = ({ isEdi
const placeholderSuffix = isEditing ? " (leave blank to keep existing)" : "";
const isEntraObo = useWatch({ name: "token_exchange_profile" }) === "entra_obo";
const requiredWhenCreating = (message: string) =>
isEditing ? undefined : { validate: { required: antdRequired(message) } };
isEditing ? undefined : { validate: { required: requiredRule(message) } };
return (
<>
@ -170,7 +170,7 @@ const TokenExchangeFormFields: React.FC<TokenExchangeFormFieldsProps> = ({ isEdi
isEntraObo
? {
validate: {
required: antdRequired("Microsoft Entra OBO requires a scope, e.g. api://<app-id>/.default"),
required: requiredRule("Microsoft Entra OBO requires a scope, e.g. api://<app-id>/.default"),
},
}
: undefined

View file

@ -60,7 +60,7 @@ import {
useMountRegistry,
type MountedFormValues,
} from "@/components/common_components/MountedFormField";
import { antdRequired, antdRules } from "@/components/common_components/antdFormRules";
import { requiredRule, validatorRules } from "@/components/common_components/formRules";
import {
allFieldsValue,
mountedPaths,
@ -774,7 +774,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
<MountedFormField
label="MCP Server Name"
name="server_name"
rules={{ validate: antdRules({ validator: (_, value) => validateMCPServerName(value) }) }}
rules={{ validate: validatorRules({ validator: (_, value) => validateMCPServerName(value) }) }}
>
{(control) => (
<Input
@ -786,7 +786,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
<MountedFormField
label="Alias"
name="alias"
rules={{ validate: antdRules({ validator: (_, value) => validateMCPServerName(value) }) }}
rules={{ validate: validatorRules({ validator: (_, value) => validateMCPServerName(value) }) }}
>
{(control) => (
<Input
@ -812,7 +812,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
label="Transport Type"
name="transport"
required
rules={{ validate: { required: antdRequired("Transport Type is required") } }}
rules={{ validate: { required: requiredRule("Transport Type is required") } }}
>
{(control) => (
<Select
@ -842,8 +842,8 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
required
rules={{
validate: {
required: antdRequired("Please enter a server URL"),
...antdRules({ validator: (_, value) => validateMCPServerUrl(value) }),
required: requiredRule("Please enter a server URL"),
...validatorRules({ validator: (_, value) => validateMCPServerUrl(value) }),
},
}}
>
@ -870,7 +870,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
}
name="spec_path"
required
rules={{ validate: { required: antdRequired("Please enter an OpenAPI spec URL") } }}
rules={{ validate: { required: requiredRule("Please enter an OpenAPI spec URL") } }}
>
{(control) => (
<Input
@ -911,7 +911,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
label="Authentication"
name="auth_type"
required
rules={{ validate: { required: antdRequired("Authentication is required") } }}
rules={{ validate: { required: requiredRule("Authentication is required") } }}
>
{(control) => (
<Select {...selectControl<string>(control)} items={AUTH_TYPE_ITEMS}>
@ -957,7 +957,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
label="Command"
name="command"
required
rules={{ validate: { required: antdRequired("Please enter a command for stdio transport") } }}
rules={{ validate: { required: requiredRule("Please enter a command for stdio transport") } }}
>
{(control) => (
<Input

View file

@ -8,59 +8,12 @@ import { PolicyAttachment } from "@/components/policies/types";
vi.mock("@/components/networking");
interface LegacyIconProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
icon: React.ComponentType;
}
interface LegacyPopoverProps {
children: React.ReactElement<React.HTMLAttributes<HTMLElement>>;
content: React.ReactNode;
onOpenChange?: (open: boolean) => void;
title?: React.ReactNode;
}
interface LegacyTooltipProps {
children: React.ReactElement<React.ButtonHTMLAttributes<HTMLButtonElement>>;
title?: React.ReactNode;
}
vi.mock("@heroicons/react/outline", () => ({
EyeIcon: function EyeIcon() {
return null;
},
}));
vi.mock("antd", async (importOriginal) => {
const actual = await importOriginal<typeof import("antd")>();
return {
...actual,
Popover: ({ children, content, onOpenChange, title }: LegacyPopoverProps) => {
const [open, setOpen] = React.useState(false);
return (
<>
{React.cloneElement(children, {
onClick: () => {
const nextOpen = !open;
setOpen(nextOpen);
onOpenChange?.(nextOpen);
},
})}
{open && (
<div>
<p>{title}</p>
{content}
</div>
)}
</>
);
},
Tooltip: ({ children, title }: LegacyTooltipProps) =>
React.cloneElement(children, { "aria-label": typeof title === "string" ? title : undefined }),
Spin: () => <span aria-hidden="true" />,
Tag: ({ children }: { children?: React.ReactNode }) => <span>{children}</span>,
};
});
const makeAttachment = (overrides: Partial<PolicyAttachment> = {}): PolicyAttachment => ({
attachment_id: "att-001",
policy_name: "my-policy",

View file

@ -10,16 +10,6 @@ import PoliciesPanel from "./index";
* In jsdom it may still run; we mock confirm as a no-op so the test fails until the panel
* uses a controlled DeleteResourceModal instead of Modal.confirm.
*/
vi.mock("antd", async (importOriginal) => {
const mod = await importOriginal<typeof import("antd")>();
return {
...mod,
Modal: Object.assign(mod.Modal, {
confirm: vi.fn(),
}),
};
});
const EXPECTED_ATTACHMENT_ID = "att-11111111-2222-3333-4444-555555555555" as const;
const networkingMocks = vi.hoisted(() => ({

View file

@ -9,18 +9,6 @@ vi.mock("@/components/networking", () => ({
searchToolQueryCall: vi.fn(),
}));
vi.mock("antd", async () => {
const actual = await vi.importActual("antd");
return {
...actual,
message: {
warning: vi.fn(),
success: vi.fn(),
error: vi.fn(),
},
};
});
const mockSearchResults = {
results: [
{

View file

@ -12,69 +12,6 @@ vi.mock("@/utils/roles", () => ({
all_admin_roles: ["Admin", "Admin Viewer", "proxy_admin", "proxy_admin_viewer", "org_admin"],
}));
vi.mock("antd", async (importOriginal) => {
const actual = await importOriginal<typeof import("antd")>();
const React = await import("react");
const SelectComponent = ({
value,
onChange,
mode,
children,
placeholder,
disabled,
style,
allowClear,
...props
}: any) => {
const isMultiple = mode === "multiple";
const selectValue = isMultiple ? (Array.isArray(value) ? value : []) : value || "";
return React.createElement(
"select",
{
multiple: isMultiple,
value: selectValue,
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedValues = Array.from(e.target.selectedOptions, (option) => option.value);
onChange(isMultiple ? selectedValues : selectedValues[0] || undefined);
},
disabled,
placeholder,
style,
"aria-label": placeholder || "Select",
role: "combobox",
...props,
},
children,
);
};
SelectComponent.displayName = "Select";
const SelectOption = ({ value: optionValue, children: optionChildren }: any) =>
React.createElement("option", { value: optionValue }, optionChildren);
SelectOption.displayName = "SelectOption";
SelectComponent.Option = SelectOption;
const Tooltip = ({ children }: { children?: React.ReactNode }) => React.createElement(React.Fragment, null, children);
Tooltip.displayName = "Tooltip";
const Checkbox = ({ checked, onChange, children, ...props }: any) =>
React.createElement(
"label",
{ style: { display: "flex", alignItems: "center", gap: "8px" } },
React.createElement("input", {
type: "checkbox",
checked: checked,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => onChange({ target: { checked: e.target.checked } }),
...props,
}),
children,
);
Checkbox.displayName = "Checkbox";
return {
...actual,
Select: SelectComponent,
Tooltip,
Checkbox,
};
});
describe("UserEditView", () => {
const MOCK_USER_DATA = {
user_id: "user-123",

View file

@ -1,4 +1,4 @@
@layer theme, base, antd, components, utilities;
@layer theme, base, components, utilities;
@import "tailwindcss";
@import "tw-animate-css";
@ -260,13 +260,3 @@
[data-slot="dialog-content"][data-nested-dialog-open] {
visibility: hidden;
}
/* Base UI portals a popup into an `isolate z-50` positioner, which an antd Modal at z-index 1000
then paints over, so options on a half-migrated page are visible but not clickable. The
positioner is not reachable from the call site, hence the parent selector. Delete this once no
route renders an antd Modal. */
body:has(.ant-modal-wrap) div:has(> [data-slot="select-content"]),
body:has(.ant-modal-wrap) div:has(> [data-slot="combobox-content"]),
body:has(.ant-modal-wrap) div:has(> [data-slot="tooltip-content"]) {
z-index: 1100;
}

View file

@ -4,7 +4,6 @@ import "./globals.css";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import AntdGlobalProvider from "@/contexts/AntdGlobalProvider";
import { AuthProvider } from "@/contexts/AuthContext";
import ReactQueryProvider from "@/contexts/ReactQueryProvider";
import { Toaster } from "@/components/ui/sonner";
@ -27,10 +26,8 @@ export default function RootLayout({
<body className={inter.className}>
<NuqsAdapter>
<ReactQueryProvider>
<AntdGlobalProvider>
<AuthProvider>{children}</AuthProvider>
<Toaster />
</AntdGlobalProvider>
<AuthProvider>{children}</AuthProvider>
<Toaster />
</ReactQueryProvider>
</NuqsAdapter>
</body>

View file

@ -17,17 +17,6 @@ vi.mock("@/app/(dashboard)/hooks/cloudzero/useCloudZeroCreate", () => ({
}),
}));
vi.mock("antd", async () => {
const actual = await vi.importActual("antd");
return {
...actual,
message: {
success: vi.fn(),
error: vi.fn(),
},
};
});
describe("CloudZeroCreateModal", () => {
let queryClient: QueryClient;

View file

@ -26,18 +26,6 @@ vi.mock("@/app/(dashboard)/hooks/cloudzero/useCloudZeroExport", () => ({
}),
}));
vi.mock("antd", async () => {
const actual = await vi.importActual("antd");
return {
...actual,
message: {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
},
};
});
describe("CloudZeroIntegrationSettings", () => {
let queryClient: QueryClient;
const mockSettings: CloudZeroSettings = {

View file

@ -18,17 +18,6 @@ vi.mock("@/app/(dashboard)/hooks/cloudzero/useCloudZeroSettings", () => ({
}),
}));
vi.mock("antd", async () => {
const actual = await vi.importActual("antd");
return {
...actual,
message: {
success: vi.fn(),
error: vi.fn(),
},
};
});
describe("CloudZeroUpdateModal", () => {
let queryClient: QueryClient;
const mockSettings: CloudZeroSettings = {

View file

@ -15,14 +15,14 @@ const HOST = "(?:(?:[a-z\\u00a1-\\uffff0-9][-_]*)*[a-z\\u00a1-\\uffff0-9]+)";
const DOMAIN = "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*";
const TLD = "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))";
const ANTD_URL_RULE_PATTERN = new RegExp(
const URL_RULE_PATTERN = new RegExp(
`(?:^(?:(?:(?:[a-z]+:)?//)|www\\.)(?:\\S+(?::\\S*)?@)?` +
`(?:localhost|${IPV4}|${IPV6}|${HOST}${DOMAIN}${TLD})` +
`(?::\\d{2,5})?(?:[/?#][^\\s"]*)?$)`,
"i",
);
const isUrl = (value: string): boolean => value.length <= 2048 && ANTD_URL_RULE_PATTERN.test(value);
const isUrl = (value: string): boolean => value.length <= 2048 && URL_RULE_PATTERN.test(value);
const pluginShape = {
name: z.string().min(1, "Required"),

View file

@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button";
import React, { useEffect, useMemo, useState } from "react";
import { FormProvider, useWatch, type UseFormReturn } from "react-hook-form";
import TeamDropdown from "../common_components/team_dropdown";
import { antdRequired } from "../common_components/antdFormRules";
import { requiredRule } from "../common_components/formRules";
import { labelWithHint } from "@/components/shared/form/LabelWithHint";
import {
MountedFormField,
@ -185,7 +185,7 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
label={labelWithHint("Select Team", "Select the team for which you want to add this model")}
name="team_id"
required
rules={{ validate: { required: antdRequired("Please select a team to continue") } }}
rules={{ validate: { required: requiredRule("Please select a team to continue") } }}
className="mb-4"
>
{(control) => (
@ -215,7 +215,7 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
label={labelWithHint("Provider", "E.g. OpenAI, Azure OpenAI, Anthropic, Bedrock, etc.")}
name="custom_llm_provider"
required
rules={{ validate: { required: antdRequired("Required") } }}
rules={{ validate: { required: requiredRule("Required") } }}
className="mb-4"
>
{(control) => (
@ -368,7 +368,7 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
required={isTeamOnly && !isAdmin}
rules={
isTeamOnly && !isAdmin
? { validate: { required: antdRequired("Please select a team.") } }
? { validate: { required: requiredRule("Please select a team.") } }
: undefined
}
>

View file

@ -9,7 +9,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Team } from "../key_team_helpers/key_list";
import { antdRules } from "../common_components/antdFormRules";
import { validatorRules } from "../common_components/formRules";
import { labelWithHint } from "@/components/shared/form/LabelWithHint";
import { MountedFormField } from "../common_components/MountedFormField";
import { UtcDateTimeInput } from "@/components/shared/form/UtcDateTimeInput";
@ -71,7 +71,7 @@ const validateNumber = (_: unknown, value: unknown) => {
const usageCostRules = {
deps: [PTU_COUNT_FIELD],
validate: antdRules({ validator: validateNumber }, ptuNoUsageCostRule(PTU_COUNT_FIELD)),
validate: validatorRules({ validator: validateNumber }, ptuNoUsageCostRule(PTU_COUNT_FIELD)),
};
const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
@ -208,7 +208,11 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
)}
rules={{
deps: REVALIDATED_WHEN_PTU_COUNT_CHANGES,
validate: antdRules({ validator: validateNumber }, ...ptuCountRules, ptuPairRule(PTU_RATE_FIELD)),
validate: validatorRules(
{ validator: validateNumber },
...ptuCountRules,
ptuPairRule(PTU_RATE_FIELD),
),
}}
className="mb-4"
>
@ -231,7 +235,11 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
)}
rules={{
deps: [PTU_COUNT_FIELD],
validate: antdRules({ validator: validateNumber }, ...ptuRateRules, ptuPairRule(PTU_COUNT_FIELD)),
validate: validatorRules(
{ validator: validateNumber },
...ptuRateRules,
ptuPairRule(PTU_COUNT_FIELD),
),
}}
className="mb-4"
>
@ -254,7 +262,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
)}
rules={{
deps: [PTU_END_FIELD],
validate: antdRules(
validate: validatorRules(
ptuStartRequiredRule(PTU_COUNT_FIELD),
ptuWindowOrderRule(PTU_END_FIELD, "start"),
),
@ -279,7 +287,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
)}
rules={{
deps: [PTU_START_FIELD],
validate: antdRules(ptuWindowOrderRule(PTU_START_FIELD, "end")),
validate: validatorRules(ptuWindowOrderRule(PTU_START_FIELD, "end")),
}}
className="mb-4"
>
@ -463,7 +471,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
"Optional litellm params used for making a litellm.completion() call.",
)}
className="mb-4 mt-4"
rules={{ validate: antdRules({ validator: formItemValidateJSON }) }}
rules={{ validate: validatorRules({ validator: formItemValidateJSON }) }}
>
{(control) => (
<Textarea
@ -500,7 +508,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
"Optional model info params. Returned when calling `/model/info` endpoint.",
)}
className="mb-0"
rules={{ validate: antdRules({ validator: formItemValidateJSON }) }}
rules={{ validate: validatorRules({ validator: formItemValidateJSON }) }}
>
{(control) => (
<Textarea

View file

@ -4,7 +4,7 @@ import { useFormContext, useWatch } from "react-hook-form";
import { DataTable } from "@/components/shared/DataTable";
import { Input } from "@/components/ui/input";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { antdRules } from "../common_components/antdFormRules";
import { validatorRules } from "../common_components/formRules";
import { MountedFormField, type MountedFormValues } from "../common_components/MountedFormField";
import { Providers } from "../provider_info_helpers";
@ -205,7 +205,7 @@ const ConditionalPublicModelName: React.FC = () => {
</span>
}
required
rules={{ validate: antdRules(modelMappingsRule) }}
rules={{ validate: validatorRules(modelMappingsRule) }}
className="mb-4"
>
{(control) => (

View file

@ -2,7 +2,7 @@ import React from "react";
import { MultiSelect } from "@/components/shared/MultiSelect";
import { useFormContext, useWatch } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { antdRequired } from "../common_components/antdFormRules";
import { requiredRule } from "../common_components/formRules";
import { labelWithHint } from "@/components/shared/form/LabelWithHint";
import { MountedFormField, type MountedFormValues } from "../common_components/MountedFormField";
import { Providers } from "../provider_info_helpers";
@ -108,7 +108,7 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
required
rules={{
validate: {
required: antdRequired(
required: requiredRule(
`Please enter ${selectedProvider === Providers.Azure ? "a deployment name" : "at least one model"}.`,
),
},
@ -173,7 +173,7 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
<MountedFormField
name="custom_model_name"
required
rules={{ validate: { required: antdRequired("Please enter a custom model name.") } }}
rules={{ validate: { required: requiredRule("Please enter a custom model name.") } }}
className="mt-2"
>
{(control) => (

View file

@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
import { Upload as UploadIcon } from "lucide-react";
import React from "react";
import { useFormContext } from "react-hook-form";
import { antdRequired } from "../common_components/antdFormRules";
import { requiredRule } from "../common_components/formRules";
import {
MountedFormField,
type MountedFieldControlProps,
@ -327,7 +327,7 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({ selecte
label={field.tooltip ? labelWithHint(field.label, field.tooltip) : field.label}
name={field.key}
required={field.required}
rules={field.required ? { validate: { required: antdRequired("Required") } } : undefined}
rules={field.required ? { validate: { required: requiredRule("Required") } } : undefined}
className={field.key === "vertex_credentials" ? "mb-0" : "mb-4"}
>
{(control) => renderFieldControl(field, control)}

View file

@ -289,7 +289,7 @@ import { Skeleton } from "@/components/ui/skeleton";
### Toast / notifications
**Gap:** `sonner` is not installed. This codebase currently uses `MessageManager` (antd-message-based) for toasts across the chat UI. Don't introduce a second toast mechanism — keep using `MessageManager` until `sonner` is added as a deliberate, separate change (see `AGENTS.md`).
**Resolved:** `sonner` is installed and mounted from the root layout. Raise toasts through `toast` from `@/lib/toast`; don't introduce a second toast mechanism.
---

View file

@ -2,16 +2,16 @@ import type { Validate } from "react-hook-form";
import type { MountedFormValues } from "./MountedFormField";
interface AntdRuleForm {
interface ValidatorRuleForm {
getFieldValue: (name: string) => unknown;
isFieldTouched?: (name: string) => boolean;
}
interface AntdRule {
interface ValidatorRule {
validator: (rule: never, value: never) => Promise<void>;
}
type AntdRuleSource = AntdRule | ((form: AntdRuleForm) => AntdRule);
type ValidatorRuleSource = ValidatorRule | ((form: ValidatorRuleForm) => ValidatorRule);
type MountedValidate = Validate<unknown, MountedFormValues>;
@ -19,17 +19,17 @@ const isBlank = (value: unknown): boolean => value === undefined || value === nu
const isEmptyList = (value: unknown): boolean => Array.isArray(value) && value.length === 0;
export const antdRequired =
export const requiredRule =
(message: string): MountedValidate =>
(value) =>
isBlank(value) || isEmptyList(value) ? message : true;
const toMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error));
export const antdRules = (...rules: readonly AntdRuleSource[]): Record<string, MountedValidate> =>
export const validatorRules = (...rules: readonly ValidatorRuleSource[]): Record<string, MountedValidate> =>
Object.fromEntries(
rules.map((rule, index) => [
`antd_${index}`,
`rule_${index}`,
async (value: unknown, values: MountedFormValues) => {
const resolved = typeof rule === "function" ? rule({ getFieldValue: (name) => values[name] }) : rule;
const validator = resolved.validator as (rule: unknown, value: unknown) => Promise<void>;

View file

@ -268,8 +268,6 @@ describe("EditAutoRouterModal classifier context window", () => {
expect(await screen.findByLabelText("Classifier system prompt")).toBeInTheDocument();
expect(baseElement.querySelectorAll('[data-slot="dialog-content"]')).toHaveLength(2);
// eslint-disable-next-line local/no-antd-class-selectors -- the assertion IS that no antd modal renders; naming the class is the point
expect(baseElement.querySelector(".ant-modal")).toBeNull();
});
it("persists an edited classifier context window size", async () => {

View file

@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import ProviderSpecificFields from "../add_model/provider_specific_fields";
import { antdRequired } from "../common_components/antdFormRules";
import { requiredRule } from "../common_components/formRules";
import { labelWithHint } from "@/components/shared/form/LabelWithHint";
import {
MountedFormField,
@ -104,7 +104,7 @@ export default function CredentialModal({
label="Credential Name:"
name="credential_name"
required
rules={{ validate: { required: antdRequired("Credential name is required") } }}
rules={{ validate: { required: requiredRule("Credential name is required") } }}
className="mb-4"
>
{(control) => (
@ -123,7 +123,7 @@ export default function CredentialModal({
label={labelWithHint("Provider:", "Helper to auto-populate provider specific fields")}
name="custom_llm_provider"
required
rules={{ validate: { required: antdRequired("Required") } }}
rules={{ validate: { required: requiredRule("Required") } }}
className="mb-4"
>
{(control) => (

View file

@ -30,7 +30,6 @@ const mockUserDropdownData = vi.hoisted(() => ({
vi.mock("./Navbar/UserDropdown/UserDropdown", async (importOriginal) => {
const React = await import("react");
const { useState } = React;
const { Button } = await import("antd");
const localStorageUtils = await import("@/utils/localStorageUtils");
return {
default: function MockUserDropdown({ onLogout }: { onLogout: () => void }) {
@ -38,9 +37,9 @@ vi.mock("./Navbar/UserDropdown/UserDropdown", async (importOriginal) => {
const [open, setOpen] = useState(false);
return (
<div>
<Button type="text" aria-label="Open account menu" onClick={() => setOpen(!open)}>
<button type="button" aria-label="Open account menu" onClick={() => setOpen(!open)}>
Account
</Button>
</button>
{open && (
<div data-testid="user-dropdown-content">
<span>{userId}</span>

View file

@ -58,43 +58,6 @@ vi.mock("../shared/errorUtils", () => ({
parseErrorMessage: (e: any) => String(e),
}));
// antd bits -> async factory & local React
vi.mock("antd", async (importOriginal) => {
const actual = await importOriginal<typeof import("antd")>();
const React = await import("react");
const Form = { useForm: () => [{}] };
function Input(props: any) {
return React.createElement("input", { ...props });
}
(Input as any).displayName = "AntdInput";
function InputNumber(props: any) {
return React.createElement("input", { ...props });
}
(InputNumber as any).displayName = "AntdInputNumber";
function Select(props: any) {
return React.createElement("select", { ...props });
}
(Select as any).displayName = "AntdSelect";
function Tooltip({ children }: any) {
return React.createElement(React.Fragment, null, children);
}
(Tooltip as any).displayName = "AntdTooltip";
function Button(props: any) {
const { children, onClick, ...rest } = props;
return React.createElement("button", { onClick, ...rest }, children);
}
(Button as any).displayName = "AntdButton";
return { ...actual, Form, Input, InputNumber, Select, Tooltip, Button };
});
// Icons -> async factory & local React
vi.mock("@heroicons/react/outline", async () => {
const React = await import("react");

View file

@ -57,13 +57,13 @@ export const estimateChecks = {
},
};
const asAntdRule = ({ isValid, message }: { isValid: (value: unknown) => boolean; message: string }) => ({
const asValidatorRule = ({ isValid, message }: { isValid: (value: unknown) => boolean; message: string }) => ({
validator: (_: unknown, value: unknown) => (isValid(value) ? Promise.resolve() : Promise.reject(new Error(message))),
});
export const estimateRules = {
perModel: asAntdRule(estimateChecks.perModel),
positive: asAntdRule(estimateChecks.positive),
perModel: asValidatorRule(estimateChecks.perModel),
positive: asValidatorRule(estimateChecks.positive),
};
export const withNormalizedEstimates = <T extends FormValues>(values: T): FormValues => {

View file

@ -5,16 +5,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { InputCard } from "./InputCard";
import { ParsedMessage } from "./prettyMessagesTypes";
vi.mock("antd", async () => {
const actual = await vi.importActual<typeof import("antd")>("antd");
return {
...actual,
message: {
success: vi.fn(),
},
};
});
describe("InputCard", () => {
const mockWriteText = vi.fn().mockResolvedValue(undefined);
const mockMessages: ParsedMessage[] = [

View file

@ -5,16 +5,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { OutputCard } from "./OutputCard";
import { ParsedMessage } from "./prettyMessagesTypes";
vi.mock("antd", async () => {
const actual = await vi.importActual<typeof import("antd")>("antd");
return {
...actual,
message: {
success: vi.fn(),
},
};
});
describe("OutputCard", () => {
const mockWriteText = vi.fn().mockResolvedValue(undefined);
const mockMessage: ParsedMessage = {

View file

@ -1,18 +1,8 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect } from "vitest";
import { PrettyMessagesView } from "./PrettyMessagesView";
vi.mock("antd", async () => {
const actual = await vi.importActual<typeof import("antd")>("antd");
return {
...actual,
message: {
success: vi.fn(),
},
};
});
describe("PrettyMessagesView", () => {
it("should render the component for standard chat completions", () => {
const request = {

View file

@ -1,8 +0,0 @@
"use client";
import React from "react";
import { StyleProvider } from "@ant-design/cssinjs";
export default function AntdGlobalProvider({ children }: { children: React.ReactNode }) {
return <StyleProvider layer>{children}</StyleProvider>;
}

View file

@ -1,14 +1,14 @@
import { describe, expect, it } from "vitest";
import { ANTD_URL_REGEX, isAntdUrl, MAX_ANTD_URL_LENGTH } from "./antdUrl";
import { URL_REGEX, isValidUrl, MAX_URL_LENGTH } from "./urlValidation";
const ASYNC_VALIDATOR_5_1_0_URL_SOURCE =
'(?:^(?:(?:(?:[a-z]+:)?\\/\\/)|www\\.)(?:\\S+(?::\\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?:(?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(?::[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,3}|:)|(?:[a-fA-F\\d]{1,4}:){3}(?:(?::[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,4}|:)|(?:[a-fA-F\\d]{1,4}:){2}(?:(?::[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(?::[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?|(?:(?:[a-z\\u00a1-\\uffff0-9][-_]*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))(?::\\d{2,5})?(?:[/?#][^\\s"]*)?$)';
describe("isAntdUrl", () => {
describe("isValidUrl", () => {
it("compiles to the exact pattern async-validator 5.1.0 uses for rule type url", () => {
expect(ANTD_URL_REGEX.source).toBe(ASYNC_VALIDATOR_5_1_0_URL_SOURCE);
expect(ANTD_URL_REGEX.flags).toBe("i");
expect(URL_REGEX.source).toBe(ASYNC_VALIDATOR_5_1_0_URL_SOURCE);
expect(URL_REGEX.flags).toBe("i");
});
it.each([
@ -19,19 +19,19 @@ describe("isAntdUrl", () => {
"https://127.0.0.1:8080/path?q=1",
"https://user:pass@example.com",
])("accepts %s the way antd does", (value) => {
expect(isAntdUrl(value)).toBe(true);
expect(isValidUrl(value)).toBe(true);
});
it.each(["example.com", "", "not a url", "https://", "ftp:/example.com", "http://exa mple.com"])(
"rejects %s the way antd does",
(value) => {
expect(isAntdUrl(value)).toBe(false);
expect(isValidUrl(value)).toBe(false);
},
);
it("rejects a url longer than the 2048 characters antd allows", () => {
const long = `https://example.com/${"a".repeat(MAX_ANTD_URL_LENGTH)}`;
expect(ANTD_URL_REGEX.test(long)).toBe(true);
expect(isAntdUrl(long)).toBe(false);
const long = `https://example.com/${"a".repeat(MAX_URL_LENGTH)}`;
expect(URL_REGEX.test(long)).toBe(true);
expect(isValidUrl(long)).toBe(false);
});
});

View file

@ -19,11 +19,11 @@ const TLD = "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))";
const PORT = "(?::\\d{2,5})?";
const PATH = '(?:[/?#][^\\s"]*)?';
export const ANTD_URL_REGEX = new RegExp(
export const URL_REGEX = new RegExp(
`(?:^(?:${PROTOCOL}|www\\.)${AUTH}(?:localhost|${V4}|${V6}|${HOST}${DOMAIN}${TLD})${PORT}${PATH}$)`,
"i",
);
export const MAX_ANTD_URL_LENGTH = 2048;
export const MAX_URL_LENGTH = 2048;
export const isAntdUrl = (value: string): boolean => value.length <= MAX_ANTD_URL_LENGTH && ANTD_URL_REGEX.test(value);
export const isValidUrl = (value: string): boolean => value.length <= MAX_URL_LENGTH && URL_REGEX.test(value);