Browse Source

feat: add/delete pipelines

Timothy J. Baek 11 months ago
parent
commit
12e60d8ebf
3 changed files with 294 additions and 3 deletions
  1. 86 0
      backend/main.py
  2. 72 0
      src/lib/apis/index.ts
  3. 136 3
      src/lib/components/admin/Settings/Pipelines.svelte

+ 86 - 0
backend/main.py

@@ -480,6 +480,92 @@ async def get_pipelines_list(user=Depends(get_admin_user)):
     }
 
 
+class AddPipelineForm(BaseModel):
+    url: str
+    urlIdx: int
+
+
+@app.post("/api/pipelines/add")
+async def add_pipeline(form_data: AddPipelineForm, user=Depends(get_admin_user)):
+
+    r = None
+    try:
+        urlIdx = form_data.urlIdx
+
+        url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
+        key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
+
+        headers = {"Authorization": f"Bearer {key}"}
+        r = requests.post(
+            f"{url}/pipelines/add", headers=headers, json={"url": form_data.url}
+        )
+
+        r.raise_for_status()
+        data = r.json()
+
+        return {**data}
+    except Exception as e:
+        # Handle connection error here
+        print(f"Connection error: {e}")
+
+        detail = "Pipeline not found"
+        if r is not None:
+            try:
+                res = r.json()
+                if "detail" in res:
+                    detail = res["detail"]
+            except:
+                pass
+
+        raise HTTPException(
+            status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
+            detail=detail,
+        )
+
+
+class DeletePipelineForm(BaseModel):
+    id: str
+    urlIdx: int
+
+
+@app.delete("/api/pipelines/delete")
+async def delete_pipeline(form_data: DeletePipelineForm, user=Depends(get_admin_user)):
+
+    r = None
+    try:
+        urlIdx = form_data.urlIdx
+
+        url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
+        key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
+
+        headers = {"Authorization": f"Bearer {key}"}
+        r = requests.delete(
+            f"{url}/pipelines/delete", headers=headers, json={"id": form_data.id}
+        )
+
+        r.raise_for_status()
+        data = r.json()
+
+        return {**data}
+    except Exception as e:
+        # Handle connection error here
+        print(f"Connection error: {e}")
+
+        detail = "Pipeline not found"
+        if r is not None:
+            try:
+                res = r.json()
+                if "detail" in res:
+                    detail = res["detail"]
+            except:
+                pass
+
+        raise HTTPException(
+            status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
+            detail=detail,
+        )
+
+
 @app.get("/api/pipelines")
 async def get_pipelines(urlIdx: Optional[int] = None, user=Depends(get_admin_user)):
     models = await get_all_models()

+ 72 - 0
src/lib/apis/index.ts

@@ -78,6 +78,78 @@ export const getPipelinesList = async (token: string = '') => {
 	return pipelines;
 };
 
+export const downloadPipeline = async (token: string, url: string, urlIdx: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines/add`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		},
+		body: JSON.stringify({
+			url: url,
+			urlIdx: urlIdx
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			if ('detail' in err) {
+				error = err.detail;
+			} else {
+				error = err;
+			}
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const deletePipeline = async (token: string, id: string, urlIdx: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines/delete`, {
+		method: 'DELETE',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		},
+		body: JSON.stringify({
+			id: id,
+			urlIdx: urlIdx
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			if ('detail' in err) {
+				error = err.detail;
+			} else {
+				error = err;
+			}
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const getPipelines = async (token: string, urlIdx?: string) => {
 	let error = null;
 

+ 136 - 3
src/lib/components/admin/Settings/Pipelines.svelte

@@ -12,7 +12,9 @@
 		updatePipelineValves,
 		getPipelines,
 		getModels,
-		getPipelinesList
+		getPipelinesList,
+		downloadPipeline,
+		deletePipeline
 	} from '$lib/apis';
 
 	import Spinner from '$lib/components/common/Spinner.svelte';
@@ -21,6 +23,8 @@
 
 	export let saveHandler: Function;
 
+	let downloading = false;
+
 	let PIPELINES_LIST = null;
 	let selectedPipelinesUrlIdx = '';
 
@@ -30,6 +34,8 @@
 	let valves_spec = null;
 	let selectedPipelineIdx = null;
 
+	let pipelineDownloadUrl = '';
+
 	const updateHandler = async () => {
 		const pipeline = pipelines[selectedPipelineIdx];
 
@@ -80,6 +86,43 @@
 		}
 	};
 
+	const addPipelineHandler = async () => {
+		downloading = true;
+		const res = await downloadPipeline(
+			localStorage.token,
+			pipelineDownloadUrl,
+			selectedPipelinesUrlIdx
+		).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			toast.success('Pipeline downloaded successfully');
+			setPipelines();
+			models.set(await getModels(localStorage.token));
+		}
+
+		downloading = false;
+	};
+
+	const deletePipelineHandler = async () => {
+		const res = await deletePipeline(
+			localStorage.token,
+			pipelines[selectedPipelineIdx].id,
+			selectedPipelinesUrlIdx
+		).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			toast.success('Pipeline deleted successfully');
+			setPipelines();
+			models.set(await getModels(localStorage.token));
+		}
+	};
+
 	onMount(async () => {
 		PIPELINES_LIST = await getPipelinesList(localStorage.token);
 
@@ -97,7 +140,7 @@
 		updateHandler();
 	}}
 >
