Browse Source

feat: import/export config

Timothy J. Baek 11 months ago
parent
commit
6bbb755997

+ 28 - 0
backend/apps/webui/routers/configs.py

@@ -3,9 +3,37 @@ from fastapi import APIRouter, Depends, Request
 from pydantic import BaseModel
 from utils.utils import get_admin_user, get_verified_user
 
+
+from config import get_config, save_config
+
 router = APIRouter()
 
 
+############################
+# ImportConfig
+############################
+
+
+class ImportConfigForm(BaseModel):
+    config: dict
+
+
+@router.post("/import", response_model=dict)
+async def import_config(form_data: ImportConfigForm, user=Depends(get_admin_user)):
+    save_config(form_data.config)
+    return get_config()
+
+
+############################
+# ExportConfig
+############################
+
+
+@router.get("/export", response_model=dict)
+async def export_config(user=Depends(get_admin_user)):
+    return get_config()
+
+
 class SetDefaultModelsForm(BaseModel):
     models: str
 

+ 57 - 0
src/lib/apis/configs/index.ts

@@ -1,6 +1,63 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 import type { Banner } from '$lib/types';
 
+export const importConfig = async (token: string, config) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/import`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			config: config
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const exportConfig = async (token: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/export`, {
+		method: 'GET',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const setDefaultModels = async (token: string, models: string) => {
 	let error = null;
 

+ 115 - 34
src/lib/components/admin/Settings/Database.svelte

@@ -7,6 +7,7 @@
 	import { config, user } from '$lib/stores';
 	import { toast } from 'svelte-sonner';
 	import { getAllUserChats } from '$lib/apis/chats';
+	import { exportConfig, importConfig } from '$lib/apis/configs';
 
 	const i18n = getContext('i18n');
 
@@ -34,6 +35,92 @@
 		<div>
 			<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
 
+			<input
+				id="config-json-input"
+				hidden
+				type="file"
+				accept=".json"
+				on:change={(e) => {
+					const file = e.target.files[0];
+					const reader = new FileReader();
+
+					reader.onload = async (e) => {
+						const res = await importConfig(localStorage.token, JSON.parse(e.target.result)).catch(
+							(error) => {
+								toast.error(error);
+							}
+						);
+
+						if (res) {
+							toast.success('Config imported successfully');
+						}
+						e.target.value = null;
+					};
+
+					reader.readAsText(file);
+				}}
+			/>
+
+			<button
+				type="button"
+				class=" flex rounded-md py-2 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
+				on:click={async () => {
+					document.getElementById('config-json-input').click();
+				}}
+			>
+				<div class=" self-center mr-3">
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 16 16"
+						fill="currentColor"
+						class="w-4 h-4"
+					>
+						<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
+						<path
+							fill-rule="evenodd"
+							d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
+							clip-rule="evenodd"
+						/>
+					</svg>
+				</div>
+				<div class=" self-center text-sm font-medium">
+					{$i18n.t('Import Config from JSON File')}
+				</div>
+			</button>
+
+			<button
+				type="button"
+				class=" flex rounded-md py-2 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
+				on:click={async () => {
+					const config = await exportConfig(localStorage.token);
+					const blob = new Blob([JSON.stringify(config)], {
+						type: 'application/json'
+					});
+					saveAs(blob, `config-${Date.now()}.json`);
+				}}
+			>
+				<div class=" self-center mr-3">
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 16 16"
+						fill="currentColor"
+						class="w-4 h-4"
+					>
+						<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
+						<path
+							fill-rule="evenodd"
+							d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
+							clip-rule="evenodd"
+						/>
+					</svg>
+				</div>
+				<div class=" self-center text-sm font-medium">
+					{$i18n.t('Export Config to JSON File')}
+				</div>
+			</button>
+
+			<hr class=" dark:border-gray-850 my-1" />
+
 			{#if $config?.features.enable_admin_export ?? true}
 				<div class="  flex w-full justify-between">
 					<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
@@ -97,40 +184,34 @@
 
 			<hr class=" dark:border-gray-850 my-1" />
 
-			<div class="  flex w-full justify-between">
-				<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
-
-				<button
-					class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
-					type="button"
-					on:click={() => {
-						downloadLiteLLMConfig(localStorage.token).catch((error) => {
-							toast.error(error);
-						});
-					}}
-				>
-					<div class=" self-center mr-3">
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 24 24"
-							fill="currentColor"
-							class="size-4"
-						>
-							<path
-								fill-rule="evenodd"
-								d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875Zm5.845 17.03a.75.75 0 0 0 1.06 0l3-3a.75.75 0 1 0-1.06-1.06l-1.72 1.72V12a.75.75 0 0 0-1.5 0v4.19l-1.72-1.72a.75.75 0 0 0-1.06 1.06l3 3Z"
-								clip-rule="evenodd"
-							/>
-							<path
-								d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
-							/>
-						</svg>
-					</div>
-					<div class=" self-center text-sm font-medium">
-						{$i18n.t('Export LiteLLM config.yaml')}
-					</div>
-				</button>
-			</div>
+			<button
+				type="button"
+				class=" flex rounded-md py-2 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
+				on:click={() => {
+					downloadLiteLLMConfig(localStorage.token).catch((error) => {
+						toast.error(error);
+					});
+				}}
+			>
+				<div class=" self-center mr-3">
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 16 16"
+						fill="currentColor"
+						class="w-4 h-4"
+					>
+						<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
+						<path
+							fill-rule="evenodd"
+							d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
+							clip-rule="evenodd"
+						/>
+					</svg>
+				</div>
+				<div class=" self-center text-sm font-medium">
+					{$i18n.t('Export LiteLLM config.yaml')}
+				</div>
+			</button>
 		</div>
 	</div>