refactor(ui): migrate CloudZero and cost tracking forms to react-hook-form and shadcn (#37312)

* refactor(ui): migrate CloudZero and cost tracking forms to react-hook-form and shadcn

Moves four forms off antd Form onto react-hook-form with the shadcn field
primitives, keeping the request payloads byte identical.

The two CloudZero modals were near duplicates, so the payload builder and the
API key input now live in shared modules next to them. The Update modal keeps
its redaction behaviour: the key field arrives empty with a "leave empty to
keep existing" hint, and an untouched save omits api_key from the request so
the stored secret survives. There is a test that fails if that regresses.

add_provider_form had no Form instance of its own, and its Form.Item wrappers
carried no name, so nothing was registered in the parent store. The parent in
cost_tracking_settings still owns an antd Form element, which stays for now,
and the migrated button keeps type="submit" so the parent's onFinish path
behaves exactly as before.

Each unit got characterization tests written against the antd version first,
then re-run unedited against the migration. Payload parity was also checked
side by side with toStrictEqual across seven scenarios.

* test(ui): guard the CloudZero null connection id against a zod type error

The proxy returns connection_id as null rather than omitting it, and a plain
z.string() rejects null. The seeding coalesces it to "" so an untouched save
reports the friendly required message instead of "expected string, received
null". Dropping that coalesce fails this test.
This commit is contained in:
yuneng-jiang 2026-08-18 11:42:58 -07:00 • committed by GitHub
parent b134c61300
commit e20e31e985
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 851 additions and 254 deletions

View file

@ -191,11 +191,6 @@
"count": 1
}
},
"src/app/(dashboard)/cost-optimization/_components/PromptCompressionTab.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/cost-tracking/_components/add_margin_form.tsx": {
"local/filename-pascal-case": {
"count": 1
@ -204,9 +199,6 @@
"src/app/(dashboard)/cost-tracking/_components/add_provider_form.tsx": {
"local/filename-pascal-case": {
"count": 1
},
"no-restricted-imports": {
"count": 2
}
},
"src/app/(dashboard)/cost-tracking/_components/cost_tracking_settings.tsx": {

View file

@ -0,0 +1,127 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import PromptCompressionTab from "./PromptCompressionTab";
const createGuardrailCall = vi.fn();
const getGuardrailsList = vi.fn();
vi.mock("@/components/networking", () => ({
createGuardrailCall: (...args: unknown[]) => createGuardrailCall(...args),
getGuardrailsList: (...args: unknown[]) => getGuardrailsList(...args),
}));
const submittedPayload = (): Record<string, unknown> => {
expect(createGuardrailCall).toHaveBeenCalledTimes(1);
return createGuardrailCall.mock.calls[0][1] as Record<string, unknown>;
};
describe("PromptCompressionTab submit payload", () => {
beforeEach(() => {
createGuardrailCall.mockClear().mockResolvedValue({});
getGuardrailsList.mockClear().mockResolvedValue({ guardrails: [] });
});
it("sends the trimmed name and api base with default_on true", async () => {
const user = userEvent.setup();
render(<PromptCompressionTab accessToken="test-token" />);
await user.type(screen.getByLabelText("Name"), " headroom-compression ");
await user.type(screen.getByLabelText("Headroom API base"), " https://headroom.example.com ");
await user.click(screen.getByRole("button", { name: "Add guardrail" }));
await vi.waitFor(() =>
expect(submittedPayload()).toEqual({
guardrail_name: "headroom-compression",
litellm_params: {
guardrail: "headroom",
mode: "pre_call",
api_base: "https://headroom.example.com",
default_on: true,
},
}),
);
expect(createGuardrailCall.mock.calls[0][0]).toBe("test-token");
});
it("sends default_on false once the apply-to-all switch is turned off", async () => {
const user = userEvent.setup();
render(<PromptCompressionTab accessToken="test-token" />);
await user.type(screen.getByLabelText("Name"), "headroom-optin");
await user.type(screen.getByLabelText("Headroom API base"), "https://headroom.example.com");
await user.click(screen.getByLabelText("Apply to all requests"));
await user.click(screen.getByRole("button", { name: "Add guardrail" }));
await vi.waitFor(() =>
expect(submittedPayload()).toEqual({
guardrail_name: "headroom-optin",
litellm_params: {
guardrail: "headroom",
mode: "pre_call",
api_base: "https://headroom.example.com",
default_on: false,
},
}),
);
});
it("blocks submission and shows both required messages when the form is empty", async () => {
const user = userEvent.setup();
render(<PromptCompressionTab accessToken="test-token" />);
await user.click(screen.getByRole("button", { name: "Add guardrail" }));
expect(await screen.findByText("Name is required")).toBeInTheDocument();
expect(screen.getByText("API base is required")).toBeInTheDocument();
expect(createGuardrailCall).not.toHaveBeenCalled();
});
it("submits when Enter is pressed inside a text field", async () => {
const user = userEvent.setup();
render(<PromptCompressionTab accessToken="test-token" />);
await user.type(screen.getByLabelText("Name"), "headroom-compression");
await user.type(screen.getByLabelText("Headroom API base"), "https://headroom.example.com{Enter}");
await vi.waitFor(() => expect(createGuardrailCall).toHaveBeenCalledTimes(1));
});
it("clears the name and restores the default switch state after a successful create", async () => {
const user = userEvent.setup();
render(<PromptCompressionTab accessToken="test-token" />);
await user.type(screen.getByLabelText("Name"), "headroom-compression");
await user.type(screen.getByLabelText("Headroom API base"), "https://headroom.example.com");
await user.click(screen.getByLabelText("Apply to all requests"));
await user.click(screen.getByRole("button", { name: "Add guardrail" }));
await vi.waitFor(() => expect(screen.getByLabelText("Name")).toHaveValue(""));
expect(screen.getByLabelText("Headroom API base")).toHaveValue("");
expect(screen.getByLabelText("Apply to all requests")).toBeChecked();
expect(getGuardrailsList).toHaveBeenCalledTimes(2);
});
it("keeps the always-on and opt-in badges for the guardrails it lists", async () => {
getGuardrailsList.mockResolvedValue({
guardrails: [
{
guardrail_id: "g-1",
guardrail_name: "always-on-one",
litellm_params: { guardrail: "headroom", api_base: "https://a.example.com", default_on: true },
},
{
guardrail_id: "g-2",
guardrail_name: "opt-in-one",
litellm_params: { guardrail: "headroom", api_base: "https://b.example.com", default_on: false },
},
],
});
render(<PromptCompressionTab accessToken="test-token" />);
expect(await screen.findByText("Always on")).toBeInTheDocument();
expect(screen.getByText("Opt-in")).toBeInTheDocument();
expect(screen.getByText("https://a.example.com")).toBeInTheDocument();
});
});

View file

@ -1,10 +1,19 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
import { Button, Form, Input, Switch } from "antd";
import { CircleHelp } from "lucide-react";
import { z } from "zod/v4";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { createGuardrailCall, getGuardrailsList } from "@/components/networking";
import { FieldGroup } from "@/components/shared/form/field";
import { FormField } from "@/components/shared/form/FormField";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { useZodForm } from "@/lib/forms/useZodForm";
import { toast } from "@/lib/toast";
import {
buildCompressionGuardrailPayload,
@ -17,14 +26,32 @@ interface PromptCompressionTabProps {
accessToken: string | null;
}
interface CompressionFormValues {
name: string;
apiBase: string;
defaultOn: boolean;
}
const compressionSchema = z.object({
name: z.string().min(1, "Name is required"),
apiBase: z.string().min(1, "API base is required"),
defaultOn: z.boolean(),
});
type CompressionFormValues = z.infer<typeof compressionSchema>;
const EMPTY_VALUES: CompressionFormValues = {
name: "",
apiBase: "",
defaultOn: true,
};
const labelWithHint = (label: string, hint: string): React.ReactNode => (
<>
{label}
<Tooltip>
<TooltipTrigger render={<CircleHelp className="size-3.5 shrink-0 cursor-help text-muted-foreground" />} />
<TooltipContent>{hint}</TooltipContent>
</Tooltip>
</>
);
const PromptCompressionTab: React.FC<PromptCompressionTabProps> = ({ accessToken }) => {
const [form] = Form.useForm<CompressionFormValues>();
const form = useZodForm(compressionSchema, { defaultValues: EMPTY_VALUES });
const [guardrails, setGuardrails] = useState<GuardrailListItem[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isSaving, setIsSaving] = useState<boolean>(false);
@ -61,7 +88,7 @@ const PromptCompressionTab: React.FC<PromptCompressionTabProps> = ({ accessToken
}),
);
toast.success("Compression guardrail created");
form.resetFields();
form.reset(EMPTY_VALUES);
await loadGuardrails();
} catch (error) {
console.error("Failed to create compression guardrail:", error);
@ -85,7 +112,7 @@ const PromptCompressionTab: React.FC<PromptCompressionTabProps> = ({ accessToken
href="https://docs.litellm.ai/docs/proxy/headroom"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline"
className="text-blue-600 underline dark:text-blue-400"
>
Headroom setup docs
</a>
@ -97,7 +124,7 @@ const PromptCompressionTab: React.FC<PromptCompressionTabProps> = ({ accessToken
</p>
)}
{!isLoading && guardrails.length > 0 && (
<ul className="divide-y divide-gray-200">
<ul className="divide-y divide-border">
{guardrails.map((guardrail) => (
<li key={guardrail.guardrail_id} className="flex items-center justify-between py-3">
<div>
@ -107,8 +134,8 @@ const PromptCompressionTab: React.FC<PromptCompressionTabProps> = ({ accessToken
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
guardrail.litellm_params?.default_on
? "bg-emerald-100 text-emerald-800"
: "bg-gray-100 text-gray-600"
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
: "bg-muted text-muted-foreground"
}`}
>
{guardrail.litellm_params?.default_on ? "Always on" : "Opt-in"}
@ -125,48 +152,57 @@ const PromptCompressionTab: React.FC<PromptCompressionTabProps> = ({ accessToken
<CardTitle>Add Headroom compression guardrail</CardTitle>
</CardHeader>
<CardContent>
<Form
form={form}
layout="vertical"
requiredMark={false}
onFinish={handleAdd}
initialValues={{ defaultOn: true }}
>
<Form.Item name="name" label="Name" rules={[{ required: true, message: "Name is required" }]}>
<Input placeholder="headroom-compression" />
</Form.Item>
<Form.Item
name="apiBase"
label="Headroom API base"
tooltip="Base URL of your Headroom compression service (LiteLLM calls its /v1/compress endpoint)"
extra="The URL where your Headroom compression service is hosted"
rules={[{ required: true, message: "API base is required" }]}
>
<Input placeholder="https://your-headroom-endpoint" />
</Form.Item>
<Form.Item name="defaultOn" label="Apply to all requests" valuePropName="checked">
<Switch />
</Form.Item>
<div className="mb-4 rounded-lg border border-yellow-200 bg-yellow-50 p-3">
<p className="text-sm text-yellow-800">
Applying compression to all requests is available to all users. Enabling it selectively per key or team
is a LiteLLM Enterprise feature. Get a trial key{" "}
<a
href="https://www.litellm.ai/#pricing"
target="_blank"
rel="noopener noreferrer"
className="underline"
<TooltipProvider>
<form onSubmit={form.handleSubmit(handleAdd)} noValidate>
<FieldGroup>
<FormField control={form.control} name="name" label="Name">
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="headroom-compression" />}
</FormField>
<FormField
control={form.control}
name="apiBase"
label={labelWithHint(
"Headroom API base",
"Base URL of your Headroom compression service (LiteLLM calls its /v1/compress endpoint)",
)}
description="The URL where your Headroom compression service is hosted"
>
here
</a>
</p>
</div>
<div className="flex justify-end">
<Button type="primary" htmlType="submit" loading={isSaving}>
Add guardrail
</Button>
</div>
</Form>
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="https://your-headroom-endpoint" />}
</FormField>
<FormField control={form.control} name="defaultOn" label="Apply to all requests">
{({ value, onChange, ref: _ref, ...field }) => (
<Switch
{...field}
nativeButton
render={<button type="button" />}
checked={value}
onCheckedChange={onChange}
/>
)}
</FormField>
</FieldGroup>
<div className="mt-6 mb-4 rounded-lg border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-950">
<p className="text-sm text-yellow-800 dark:text-yellow-300">
Applying compression to all requests is available to all users. Enabling it selectively per key or
team is a LiteLLM Enterprise feature. Get a trial key{" "}
<a
href="https://www.litellm.ai/#pricing"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
here
</a>
</p>
</div>
<div className="flex justify-end">
<Button type="submit" disabled={isSaving}>
{isSaving && <UiLoadingSpinner className="size-4" />}
Add guardrail
</Button>
</div>
</form>
</TooltipProvider>
</CardContent>
</Card>
</div>

View file

@ -0,0 +1,68 @@
// eslint-disable-next-line no-restricted-imports -- the parent cost_tracking_settings still owns this antd Form, and the point of this test is that AddProviderForm registers nothing in it
import { Form } from "antd";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../../../tests/test-utils";
import AddProviderForm from "./add_provider_form";
import { DiscountConfig } from "./types";
const onAddProvider = vi.fn();
const onParentFinish = vi.fn();
const readParentStore = vi.fn();
const ParentOwnedForm = () => {
const [form] = Form.useForm();
return (
<Form form={form} onFinish={onParentFinish} layout="vertical" className="space-y-6">
<AddProviderForm
discountConfig={{} as DiscountConfig}
selectedProvider="OpenAI"
newDiscount="5"
onProviderChange={vi.fn()}
onDiscountChange={vi.fn()}
onAddProvider={() => {
readParentStore(form.getFieldsValue());
onAddProvider();
}}
/>
</Form>
);
};
describe("AddProviderForm inside the antd form its parent owns", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("registers no field in the parent FormInstance, so the parent's resetFields is a no-op", async () => {
const user = userEvent.setup();
renderWithProviders(<ParentOwnedForm />);
await user.click(screen.getByRole("button", { name: /add provider discount/i }));
expect(readParentStore).toHaveBeenCalledTimes(1);
expect(readParentStore.mock.calls[0][0]).toEqual({});
});
it("drives both the onAddProvider prop and the parent form submit from one click", async () => {
const user = userEvent.setup();
renderWithProviders(<ParentOwnedForm />);
await user.click(screen.getByRole("button", { name: /add provider discount/i }));
expect(onAddProvider).toHaveBeenCalledTimes(1);
expect(onParentFinish).toHaveBeenCalledTimes(1);
});
it("treats Enter in the discount field exactly like a click on the add button", async () => {
const user = userEvent.setup();
renderWithProviders(<ParentOwnedForm />);
await user.type(screen.getByPlaceholderText("5"), "{Enter}");
await vi.waitFor(() => expect(onParentFinish).toHaveBeenCalledTimes(1));
expect(onAddProvider).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,11 +1,28 @@
import React from "react";
import { TextInput, Button } from "@tremor/react";
import { Select as AntdSelect, Form, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Providers, provider_map } from "@/components/provider_info_helpers";
import { CircleHelp } from "lucide-react";
import { Logo } from "@/components/molecules/logo/Logo";
import { Providers, provider_map } from "@/components/provider_info_helpers";
import { Field, FieldGroup, FieldLabel } from "@/components/shared/form/field";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import { Input } from "@/components/ui/input";
import { InputGroupAddon } from "@/components/ui/input-group";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { DiscountConfig } from "./types";
interface ProviderOption {
value: string;
label: string;
}
interface AddProviderFormProps {
discountConfig: DiscountConfig;
selectedProvider: string | undefined;
@ -15,6 +32,35 @@ interface AddProviderFormProps {
onAddProvider: () => void;
}
const PROVIDER_FIELD_ID = "add-provider-discount-provider";
const DISCOUNT_FIELD_ID = "add-provider-discount-percentage";
const providerOptionsWithoutDiscount = (discountConfig: DiscountConfig): ProviderOption[] =>
Object.entries(Providers)
.filter(([providerEnum]) => {
const providerValue = provider_map[providerEnum as keyof typeof provider_map];
return !(providerValue && discountConfig[providerValue]);
})
.map(([value, label]) => ({ value, label }));
const selectedProviderOption = (selectedProvider: string | undefined): ProviderOption | null => {
if (!selectedProvider) {
return null;
}
const label = Providers[selectedProvider as keyof typeof Providers];
return label ? { value: selectedProvider, label } : null;
};
const labelWithHint = (label: string, hint: string): React.ReactNode => (
<>
{label}
<Tooltip>
<TooltipTrigger render={<CircleHelp className="size-3.5 shrink-0 cursor-help text-muted-foreground" />} />
<TooltipContent>{hint}</TooltipContent>
</Tooltip>
</>
);
const AddProviderForm: React.FC<AddProviderFormProps> = ({
discountConfig,
selectedProvider,
@ -23,79 +69,71 @@ const AddProviderForm: React.FC<AddProviderFormProps> = ({
onDiscountChange,
onAddProvider,
}) => {
const options = providerOptionsWithoutDiscount(discountConfig);
const selectedOption = selectedProviderOption(selectedProvider);
return (
<div className="space-y-6">
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Provider
<Tooltip title="Select the LLM provider you want to configure a discount for">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
rules={[{ required: true, message: "Please select a provider" }]}
>
<AntdSelect
showSearch
placeholder="Select provider"
value={selectedProvider}
onChange={onProviderChange}
style={{ width: "100%" }}
size="large"
optionFilterProp="children"
filterOption={(input, option) =>
String(option?.label ?? "")
.toLowerCase()
.includes(input.toLowerCase())
}
>
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => {
const providerValue = provider_map[providerEnum as keyof typeof provider_map];
// Only show providers that don't already have a discount configured
if (providerValue && discountConfig[providerValue]) {
return null;
}
return (
<AntdSelect.Option key={providerEnum} value={providerEnum} label={providerDisplayName}>
<div className="flex items-center space-x-2">
<Logo provider={providerEnum} label={providerDisplayName} className="w-5 h-5" />
<span>{providerDisplayName}</span>
</div>
</AntdSelect.Option>
);
})}
</AntdSelect>
</Form.Item>
<TooltipProvider>
<div className="space-y-6">
<FieldGroup>
<Field>
<FieldLabel htmlFor={PROVIDER_FIELD_ID}>
{labelWithHint("Provider", "Select the LLM provider you want to configure a discount for")}
</FieldLabel>
<Combobox
items={options}
value={selectedOption}
onValueChange={(option: ProviderOption | null) => onProviderChange(option?.value)}
itemToStringLabel={(option: ProviderOption) => option.label}
isItemEqualToValue={(option: ProviderOption, value: ProviderOption) => option.value === value.value}
>
<ComboboxInput id={PROVIDER_FIELD_ID} placeholder="Select provider" className="w-full">
{selectedOption && (
<InputGroupAddon align="inline-start">
<Logo provider={selectedOption.value} label={selectedOption.label} className="w-5 h-5" />
</InputGroupAddon>
)}
</ComboboxInput>
<ComboboxContent>
<ComboboxEmpty>No providers found</ComboboxEmpty>
<ComboboxList>
{(option: ProviderOption) => (
<ComboboxItem key={option.value} value={option}>
<span className="flex items-center space-x-2">
<Logo provider={option.value} label={option.label} className="w-5 h-5" />
<span>{option.label}</span>
</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Discount Percentage
<Tooltip title="Enter a percentage value (e.g., 5 for 5% discount)">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
rules={[{ required: true, message: "Please enter a discount percentage" }]}
>
<div className="flex items-center gap-2">
<TextInput
placeholder="5"
value={newDiscount}
onValueChange={onDiscountChange}
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500 flex-1"
/>
<span className="text-gray-600">%</span>
<Field>
<FieldLabel htmlFor={DISCOUNT_FIELD_ID}>
{labelWithHint("Discount Percentage", "Enter a percentage value (e.g., 5 for 5% discount)")}
</FieldLabel>
<div className="flex items-center gap-2">
<Input
id={DISCOUNT_FIELD_ID}
placeholder="5"
value={newDiscount}
onChange={(event) => onDiscountChange(event.target.value)}
className="flex-1 rounded-lg"
/>
<span className="text-muted-foreground">%</span>
</div>
</Field>
</FieldGroup>
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-border">
<Button type="submit" onClick={onAddProvider} disabled={!selectedProvider || !newDiscount}>
Add Provider Discount
</Button>
</div>
</Form.Item>
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
<Button variant="primary" onClick={onAddProvider} disabled={!selectedProvider || !newDiscount}>
Add Provider Discount
</Button>
</div>
</div>
</TooltipProvider>
);
};

View file

@ -0,0 +1,95 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import CloudZeroCreateModal from "./CloudZeroCreateModal";
const mutate = vi.fn();
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
__esModule: true,
default: () => ({ accessToken: "test-token" }),
}));
vi.mock("@/app/(dashboard)/hooks/cloudzero/useCloudZeroCreate", () => ({
useCloudZeroCreate: () => ({ mutate, isPending: false }),
}));
const renderModal = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return render(
<QueryClientProvider client={queryClient}>
<CloudZeroCreateModal open={true} onOk={vi.fn()} onCancel={vi.fn()} />
</QueryClientProvider>,
);
};
const submittedPayload = (): Record<string, unknown> => {
expect(mutate).toHaveBeenCalledTimes(1);
return mutate.mock.calls[0][0] as Record<string, unknown>;
};
describe("CloudZeroCreateModal submit payload", () => {
beforeEach(() => {
mutate.mockClear();
});
it("sends every filled field verbatim", async () => {
const user = userEvent.setup();
renderModal();
await user.type(screen.getByLabelText("CloudZero API Key"), "cz-secret-key");
await user.type(screen.getByLabelText("Connection ID"), "conn-42");
await user.type(screen.getByLabelText("Timezone"), "America/New_York");
await user.click(screen.getByRole("button", { name: "Create" }));
await vi.waitFor(() =>
expect(submittedPayload()).toEqual({
connection_id: "conn-42",
timezone: "America/New_York",
api_key: "cz-secret-key",
}),
);
});
it("defaults an untouched timezone to UTC", async () => {
const user = userEvent.setup();
renderModal();
await user.type(screen.getByLabelText("CloudZero API Key"), "cz-secret-key");
await user.type(screen.getByLabelText("Connection ID"), "conn-42");
await user.click(screen.getByRole("button", { name: "Create" }));
await vi.waitFor(() =>
expect(submittedPayload()).toEqual({
connection_id: "conn-42",
timezone: "UTC",
api_key: "cz-secret-key",
}),
);
});
it("blocks submission and shows both required messages when the form is empty", async () => {
const user = userEvent.setup();
renderModal();
await user.click(screen.getByRole("button", { name: "Create" }));
expect(await screen.findByText("Please enter your CloudZero API key")).toBeInTheDocument();
expect(screen.getByText("Please enter your CloudZero connection ID")).toBeInTheDocument();
expect(mutate).not.toHaveBeenCalled();
});
it("does not submit when Enter is pressed inside a text field", async () => {
const user = userEvent.setup();
renderModal();
await user.type(screen.getByLabelText("CloudZero API Key"), "cz-secret-key");
await user.type(screen.getByLabelText("Connection ID"), "conn-42{Enter}");
expect(mutate).not.toHaveBeenCalled();
});
});

View file

@ -1,8 +1,18 @@
import { Form, Modal, Input } from "antd";
import { toast } from "@/lib/toast";
import { Modal } from "antd";
import { useEffect } from "react";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { z } from "zod/v4";
import { useCloudZeroCreate } from "@/app/(dashboard)/hooks/cloudzero/useCloudZeroCreate";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { FieldGroup } from "@/components/shared/form/field";
import { FormField } from "@/components/shared/form/FormField";
import { Input } from "@/components/ui/input";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useZodForm } from "@/lib/forms/useZodForm";
import { toast } from "@/lib/toast";
import { CloudZeroApiKeyInput, labelWithHint } from "./CloudZeroFormControls";
import { buildCloudZeroPayload, EMPTY_CLOUDZERO_FORM_VALUES, type CloudZeroFormValues } from "./cloudZeroPayload";
interface CloudZeroCreationModalProps {
open: boolean;
@ -10,50 +20,38 @@ interface CloudZeroCreationModalProps {
onCancel: () => void;
}
const createSchema = z.object({
api_key: z.string().min(1, "Please enter your CloudZero API key"),
connection_id: z.string().min(1, "Please enter your CloudZero connection ID"),
timezone: z.string(),
});
export default function CloudZeroCreationModal({ open, onOk, onCancel }: CloudZeroCreationModalProps) {
const { accessToken } = useAuthorized();
const [form] = Form.useForm();
const form = useZodForm(createSchema, { defaultValues: EMPTY_CLOUDZERO_FORM_VALUES });
const createMutation = useCloudZeroCreate(accessToken || "");
useEffect(() => {
if (open) {
form.resetFields();
form.reset(EMPTY_CLOUDZERO_FORM_VALUES);
}
}, [open, form]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
createMutation.mutate(
{
connection_id: values.connection_id,
timezone: values.timezone || "UTC",
...(values.api_key && { api_key: values.api_key }),
},
{
onSuccess: () => {
toast.success("CloudZero integration created successfully");
form.resetFields();
onOk();
},
onError: (error: any) => {
if (error?.errorFields) {
return;
}
toast.error(error?.message || "Failed to create CloudZero integration");
},
},
);
} catch (error: any) {
if (error?.errorFields) {
return;
}
toast.error(error?.message || "Failed to create CloudZero integration");
}
const handleSubmit = (values: CloudZeroFormValues) => {
createMutation.mutate(buildCloudZeroPayload(values), {
onSuccess: () => {
toast.success("CloudZero integration created successfully");
form.reset(EMPTY_CLOUDZERO_FORM_VALUES);
onOk();
},
onError: (error: Error) => {
toast.error(error.message || "Failed to create CloudZero integration");
},
});
};
const handleCancel = () => {
form.resetFields();
form.reset(EMPTY_CLOUDZERO_FORM_VALUES);
onCancel();
};
@ -61,7 +59,7 @@ export default function CloudZeroCreationModal({ open, onOk, onCancel }: CloudZe
<Modal
title="Create CloudZero Integration"
open={open}
onOk={handleSubmit}
onOk={() => void form.handleSubmit(handleSubmit)()}
onCancel={handleCancel}
confirmLoading={createMutation.isPending}
okText={createMutation.isPending ? "Creating..." : "Create"}
@ -73,29 +71,27 @@ export default function CloudZeroCreationModal({ open, onOk, onCancel }: CloudZe
disabled: createMutation.isPending,
}}
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item
label="CloudZero API Key"
name="api_key"
rules={[{ required: true, message: "Please enter your CloudZero API key" }]}
>
<Input.Password placeholder="Enter your CloudZero API key" />
</Form.Item>
<Form.Item
label="Connection ID"
name="connection_id"
rules={[{ required: true, message: "Please enter your CloudZero connection ID" }]}
>
<Input placeholder="Enter your CloudZero connection ID" />
</Form.Item>
<Form.Item
label="Timezone"
name="timezone"
tooltip="Timezone for date handling (defaults to UTC if not provided)"
>
<Input placeholder="UTC" />
</Form.Item>
</Form>
<TooltipProvider>
<form onSubmit={(event) => event.preventDefault()} noValidate>
<FieldGroup>
<FormField control={form.control} name="api_key" label="CloudZero API Key">
{({ ref, ...field }) => (
<CloudZeroApiKeyInput {...field} ref={ref} placeholder="Enter your CloudZero API key" />
)}
</FormField>
<FormField control={form.control} name="connection_id" label="Connection ID">
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="Enter your CloudZero connection ID" />}
</FormField>
<FormField
control={form.control}
name="timezone"
label={labelWithHint("Timezone", "Timezone for date handling (defaults to UTC if not provided)")}
>
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="UTC" />}
</FormField>
</FieldGroup>
</form>
</TooltipProvider>
</Modal>
);
}

View file

@ -0,0 +1,41 @@
"use client";
import { CircleHelp, Eye, EyeOff } from "lucide-react";
import * as React from "react";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export const labelWithHint = (label: string, hint: string): React.ReactNode => (
<>
{label}
<Tooltip>
<TooltipTrigger render={<CircleHelp className="size-3.5 shrink-0 cursor-help text-muted-foreground" />} />
<TooltipContent>{hint}</TooltipContent>
</Tooltip>
</>
);
export const CloudZeroApiKeyInput = React.forwardRef<
HTMLInputElement,
Omit<React.ComponentPropsWithoutRef<"input">, "type">
>(({ className, ...props }, ref) => {
const [revealed, setRevealed] = React.useState(false);
return (
<InputGroup className={className}>
<InputGroupInput {...props} ref={ref} type={revealed ? "text" : "password"} />
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
variant="ghost"
aria-label={revealed ? "Hide API key" : "Show API key"}
onClick={() => setRevealed((current) => !current)}
>
{revealed ? <EyeOff /> : <Eye />}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
);
});
CloudZeroApiKeyInput.displayName = "CloudZeroApiKeyInput";

View file

@ -0,0 +1,139 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import CloudZeroUpdateModal from "./CloudZeroUpdateModal";
import { CloudZeroSettings } from "./types";
const mutate = vi.fn();
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
__esModule: true,
default: () => ({ accessToken: "test-token" }),
}));
vi.mock("@/app/(dashboard)/hooks/cloudzero/useCloudZeroSettings", () => ({
useCloudZeroUpdateSettings: () => ({ mutate, isPending: false }),
}));
const STORED_SETTINGS: CloudZeroSettings = {
connection_id: "stored-connection-id",
api_key_masked: "sk-cz-****last4",
timezone: "Europe/Berlin",
status: "Active",
};
const renderModal = (settings: CloudZeroSettings = STORED_SETTINGS) => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return render(
<QueryClientProvider client={queryClient}>
<CloudZeroUpdateModal open={true} onOk={vi.fn()} onCancel={vi.fn()} settings={settings} />
</QueryClientProvider>,
);
};
const submittedPayload = (): Record<string, unknown> => {
expect(mutate).toHaveBeenCalledTimes(1);
return mutate.mock.calls[0][0] as Record<string, unknown>;
};
describe("CloudZeroUpdateModal submit payload", () => {
beforeEach(() => {
mutate.mockClear();
});
it("seeds the stored connection id and timezone but never the stored key", () => {
renderModal();
expect(screen.getByLabelText("Connection ID")).toHaveValue("stored-connection-id");
expect(screen.getByLabelText("Timezone")).toHaveValue("Europe/Berlin");
expect(screen.getByLabelText("CloudZero API Key")).toHaveValue("");
});
it("omits api_key entirely when the key field is left untouched, preserving the stored secret", async () => {
const user = userEvent.setup();
renderModal();
await user.click(screen.getByRole("button", { name: "Update" }));
await vi.waitFor(() =>
expect(submittedPayload()).toEqual({
connection_id: "stored-connection-id",
timezone: "Europe/Berlin",
}),
);
expect("api_key" in submittedPayload()).toBe(false);
});
it("sends api_key only once the user types a replacement key", async () => {
const user = userEvent.setup();
renderModal();
await user.type(screen.getByLabelText("CloudZero API Key"), "cz-rotated-key");
await user.click(screen.getByRole("button", { name: "Update" }));
await vi.waitFor(() =>
expect(submittedPayload()).toEqual({
connection_id: "stored-connection-id",
timezone: "Europe/Berlin",
api_key: "cz-rotated-key",
}),
);
});
it("falls back to UTC when the stored timezone is cleared", async () => {
const user = userEvent.setup();
renderModal();
await user.clear(screen.getByLabelText("Timezone"));
await user.click(screen.getByRole("button", { name: "Update" }));
await vi.waitFor(() =>
expect(submittedPayload()).toEqual({
connection_id: "stored-connection-id",
timezone: "UTC",
}),
);
});
it("falls back to UTC when the stored settings carry no timezone", () => {
renderModal({ ...STORED_SETTINGS, timezone: null });
expect(screen.getByLabelText("Timezone")).toHaveValue("UTC");
});
it("reports a null connection id from the server as the required field, not as a type error", async () => {
const user = userEvent.setup();
renderModal({ ...STORED_SETTINGS, connection_id: null, timezone: null });
await user.click(screen.getByRole("button", { name: "Update" }));
expect(await screen.findByText("Please enter your CloudZero connection ID")).toBeInTheDocument();
expect(screen.queryByText(/expected string, received null/i)).not.toBeInTheDocument();
expect(mutate).not.toHaveBeenCalled();
});
it("blocks submission when the connection id is cleared, and leaves the key optional", async () => {
const user = userEvent.setup();
renderModal();
await user.clear(screen.getByLabelText("Connection ID"));
await user.click(screen.getByRole("button", { name: "Update" }));
expect(await screen.findByText("Please enter your CloudZero connection ID")).toBeInTheDocument();
expect(screen.queryByText("Please enter your CloudZero API key")).not.toBeInTheDocument();
expect(mutate).not.toHaveBeenCalled();
});
it("does not submit when Enter is pressed inside a text field", async () => {
const user = userEvent.setup();
renderModal();
await user.type(screen.getByLabelText("CloudZero API Key"), "cz-rotated-key{Enter}");
expect(mutate).not.toHaveBeenCalled();
});
});

View file

@ -1,8 +1,18 @@
import { Modal } from "antd";
import { useEffect } from "react";
import { z } from "zod/v4";
import { useCloudZeroUpdateSettings } from "@/app/(dashboard)/hooks/cloudzero/useCloudZeroSettings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { Form, Input, Modal } from "antd";
import { FieldGroup } from "@/components/shared/form/field";
import { FormField } from "@/components/shared/form/FormField";
import { Input } from "@/components/ui/input";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useZodForm } from "@/lib/forms/useZodForm";
import { toast } from "@/lib/toast";
import { useEffect } from "react";
import { CloudZeroApiKeyInput, labelWithHint } from "./CloudZeroFormControls";
import { buildCloudZeroPayload, EMPTY_CLOUDZERO_FORM_VALUES, type CloudZeroFormValues } from "./cloudZeroPayload";
import { CloudZeroSettings } from "./types";
interface CloudZeroUpdateModalProps {
@ -12,56 +22,44 @@ interface CloudZeroUpdateModalProps {
settings: CloudZeroSettings;
}
const updateSchema = z.object({
api_key: z.string(),
connection_id: z.string().min(1, "Please enter your CloudZero connection ID"),
timezone: z.string(),
});
export default function CloudZeroUpdateModal({ open, onOk, onCancel, settings }: CloudZeroUpdateModalProps) {
const { accessToken } = useAuthorized();
const [form] = Form.useForm();
const form = useZodForm(updateSchema, { defaultValues: EMPTY_CLOUDZERO_FORM_VALUES });
const updateMutation = useCloudZeroUpdateSettings(accessToken || "");
useEffect(() => {
if (open && settings) {
form.setFieldsValue({
connection_id: settings.connection_id,
form.reset({
connection_id: settings.connection_id ?? "",
timezone: settings.timezone || "UTC",
api_key: "",
});
} else if (open) {
form.resetFields();
form.reset(EMPTY_CLOUDZERO_FORM_VALUES);
}
}, [open, settings, form]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
updateMutation.mutate(
{
connection_id: values.connection_id,
timezone: values.timezone || "UTC",
...(values.api_key && { api_key: values.api_key }),
},
{
onSuccess: () => {
toast.success("CloudZero integration updated successfully");
form.resetFields();
onOk();
},
onError: (error: any) => {
if (error?.errorFields) {
return;
}
toast.error(error?.message || "Failed to update CloudZero integration");
},
},
);
} catch (error: any) {
if (error?.errorFields) {
return;
}
toast.error(error?.message || "Failed to update CloudZero integration");
}
const handleSubmit = (values: CloudZeroFormValues) => {
updateMutation.mutate(buildCloudZeroPayload(values), {
onSuccess: () => {
toast.success("CloudZero integration updated successfully");
form.reset(EMPTY_CLOUDZERO_FORM_VALUES);
onOk();
},
onError: (error: Error) => {
toast.error(error.message || "Failed to update CloudZero integration");
},
});
};
const handleCancel = () => {
form.resetFields();
form.reset(EMPTY_CLOUDZERO_FORM_VALUES);
onCancel();
};
@ -69,7 +67,7 @@ export default function CloudZeroUpdateModal({ open, onOk, onCancel, settings }:
<Modal
title="Edit CloudZero Integration"
open={open}
onOk={handleSubmit}
onOk={() => void form.handleSubmit(handleSubmit)()}
onCancel={handleCancel}
confirmLoading={updateMutation.isPending}
okText={updateMutation.isPending ? "Updating..." : "Update"}
@ -81,30 +79,31 @@ export default function CloudZeroUpdateModal({ open, onOk, onCancel, settings }:
disabled: updateMutation.isPending,
}}
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item
label="CloudZero API Key"
name="api_key"
rules={[{ required: false, message: "Please enter your CloudZero API key" }]}
tooltip="Leave empty to keep the existing API key"
>
<Input.Password placeholder="Leave empty to keep existing" />
</Form.Item>
<Form.Item
label="Connection ID"
name="connection_id"
rules={[{ required: true, message: "Please enter your CloudZero connection ID" }]}
>
<Input placeholder="Enter your CloudZero connection ID" />
</Form.Item>
<Form.Item
label="Timezone"
name="timezone"
tooltip="Timezone for date handling (defaults to UTC if not provided)"
>
<Input placeholder="UTC" />
</Form.Item>
</Form>
<TooltipProvider>
<form onSubmit={(event) => event.preventDefault()} noValidate>
<FieldGroup>
<FormField
control={form.control}
name="api_key"
label={labelWithHint("CloudZero API Key", "Leave empty to keep the existing API key")}
>
{({ ref, ...field }) => (
<CloudZeroApiKeyInput {...field} ref={ref} placeholder="Leave empty to keep existing" />
)}
</FormField>
<FormField control={form.control} name="connection_id" label="Connection ID">
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="Enter your CloudZero connection ID" />}
</FormField>
<FormField
control={form.control}
name="timezone"
label={labelWithHint("Timezone", "Timezone for date handling (defaults to UTC if not provided)")}
>
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="UTC" />}
</FormField>
</FieldGroup>
</form>
</TooltipProvider>
</Modal>
);
}

View file

@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { buildCloudZeroPayload } from "./cloudZeroPayload";
describe("buildCloudZeroPayload", () => {
it("passes a filled form through verbatim", () => {
expect(
buildCloudZeroPayload({ api_key: "cz-key", connection_id: "conn-1", timezone: "America/New_York" }),
).toStrictEqual({
connection_id: "conn-1",
timezone: "America/New_York",
api_key: "cz-key",
});
});
it("omits the api_key key entirely when the field is blank, so a stored secret survives an untouched save", () => {
const payload = buildCloudZeroPayload({ api_key: "", connection_id: "conn-1", timezone: "UTC" });
expect("api_key" in payload).toBe(false);
expect(payload).toStrictEqual({ connection_id: "conn-1", timezone: "UTC" });
});
it.each([
["blank", ""],
["absent", undefined],
])("falls back to UTC when the timezone is %s", (_label, timezone) => {
const payload = buildCloudZeroPayload({
api_key: "cz-key",
connection_id: "conn-1",
timezone: timezone as string,
});
expect(payload.timezone).toBe("UTC");
});
it("never invents a timezone default over a real value", () => {
expect(buildCloudZeroPayload({ api_key: "", connection_id: "conn-1", timezone: "UTC+2" }).timezone).toBe("UTC+2");
});
it("keeps a whitespace-only api_key, matching the truthiness check the antd modals used", () => {
expect(buildCloudZeroPayload({ api_key: " ", connection_id: "conn-1", timezone: "UTC" }).api_key).toBe(" ");
});
});

View file

@ -0,0 +1,23 @@
export interface CloudZeroFormValues {
api_key: string;
connection_id: string;
timezone: string;
}
export interface CloudZeroPayload {
connection_id: string;
timezone: string;
api_key?: string;
}
export const EMPTY_CLOUDZERO_FORM_VALUES: CloudZeroFormValues = {
api_key: "",
connection_id: "",
timezone: "",
};
export const buildCloudZeroPayload = (values: CloudZeroFormValues): CloudZeroPayload => ({
connection_id: values.connection_id,
timezone: values.timezone || "UTC",
...(values.api_key && { api_key: values.api_key }),
});