-	<div class=" space-y-2 pr-1.5 overflow-y-scroll max-h-80 h-full">
+	<div class="  pr-1.5 overflow-y-scroll max-h-80 h-full">
 		{#if PIPELINES_LIST !== null}
 			<div class="flex w-full justify-between mb-2">
 				<div class=" self-center text-sm font-semibold">
@@ -133,6 +176,75 @@
 				</div>
 			{/if}
 
+			<div class=" my-2">
+				<div class=" mb-2 text-sm font-medium">
+					{$i18n.t('Install from Github URL')}
+				</div>
+				<div class="flex w-full">
+					<div class="flex-1 mr-2">
+						<input
+							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							placeholder={$i18n.t('Enter Github Raw URL')}
+							bind:value={pipelineDownloadUrl}
+						/>
+					</div>
+					<button
+						class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+						on:click={() => {
+							addPipelineHandler();
+						}}
+						disabled={downloading}
+						type="button"
+					>
+						{#if downloading}
+							<div class="self-center">
+								<svg
+									class=" w-4 h-4"
+									viewBox="0 0 24 24"
+									fill="currentColor"
+									xmlns="http://www.w3.org/2000/svg"
+								>
+									<style>
+										.spinner_ajPY {
+											transform-origin: center;
+											animation: spinner_AtaB 0.75s infinite linear;
+										}
+
+										@keyframes spinner_AtaB {
+											100% {
+												transform: rotate(360deg);
+											}
+										}
+									</style>
+									<path
+										d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+										opacity=".25"
+									/>
+									<path
+										d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+										class="spinner_ajPY"
+									/>
+								</svg>
+							</div>
+						{:else}
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 16 16"
+								fill="currentColor"
+								class="w-4 h-4"
+							>
+								<path
+									d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
+								/>
+								<path
+									d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
+								/>
+							</svg>
+						{/if}
+					</button>
+				</div>
+			</div>
+
 			<hr class=" dark:border-gray-800 my-3 w-full" />
 
 			{#if pipelines !== null}
@@ -145,7 +257,7 @@
 					<div class="space-y-1">
 						{#if pipelines.length > 0}
 							<div class="flex gap-2">
-								<div class="flex-1 pb-1">
+								<div class="flex-1">
 									<select
 										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
 										bind:value={selectedPipelineIdx}
@@ -162,6 +274,27 @@
 										{/each}
 									</select>
 								</div>
+
+								<button
+									class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+									on:click={() => {
+										deletePipelineHandler();
+									}}
+									type="button"
+								>
+									<svg
+										xmlns="http://www.w3.org/2000/svg"
+										viewBox="0 0 16 16"
+										fill="currentColor"
+										class="w-4 h-4"
+									>
+										<path
+											fill-rule="evenodd"
+											d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
+											clip-rule="evenodd"
+										/>
+									</svg>
+								</button>
 							</div>
 						{/if}