litellm/ui/litellm-dashboard/src/components/cloudzero_export_modal.tsx
2026-02-02 19:04:18 -08:00

324 lines
10 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { Text, Button, Callout, TextInput } from "@tremor/react";
import { Modal, Form, Spin, Select } from "antd";
import { getGlobalLitellmHeaderName } from "@/components/networking";
import NotificationsManager from "./molecules/notifications_manager";
interface CloudZeroExportModalProps {
isOpen: boolean;
onClose: () => void;
accessToken: string | null;
}
interface CloudZeroSettings {
api_key: string;
connection_id: string;
}
interface CloudZeroSettingsView {
api_key_masked: string;
connection_id: string;
status: string;
}
type ExportType = "cloudzero" | "csv";
const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onClose, accessToken }) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [existingSettings, setExistingSettings] = useState<CloudZeroSettingsView | null>(null);
const [settingsLoading, setSettingsLoading] = useState(false);
const [exportType, setExportType] = useState<ExportType>("cloudzero");
const [exportLoading, setExportLoading] = useState(false);
// Load existing settings when modal opens
useEffect(() => {
if (isOpen && accessToken) {
loadExistingSettings();
}
}, [isOpen, accessToken]);
const loadExistingSettings = async () => {
setSettingsLoading(true);
try {
const response = await fetch("/cloudzero/settings", {
method: "GET",
headers: {
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (response.ok) {
const settings = await response.json();
setExistingSettings(settings);
// Pre-populate form with existing settings (except masked API key)
form.setFieldsValue({
connection_id: settings.connection_id,
});
} else if (response.status !== 404) {
// 404 means no settings configured yet, which is fine
const errorData = await response.json();
NotificationsManager.fromBackend(`Failed to load existing settings: ${errorData.error || "Unknown error"}`);
}
} catch (error) {
console.error("Error loading CloudZero settings:", error);
NotificationsManager.fromBackend("Failed to load existing settings");
} finally {
setSettingsLoading(false);
}
};
const handleSaveCloudZeroSettings = async (values: CloudZeroSettings) => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
return;
}
setLoading(true);
try {
const endpoint = existingSettings ? "/cloudzero/settings" : "/cloudzero/init";
const method = existingSettings ? "PUT" : "POST";
// Add default timezone for backend compatibility
const payload = {
...values,
timezone: "UTC",
};
const response = await fetch(endpoint, {
method,
headers: {
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const data = await response.json();
if (response.ok) {
NotificationsManager.success(data.message || "CloudZero settings saved successfully");
setExistingSettings({
api_key_masked: values.api_key.substring(0, 4) + "****" + values.api_key.slice(-4),
connection_id: values.connection_id,
status: "configured",
});
return true;
} else {
NotificationsManager.fromBackend(data.error || "Failed to save CloudZero settings");
return false;
}
} catch (error) {
console.error("Error saving CloudZero settings:", error);
NotificationsManager.fromBackend("Failed to save CloudZero settings");
return false;
} finally {
setLoading(false);
}
};
const handleExportCloudZero = async () => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
return;
}
setExportLoading(true);
try {
const response = await fetch("/cloudzero/export", {
method: "POST",
headers: {
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
limit: 100000,
operation: "replace_hourly",
}),
});
const data = await response.json();
if (response.ok) {
NotificationsManager.success(data.message || "Export to CloudZero completed successfully");
onClose();
} else {
NotificationsManager.fromBackend(data.error || "Failed to export to CloudZero");
}
} catch (error) {
console.error("Error exporting to CloudZero:", error);
NotificationsManager.fromBackend("Failed to export to CloudZero");
} finally {
setExportLoading(false);
}
};
const handleExportCSV = async () => {
setExportLoading(true);
try {
// TODO: Implement CSV export functionality
NotificationsManager.info("CSV export functionality coming soon!");
onClose();
} catch (error) {
console.error("Error exporting CSV:", error);
NotificationsManager.fromBackend("Failed to export CSV");
} finally {
setExportLoading(false);
}
};
const handleExport = async () => {
if (exportType === "cloudzero") {
// Check if settings exist, if not save them first
if (!existingSettings) {
const values = await form.validateFields();
const success = await handleSaveCloudZeroSettings(values);
if (!success) return;
}
await handleExportCloudZero();
} else {
await handleExportCSV();
}
};
const handleModalClose = () => {
form.resetFields();
setExportType("cloudzero");
setExistingSettings(null);
onClose();
};
const exportOptions = [
{
value: "cloudzero",
label: (
<div className="flex items-center gap-2">
<img
src="/cloudzero.png"
alt="CloudZero"
className="w-5 h-5"
onError={(e) => {
// Fallback to text if image fails to load
(e.target as HTMLImageElement).style.display = "none";
}}
/>
<span>Export to CloudZero</span>
</div>
),
},
{
value: "csv",
label: (
<div className="flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<span>Export to CSV</span>
</div>
),
},
];
return (
<Modal title="Export Data" open={isOpen} onCancel={handleModalClose} footer={null} width={600} destroyOnHidden>
<div className="space-y-4">
{/* Export Type Selection */}
<div>
<Text className="font-medium mb-2 block">Export Destination</Text>
<Select value={exportType} onChange={setExportType} options={exportOptions} className="w-full" size="large" />
</div>
{/* CloudZero Configuration */}
{exportType === "cloudzero" && (
<div>
{settingsLoading ? (
<div className="flex justify-center py-8">
<Spin size="large" />
</div>
) : (
<>
{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>
API Key: {existingSettings.api_key_masked}
<br />
Connection ID: {existingSettings.connection_id}
</Text>
</Callout>
)}
{!existingSettings && (
<Form form={form} layout="vertical">
<Form.Item
label="CloudZero API Key"
name="api_key"
rules={[{ required: true, message: "Please enter your CloudZero API key" }]}
>
<TextInput type="password" placeholder="Enter your CloudZero API key" />
</Form.Item>
<Form.Item
label="Connection ID"
name="connection_id"
rules={[{ required: true, message: "Please enter the CloudZero connection ID" }]}
>
<TextInput placeholder="Enter CloudZero connection ID" />
</Form.Item>
</Form>
)}
</>
)}
</div>
)}
{/* 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>
)}
{/* Action Buttons */}
<div className="flex justify-end space-x-2 pt-4">
<Button variant="secondary" onClick={handleModalClose}>
Cancel
</Button>
<Button onClick={handleExport} loading={loading || exportLoading} disabled={loading || exportLoading}>
{exportType === "cloudzero" ? "Export to CloudZero" : "Export CSV"}
</Button>
</div>
</div>
</Modal>
);
};
export default CloudZeroExportModal;