feat ui: add import and export config
This commit is contained in:
212
ui/src/App.tsx
212
ui/src/App.tsx
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { SettingsDialog } from "@/components/SettingsDialog";
|
import { SettingsDialog } from "@/components/SettingsDialog";
|
||||||
@@ -9,23 +9,30 @@ import { JsonEditor } from "@/components/JsonEditor";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useConfig } from "@/components/ConfigProvider";
|
import { useConfig } from "@/components/ConfigProvider";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { Settings, Languages, Save, RefreshCw, FileJson } from "lucide-react";
|
import { Settings, Languages, Save, RefreshCw, FileJson, Upload, Download } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { Toast } from "@/components/ui/toast";
|
import { Toast } from "@/components/ui/toast";
|
||||||
import "@/styles/animations.css";
|
import "@/styles/animations.css";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { config, error } = useConfig();
|
const { config, setConfig, error } = useConfig();
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
|
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
|
||||||
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
||||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
@@ -153,6 +160,43 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportConfig = () => {
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
const configString = JSON.stringify(config, null, 2);
|
||||||
|
const blob = new Blob([configString], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "claude-code-router-config.json";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const importConfig = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
try {
|
||||||
|
const configString = event.target?.result as string;
|
||||||
|
const importedConfig = JSON.parse(configString);
|
||||||
|
setConfig(importedConfig);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse config file:", error);
|
||||||
|
setToast({ message: t('settings.import_error'), type: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
if (isCheckingAuth) {
|
if (isCheckingAuth) {
|
||||||
return (
|
return (
|
||||||
@@ -180,51 +224,93 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-gray-50 font-sans">
|
<TooltipProvider>
|
||||||
<header className="flex h-16 items-center justify-between border-b bg-white px-6">
|
<div className="h-screen bg-gray-50 font-sans">
|
||||||
<h1 className="text-xl font-semibold text-gray-800">{t('app.title')}</h1>
|
<header className="flex h-16 items-center justify-between border-b bg-white px-6">
|
||||||
<div className="flex items-center gap-2">
|
<h1 className="text-xl font-semibold text-gray-800">{t('app.title')}</h1>
|
||||||
<Button variant="ghost" size="icon" onClick={() => setIsSettingsOpen(true)} className="transition-all-ease hover:scale-110">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="h-5 w-5" />
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" onClick={() => setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
|
<Button variant="ghost" size="icon" onClick={importConfig} className="transition-all-ease hover:scale-110">
|
||||||
<FileJson className="h-5 w-5" />
|
<Download className="h-5 w-5" />
|
||||||
</Button>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
|
|
||||||
<Languages className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-32 p-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Button
|
|
||||||
variant={i18n.language.startsWith('en') ? 'default' : 'ghost'}
|
|
||||||
className="w-full justify-start transition-all-ease hover:scale-[1.02]"
|
|
||||||
onClick={() => i18n.changeLanguage('en')}
|
|
||||||
>
|
|
||||||
English
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</TooltipTrigger>
|
||||||
variant={i18n.language.startsWith('zh') ? 'default' : 'ghost'}
|
<TooltipContent>
|
||||||
className="w-full justify-start transition-all-ease hover:scale-[1.02]"
|
<p>{t('app.import_config')}</p>
|
||||||
onClick={() => i18n.changeLanguage('zh')}
|
</TooltipContent>
|
||||||
>
|
</Tooltip>
|
||||||
中文
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" onClick={exportConfig} className="transition-all-ease hover:scale-110">
|
||||||
|
<Upload className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</PopoverContent>
|
<TooltipContent>
|
||||||
</Popover>
|
<p>{t('app.export_config')}</p>
|
||||||
<Button onClick={saveConfig} variant="outline" className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
|
</TooltipContent>
|
||||||
<Save className="mr-2 h-4 w-4" />
|
</Tooltip>
|
||||||
{t('app.save')}
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger asChild>
|
||||||
<Button onClick={saveConfigAndRestart} className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
|
<Button variant="ghost" size="icon" onClick={() => setIsSettingsOpen(true)} className="transition-all-ease hover:scale-110">
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<Settings className="h-5 w-5" />
|
||||||
{t('app.save_and_restart')}
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</div>
|
<TooltipContent>
|
||||||
</header>
|
<p>{t('app.settings')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
|
||||||
|
<FileJson className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t('json_editor.title')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Popover>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
|
||||||
|
<Languages className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t('app.change_language')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent className="w-32 p-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Button
|
||||||
|
variant={i18n.language.startsWith('en') ? 'default' : 'ghost'}
|
||||||
|
className="w-full justify-start transition-all-ease hover:scale-[1.02]"
|
||||||
|
onClick={() => i18n.changeLanguage('en')}
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={i18n.language.startsWith('zh') ? 'default' : 'ghost'}
|
||||||
|
className="w-full justify-start transition-all-ease hover:scale-[1.02]"
|
||||||
|
onClick={() => i18n.changeLanguage('zh')}
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Button onClick={saveConfig} variant="outline" className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
{t('app.save')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={saveConfigAndRestart} className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
{t('app.save_and_restart')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
<main className="flex h-[calc(100vh-4rem)] gap-4 p-4">
|
<main className="flex h-[calc(100vh-4rem)] gap-4 p-4">
|
||||||
<div className="w-3/5">
|
<div className="w-3/5">
|
||||||
<Providers />
|
<Providers />
|
||||||
@@ -238,20 +324,28 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<SettingsDialog isOpen={isSettingsOpen} onOpenChange={setIsSettingsOpen} />
|
<SettingsDialog isOpen={isSettingsOpen} onOpenChange={setIsSettingsOpen} />
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
open={isJsonEditorOpen}
|
open={isJsonEditorOpen}
|
||||||
onOpenChange={setIsJsonEditorOpen}
|
onOpenChange={setIsJsonEditorOpen}
|
||||||
showToast={(message, type) => setToast({ message, type })}
|
showToast={(message, type) => setToast({ message, type })}
|
||||||
/>
|
|
||||||
{toast && (
|
|
||||||
<Toast
|
|
||||||
message={toast.message}
|
|
||||||
type={toast.type}
|
|
||||||
onClose={() => setToast(null)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{toast && (
|
||||||
</div>
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
onClose={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept=".json"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
ui/src/components/ui/tooltip.tsx
Normal file
28
ui/src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
@@ -12,7 +12,10 @@
|
|||||||
"config_saved_success": "Config saved successfully",
|
"config_saved_success": "Config saved successfully",
|
||||||
"config_saved_failed": "Failed to save config",
|
"config_saved_failed": "Failed to save config",
|
||||||
"config_saved_restart_success": "Config saved and service restarted successfully",
|
"config_saved_restart_success": "Config saved and service restarted successfully",
|
||||||
"config_saved_restart_failed": "Failed to save config and restart service"
|
"config_saved_restart_failed": "Failed to save config and restart service",
|
||||||
|
"import_config": "Import Config",
|
||||||
|
"export_config": "Export Config",
|
||||||
|
"change_language": "Change Language"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Sign in to your account",
|
"title": "Sign in to your account",
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
"config_saved_success": "配置保存成功",
|
"config_saved_success": "配置保存成功",
|
||||||
"config_saved_failed": "配置保存失败",
|
"config_saved_failed": "配置保存失败",
|
||||||
"config_saved_restart_success": "配置保存并服务重启成功",
|
"config_saved_restart_success": "配置保存并服务重启成功",
|
||||||
"config_saved_restart_failed": "配置保存并服务重启失败"
|
"config_saved_restart_failed": "配置保存并服务重启失败",
|
||||||
|
"import_config": "导入配置",
|
||||||
|
"export_config": "导出配置",
|
||||||
|
"change_language": "切换语言"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "登录到您的账户",
|
"title": "登录到您的账户",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts"],"version":"5.8.3"}
|
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/components/ui/tooltip.tsx","./src/lib/api.ts","./src/lib/utils.ts"],"version":"5.8.3"}
|
||||||
Reference in New Issue
Block a user