refactor(ui): move the admin, SSO, SCIM, alerting and fallback forms off tremor (#37315)

* refactor(ui): move the admin, SSO, SCIM, alerting and fallback forms off tremor

Swaps the tremor Button, Card, Callout, Grid, Divider, Text, Title, TextInput, Table parts, Badge, Icon and Switch in these nine files for the shadcn layer and lucide icons, keeping antd in place. The SCIM create-token button keeps an explicit type="submit"; the two SCIM copy buttons sit outside the antd form so they stay plain buttons, and the alerting form's Enterprise Feature upsell button is a link wrapper rather than a save action, so it deliberately stays type="button" while the form keeps its own Update Settings submit. The teal login callout on the admin panel maps to the info Alert variant since no teal variant exists. Prunes the nine tremor no-restricted-imports suppressions these files no longer need.

* fix(ui): keep the alerting settings name column left aligned

tremor's TableCell hardcoded text-left, so the align="center" attribute never took effect. The shadcn cell has no text-align of its own, so translating that attribute into text-center would have centered the field name and its description for the first time.

* fix(ui): restore the CloudZero key reveal toggle and the SCIM divider gap

tremor's TextInput drew its own show/hide button whenever the type was
password and the field was not disabled, so the straight pass-through to
the plain shadcn Input silently deleted that affordance from the CloudZero
API key field. Rebuilds it with the InputGroup reveal pattern that
email_settings.tsx already uses, behind a small local control so the antd
Form.Item keeps its id, value and onChange wiring and the field still
matches its shadcn sibling in the same form.

Also puts the SCIM separator back on my-6: tremor's Divider was
"w-full mx-auto my-6", and the conversion shipped my-4, tightening that
gap by 8px on each side. The disabled SCIM token field stays a bare Input
because tremor suppressed its toggle when disabled too.
This commit is contained in:
ryan-crabbe-berri 2026-08-18 15:36:35 -07:00 committed by GitHub
parent e75b4b1c2a
commit eef41c9987
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 205 additions and 211 deletions

View file

@ -16,7 +16,7 @@
},
"src/app/(dashboard)/admin-panel/_components/AdminPanel.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
@ -1647,9 +1647,6 @@
}
},
"src/components/SCIM.tsx": {
"no-restricted-imports": {
"count": 2
},
"react-hooks/set-state-in-effect": {
"count": 1
}
@ -1661,7 +1658,7 @@
},
"src/components/SSOModals.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
}
},
"src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx": {
@ -1705,11 +1702,6 @@
"count": 1
}
},
"src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.tsx": {
"no-restricted-imports": {
"count": 2
}
},
"src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx": {
"max-nested-callbacks": {
"count": 1
@ -1760,7 +1752,7 @@
},
"src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
@ -2024,10 +2016,7 @@
"count": 1
},
"no-nested-ternary": {
"count": 4
},
"no-restricted-imports": {
"count": 2
"count": 1
}
},
"src/components/bulk_create_users_button.tsx": {
@ -2091,7 +2080,7 @@
"count": 1
},
"no-restricted-imports": {
"count": 2
"count": 1
},
"no-restricted-syntax": {
"count": 3
@ -2461,7 +2450,7 @@
"count": 1
},
"no-restricted-imports": {
"count": 2
"count": 1
}
},
"src/components/organisms/RegenerateKeyModal.tsx": {

View file

@ -3,18 +3,12 @@
* Use this to avoid sharing master key with others
*/
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import {
Button,
Callout,
Card,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
} from "@tremor/react";
import { Alert, Modal, Space, Tabs, Typography } from "antd";
import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Alert as AntdAlert, Modal, Space, Tabs, Typography } from "antd";
import { Info } from "lucide-react";
import React, { useEffect, useState } from "react";
import NewBadge from "@/components/common_components/NewBadge";
import { useBaseUrl } from "@/components/constants";
@ -37,7 +31,6 @@ import UIAccessControlForm from "@/components/UIAccessControlForm";
import { z } from "zod/v4";
import { FieldGroup } from "@/components/shared/form/field";
import { FormField } from "@/components/shared/form/FormField";
import { Button as ShadcnButton } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useZodForm } from "@/lib/forms/useZodForm";
@ -59,7 +52,7 @@ const AddAllowedIPForm = ({ onSubmit }: { onSubmit: (values: AllowedIPFormValues
{({ ref, ...field }) => <Input ref={ref} placeholder="Enter IP address" {...field} />}
</FormField>
<div>
<ShadcnButton type="submit">Add IP Address</ShadcnButton>
<Button type="submit">Add IP Address</Button>
</div>
</FieldGroup>
</form>
@ -228,9 +221,9 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
label: "Security Settings",
children: (
<>
<Card>
<Card className="block p-6">
<Title level={4}> Security Settings</Title>
<Alert
<AntdAlert
message="SSO Configuration Deprecated"
description="Editing SSO Settings on this page is deprecated and will be removed in a future version. Please use the SSO Settings tab for SSO configuration."
type="warning"
@ -298,19 +291,19 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
]}
>
<Table>
<TableHead>
<TableHeader>
<TableRow>
<TableHeaderCell>IP Address</TableHeaderCell>
<TableHeaderCell className="text-right">Action</TableHeaderCell>
<TableHead>IP Address</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHead>
</TableHeader>
<TableBody>
{allowedIPs.map((ip, index) => (
<TableRow key={index}>
<TableCell>{ip}</TableCell>
<TableCell className="text-right">
{ip !== all_ip_address_allowed && (
<Button onClick={() => handleDeleteIP(ip)} color="red" size="xs">
<Button onClick={() => handleDeleteIP(ip)} variant="destructive" size="sm">
Delete
</Button>
)}
@ -365,12 +358,16 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
/>
</Modal>
</div>
<Callout title="Login without SSO" color="teal">
If you need to login without sso, you can access{" "}
<a href={nonSssoUrl} target="_blank" rel="noopener noreferrer">
<b>{nonSssoUrl}</b>{" "}
</a>
</Callout>
<Alert variant="info">
<Info />
<AlertTitle>Login without SSO</AlertTitle>
<AlertDescription>
If you need to login without sso, you can access{" "}
<a href={nonSssoUrl} target="_blank" rel="noopener noreferrer">
<b>{nonSssoUrl}</b>{" "}
</a>
</AlertDescription>
</Alert>
</>
),
},

View file

@ -1,21 +1,17 @@
import React, { useState, useEffect } from "react";
import { Card, Title, Text, Grid, Callout, Divider } from "@tremor/react";
import { z } from "zod/v4";
import { keyCreateCall } from "./networking";
import { CopyToClipboard } from "react-copy-to-clipboard";
import {
LinkOutlined,
KeyOutlined,
CopyOutlined,
ExclamationCircleOutlined,
PlusCircleOutlined,
} from "@ant-design/icons";
import { CircleAlert, CirclePlus, Copy, Info, KeyRound, Link } from "lucide-react";
import { parseErrorMessage } from "./shared/errorUtils";
import { toast } from "@/lib/toast";
import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
import { FieldGroup } from "@/components/shared/form/field";
import { FormField } from "@/components/shared/form/FormField";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { useZodForm } from "@/lib/forms/useZodForm";
@ -80,114 +76,121 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
};
return (
<Grid numItems={1}>
<div className="grid grid-cols-1">
<Card>
<div className="flex items-center mb-4">
<Title>SCIM Configuration</Title>
</div>
<Text className="text-muted-foreground">
System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and
groups in LiteLLM.
</Text>
<Divider />
<div className="space-y-8">
{/* Step 1: SCIM URL */}
<div>
<div className="flex items-center mb-2">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300 mr-2">
1
</div>
<Title className="text-lg flex items-center">
<LinkOutlined className="h-5 w-5 mr-2" />
SCIM Tenant URL
</Title>
</div>
<Text className="text-muted-foreground mb-3">
Use this URL in your identity provider SCIM integration settings.
</Text>
<div className="flex items-center">
<Input value={scimBaseUrl} disabled={true} readOnly className="grow" />
<CopyToClipboard text={scimBaseUrl} onCopy={() => toast.success("URL copied to clipboard")}>
<Button type="button" className="ml-2 flex items-center">
<CopyOutlined className="h-4 w-4 mr-1" />
Copy
</Button>
</CopyToClipboard>
</div>
<CardContent>
<div className="flex items-center mb-4">
<CardTitle>SCIM Configuration</CardTitle>
</div>
<p className="text-muted-foreground">
System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users
and groups in LiteLLM.
</p>
{/* Step 2: SCIM Token */}
<div>
<div className="flex items-center mb-2">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300 mr-2">
2
<Separator className="my-6" />
<div className="space-y-8">
{/* Step 1: SCIM URL */}
<div>
<div className="flex items-center mb-2">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300 mr-2">
1
</div>
<h3 className="text-lg font-medium flex items-center">
<Link className="h-5 w-5 mr-2" />
SCIM Tenant URL
</h3>
</div>
<p className="text-muted-foreground mb-3">
Use this URL in your identity provider SCIM integration settings.
</p>
<div className="flex items-center">
<Input value={scimBaseUrl} disabled={true} readOnly className="grow" />
<CopyToClipboard text={scimBaseUrl} onCopy={() => toast.success("URL copied to clipboard")}>
<Button type="button" className="ml-2 flex items-center">
<Copy />
Copy
</Button>
</CopyToClipboard>
</div>
<Title className="text-lg flex items-center">
<KeyOutlined className="h-5 w-5 mr-2" />
Authentication Token
</Title>
</div>
<Callout title="Using SCIM" color="blue" className="mb-4">
You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider
configuration.
</Callout>
{/* Step 2: SCIM Token */}
<div>
<div className="flex items-center mb-2">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300 mr-2">
2
</div>
<h3 className="text-lg font-medium flex items-center">
<KeyRound className="h-5 w-5 mr-2" />
Authentication Token
</h3>
</div>
{!tokenData ? (
<div className="bg-muted p-4 rounded-lg">
<form onSubmit={form.handleSubmit(handleCreateSCIMToken)}>
<FieldGroup>
<FormField control={form.control} name="key_alias" label="Token Name">
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="SCIM Access Token" />}
</FormField>
<div>
<Button type="submit" disabled={isCreatingToken} className="flex items-center">
{isCreatingToken ? (
<UiLoadingSpinner className="size-4 mr-1" />
) : (
<KeyOutlined className="h-4 w-4 mr-1" />
)}
Create SCIM Token
<Alert variant="info" className="mb-4">
<Info />
<AlertTitle>Using SCIM</AlertTitle>
<AlertDescription>
You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM
provider configuration.
</AlertDescription>
</Alert>
{!tokenData ? (
<div className="bg-muted p-4 rounded-lg">
<form onSubmit={form.handleSubmit(handleCreateSCIMToken)}>
<FieldGroup>
<FormField control={form.control} name="key_alias" label="Token Name">
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="SCIM Access Token" />}
</FormField>
<div>
<Button
type="submit"
disabled={isCreatingToken}
aria-busy={isCreatingToken}
className="flex items-center"
>
{isCreatingToken ? <UiLoadingSpinner className="size-4" /> : <KeyRound />}
Create SCIM Token
</Button>
</div>
</FieldGroup>
</form>
</div>
) : (
<Card className="block p-6 border border-yellow-300 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950">
<div className="flex items-center mb-2 text-yellow-800 dark:text-yellow-300">
<CircleAlert className="h-5 w-5 mr-2" />
<h4 className="text-lg font-medium text-yellow-800 dark:text-yellow-300">Your SCIM Token</h4>
</div>
<p className="text-yellow-800 dark:text-yellow-300 mb-4 font-medium">
Make sure to copy this token now. You will not be able to see it again.
</p>
<div className="flex items-center">
<Input value={tokenData.key} className="grow mr-2" type="password" disabled={true} readOnly />
<CopyToClipboard text={tokenData.key} onCopy={() => toast.success("Token copied to clipboard")}>
<Button type="button" className="flex items-center">
<Copy />
Copy
</Button>
</div>
</FieldGroup>
</form>
</div>
) : (
<Card className="border border-yellow-300 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950">
<div className="flex items-center mb-2 text-yellow-800 dark:text-yellow-300">
<ExclamationCircleOutlined className="h-5 w-5 mr-2" />
<Title className="text-lg text-yellow-800 dark:text-yellow-300">Your SCIM Token</Title>
</div>
<Text className="text-yellow-800 dark:text-yellow-300 mb-4 font-medium">
Make sure to copy this token now. You will not be able to see it again.
</Text>
<div className="flex items-center">
<Input value={tokenData.key} className="grow mr-2" type="password" disabled={true} readOnly />
<CopyToClipboard text={tokenData.key} onCopy={() => toast.success("Token copied to clipboard")}>
<Button type="button" className="flex items-center">
<CopyOutlined className="h-4 w-4 mr-1" />
Copy
</Button>
</CopyToClipboard>
</div>
<Button
type="button"
variant="secondary"
className="mt-4 flex items-center"
onClick={() => setTokenData(null)}
>
<PlusCircleOutlined className="h-4 w-4 mr-1" />
Create Another Token
</Button>
</Card>
)}
</CopyToClipboard>
</div>
<Button
type="button"
variant="secondary"
className="mt-4 flex items-center"
onClick={() => setTokenData(null)}
>
<CirclePlus />
Create Another Token
</Button>
</Card>
)}
</div>
</div>
</div>
</CardContent>
</Card>
</Grid>
</div>
);
};

