feat(components): wire config export/import into registry and image editors
This commit is contained in:
@@ -5,6 +5,8 @@ import { useTranslations } from "next-intl";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { errorToast } from "@/lib/errorReporter";
|
||||
import { ConfigImportDialog } from "@/app/components/ConfigImportDialog";
|
||||
import { downloadTextFile, exportFilename } from "@/lib/configTransfer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
getImageGeneration,
|
||||
@@ -372,6 +374,28 @@ export function ImageModelsEditor() {
|
||||
const [forbidden, setForbidden] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const t = useTranslations("imageModels");
|
||||
const tc = useTranslations("configTransfer");
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const runExport = useCallback(async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const response = await fetch("/api/image-generation/export", {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) {
|
||||
errorToast(
|
||||
"models.image-export",
|
||||
tc("exportFailed", { status: String(response.status) })
|
||||
);
|
||||
return;
|
||||
}
|
||||
downloadTextFile(exportFilename("image-generation"), await response.text());
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}, [tc]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoadError(null);
|
||||
@@ -497,28 +521,49 @@ export function ImageModelsEditor() {
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium text-muted-foreground">{t("modelsHeading")}</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setDraft({
|
||||
...draft,
|
||||
models: [
|
||||
...draft.models,
|
||||
{
|
||||
...newImageModelDraft(draft.models),
|
||||
api_key_configured: false,
|
||||
api_key_hint: null,
|
||||
paramsText: "",
|
||||
uid: newDraftUid(),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("addModel")}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={exporting}
|
||||
onClick={() => void runExport()}
|
||||
>
|
||||
{exporting ? tc("exporting") : tc("export")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={dirty}
|
||||
title={dirty ? tc("importDisabledWhileDirty") : undefined}
|
||||
onClick={() => setImportOpen(true)}
|
||||
>
|
||||
{tc("import")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setDraft({
|
||||
...draft,
|
||||
models: [
|
||||
...draft.models,
|
||||
{
|
||||
...newImageModelDraft(draft.models),
|
||||
api_key_configured: false,
|
||||
api_key_hint: null,
|
||||
paramsText: "",
|
||||
uid: newDraftUid(),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("addModel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{draft.models.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">{t("empty")}</p>
|
||||
@@ -570,6 +615,12 @@ export function ImageModelsEditor() {
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{t("apiKeyWriteOnlyNote")}
|
||||
</p>
|
||||
<ConfigImportDialog
|
||||
kind="image-generation"
|
||||
open={importOpen}
|
||||
onOpenChange={setImportOpen}
|
||||
onImported={load}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useTranslations } from "next-intl";
|
||||
import { ChevronDown, ChevronRight, Eye, EyeOff } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { errorToast } from "@/lib/errorReporter";
|
||||
import { ConfigImportDialog } from "@/app/components/ConfigImportDialog";
|
||||
import { downloadTextFile, exportFilename } from "@/lib/configTransfer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { invalidateAvailableModels } from "@/app/hooks/useAvailableModels";
|
||||
import type {
|
||||
@@ -934,6 +936,9 @@ function ProviderEditor({
|
||||
* controls from `adapter_specs` and surfaces 422 detail paths verbatim. */
|
||||
export function RegistryEditor() {
|
||||
const t = useTranslations("modelConfig");
|
||||
const tc = useTranslations("configTransfer");
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [data, setData] = useState<GetModelRegistryResponse | null>(null);
|
||||
const [draft, setDraft] = useState<RegistryV4 | null>(null);
|
||||
const [credentialWrites, setCredentialWrites] = useState<Map<string, string>>(
|
||||
@@ -983,6 +988,25 @@ export function RegistryEditor() {
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const runExport = useCallback(async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const response = await fetch("/api/model-registry/export", {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) {
|
||||
errorToast(
|
||||
"models.registry-export",
|
||||
tc("exportFailed", { status: String(response.status) })
|
||||
);
|
||||
return;
|
||||
}
|
||||
downloadTextFile(exportFilename("model-registry"), await response.text());
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}, [tc]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
@@ -1282,6 +1306,29 @@ export function RegistryEditor() {
|
||||
{t("registry.unsavedChanges")}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-1" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={exporting}
|
||||
onClick={() => void runExport()}
|
||||
>
|
||||
{exporting ? tc("exporting") : tc("export")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={dirty}
|
||||
title={dirty ? tc("importDisabledWhileDirty") : undefined}
|
||||
onClick={() => setImportOpen(true)}
|
||||
>
|
||||
{tc("import")}
|
||||
</Button>
|
||||
<p className="basis-full text-[11px] text-muted-foreground">
|
||||
{tc("exportSecretsWarning")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 rounded-lg border border-border p-4 sm:grid-cols-2">
|
||||
@@ -1536,6 +1583,12 @@ export function RegistryEditor() {
|
||||
mono: (chunks) => <span className="font-mono">{chunks}</span>,
|
||||
})}
|
||||
</p>
|
||||
<ConfigImportDialog
|
||||
kind="model-registry"
|
||||
open={importOpen}
|
||||
onOpenChange={setImportOpen}
|
||||
onImported={load}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user