mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
Merge pull request #36966 from BerriAI/litellm_shadcn_smallfiles_0814
refactor(ui): migrate ten small dashboard files off antd and tremor
This commit is contained in:
commit
f03df1bb42
16 changed files with 218 additions and 97 deletions
|
|
@ -1653,21 +1653,11 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/onboarding/OnboardingErrorView.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/onboarding/OnboardingFormBody.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/onboarding/OnboardingLoadingView.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/AIHub/ModelHubTable.test.tsx": {
|
||||
"max-params": {
|
||||
"count": 1
|
||||
|
|
@ -1847,9 +1837,6 @@
|
|||
}
|
||||
},
|
||||
"src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
|
|
@ -2388,11 +2375,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/key_team_helpers/TagRateLimitEditor.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/key_team_helpers/fetch_available_models_team_key.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
|
|
@ -2715,17 +2697,9 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/router_settings/LatencyBasedConfiguration.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/router_settings/ReliabilityRetriesSection.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/router_settings/RoutingStrategySelector.tsx": {
|
||||
|
|
@ -2733,18 +2707,10 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/router_settings/TagFilteringToggle.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/router_settings/index.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"prefer-const": {
|
||||
"count": 2
|
||||
}
|
||||
|
|
@ -2781,11 +2747,6 @@
|
|||
"count": 4
|
||||
}
|
||||
},
|
||||
"src/components/shared/CreatedKeyDisplay.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/shared/advanced_date_picker.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
|
|
@ -2850,9 +2811,6 @@
|
|||
"src/components/shared/numerical_input.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/shared/table_cells/cell_tooltip.tsx": {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ describe("OnboardingErrorView", () => {
|
|||
expect(screen.getByText("Failed to load invitation")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should expose the failure as an alert to assistive technology", () => {
|
||||
render(<OnboardingErrorView />);
|
||||
expect(screen.getByRole("alert")).toHaveTextContent("Failed to load invitation");
|
||||
});
|
||||
|
||||
it("should show the expiry description", () => {
|
||||
render(<OnboardingErrorView />);
|
||||
expect(screen.getByText("The invitation link may be invalid or expired.")).toBeInTheDocument();
|
||||
|
|
@ -16,7 +21,6 @@ describe("OnboardingErrorView", () => {
|
|||
|
||||
it("should render a Back to Login link pointing to /ui/login/", () => {
|
||||
render(<OnboardingErrorView />);
|
||||
// antd Button with href renders as an <a> element
|
||||
const link = screen.getByRole("link", { name: "Back to Login" });
|
||||
expect(link).toHaveAttribute("href", "/ui/login/");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
import React from "react";
|
||||
import { Alert, Button } from "antd";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { getLoginUrl } from "@/utils/returnUrlUtils";
|
||||
|
||||
export function OnboardingErrorView() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md mt-10">
|
||||
<Alert
|
||||
type="error"
|
||||
message="Failed to load invitation"
|
||||
description="The invitation link may be invalid or expired."
|
||||
showIcon
|
||||
/>
|
||||
<Alert variant="error">
|
||||
<CircleAlert />
|
||||
<AlertTitle>Failed to load invitation</AlertTitle>
|
||||
<AlertDescription>The invitation link may be invalid or expired.</AlertDescription>
|
||||
</Alert>
|
||||
<div className="mt-4">
|
||||
<Button href={getLoginUrl()}>Back to Login</Button>
|
||||
<a href={getLoginUrl()} className={buttonVariants({ variant: "outline" })}>
|
||||
Back to Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { OnboardingLoadingView } from "./OnboardingLoadingView";
|
||||
|
||||
describe("OnboardingLoadingView", () => {
|
||||
it("should render a spinner container", () => {
|
||||
const { container } = render(<OnboardingLoadingView />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
it("should expose the loading state to assistive technology", () => {
|
||||
render(<OnboardingLoadingView />);
|
||||
expect(screen.getByRole("status", { name: "Loading invitation" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should apply centering layout classes", () => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React from "react";
|
||||
import { Spin } from "antd";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
|
||||
export function OnboardingLoadingView() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md mt-10 flex justify-center">
|
||||
<Spin indicator={<LoadingOutlined spin />} size="large" />
|
||||
<UiLoadingSpinner role="status" aria-label="Loading invitation" className="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
* Works with forms - reads from and writes to router_settings.fallbacks
|
||||
*/
|
||||
|
||||
import { Button as TremorButton } from "@tremor/react";
|
||||
import { Button } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import NotificationManager from "../../../molecules/notifications_manager";
|
||||
import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models";
|
||||
|
|
@ -119,13 +119,10 @@ export default function AddFallbacks({ accessToken, value = [], onChange }: AddF
|
|||
|
||||
return (
|
||||
<div>
|
||||
<TremorButton
|
||||
className="mx-auto"
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
icon={() => <span className="mr-1">+</span>}
|
||||
>
|
||||
<Button className="mx-auto" onClick={() => setIsModalVisible(true)}>
|
||||
<span>+</span>
|
||||
Add Fallbacks
|
||||
</TremorButton>
|
||||
</Button>
|
||||
<AddFallbacksModal open={isModalVisible} onCancel={handleCancel}>
|
||||
<FallbackSelectionForm
|
||||
key={modalKey}
|
||||
|
|
@ -138,15 +135,11 @@ export default function AddFallbacks({ accessToken, value = [], onChange }: AddF
|
|||
{/* Footer with Cancel and Save buttons */}
|
||||
{groups.length > 0 && (
|
||||
<div className="flex items-center justify-end space-x-3 pt-6 mt-6 border-t border-gray-100">
|
||||
<Button type="default" onClick={handleCancel} disabled={isSaving}>
|
||||
<Button variant="outline" onClick={handleCancel} disabled={isSaving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleSaveAll}
|
||||
disabled={groups.length === 0 || isSaving}
|
||||
loading={isSaving}
|
||||
>
|
||||
<Button variant="outline" onClick={handleSaveAll} disabled={groups.length === 0 || isSaving}>
|
||||
{isSaving && <UiLoadingSpinner className="size-4" />}
|
||||
{isSaving ? "Saving Configuration..." : "Save All Configurations"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
import React, { useState } from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { TagRateLimitEditor, TagRateLimitEntry, tagLimitsToRows, tagRowsToLimits } from "./TagRateLimitEditor";
|
||||
|
||||
function Harness({ initial = [] as TagRateLimitEntry[], onValue }: { initial?: TagRateLimitEntry[]; onValue?: any }) {
|
||||
const [rows, setRows] = useState<TagRateLimitEntry[]>(initial);
|
||||
return (
|
||||
<TagRateLimitEditor
|
||||
value={rows}
|
||||
onChange={(next) => {
|
||||
setRows(next);
|
||||
onValue?.(next);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const rowsWith = (tag: string, rpm: number | null): TagRateLimitEntry[] => [{ id: "r1", tag, rpm_limit: rpm }];
|
||||
|
||||
describe("TagRateLimitEditor", () => {
|
||||
it("should render one tag and one RPM field per row", () => {
|
||||
render(<Harness initial={rowsWith("cell-1", 100)} />);
|
||||
expect(screen.getByRole("textbox", { name: "Tag" })).toHaveValue("cell-1");
|
||||
expect(screen.getByRole("spinbutton", { name: "RPM limit" })).toHaveValue(100);
|
||||
});
|
||||
|
||||
it("should add a row when Add Tag Limit is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness />);
|
||||
expect(screen.queryAllByRole("textbox", { name: "Tag" })).toHaveLength(0);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /add tag limit/i }));
|
||||
|
||||
expect(screen.getAllByRole("textbox", { name: "Tag" })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should let the user type a tag name", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness initial={rowsWith("", null)} />);
|
||||
|
||||
await user.type(screen.getByRole("textbox", { name: "Tag" }), "cell-2");
|
||||
|
||||
expect(screen.getByRole("textbox", { name: "Tag" })).toHaveValue("cell-2");
|
||||
});
|
||||
|
||||
it("should record the typed RPM limit as a number, not a string", async () => {
|
||||
const user = userEvent.setup();
|
||||
const seen: TagRateLimitEntry[][] = [];
|
||||
render(<Harness initial={rowsWith("cell-1", null)} onValue={(v: TagRateLimitEntry[]) => seen.push(v)} />);
|
||||
|
||||
await user.type(screen.getByRole("spinbutton", { name: "RPM limit" }), "60");
|
||||
|
||||
const latest = seen[seen.length - 1][0];
|
||||
expect(latest.rpm_limit).toBe(60);
|
||||
expect(typeof latest.rpm_limit).toBe("number");
|
||||
});
|
||||
|
||||
it("should reset the RPM limit to null when the field is cleared", async () => {
|
||||
const user = userEvent.setup();
|
||||
const seen: TagRateLimitEntry[][] = [];
|
||||
render(<Harness initial={rowsWith("cell-1", 60)} onValue={(v: TagRateLimitEntry[]) => seen.push(v)} />);
|
||||
|
||||
await user.clear(screen.getByRole("spinbutton", { name: "RPM limit" }));
|
||||
|
||||
expect(seen[seen.length - 1][0].rpm_limit).toBeNull();
|
||||
});
|
||||
|
||||
it("should remove only the clicked row", async () => {
|
||||
const user = userEvent.setup();
|
||||
const initial: TagRateLimitEntry[] = [
|
||||
{ id: "r1", tag: "keep-me", rpm_limit: 10 },
|
||||
{ id: "r2", tag: "delete-me", rpm_limit: 20 },
|
||||
];
|
||||
render(<Harness initial={initial} />);
|
||||
|
||||
await user.click(screen.getAllByRole("button", { name: "Remove tag limit" })[1]);
|
||||
|
||||
const tags = screen.getAllByRole("textbox", { name: "Tag" });
|
||||
expect(tags).toHaveLength(1);
|
||||
expect(tags[0]).toHaveValue("keep-me");
|
||||
});
|
||||
|
||||
it("should not submit the surrounding form when a row is removed", async () => {
|
||||
const user = userEvent.setup();
|
||||
let submitted = false;
|
||||
render(
|
||||
<form
|
||||
onSubmit={() => {
|
||||
submitted = true;
|
||||
}}
|
||||
>
|
||||
<Harness initial={rowsWith("cell-1", 10)} />
|
||||
</form>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Remove tag limit" }));
|
||||
|
||||
expect(submitted).toBe(false);
|
||||
expect(screen.queryAllByRole("textbox", { name: "Tag" })).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("tagRowsToLimits", () => {
|
||||
it("should map named rows with numeric limits into the rpm map", () => {
|
||||
expect(tagRowsToLimits([{ id: "a", tag: "cell-1", rpm_limit: 60 }])).toEqual({ tag_rpm_limit: { "cell-1": 60 } });
|
||||
});
|
||||
|
||||
it("should drop rows with a blank tag or a null limit", () => {
|
||||
const rows: TagRateLimitEntry[] = [
|
||||
{ id: "a", tag: " ", rpm_limit: 60 },
|
||||
{ id: "b", tag: "cell-2", rpm_limit: null },
|
||||
];
|
||||
expect(tagRowsToLimits(rows)).toEqual({ tag_rpm_limit: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe("tagLimitsToRows", () => {
|
||||
it("should rebuild rows from a stored rpm map", () => {
|
||||
const rows = tagLimitsToRows({ "cell-1": 60 });
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({ tag: "cell-1", rpm_limit: 60 });
|
||||
});
|
||||
|
||||
it("should ignore non-numeric entries", () => {
|
||||
expect(tagLimitsToRows({ "cell-1": "sixty" })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Button, Input, InputNumber } from "antd";
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export interface TagRateLimitEntry {
|
||||
// Stable identity for React list keys so deleting a middle row doesn't shift
|
||||
|
|
@ -72,25 +73,29 @@ export function TagRateLimitEditor({ value, onChange }: TagRateLimitEditorProps)
|
|||
{value.map((row, idx) => (
|
||||
<div key={row.id} style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 12 }}>
|
||||
<Input
|
||||
aria-label="Tag"
|
||||
value={row.tag}
|
||||
onChange={(e) => updateRow(idx, "tag", e.target.value)}
|
||||
placeholder="Tag (e.g. cell-1)"
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
<InputNumber
|
||||
<Input
|
||||
aria-label="RPM limit"
|
||||
type="number"
|
||||
min={0}
|
||||
value={row.rpm_limit ?? undefined}
|
||||
onChange={(v) => updateRow(idx, "rpm_limit", v ?? null)}
|
||||
value={row.rpm_limit ?? ""}
|
||||
onChange={(e) => updateRow(idx, "rpm_limit", e.target.value === "" ? null : Number(e.target.value))}
|
||||
placeholder="RPM"
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<Button type="text" danger size="small" onClick={() => removeRow(idx)} style={{ padding: "0 4px" }}>
|
||||
<Button variant="destructive" size="sm" aria-label="Remove tag limit" onClick={() => removeRow(idx)}>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
size="small"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
addRow();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { Input } from "antd";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface routingStrategyArgs {
|
||||
ttl?: number;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { Input } from "antd";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface ReliabilityRetriesSectionProps {
|
||||
routerSettings: { [key: string]: any };
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ describe("TagFilteringToggle", () => {
|
|||
expect(screen.getByText("Enable Tag Filtering")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should name the switch with the metadata label so it is reachable by accessible name", () => {
|
||||
render(<TagFilteringToggle enabled={false} routerFieldsMetadata={baseMetadata} onToggle={vi.fn()} />);
|
||||
expect(screen.getByRole("switch", { name: "Tag Filtering" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the label from metadata when provided", () => {
|
||||
render(<TagFilteringToggle enabled={false} routerFieldsMetadata={baseMetadata} onToggle={vi.fn()} />);
|
||||
expect(screen.getByText("Tag Filtering")).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { Switch } from "antd";
|
||||
import React, { useId } from "react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
interface TagFilteringToggleProps {
|
||||
enabled: boolean;
|
||||
|
|
@ -8,11 +8,13 @@ interface TagFilteringToggleProps {
|
|||
}
|
||||
|
||||
const TagFilteringToggle: React.FC<TagFilteringToggleProps> = ({ enabled, routerFieldsMetadata, onToggle }) => {
|
||||
const toggleId = useId();
|
||||
|
||||
return (
|
||||
<div className="space-y-3 max-w-3xl">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs font-medium text-gray-700 uppercase tracking-wide">
|
||||
<label htmlFor={toggleId} className="text-xs font-medium text-gray-700 uppercase tracking-wide">
|
||||
{routerFieldsMetadata["enable_tag_filtering"]?.ui_field_name || "Enable Tag Filtering"}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
|
|
@ -32,7 +34,7 @@ const TagFilteringToggle: React.FC<TagFilteringToggleProps> = ({ enabled, router
|
|||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={enabled} onChange={onToggle} className="ml-4" />
|
||||
<Switch id={toggleId} checked={enabled} onCheckedChange={onToggle} className="ml-4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -134,6 +134,30 @@ describe("RouterSettings", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("should send the edited input value, not the loaded one, on Save Changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("strategy-select")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const numRetries = await screen.findByRole("textbox", { name: /num_retries/i });
|
||||
await user.clear(numRetries);
|
||||
await user.type(numRetries, "42");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(setCallbacksCall).toHaveBeenCalledWith(
|
||||
"test-token",
|
||||
expect.objectContaining({
|
||||
router_settings: expect.objectContaining({ num_retries: 42 }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should show a success notification after saving", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterSettings {...defaultProps} />);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Button } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { getCallbacksCall, getRouterSettingsCall, setCallbacksCall } from "../networking";
|
||||
import RouterSettingsForm, { RouterSettingsFormValue } from "./RouterSettingsForm";
|
||||
|
|
@ -190,10 +190,10 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
|
|||
|
||||
{/* Actions - Sticky at bottom */}
|
||||
<div className="border-t border-gray-200 pt-6 flex justify-end gap-3">
|
||||
<Button onClick={() => window.location.reload()}>Reset</Button>
|
||||
<Button type="primary" onClick={handleSaveChanges}>
|
||||
Save Changes
|
||||
<Button variant="outline" onClick={() => window.location.reload()}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={handleSaveChanges}>Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { Button } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
|
||||
interface CreatedKeyDisplayProps {
|
||||
|
|
@ -41,9 +41,7 @@ const CreatedKeyDisplay: React.FC<CreatedKeyDisplayProps> = ({ apiKey }) => {
|
|||
</div>
|
||||
|
||||
<CopyToClipboard text={apiKey} onCopy={handleCopy}>
|
||||
<Button type="primary" style={{ marginTop: 12 }}>
|
||||
{copied ? "Copied!" : "Copy Virtual Key"}
|
||||
</Button>
|
||||
<Button className="mt-3">{copied ? "Copied!" : "Copy Virtual Key"}</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { NumberInput } from "@tremor/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface NumericalInputProps {
|
||||
step?: number;
|
||||
|
|
@ -7,7 +7,7 @@ interface NumericalInputProps {
|
|||
placeholder?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
onChange?: any; // Using any to avoid type conflicts with Tremor's NumberInput
|
||||
onChange?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ interface NumericalInputProps {
|
|||
* @param {number} [props.min] - Minimum value
|
||||
* @param {number} [props.max] - Maximum value
|
||||
* @param {Function} [props.onChange] - On change handler
|
||||
* @param {any} props.rest - Additional props passed to NumberInput
|
||||
* @param {any} props.rest - Additional props passed to Input
|
||||
*/
|
||||
const NumericalInput: React.FC<NumericalInputProps> = ({
|
||||
step = 0.01,
|
||||
|
|
@ -32,7 +32,8 @@ const NumericalInput: React.FC<NumericalInputProps> = ({
|
|||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<NumberInput
|
||||
<Input
|
||||
type="number"
|
||||
onWheel={(event) => event.currentTarget.blur()}
|
||||
step={step}
|
||||
style={style}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue