mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): migrate caching (cache_settings) section to shadcn
- index.tsx (CacheSettings): @tremor Button/Accordion \u2192 shadcn Button/ Accordion. Added a renderField helper to dedupe the per-category map. Section h4/h5 headers use semantic foreground tokens. - CacheFieldRenderer: @tremor NumberInput/TextInput + antd Select \u2192 shadcn Input/Textarea/Select with an inline searchable embedding-model Select (Search input inside SelectContent; stopPropagation on keydown so typing in the search box doesn't fight the Select's keyboard nav). - RedisTypeSelector: @tremor Select \u2192 shadcn Select + Label. Gates: TS \u2713 | Lint \u2713 | Vitest 9/9 \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
ad60a746f8
commit
3b540f0ec2
4 changed files with 266 additions and 188 deletions
|
|
@ -100,6 +100,16 @@ Layer abbreviations: **TS** (tsc --noEmit), **Lint** (eslint), **Vitest**
|
|||
section (CostTrackingSettings/*) is pending in a follow-up commit.
|
||||
- Tests updated for required-field label regex matching.
|
||||
|
||||
## 26. Caching (caching)
|
||||
|
||||
- Cycles used: 1 / 7
|
||||
- Layer outcomes per cycle:
|
||||
- cycle 1: TS ✓ | Lint ✓ | Vitest ✓ (9/9) | Build ✓ | Parity ⏭ | Snap ⏭
|
||||
- Final status: **done**
|
||||
- Scope: cache_settings/index.tsx (top-level CacheSettings), CacheFieldRenderer
|
||||
(per-type input renderer), RedisTypeSelector. Inline searchable Select for
|
||||
embedding models replaces antd Select with showSearch.
|
||||
|
||||
## 24. Guardrails Monitor (guardrails-monitor)
|
||||
|
||||
- Cycles used: 1 / 7
|
||||
|
|
|
|||
|
|
@ -1,53 +1,83 @@
|
|||
"use client";
|
||||
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { NumberInput, TextInput } from "@tremor/react";
|
||||
import { Select } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
fetchAvailableModels,
|
||||
ModelGroup,
|
||||
} from "../playground/llm_calls/fetch_models";
|
||||
import NumericalInput from "../shared/numerical_input";
|
||||
|
||||
interface CacheFieldRendererProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
field: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
currentValue: any;
|
||||
}
|
||||
|
||||
const CacheFieldRenderer: React.FC<CacheFieldRendererProps> = ({ field, currentValue }) => {
|
||||
const CacheFieldRenderer: React.FC<CacheFieldRendererProps> = ({
|
||||
field,
|
||||
currentValue,
|
||||
}) => {
|
||||
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
|
||||
const [selectedModel, setSelectedModel] = useState<string>(currentValue || "");
|
||||
const [selectedModel, setSelectedModel] = useState<string>(
|
||||
currentValue || "",
|
||||
);
|
||||
const [modelQuery, setModelQuery] = useState("");
|
||||
const { accessToken } = useAuthorized();
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken) return;
|
||||
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const uniqueModels = await fetchAvailableModels(accessToken);
|
||||
console.log("Fetched models for selector:", uniqueModels);
|
||||
|
||||
if (uniqueModels.length > 0) {
|
||||
setModelInfo(uniqueModels);
|
||||
}
|
||||
if (uniqueModels.length > 0) setModelInfo(uniqueModels);
|
||||
} catch (error) {
|
||||
console.error("Error fetching model info:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadModels();
|
||||
}, [accessToken]);
|
||||
|
||||
const embeddingModels = useMemo(
|
||||
() =>
|
||||
modelInfo.filter((option: ModelGroup) => option.mode === "embedding"),
|
||||
[modelInfo],
|
||||
);
|
||||
const filteredEmbeddingModels = useMemo(
|
||||
() =>
|
||||
embeddingModels.filter((m) =>
|
||||
modelQuery
|
||||
? m.model_group.toLowerCase().includes(modelQuery.toLowerCase())
|
||||
: true,
|
||||
),
|
||||
[embeddingModels, modelQuery],
|
||||
);
|
||||
|
||||
if (field.field_type === "Boolean") {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">{field.ui_field_name}</label>
|
||||
<Label className="text-sm font-medium">{field.ui_field_name}</Label>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={field.field_name}
|
||||
defaultChecked={currentValue === true || currentValue === "true"}
|
||||
className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
|
||||
className="h-4 w-4 text-primary focus:ring-ring border-input rounded"
|
||||
/>
|
||||
<span className="ml-2 text-sm text-gray-500">{field.field_description}</span>
|
||||
<span className="ml-2 text-sm text-muted-foreground">
|
||||
{field.field_description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -56,14 +86,17 @@ const CacheFieldRenderer: React.FC<CacheFieldRendererProps> = ({ field, currentV
|
|||
if (field.field_type === "Integer" || field.field_type === "Float") {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">{field.ui_field_name}</label>
|
||||
<Label className="text-sm font-medium">{field.ui_field_name}</Label>
|
||||
<NumericalInput
|
||||
name={field.field_name}
|
||||
type="number"
|
||||
step={field.field_type === "Float" ? 0.01 : 1}
|
||||
defaultValue={currentValue}
|
||||
placeholder={field.field_description}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">{field.field_description}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{field.field_description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -71,77 +104,85 @@ const CacheFieldRenderer: React.FC<CacheFieldRendererProps> = ({ field, currentV
|
|||
if (field.field_type === "List") {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">{field.ui_field_name}</label>
|
||||
<textarea
|
||||
<Label className="text-sm font-medium">{field.ui_field_name}</Label>
|
||||
<Textarea
|
||||
name={field.field_name}
|
||||
defaultValue={typeof currentValue === "object" ? JSON.stringify(currentValue, null, 2) : currentValue}
|
||||
defaultValue={
|
||||
typeof currentValue === "object"
|
||||
? JSON.stringify(currentValue, null, 2)
|
||||
: currentValue
|
||||
}
|
||||
placeholder={field.field_description}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">{field.field_description}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{field.field_description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.field_type === "Models_Select") {
|
||||
const embeddingModels = modelInfo
|
||||
.filter((option: ModelGroup) => option.mode === "embedding")
|
||||
.map((option: ModelGroup) => ({
|
||||
value: option.model_group,
|
||||
label: option.model_group,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">{field.ui_field_name}</label>
|
||||
<Select
|
||||
value={selectedModel}
|
||||
onChange={setSelectedModel}
|
||||
showSearch={true}
|
||||
placeholder="Search and select a model..."
|
||||
options={embeddingModels}
|
||||
style={{ width: "100%" }}
|
||||
className="rounded-md"
|
||||
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
/>
|
||||
<Label className="text-sm font-medium">{field.ui_field_name}</Label>
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Search and select a model..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<div className="px-2 pt-1 pb-2">
|
||||
<Input
|
||||
placeholder="Search…"
|
||||
value={modelQuery}
|
||||
onChange={(e) => setModelQuery(e.target.value)}
|
||||
className="h-8"
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
{filteredEmbeddingModels.length === 0 ? (
|
||||
<div className="py-2 px-3 text-sm text-muted-foreground">
|
||||
No matches
|
||||
</div>
|
||||
) : (
|
||||
filteredEmbeddingModels.map((m) => (
|
||||
<SelectItem key={m.model_group} value={m.model_group}>
|
||||
{m.model_group}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* Hidden input to capture the value for form submission */}
|
||||
<input type="hidden" name={field.field_name} value={selectedModel} />
|
||||
{field.field_description && <p className="text-xs text-gray-500">{field.field_description}</p>}
|
||||
{field.field_description && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{field.field_description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render number input for numeric fields
|
||||
if (field.field_type === "Integer" || field.field_type === "Float") {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">{field.ui_field_name}</label>
|
||||
<NumberInput
|
||||
name={field.field_name}
|
||||
defaultValue={currentValue}
|
||||
placeholder={field.field_description}
|
||||
step={field.field_type === "Float" ? 0.01 : 1}
|
||||
/>
|
||||
{field.field_description && <p className="text-xs text-gray-500">{field.field_description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Determine input type for text-based fields
|
||||
const inputType: "text" | "password" | "email" | "url" | undefined =
|
||||
field.field_name === "password" || field.field_name.includes("password") ? "password" : "text";
|
||||
const inputType: "text" | "password" | "email" | "url" =
|
||||
field.field_name === "password" || field.field_name.includes("password")
|
||||
? "password"
|
||||
: "text";
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">{field.ui_field_name}</label>
|
||||
<TextInput
|
||||
<Label className="text-sm font-medium">{field.ui_field_name}</Label>
|
||||
<Input
|
||||
name={field.field_name}
|
||||
type={inputType}
|
||||
defaultValue={currentValue}
|
||||
placeholder={field.field_description}
|
||||
/>
|
||||
{field.field_description && <p className="text-xs text-gray-500">{field.field_description}</p>}
|
||||
{field.field_description && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{field.field_description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import React from "react";
|
||||
import { Select, SelectItem } from "@tremor/react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface RedisTypeSelectorProps {
|
||||
redisType: string;
|
||||
|
|
@ -7,18 +14,28 @@ interface RedisTypeSelectorProps {
|
|||
onTypeChange: (type: string) => void;
|
||||
}
|
||||
|
||||
const RedisTypeSelector: React.FC<RedisTypeSelectorProps> = ({ redisType, redisTypeDescriptions, onTypeChange }) => {
|
||||
const RedisTypeSelector: React.FC<RedisTypeSelectorProps> = ({
|
||||
redisType,
|
||||
redisTypeDescriptions,
|
||||
onTypeChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">Redis Type</label>
|
||||
<Label className="text-sm font-medium">Redis Type</Label>
|
||||
<Select value={redisType} onValueChange={onTypeChange}>
|
||||
<SelectItem value="node">Node (Single Instance)</SelectItem>
|
||||
<SelectItem value="cluster">Cluster</SelectItem>
|
||||
<SelectItem value="sentinel">Sentinel</SelectItem>
|
||||
<SelectItem value="semantic">Semantic</SelectItem>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="node">Node (Single Instance)</SelectItem>
|
||||
<SelectItem value="cluster">Cluster</SelectItem>
|
||||
<SelectItem value="sentinel">Sentinel</SelectItem>
|
||||
<SelectItem value="semantic">Semantic</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500">
|
||||
{redisTypeDescriptions[redisType] || "Select the type of Redis deployment you're using"}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{redisTypeDescriptions[redisType] ||
|
||||
"Select the type of Redis deployment you're using"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Button, Accordion, AccordionHeader, AccordionBody } from "@tremor/react";
|
||||
import { getCacheSettingsCall, testCacheConnectionCall, updateCacheSettingsCall } from "../networking";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
getCacheSettingsCall,
|
||||
testCacheConnectionCall,
|
||||
updateCacheSettingsCall,
|
||||
} from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import RedisTypeSelector from "./RedisTypeSelector";
|
||||
import CacheFieldRenderer from "./CacheFieldRenderer";
|
||||
|
|
@ -12,10 +22,14 @@ interface CacheSettingsProps {
|
|||
userID: string | null;
|
||||
}
|
||||
|
||||
const CacheSettings: React.FC<CacheSettingsProps> = ({ accessToken, userRole, userID }) => {
|
||||
const CacheSettings: React.FC<CacheSettingsProps> = ({ accessToken }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [cacheSettings, setCacheSettings] = useState<{ [key: string]: any }>({});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [fields, setFields] = useState<any[]>([]);
|
||||
const [redisTypeDescriptions, setRedisTypeDescriptions] = useState<{ [key: string]: string }>({});
|
||||
const [redisTypeDescriptions, setRedisTypeDescriptions] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
const [redisType, setRedisType] = useState<string>("node");
|
||||
const [isTesting, setIsTesting] = useState<boolean>(false);
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
|
|
@ -23,21 +37,13 @@ const CacheSettings: React.FC<CacheSettingsProps> = ({ accessToken, userRole, us
|
|||
const loadCacheSettings = useCallback(async () => {
|
||||
try {
|
||||
const data = await getCacheSettingsCall(accessToken!);
|
||||
console.log("cache settings from API", data);
|
||||
|
||||
if (data.fields) {
|
||||
setFields(data.fields);
|
||||
}
|
||||
|
||||
// Set current values
|
||||
if (data.fields) setFields(data.fields);
|
||||
if (data.current_values) {
|
||||
setCacheSettings(data.current_values);
|
||||
if (data.current_values.redis_type) {
|
||||
setRedisType(data.current_values.redis_type);
|
||||
}
|
||||
}
|
||||
|
||||
// Store Redis type descriptions
|
||||
if (data.redis_type_descriptions) {
|
||||
setRedisTypeDescriptions(data.redis_type_descriptions);
|
||||
}
|
||||
|
|
@ -48,49 +54,42 @@ const CacheSettings: React.FC<CacheSettingsProps> = ({ accessToken, userRole, us
|
|||
}, [accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
if (!accessToken) return;
|
||||
loadCacheSettings();
|
||||
}, [accessToken, loadCacheSettings]);
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accessToken) return;
|
||||
setIsTesting(true);
|
||||
try {
|
||||
const testSettings = gatherFormValues(fields, redisType);
|
||||
const result = await testCacheConnectionCall(accessToken, testSettings);
|
||||
|
||||
if (result.status === "success") {
|
||||
NotificationsManager.success("Cache connection test successful!");
|
||||
} else {
|
||||
NotificationsManager.fromBackend(`Connection test failed: ${result.message || result.error}`);
|
||||
NotificationsManager.fromBackend(
|
||||
`Connection test failed: ${result.message || result.error}`,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Test connection error:", error);
|
||||
NotificationsManager.fromBackend(`Connection test failed: ${error.message || "Unknown error"}`);
|
||||
NotificationsManager.fromBackend(
|
||||
`Connection test failed: ${error.message || "Unknown error"}`,
|
||||
);
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveChanges = async () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accessToken) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const settingsToSave = gatherFormValues(fields, redisType);
|
||||
if (redisType === "semantic") {
|
||||
settingsToSave.type = "redis-semantic";
|
||||
}
|
||||
if (redisType === "semantic") settingsToSave.type = "redis-semantic";
|
||||
await updateCacheSettingsCall(accessToken, settingsToSave);
|
||||
NotificationsManager.success("Cache settings updated successfully");
|
||||
// Reload settings to reflect saved values
|
||||
await loadCacheSettings();
|
||||
} catch (error) {
|
||||
console.error("Failed to save cache settings:", error);
|
||||
|
|
@ -100,136 +99,147 @@ const CacheSettings: React.FC<CacheSettingsProps> = ({ accessToken, userRole, us
|
|||
}
|
||||
};
|
||||
|
||||
if (!accessToken) {
|
||||
return null;
|
||||
}
|
||||
if (!accessToken) return null;
|
||||
|
||||
const { basicFields, sslFields, cacheManagementFields, gcpFields, clusterFields, sentinelFields, semanticFields } =
|
||||
groupFieldsByCategory(fields, redisType);
|
||||
const {
|
||||
basicFields,
|
||||
sslFields,
|
||||
cacheManagementFields,
|
||||
gcpFields,
|
||||
clusterFields,
|
||||
sentinelFields,
|
||||
semanticFields,
|
||||
} = groupFieldsByCategory(fields, redisType);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const renderField = (field: any) => {
|
||||
if (!field) return null;
|
||||
const currentValue =
|
||||
cacheSettings[field.field_name] ?? field.field_default ?? "";
|
||||
return (
|
||||
<CacheFieldRenderer
|
||||
key={field.field_name}
|
||||
field={field}
|
||||
currentValue={currentValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-8 py-2">
|
||||
<div className="space-y-6">
|
||||
<div className="max-w-3xl">
|
||||
<h3 className="text-sm font-medium text-gray-900">Cache Settings</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">Configure Redis cache for LiteLLM</p>
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
Cache Settings
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Configure Redis cache for LiteLLM
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Redis Type Selector */}
|
||||
<RedisTypeSelector
|
||||
redisType={redisType}
|
||||
redisTypeDescriptions={redisTypeDescriptions}
|
||||
onTypeChange={setRedisType}
|
||||
/>
|
||||
|
||||
{/* Basic Fields */}
|
||||
<div className="space-y-6 pt-4 border-t border-gray-200">
|
||||
<h4 className="text-sm font-medium text-gray-900">Connection Settings</h4>
|
||||
<div className="space-y-6 pt-4 border-t border-border">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
Connection Settings
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{basicFields.map((field: any) => {
|
||||
if (!field) return null;
|
||||
const currentValue = cacheSettings[field.field_name] ?? field.field_default ?? "";
|
||||
return <CacheFieldRenderer key={field.field_name} field={field} currentValue={currentValue} />;
|
||||
})}
|
||||
{basicFields.map(renderField)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Redis Type-Specific Fields */}
|
||||
{redisType === "cluster" && clusterFields.length > 0 && (
|
||||
<div className="space-y-6 pt-4 border-t border-gray-200">
|
||||
<h4 className="text-sm font-medium text-gray-900">Cluster Configuration</h4>
|
||||
<div className="space-y-6 pt-4 border-t border-border">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
Cluster Configuration
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{clusterFields.map((field: any) => {
|
||||
const currentValue = cacheSettings[field.field_name] ?? field.field_default ?? "";
|
||||
return <CacheFieldRenderer key={field.field_name} field={field} currentValue={currentValue} />;
|
||||
})}
|
||||
{clusterFields.map(renderField)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{redisType === "sentinel" && sentinelFields.length > 0 && (
|
||||
<div className="space-y-6 pt-4 border-t border-gray-200">
|
||||
<h4 className="text-sm font-medium text-gray-900">Sentinel Configuration</h4>
|
||||
<div className="space-y-6 pt-4 border-t border-border">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
Sentinel Configuration
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{sentinelFields.map((field: any) => {
|
||||
const currentValue = cacheSettings[field.field_name] ?? field.field_default ?? "";
|
||||
return <CacheFieldRenderer key={field.field_name} field={field} currentValue={currentValue} />;
|
||||
})}
|
||||
{sentinelFields.map(renderField)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{redisType === "semantic" && semanticFields.length > 0 && (
|
||||
<div className="space-y-6 pt-4 border-t border-gray-200">
|
||||
<h4 className="text-sm font-medium text-gray-900">Semantic Configuration</h4>
|
||||
<div className="space-y-6 pt-4 border-t border-border">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
Semantic Configuration
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{semanticFields.map((field: any) => {
|
||||
const currentValue = cacheSettings[field.field_name] ?? field.field_default ?? "";
|
||||
return <CacheFieldRenderer key={field.field_name} field={field} currentValue={currentValue} />;
|
||||
})}
|
||||
{semanticFields.map(renderField)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Advanced Settings Accordion */}
|
||||
<Accordion className="mt-4">
|
||||
<AccordionHeader>
|
||||
<span className="text-sm font-medium text-gray-900">Advanced Settings</span>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<div className="space-y-6">
|
||||
{/* SSL Settings */}
|
||||
{sslFields.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h5 className="text-sm font-medium text-gray-700">SSL Settings</h5>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{sslFields.map((field: any) => {
|
||||
if (!field) return null;
|
||||
const currentValue = cacheSettings[field.field_name] ?? field.field_default ?? "";
|
||||
return <CacheFieldRenderer key={field.field_name} field={field} currentValue={currentValue} />;
|
||||
})}
|
||||
<Accordion type="single" collapsible className="mt-4">
|
||||
<AccordionItem value="advanced">
|
||||
<AccordionTrigger className="text-sm font-medium">
|
||||
Advanced Settings
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-6">
|
||||
{sslFields.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h5 className="text-sm font-medium text-foreground">
|
||||
SSL Settings
|
||||
</h5>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{sslFields.map(renderField)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Cache Management */}
|
||||
{cacheManagementFields.length > 0 && (
|
||||
<div className="space-y-4 pt-4 border-t border-gray-200">
|
||||
<h5 className="text-sm font-medium text-gray-700">Cache Management</h5>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{cacheManagementFields.map((field: any) => {
|
||||
if (!field) return null;
|
||||
const currentValue = cacheSettings[field.field_name] ?? field.field_default ?? "";
|
||||
return <CacheFieldRenderer key={field.field_name} field={field} currentValue={currentValue} />;
|
||||
})}
|
||||
{cacheManagementFields.length > 0 && (
|
||||
<div className="space-y-4 pt-4 border-t border-border">
|
||||
<h5 className="text-sm font-medium text-foreground">
|
||||
Cache Management
|
||||
</h5>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{cacheManagementFields.map(renderField)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* GCP Authentication */}
|
||||
{gcpFields.length > 0 && (
|
||||
<div className="space-y-4 pt-4 border-t border-gray-200">
|
||||
<h5 className="text-sm font-medium text-gray-700">GCP Authentication</h5>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{gcpFields.map((field: any) => {
|
||||
if (!field) return null;
|
||||
const currentValue = cacheSettings[field.field_name] ?? field.field_default ?? "";
|
||||
return <CacheFieldRenderer key={field.field_name} field={field} currentValue={currentValue} />;
|
||||
})}
|
||||
{gcpFields.length > 0 && (
|
||||
<div className="space-y-4 pt-4 border-t border-border">
|
||||
<h5 className="text-sm font-medium text-foreground">
|
||||
GCP Authentication
|
||||
</h5>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{gcpFields.map(renderField)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionBody>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="border-t border-gray-200 pt-6 flex justify-end gap-3">
|
||||
<Button variant="secondary" size="sm" onClick={handleTestConnection} disabled={isTesting} className="text-sm">
|
||||
<div className="border-t border-border pt-6 flex justify-end gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleTestConnection}
|
||||
disabled={isTesting}
|
||||
>
|
||||
{isTesting ? "Testing..." : "Test Connection"}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSaveChanges} disabled={isSaving} className="text-sm font-medium">
|
||||
<Button size="sm" onClick={handleSaveChanges} disabled={isSaving}>
|
||||
{isSaving ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue