fix: improve error handling and config validation

- Add fallback mechanism for service startup with default config
- Implement config file backup before saving
- Add robust validation for config data in UI components
- Improve error handling and user feedback in UI
- Fix potential null/undefined access in provider and router components

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
musistudio
2025-07-30 15:39:44 +08:00
parent e560db85f4
commit 1d7374067e
11 changed files with 429 additions and 151 deletions

View File

@@ -137,10 +137,52 @@ async function main() {
startProcess.unref();
if (!(await waitForService())) {
console.error(
"Service startup timeout, please manually run `ccr start` to start the service"
);
process.exit(1);
// If service startup fails, try to start with default config
console.log("Service startup timeout, trying to start with default configuration...");
const { initDir, writeConfigFile, backupConfigFile } = require("./utils");
try {
// Initialize directories
await initDir();
// Backup existing config file if it exists
const backupPath = await backupConfigFile();
if (backupPath) {
console.log(`Backed up existing configuration file to ${backupPath}`);
}
// Create a minimal default config file
await writeConfigFile({
"PORT": 3456,
"Providers": [],
"Router": {}
});
console.log("Created minimal default configuration file at ~/.claude-code-router/config.json");
console.log("Please edit this file with your actual configuration.");
// Try starting the service again
const restartProcess = spawn("node", [cliPath, "start"], {
detached: true,
stdio: "ignore",
});
restartProcess.on("error", (error) => {
console.error("Failed to start service with default config:", error.message);
process.exit(1);
});
restartProcess.unref();
if (!(await waitForService(15000))) { // Wait a bit longer for the first start
console.error(
"Service startup still failing. Please manually run `ccr start` to start the service and check the logs."
);
process.exit(1);
}
} catch (error: any) {
console.error("Failed to create default configuration:", error.message);
process.exit(1);
}
}
}

View File