View file

@ -1,7 +1,6 @@
import React, { useEffect, useState } from "react";
import { FormProvider, useWatch, type UseFormReturn } from "react-hook-form";
import { Modal } from "antd";
import { Text } from "@tremor/react";
import { getSSOSettings, updateSSOSettings } from "./networking";
import { toast } from "@/lib/toast";
import { parseErrorMessage } from "./shared/errorUtils";
@ -317,10 +316,10 @@ const SSOModals: React.FC<SSOModalsProps> = ({
onCancel={handleInstructionsCancel}
>
<p>Follow these steps to complete the SSO setup:</p>
<Text className="mt-2">1. DO NOT Exit this TAB</Text>
<Text className="mt-2">2. Open a new tab, visit your proxy base url</Text>
<Text className="mt-2">3. Confirm your SSO is configured correctly and you can login on the new Tab</Text>
<Text className="mt-2">4. If Step 3 is successful, you can close this tab</Text>
<p className="text-sm mt-2">1. DO NOT Exit this TAB</p>
<p className="text-sm mt-2">2. Open a new tab, visit your proxy base url</p>
<p className="text-sm mt-2">3. Confirm your SSO is configured correctly and you can login on the new Tab</p>
<p className="text-sm mt-2">4. If Step 3 is successful, you can close this tab</p>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button type="button" onClick={handleInstructionsOk}>
Done

View file

@ -4,7 +4,7 @@
* Decoupled from form submission logic
*/
import { Button } from "@tremor/react";
import { Button } from "@/components/ui/button";
import { Tabs } from "antd";
import { Plus } from "lucide-react";
import React, { useEffect, useState } from "react";
@ -98,7 +98,8 @@ export function FallbackSelectionForm({
return (
<div className="text-center py-12 bg-gray-50 rounded-lg border border-dashed border-gray-300">
<p className="text-gray-500 mb-4">No fallback groups configured</p>
<Button variant="primary" onClick={handleAddGroup} icon={() => <Plus className="w-4 h-4" />}>
<Button onClick={handleAddGroup}>
<Plus className="w-4 h-4" />
Create First Group
</Button>
</div>

View file

@ -174,9 +174,7 @@ describe("DynamicForm change notifications", () => {
const user = userEvent.setup();
const { handleResetField } = renderForm();
const row = screen.getByText("region_name").closest("tr");
const reset = row?.querySelector(".tremor-Icon-root");
await user.click(reset as Element);
await user.click(screen.getByRole("button", { name: "Reset region_name" }));
expect(handleResetField).toHaveBeenCalledWith("region_name", 1);
});

View file

@ -1,8 +1,11 @@
import React from "react";
import { useForm } from "react-hook-form";
import { TrashIcon, CheckCircleIcon } from "@heroicons/react/outline";
import { Button, Badge, Icon, Text, TableRow, TableCell, Switch } from "@tremor/react";
import { CircleCheck, Trash2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { TableCell, TableRow } from "@/components/ui/table";
interface AlertingSetting {
field_name: string;
@ -71,7 +74,13 @@ const DynamicForm: React.FC<DynamicFormProps> = ({
);
}
if (setting.field_type === "Boolean") {
return <Switch checked={setting.field_value} onChange={(checked) => handleToggle(setting, checked)} />;
return (
<Switch
aria-label={setting.field_name}
checked={setting.field_value}
onCheckedChange={(checked) => handleToggle(setting, checked)}
/>
);
}
return <Input value={setting.field_value ?? ""} onChange={(event) => handleTextChange(setting, event)} />;
};
@ -80,8 +89,8 @@ const DynamicForm: React.FC<DynamicFormProps> = ({
<form onSubmit={form.handleSubmit(onFinish)} noValidate>
{alertingSettings.map((value, index) => (
<TableRow key={index}>
<TableCell align="center">
<Text>{value.field_name}</Text>
<TableCell>
<p className="text-sm">{value.field_name}</p>
<p className="mt-1 text-[0.65rem] italic text-muted-foreground">{value.field_description}</p>
</TableCell>
{value.premium_field && !premiumUser ? (
@ -97,19 +106,27 @@ const DynamicForm: React.FC<DynamicFormProps> = ({
)}
<TableCell>
{value.stored_in_db == true ? (
<Badge icon={CheckCircleIcon} className="text-white">
<Badge variant="secondary">
<CircleCheck />
In DB
</Badge>
) : value.stored_in_db == false ? (
<Badge className="text-gray bg-background outline-solid">In Config</Badge>
<Badge variant="outline">In Config</Badge>
) : (
<Badge className="text-gray bg-background outline-solid">Not Set</Badge>
<Badge variant="outline">Not Set</Badge>
)}
</TableCell>
<TableCell>
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name, index)}>
Reset
</Icon>
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={`Reset ${value.field_name}`}
onClick={() => handleResetField(value.field_name, index)}
className="text-red-500"
>
<Trash2 className="size-5" />
</Button>
</TableCell>
</TableRow>
))}

View file

@ -1,13 +1,16 @@
import React, { useState, useEffect } from "react";
import { Text, Button, Callout } from "@tremor/react";
import { Modal, Spin, Select } from "antd";
import { CircleCheck, FileDown } from "lucide-react";
import { z } from "zod/v4";
import { getGlobalLitellmHeaderName } from "@/components/networking";
import { toast } from "@/lib/toast";
import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
import { PasswordInput } from "@/components/shared/PasswordInput";
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 { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { useZodForm } from "@/lib/forms/useZodForm";
const cloudZeroSettingsSchema = z.object({
@ -242,7 +245,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
<div className="space-y-4">
{/* Export Type Selection */}
<div>
<Text className="font-medium mb-2 block">Export Destination</Text>
<p className="text-sm font-medium mb-2 block">Export Destination</p>
<Select value={exportType} onChange={setExportType} options={exportOptions} className="w-full" size="large" />
</div>
@ -256,27 +259,15 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
) : (
<>
{existingSettings && (
<Callout
title="Existing CloudZero Configuration"
icon={() => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)}
color="green"
className="mb-4"
>
<Text>
<Alert className="mb-4">
<CircleCheck />
<AlertTitle>Existing CloudZero Configuration</AlertTitle>
<AlertDescription>
API Key: {existingSettings.api_key_masked}
<br />
Connection ID: {existingSettings.connection_id}
</Text>
</Callout>
</AlertDescription>
</Alert>
)}
{!existingSettings && (
@ -303,25 +294,27 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
{/* CSV Export Info */}
{exportType === "csv" && (
<Callout
title="CSV Export"
icon={() => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
)}
color="blue"
>
<Text>Export your usage data as a CSV file for analysis in spreadsheet applications.</Text>
</Callout>
<Alert variant="info">
<FileDown />
<AlertTitle>CSV Export</AlertTitle>
<AlertDescription>
Export your usage data as a CSV file for analysis in spreadsheet applications.
</AlertDescription>
</Alert>
)}
{/* Action Buttons */}
<div className="flex justify-end space-x-2 pt-4">
<Button variant="secondary" onClick={handleModalClose}>
<Button type="button" variant="secondary" onClick={handleModalClose}>
Cancel
</Button>
<Button onClick={handleExport} loading={loading || exportLoading} disabled={loading || exportLoading}>
<Button
type="button"
onClick={handleExport}
disabled={loading || exportLoading}
aria-busy={loading || exportLoading}
>
{(loading || exportLoading) && <UiLoadingSpinner className="size-4" />}
{exportType === "cloudzero" ? "Export to CloudZero" : "Export CSV"}
</Button>
</div>

View file

@ -1,7 +1,6 @@
import React from "react";
import { Button, Modal, Typography } from "antd";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { Text } from "@tremor/react";
import { toast } from "@/lib/toast";
export interface InvitationLink {
@ -90,14 +89,12 @@ export default function OnboardingModal({
: "Copy and send the generated link to the user to reset their password."}
</Paragraph>
<div className="flex justify-between pt-5 pb-2">
<Text className="text-base">User ID</Text>
<Text>{invitationLinkData?.user_id}</Text>
<p className="text-base">User ID</p>
<p className="text-sm">{invitationLinkData?.user_id}</p>
</div>
<div className="flex justify-between pt-5 pb-2">
<Text>{modalType === "invitation" ? "Invitation Link" : "Reset Password Link"}</Text>
<Text>
<Text>{getInvitationUrl()}</Text>
</Text>
<p className="text-sm">{modalType === "invitation" ? "Invitation Link" : "Reset Password Link"}</p>
<p className="text-sm">{getInvitationUrl()}</p>
</div>
<div className="flex justify-end mt-5">
<CopyToClipboard text={getInvitationUrl()} onCopy={() => toast.success("Copied!")}>