@@ -28,6 +28,14 @@ export const createServer = (config: any): Server => {
// Add endpoint to save config.json
server.app.post("/api/config", async (req) => {
const newConfig = req.body;
// Backup existing config file if it exists
const { backupConfigFile } = await import("./utils");
const backupPath = await backupConfigFile();
if (backupPath) {
console.log(`Backed up existing configuration file to ${backupPath}`);
}
await writeConfigFile(newConfig);
return { success: true, message: "Config saved successfully" };
});

View File

@@ -85,6 +85,20 @@ export const readConfigFile = async () => {
}
};
export const backupConfigFile = async () => {
try {
if (await fs.access(CONFIG_FILE).then(() => true).catch(() => false)) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${CONFIG_FILE}.${timestamp}.bak`;
await fs.copyFile(CONFIG_FILE, backupPath);
return backupPath;
}
} catch (error) {
console.error("Failed to backup config file:", error);
}
return null;
};
export const writeConfigFile = async (config: any) => {
await ensureDir(HOME_DIR);
const configWithComment = `${JSON.stringify(config, null, 2)}`;

View File

@@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CCR UI</title>
</head>

View File

@@ -75,87 +75,108 @@ function App() {
}, [config, navigate]);
const saveConfig = async () => {
if (config) {
try {
// Save to API
const response = await api.updateConfig(config);
// Handle case where config might be null or undefined
if (!config) {
setToast({ message: t('app.config_missing'), type: 'error' });
return;
}
try {
// Save to API
const response = await api.updateConfig(config);
// Show success message or handle as needed
console.log('Config saved successfully');
// 根据响应信息进行提示
if (response && typeof response === 'object' && 'success' in response) {
const apiResponse = response as { success: boolean; message?: string };
if (apiResponse.success) {
setToast({ message: apiResponse.message || t('app.config_saved_success'), type: 'success' });
} else {
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
}
} else {
// 默认成功提示
setToast({ message: t('app.config_saved_success'), type: 'success' });
}
} catch (error) {
console.error('Failed to save config:', error);
// Handle error appropriately
setToast({ message: t('app.config_saved_failed') + ': ' + (error as Error).message, type: 'error' });
}
};
const saveConfigAndRestart = async () => {
// Handle case where config might be null or undefined
if (!config) {
setToast({ message: t('app.config_missing'), type: 'error' });
return;
}
try {
// Save to API
const response = await api.updateConfig(config);
// Check if save was successful before restarting
let saveSuccessful = true;
if (response && typeof response === 'object' && 'success' in response) {
const apiResponse = response as { success: boolean; message?: string };
if (!apiResponse.success) {
saveSuccessful = false;
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
}
}
// Only restart if save was successful
if (saveSuccessful) {
// Restart service
const response = await api.restartService();
// Show success message or handle as needed
console.log('Config saved successfully');
console.log('Config saved and service restarted successfully');
// 根据响应信息进行提示
if (response && typeof response === 'object' && 'success' in response) {
const apiResponse = response as { success: boolean; message?: string };
if (apiResponse.success) {
setToast({ message: apiResponse.message || t('app.config_saved_success'), type: 'success' });
} else {
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
setToast({ message: apiResponse.message || t('app.config_saved_restart_success'), type: 'success' });
}
} else {
// 默认成功提示
setToast({ message: t('app.config_saved_success'), type: 'success' });
setToast({ message: t('app.config_saved_restart_success'), type: 'success' });
}
} catch (error) {
console.error('Failed to save config:', error);
// Handle error appropriately
setToast({ message: t('app.config_saved_failed') + ': ' + (error as Error).message, type: 'error' });
}
}
};
const saveConfigAndRestart = async () => {
if (config) {
try {
// Save to API
const response = await api.updateConfig(config);
// Check if save was successful before restarting
let saveSuccessful = true;
if (response && typeof response === 'object' && 'success' in response) {
const apiResponse = response as { success: boolean; message?: string };
if (!apiResponse.success) {
saveSuccessful = false;
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
}
}
// Only restart if save was successful
if (saveSuccessful) {
// Restart service
const response = await api.restartService();
// Show success message or handle as needed
console.log('Config saved and service restarted successfully');
// 根据响应信息进行提示
if (response && typeof response === 'object' && 'success' in response) {
const apiResponse = response as { success: boolean; message?: string };
if (apiResponse.success) {
setToast({ message: apiResponse.message || t('app.config_saved_restart_success'), type: 'success' });
}
} else {
// 默认成功提示
setToast({ message: t('app.config_saved_restart_success'), type: 'success' });
}
}
} catch (error) {
console.error('Failed to save config and restart:', error);
// Handle error appropriately
setToast({ message: t('app.config_saved_restart_failed') + ': ' + (error as Error).message, type: 'error' });
}
} catch (error) {
console.error('Failed to save config and restart:', error);
// Handle error appropriately
setToast({ message: t('app.config_saved_restart_failed') + ': ' + (error as Error).message, type: 'error' });
}
};
if (isCheckingAuth) {
return <div>Loading...</div>;
return (
<div className="h-screen bg-gray-50 font-sans flex items-center justify-center">
<div className="text-gray-500">Loading application...</div>
</div>
);
}
if (error) {
return <div>Error: {error.message}</div>;
return (
<div className="h-screen bg-gray-50 font-sans flex items-center justify-center">
<div className="text-red-500">Error: {error.message}</div>
</div>
);
}
// Handle case where config is null or undefined
if (!config) {
return <div>Loading...</div>;
return (
<div className="h-screen bg-gray-50 font-sans flex items-center justify-center">
<div className="text-gray-500">Loading configuration...</div>
</div>
);
}
return (

View File

@@ -108,7 +108,32 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
try {
// Try to fetch config regardless of API key presence
const data = await api.getConfig();
setConfig(data);
// Validate the received data to ensure it has the expected structure
const validConfig = {
LOG: typeof data.LOG === 'boolean' ? data.LOG : false,
CLAUDE_PATH: typeof data.CLAUDE_PATH === 'string' ? data.CLAUDE_PATH : '',
HOST: typeof data.HOST === 'string' ? data.HOST : '127.0.0.1',
PORT: typeof data.PORT === 'number' ? data.PORT : 3456,
APIKEY: typeof data.APIKEY === 'string' ? data.APIKEY : '',
transformers: Array.isArray(data.transformers) ? data.transformers : [],
Providers: Array.isArray(data.Providers) ? data.Providers : [],
Router: data.Router && typeof data.Router === 'object' ? {
default: typeof data.Router.default === 'string' ? data.Router.default : '',
background: typeof data.Router.background === 'string' ? data.Router.background : '',
think: typeof data.Router.think === 'string' ? data.Router.think : '',
longContext: typeof data.Router.longContext === 'string' ? data.Router.longContext : '',
webSearch: typeof data.Router.webSearch === 'string' ? data.Router.webSearch : ''
} : {
default: '',
background: '',
think: '',
longContext: '',
webSearch: ''
}
};
setConfig(validConfig);
} catch (err) {
console.error('Failed to fetch config:', err);
// If we get a 401, the API client will redirect to login

View File

@@ -10,29 +10,74 @@ interface ProviderListProps {
}
export function ProviderList({ providers, onEdit, onRemove }: ProviderListProps) {
// Handle case where providers might be null or undefined
if (!providers || !Array.isArray(providers)) {
return (
<div className="space-y-3">
<div className="flex items-center justify-center rounded-md border bg-white p-8 text-gray-500">
No providers configured
</div>
</div>
);
}
return (
<div className="space-y-3">
{providers.map((provider, index) => (
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
<div className="flex-1 space-y-1.5">
<p className="text-md font-semibold text-gray-800">{provider.name}</p>
<p className="text-sm text-gray-500">{provider.api_base_url}</p>
<div className="flex flex-wrap gap-2 pt-2">
{provider.models.map((model) => (
<Badge key={model} variant="outline" className="font-normal transition-all-ease hover:scale-105">{model}</Badge>
))}
{providers.map((provider, index) => {
// Handle case where individual provider might be null or undefined
if (!provider) {
return (
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
<div className="flex-1 space-y-1.5">
<p className="text-md font-semibold text-gray-800">Invalid Provider</p>
<p className="text-sm text-gray-500">Provider data is missing</p>
</div>
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110" disabled>
<Pencil className="h-4 w-4" />
</Button>
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
</Button>
</div>
</div>
);
}
// Handle case where provider.name might be null or undefined
const providerName = provider.name || "Unnamed Provider";
// Handle case where provider.api_base_url might be null or undefined
const apiBaseUrl = provider.api_base_url || "No API URL";
// Handle case where provider.models might be null or undefined
const models = Array.isArray(provider.models) ? provider.models : [];
return (
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
<div className="flex-1 space-y-1.5">
<p className="text-md font-semibold text-gray-800">{providerName}</p>
<p className="text-sm text-gray-500">{apiBaseUrl}</p>
<div className="flex flex-wrap gap-2 pt-2">
{models.map((model, modelIndex) => (
// Handle case where model might be null or undefined
<Badge key={modelIndex} variant="outline" className="font-normal transition-all-ease hover:scale-105">
{model || "Unnamed Model"}
</Badge>
))}
</div>
</div>
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110">
<Pencil className="h-4 w-4" />
</Button>
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
</Button>
</div>
</div>
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110">
<Pencil className="h-4 w-4" />
</Button>
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
</Button>
</div>
</div>
))}
);
})}
</div>
);
}

View File

@@ -46,10 +46,23 @@ export function Providers() {
fetchTransformers();
}, []);
// Handle case where config is null or undefined
if (!config) {
return null;
return (
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
<CardHeader className="flex flex-row items-center justify-between border-b p-4">
<CardTitle className="text-lg">{t("providers.title")}</CardTitle>
</CardHeader>
<CardContent className="flex-grow flex items-center justify-center p-4">
<div className="text-gray-500">Loading providers configuration...</div>
</CardContent>
</Card>
);
}
// Validate config.Providers to ensure it's an array
const validProviders = Array.isArray(config.Providers) ? config.Providers : [];
const handleAddProvider = () => {
const newProviders = [...config.Providers, { name: "", api_base_url: "", api_key: "", models: [] }];
@@ -307,8 +320,16 @@ export function Providers() {
const handleAddModel = (index: number, model: string) => {
if (!model.trim()) return;
// Handle case where config.Providers might be null or undefined
if (!config || !Array.isArray(config.Providers)) return;
// Handle case where the provider at the given index might be null or undefined
if (!config.Providers[index]) return;
const newProviders = [...config.Providers];
const models = [...newProviders[index].models];
// Handle case where provider.models might be null or undefined
const models = Array.isArray(newProviders[index].models) ? [...newProviders[index].models] : [];
// Check if model already exists
if (!models.includes(model.trim())) {
@@ -319,24 +340,36 @@ export function Providers() {
};
const handleRemoveModel = (providerIndex: number, modelIndex: number) => {
// Handle case where config.Providers might be null or undefined
if (!config || !Array.isArray(config.Providers)) return;
// Handle case where the provider at the given index might be null or undefined
if (!config.Providers[providerIndex]) return;
const newProviders = [...config.Providers];
const models = [...newProviders[providerIndex].models];
models.splice(modelIndex, 1);
newProviders[providerIndex].models = models;
setConfig({ ...config, Providers: newProviders });
// Handle case where provider.models might be null or undefined
const models = Array.isArray(newProviders[providerIndex].models) ? [...newProviders[providerIndex].models] : [];
// Handle case where modelIndex might be out of bounds
if (modelIndex >= 0 && modelIndex < models.length) {
models.splice(modelIndex, 1);
newProviders[providerIndex].models = models;
setConfig({ ...config, Providers: newProviders });
}
};
const editingProvider = editingProviderIndex !== null ? config.Providers[editingProviderIndex] : null;
const editingProvider = editingProviderIndex !== null ? validProviders[editingProviderIndex] : null;
return (
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
<CardHeader className="flex flex-row items-center justify-between border-b p-4">
<CardTitle className="text-lg">{t("providers.title")} <span className="text-sm font-normal text-gray-500">({config.Providers.length})</span></CardTitle>
<CardTitle className="text-lg">{t("providers.title")} <span className="text-sm font-normal text-gray-500">({validProviders.length})</span></CardTitle>
<Button onClick={handleAddProvider}>{t("providers.add")}</Button>
</CardHeader>
<CardContent className="flex-grow overflow-y-auto p-4">
<ProviderList
providers={config.Providers}
providers={validProviders}
onEdit={setEditingProviderIndex}
onRemove={setDeletingProviderIndex}
/>
@@ -356,15 +389,15 @@ export function Providers() {
<div className="space-y-4 p-4 overflow-y-auto flex-grow">
<div className="space-y-2">
<Label htmlFor="name">{t("providers.name")}</Label>
<Input id="name" value={editingProvider.name} onChange={(e) => handleProviderChange(editingProviderIndex, 'name', e.target.value)} />
<Input id="name" value={editingProvider.name || ''} onChange={(e) => handleProviderChange(editingProviderIndex, 'name', e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="api_base_url">{t("providers.api_base_url")}</Label>
<Input id="api_base_url" value={editingProvider.api_base_url} onChange={(e) => handleProviderChange(editingProviderIndex, 'api_base_url', e.target.value)} />
<Input id="api_base_url" value={editingProvider.api_base_url || ''} onChange={(e) => handleProviderChange(editingProviderIndex, 'api_base_url', e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="api_key">{t("providers.api_key")}</Label>
<Input id="api_key" type="password" value={editingProvider.api_key} onChange={(e) => handleProviderChange(editingProviderIndex, 'api_key', e.target.value)} />
<Input id="api_key" type="password" value={editingProvider.api_key || ''} onChange={(e) => handleProviderChange(editingProviderIndex, 'api_key', e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="models">{t("providers.models")}</Label>
@@ -374,7 +407,7 @@ export function Providers() {
{hasFetchedModels[editingProviderIndex] ? (
<ComboInput
ref={comboInputRef}
options={editingProvider.models.map(model => ({ label: model, value: model }))}
options={(editingProvider.models || []).map(model => ({ label: model, value: model }))}
value=""
onChange={(_) => {
// 只更新输入值,不添加模型
@@ -431,7 +464,7 @@ export function Providers() {
</Button> */}
</div>
<div className="flex flex-wrap gap-2 pt-2">
{editingProvider.models.map((model, modelIndex) => (
{(editingProvider.models || []).map((model, modelIndex) => (
<Badge key={modelIndex} variant="outline" className="font-normal flex items-center gap-1">
{model}
<button
@@ -596,11 +629,11 @@ export function Providers() {
</div>
{/* Model-specific Transformers */}
{editingProvider.models.length > 0 && (
{editingProvider.models && editingProvider.models.length > 0 && (
<div className="space-y-2">
<Label>{t("providers.model_transformers")}</Label>
<div className="space-y-3">
{editingProvider.models.map((model, modelIndex) => (
{(editingProvider.models || []).map((model, modelIndex) => (
<div key={modelIndex} className="border rounded-md p-3">
<div className="font-medium text-sm mb-2">{model}</div>
{/* Add new transformer */}

View File

@@ -8,21 +8,54 @@ export function Router() {
const { t } = useTranslation();
const { config, setConfig } = useConfig();
// Handle case where config is null or undefined
if (!config) {
return null;
return (
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
<CardHeader className="border-b p-4">
<CardTitle className="text-lg">{t("router.title")}</CardTitle>
</CardHeader>
<CardContent className="flex-grow flex items-center justify-center p-4">
<div className="text-gray-500">Loading router configuration...</div>
</CardContent>
</Card>
);
}
// Handle case where config.Router is null or undefined
const routerConfig = config.Router || {
default: "",
background: "",
think: "",
longContext: "",
webSearch: ""
};
const handleRouterChange = (field: string, value: string) => {
const newRouter = { ...config.Router, [field]: value };
// Handle case where config.Router might be null or undefined
const currentRouter = config.Router || {};
const newRouter = { ...currentRouter, [field]: value };
setConfig({ ...config, Router: newRouter });
};
const modelOptions = config.Providers.flatMap((provider) =>
provider.models.map((model) => ({
value: `${provider.name},${model}`,
label: `${provider.name}, ${model}`,
}))
);
// Handle case where config.Providers might be null or undefined
const providers = Array.isArray(config.Providers) ? config.Providers : [];
const modelOptions = providers.flatMap((provider) => {
// Handle case where individual provider might be null or undefined
if (!provider) return [];
// Handle case where provider.models might be null or undefined
const models = Array.isArray(provider.models) ? provider.models : [];
// Handle case where provider.name might be null or undefined
const providerName = provider.name || "Unknown Provider";
return models.map((model) => ({
value: `${providerName},${model || "Unknown Model"}`,
label: `${providerName}, ${model || "Unknown Model"}`,
}));
});
return (
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
@@ -34,7 +67,7 @@ export function Router() {
<Label>{t("router.default")}</Label>
<Combobox
options={modelOptions}
value={config.Router.default}
value={routerConfig.default || ""}
onChange={(value) => handleRouterChange("default", value)}
placeholder={t("router.selectModel")}
searchPlaceholder={t("router.searchModel")}
@@ -45,7 +78,7 @@ export function Router() {
<Label>{t("router.background")}</Label>
<Combobox
options={modelOptions}
value={config.Router.background}
value={routerConfig.background || ""}
onChange={(value) => handleRouterChange("background", value)}
placeholder={t("router.selectModel")}
searchPlaceholder={t("router.searchModel")}
@@ -56,7 +89,7 @@ export function Router() {
<Label>{t("router.think")}</Label>
<Combobox
options={modelOptions}
value={config.Router.think}
value={routerConfig.think || ""}
onChange={(value) => handleRouterChange("think", value)}
placeholder={t("router.selectModel")}
searchPlaceholder={t("router.searchModel")}
@@ -67,7 +100,7 @@ export function Router() {
<Label>{t("router.longContext")}</Label>
<Combobox
options={modelOptions}
value={config.Router.longContext}
value={routerConfig.longContext || ""}
onChange={(value) => handleRouterChange("longContext", value)}
placeholder={t("router.selectModel")}
searchPlaceholder={t("router.searchModel")}
@@ -78,7 +111,7 @@ export function Router() {
<Label>{t("router.webSearch")}</Label>
<Combobox
options={modelOptions}
value={config.Router.webSearch}
value={routerConfig.webSearch || ""}
onChange={(value) => handleRouterChange("webSearch", value)}
placeholder={t("router.selectModel")}
searchPlaceholder={t("router.searchModel")}

View File

@@ -9,24 +9,66 @@ interface TransformerListProps {
}
export function TransformerList({ transformers, onEdit, onRemove }: TransformerListProps) {
// Handle case where transformers might be null or undefined
if (!transformers || !Array.isArray(transformers)) {
return (
<div className="space-y-3">
<div className="flex items-center justify-center rounded-md border bg-white p-8 text-gray-500">
No transformers configured
</div>
</div>
);
}
return (
<div className="space-y-3">
{transformers.map((transformer, index) => (
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
<div className="flex-1 space-y-1.5">
<p className="text-md font-semibold text-gray-800">{transformer.path}</p>
<p className="text-sm text-gray-500">{transformer.options.project}</p>
{transformers.map((transformer, index) => {
// Handle case where individual transformer might be null or undefined
if (!transformer) {
return (
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
<div className="flex-1 space-y-1.5">
<p className="text-md font-semibold text-gray-800">Invalid Transformer</p>
<p className="text-sm text-gray-500">Transformer data is missing</p>
</div>
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110" disabled>
<Pencil className="h-4 w-4" />
</Button>
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
</Button>
</div>
</div>
);
}
// Handle case where transformer.path might be null or undefined
const transformerPath = transformer.path || "Unnamed Transformer";
// Handle case where transformer.options might be null or undefined
const options = transformer.options || {};
// Handle case where options.project might be null or undefined
const project = options.project || "No Project";
return (
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
<div className="flex-1 space-y-1.5">
<p className="text-md font-semibold text-gray-800">{transformerPath}</p>
<p className="text-sm text-gray-500">{project}</p>
</div>
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110">
<Pencil className="h-4 w-4" />
</Button>
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
</Button>
</div>
</div>
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110">
<Pencil className="h-4 w-4" />
</Button>
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
</Button>
</div>
</div>
))}
);
})}
</div>
);
}

View File

@@ -23,27 +23,40 @@ export function Transformers() {
const [deletingTransformerIndex, setDeletingTransformerIndex] = useState<number | null>(null);
const [newTransformer, setNewTransformer] = useState<{ path: string; options: { [key: string]: string } } | null>(null);
// Handle case where config is null or undefined
if (!config) {
return null;
return (
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
<CardHeader className="flex flex-row items-center justify-between border-b p-4">
<CardTitle className="text-lg">{t("transformers.title")}</CardTitle>
</CardHeader>
<CardContent className="flex-grow flex items-center justify-center p-4">
<div className="text-gray-500">Loading transformers configuration...</div>
</CardContent>
</Card>
);
}
// Validate config.transformers to ensure it's an array
const validTransformers = Array.isArray(config.transformers) ? config.transformers : [];
const handleAddTransformer = () => {
const newTransformer = { path: "", options: {} };
setNewTransformer(newTransformer);
setEditingTransformerIndex(config.transformers.length); // Use the length as index for the new item
setEditingTransformerIndex(validTransformers.length); // Use the length as index for the new item
};
const handleRemoveTransformer = (index: number) => {
const newTransformers = [...config.transformers];
const newTransformers = [...validTransformers];
newTransformers.splice(index, 1);
setConfig({ ...config, transformers: newTransformers });
setDeletingTransformerIndex(null);
};
const handleTransformerChange = (index: number, field: string, value: string, optionKey?: string) => {
if (index < config.transformers.length) {
if (index < validTransformers.length) {
// Editing an existing transformer
const newTransformers = [...config.transformers];
const newTransformers = [...validTransformers];
if (optionKey !== undefined) {
newTransformers[index].options[optionKey] = value;
} else {
@@ -65,15 +78,15 @@ export function Transformers() {
};
const editingTransformer = editingTransformerIndex !== null ?
(editingTransformerIndex < config.transformers.length ?
config.transformers[editingTransformerIndex] :
(editingTransformerIndex < validTransformers.length ?
validTransformers[editingTransformerIndex] :
newTransformer) :
null;
const handleSaveTransformer = () => {
if (newTransformer && editingTransformerIndex === config.transformers.length) {
if (newTransformer && editingTransformerIndex === validTransformers.length) {
// Saving a new transformer
const newTransformers = [...config.transformers, newTransformer];
const newTransformers = [...validTransformers, newTransformer];
setConfig({ ...config, transformers: newTransformers });
}
// Close the dialog
@@ -90,12 +103,12 @@ export function Transformers() {
return (
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
<CardHeader className="flex flex-row items-center justify-between border-b p-4">
<CardTitle className="text-lg">{t("transformers.title")} <span className="text-sm font-normal text-gray-500">({config.transformers.length})</span></CardTitle>
<CardTitle className="text-lg">{t("transformers.title")} <span className="text-sm font-normal text-gray-500">({validTransformers.length})</span></CardTitle>
<Button onClick={handleAddTransformer}>{t("transformers.add")}</Button>
</CardHeader>
<CardContent className="flex-grow overflow-y-auto p-4">
<TransformerList
transformers={config.transformers}
transformers={validTransformers}
onEdit={setEditingTransformerIndex}
onRemove={setDeletingTransformerIndex}
/>
@@ -113,7 +126,7 @@ export function Transformers() {
<Label htmlFor="transformer-path">{t("transformers.path")}</Label>
<Input
id="transformer-path"
value={editingTransformer.path}
value={editingTransformer.path || ''}
onChange={(e) => handleTransformerChange(editingTransformerIndex, "path", e.target.value)}
/>
</div>
@@ -124,11 +137,12 @@ export function Transformers() {
variant="outline"
size="sm"
onClick={() => {
const newKey = `param${Object.keys(editingTransformer.options).length + 1}`;
const options = editingTransformer.options || {};
const newKey = `param${Object.keys(options).length + 1}`;
if (editingTransformerIndex !== null) {
const newOptions = { ...editingTransformer.options, [newKey]: "" };
if (editingTransformerIndex < config.transformers.length) {
const newTransformers = [...config.transformers];
const newOptions = { ...options, [newKey]: "" };
if (editingTransformerIndex < validTransformers.length) {
const newTransformers = [...validTransformers];
newTransformers[editingTransformerIndex].options = newOptions;
setConfig({ ...config, transformers: newTransformers });
} else if (newTransformer) {
@@ -140,17 +154,18 @@ export function Transformers() {
<Plus className="h-4 w-4" />
</Button>
</div>
{Object.entries(editingTransformer.options).map(([key, value]) => (
{Object.entries(editingTransformer.options || {}).map(([key, value]) => (
<div key={key} className="flex items-center gap-2">
<Input
value={key}
onChange={(e) => {
const newOptions = { ...editingTransformer.options };
const options = editingTransformer.options || {};
const newOptions = { ...options };
delete newOptions[key];
newOptions[e.target.value] = value;
if (editingTransformerIndex !== null) {
if (editingTransformerIndex < config.transformers.length) {
const newTransformers = [...config.transformers];
if (editingTransformerIndex < validTransformers.length) {
const newTransformers = [...validTransformers];
newTransformers[editingTransformerIndex].options = newOptions;
setConfig({ ...config, transformers: newTransformers });
} else if (newTransformer) {
@@ -174,10 +189,11 @@ export function Transformers() {
size="icon"
onClick={() => {
if (editingTransformerIndex !== null) {
const newOptions = { ...editingTransformer.options };
const options = editingTransformer.options || {};
const newOptions = { ...options };
delete newOptions[key];
if (editingTransformerIndex < config.transformers.length) {
const newTransformers = [...config.transformers];
if (editingTransformerIndex < validTransformers.length) {
const newTransformers = [...validTransformers];
newTransformers[editingTransformerIndex].options = newOptions;
setConfig({ ...config, transformers: newTransformers });
} else if (newTransformer) {