ai / frontend /src /lib /components /AddToolServerModal.svelte
fdrah
initial-commit
9e93b10
Raw
History Blame Contribute Delete
33.2 kB
<script lang="ts">
import { v4 as uuidv4 } from "uuid";
import fileSaver from "file-saver";
const { saveAs } = fileSaver;
import { toast } from "svelte-sonner";
import { getContext, onMount } from "svelte";
const i18n = getContext("i18n");
import { settings } from "$lib/stores";
import Modal from "$lib/components/common/Modal.svelte";
import Plus from "$lib/components/icons/Plus.svelte";
import Minus from "$lib/components/icons/Minus.svelte";
import PencilSolid from "$lib/components/icons/PencilSolid.svelte";
import SensitiveInput from "$lib/components/common/SensitiveInput.svelte";
import Tooltip from "$lib/components/common/Tooltip.svelte";
import Switch from "$lib/components/common/Switch.svelte";
import Tags from "./common/Tags.svelte";
import { getToolServerData } from "$lib/apis";
import {
verifyToolServerConnection,
registerOAuthClient,
} from "$lib/apis/configs";
import AccessControlModal from "$lib/components/workspace/common/AccessControlModal.svelte";
import LockClosed from "$lib/components/icons/LockClosed.svelte";
import Spinner from "$lib/components/common/Spinner.svelte";
import XMark from "$lib/components/icons/XMark.svelte";
import Textarea from "./common/Textarea.svelte";
import ConfirmDialog from "$lib/components/common/ConfirmDialog.svelte";
export let onSubmit: Function = () => {};
export let onDelete: Function = () => {};
export let show = false;
export let edit = false;
export let direct = false;
export let connection = null;
let inputElement = null;
let type = "openapi"; // 'openapi', 'mcp'
let url = "";
let spec_type = "url"; // 'url', 'json'
let spec = ""; // used when spec_type is 'json'
let path = "openapi.json";
let auth_type = "bearer";
let key = "";
let headers = "";
let functionNameFilterList = "";
let accessGrants = [];
let id = "";
let name = "";
let description = "";
let oauthClientInfo = null;
let oauthClientId = "";
let oauthClientSecret = "";
let oauthServerUrl = "";
let enable = true;
let loading = false;
let showAdvanced = false;
let showAccessControlModal = false;
let showDeleteConfirmDialog = false;
const registerOAuthClientHandler = async () => {
if (url === "") {
toast.error($i18n.t("Please enter a valid URL"));
return;
}
if (id === "") {
toast.error($i18n.t("Please enter a valid ID"));
return;
}
if (
auth_type === "oauth_2.1_static" &&
(!oauthClientId || !oauthClientSecret)
) {
toast.error($i18n.t("Please enter Client ID and Client Secret"));
return;
}
// client_id is the tool server ID (used as the internal lookup key for both flows).
// For static, client_secret signals the backend to use the static credential path.
// The actual OAuth client_id/secret come from the connection info at save time.
const formData: {
url: string;
client_id: string;
client_secret?: string;
oauth_server_url?: string;
} = {
url: url,
client_id: id,
...(auth_type === "oauth_2.1_static"
? {
client_secret: oauthClientSecret,
oauth_server_url: oauthServerUrl,
}
: {}),
};
const res = await registerOAuthClient(
localStorage.token,
formData,
"mcp",
).catch((err) => {
toast.error($i18n.t("Registration failed"));
return null;
});
if (res) {
toast.warning(
$i18n.t(
"Please save the connection to persist the OAuth client information and do not change the ID",
),
);
toast.success($i18n.t("Registration successful"));
console.debug("Registration successful", res);
oauthClientInfo = res?.oauth_client_info ?? null;
}
};
const verifyHandler = async () => {
if (url === "") {
toast.error($i18n.t("Please enter a valid URL"));
return;
}
if (["openapi", ""].includes(type)) {
if (spec_type === "json" && spec === "") {
toast.error($i18n.t("Please enter a valid JSON spec"));
return;
}
if (spec_type === "url" && path === "") {
toast.error($i18n.t("Please enter a valid path"));
return;
}
}
if (headers) {
try {
let _headers = JSON.parse(headers);
if (typeof _headers !== "object" || Array.isArray(_headers)) {
_headers = null;
throw new Error("Headers must be a valid JSON object");
}
headers = JSON.stringify(_headers, null, 2);
} catch (error) {
toast.error($i18n.t("Headers must be a valid JSON object"));
return;
}
}
if (direct) {
const res = await getToolServerData(
auth_type === "bearer" ? key : localStorage.token,
path.includes("://")
? path
: `${url}${path.startsWith("/") ? "" : "/"}${path}`,
).catch((err) => {
toast.error($i18n.t("Connection failed"));
});
if (res) {
toast.success($i18n.t("Connection successful"));
console.debug("Connection successful", res);
}
} else {
const res = await verifyToolServerConnection(localStorage.token, {
url,
path,
type,
auth_type,
headers: headers ? JSON.parse(headers) : undefined,
key,
config: {
enable: enable,
access_grants: accessGrants,
},
info: {
id,
name,
description,
},
}).catch((err) => {
toast.error($i18n.t("Connection failed"));
});
if (res) {
toast.success($i18n.t("Connection successful"));
console.debug("Connection successful", res);
}
}
};
const importHandler = async (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const json = event.target.result;
console.log("importHandler", json);
try {
let data = JSON.parse(json);
// validate data
if (Array.isArray(data)) {
if (data.length === 0) {
toast.error($i18n.t("Please select a valid JSON file"));
return;
}
data = data[0];
}
if (data.type) type = data.type;
if (data.url) url = data.url;
if (data.spec_type) spec_type = data.spec_type;
if (data.spec) spec = data.spec;
if (data.path) path = data.path;
if (data.auth_type) auth_type = data.auth_type;
if (data.headers)
headers = JSON.stringify(data.headers, null, 2);
if (data.key) key = data.key;
if (data.info) {
id = data.info.id ?? "";
name = data.info.name ?? "";
description = data.info.description ?? "";
}
if (data.config) {
enable = data.config.enable ?? true;
accessGrants = data.config.access_grants ?? [];
}
toast.success($i18n.t("Import successful"));
} catch (error) {
toast.error($i18n.t("Please select a valid JSON file"));
}
};
reader.readAsText(file);
};
const exportHandler = async () => {
// export current connection as json file
const json = JSON.stringify([
{
type,
url,
spec_type,
spec,
path,
auth_type,
headers: headers ? JSON.parse(headers) : undefined,
key,
info: {
id: id,
name: name,
description: description,
},
},
]);
const blob = new Blob([json], {
type: "application/json",
});
saveAs(blob, `tool-server-${id || name || "export"}.json`);
};
const submitHandler = async () => {
loading = true;
// remove trailing slash from url for non-MCP connections
// MCP servers may require a trailing slash; stripping it can cause
// 301 redirects that lose auth headers (see #21179)
if (type !== "mcp") {
url = url.replace(/\/$/, "");
}
if (id.includes(":") || id.includes("|")) {
toast.error($i18n.t('ID cannot contain ":" or "|" characters'));
loading = false;
return;
}
if (
type === "mcp" &&
["oauth_2.1", "oauth_2.1_static"].includes(auth_type) &&
!oauthClientInfo
) {
toast.error($i18n.t("Please register the OAuth client"));
loading = false;
return;
}
// validate spec
if (spec_type === "json") {
try {
const specJSON = JSON.parse(spec);
spec = JSON.stringify(specJSON, null, 2);
} catch (e) {
toast.error($i18n.t("Please enter a valid JSON spec"));
loading = false;
return;
}
}
if (headers) {
try {
const _headers = JSON.parse(headers);
if (typeof _headers !== "object" || Array.isArray(_headers)) {
throw new Error("Headers must be a valid JSON object");
}
headers = JSON.stringify(_headers, null, 2);
} catch (error) {
toast.error($i18n.t("Headers must be a valid JSON object"));
loading = false;
return;
}
}
const connection = {
type,
url,
spec_type,
spec,
path,
auth_type,
headers: headers ? JSON.parse(headers) : undefined,
key,
config: {
enable: enable,
function_name_filter_list: functionNameFilterList,
access_grants: accessGrants,
},
info: {
id: id,
name: name,
description: description,
...(oauthClientInfo
? { oauth_client_info: oauthClientInfo }
: {}),
...(auth_type === "oauth_2.1_static"
? {
oauth_client_id: oauthClientId,
oauth_client_secret: oauthClientSecret,
oauth_server_url: oauthServerUrl,
}
: {}),
},
};
await onSubmit(connection);
loading = false;
show = false;
// reset form
type = "openapi";
url = "";
spec_type = "url";
spec = "";
path = "openapi.json";
key = "";
auth_type = "bearer";
id = "";
name = "";
description = "";
oauthClientInfo = null;
oauthClientId = "";
oauthClientSecret = "";
oauthServerUrl = "";
enable = true;
functionNameFilterList = "";
accessGrants = [];
};
const init = () => {
if (connection) {
type = connection?.type ?? "openapi";
url = connection.url;
spec_type = connection?.spec_type ?? "url";
spec = connection?.spec ?? "";
path = connection?.path ?? "openapi.json";
auth_type = connection?.auth_type ?? "bearer";
headers = connection?.headers
? JSON.stringify(connection.headers, null, 2)
: "";
key = connection?.key ?? "";
id = connection.info?.id ?? "";
name = connection.info?.name ?? "";
description = connection.info?.description ?? "";
oauthClientInfo = connection.info?.oauth_client_info ?? null;
oauthClientId = connection.info?.oauth_client_id ?? "";
oauthClientSecret = connection.info?.oauth_client_secret ?? "";
oauthServerUrl = connection.info?.oauth_server_url ?? "";
enable = connection.config?.enable ?? true;
functionNameFilterList =
connection.config?.function_name_filter_list ?? "";
accessGrants = connection.config?.access_grants ?? [];
}
};
$: if (show) {
init();
}
onMount(() => {
init();
});
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
<h1 class=" text-lg font-medium self-center font-primary">
{#if edit}
{$i18n.t("Edit Connection")}
{:else}
{$i18n.t("Add Connection")}
{/if}
</h1>
<div class="flex items-center gap-3">
<div class="flex gap-1.5 text-xs justify-end">
<button
class=" hover:underline"
type="button"
on:click={() => {
inputElement?.click();
}}
>
{$i18n.t("Import")}
</button>
<button
class=" hover:underline"
type="button"
on:click={exportHandler}
>
{$i18n.t("Export")}
</button>
</div>
<button
class="self-center"
aria-label={$i18n.t("Close Configure Connection Modal")}
on:click={() => {
show = false;
}}
>
<XMark className={"size-5"} />
</button>
</div>
</div>
<div
class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200"
>
<div
class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"
>
<input
bind:this={inputElement}
type="file"
hidden
accept=".json"
on:change={(e) => {
importHandler(e);
}}
/>
<form
class="flex flex-col w-full"
on:submit={(e) => {
e.preventDefault();
submitHandler();
}}
>
<div class="px-1">
<div class="flex gap-2 mb-1.5">
<div
class="flex w-full justify-between items-center"
>
<div class=" text-xs text-gray-500">
{$i18n.t("Type")}
</div>
<div class="">
{#if !direct}
<button
on:click={() => {
type = ["", "openapi"].includes(
type,
)
? "mcp"
: "openapi";
}}
type="button"
class=" text-xs text-gray-700 dark:text-gray-300"
>
{#if ["", "openapi"].includes(type)}
{$i18n.t("OpenAPI")}
{:else if type === "mcp"}
{$i18n.t("MCP")}
<span class="text-gray-500"
>{$i18n.t(
"Streamable HTTP",
)}</span
>
{/if}
</button>
{:else}
<div
class="text-xs text-gray-700 dark:text-gray-300"
>
{$i18n.t("OpenAPI")}
</div>
{/if}
</div>
</div>
</div>
<div class="flex gap-2">
<div class="flex flex-col flex-1">
<div class="flex justify-between mb-0.5">
<label
for="enter-name"
class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>{$i18n.t("Name")}</label
>
</div>
<div class="flex flex-1 items-center">
<input
id="enter-name"
class={`w-full flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? "placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"}`}
type="text"
bind:value={name}
placeholder={$i18n.t("Enter name")}
autocomplete="off"
/>
</div>
</div>
{#if !direct}
<div class="flex flex-col flex-1">
<div class="flex justify-between mb-0.5">
<label
for="enter-id"
class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>{$i18n.t("ID")}
{#if type !== "mcp"}<span
class="opacity-50"
>({$i18n.t(
"optional",
)})</span
>{/if}</label
>
</div>
<div class="flex flex-1 items-center">
<input
id="enter-id"
class={`w-full flex-1 text-sm bg-transparent font-mono ${($settings?.highContrastMode ?? false) ? "placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"}`}
type="text"
bind:value={id}
placeholder="auto"
autocomplete="off"
required={type === "mcp"}
/>
</div>
</div>
{/if}
</div>
<div class="flex flex-col w-full mt-1 mb-1.5">
<label
for="description"
class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>{$i18n.t("Description")}</label
>
<div class="flex-1">
<input
id="description"
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? "placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"}`}
type="text"
bind:value={description}
placeholder={$i18n.t("Enter description")}
autocomplete="off"
/>
</div>
</div>
<div class="flex gap-2">
<div class="flex flex-col w-full">
<div class="flex justify-between mb-0.5">
<label
for="api-base-url"
class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>{$i18n.t("URL")}</label
>
</div>
<div class="flex flex-1 items-center">
<input
id="api-base-url"
class={`w-full flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? "placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"}`}
type="text"
bind:value={url}
placeholder={$i18n.t("API Base URL")}
autocomplete="off"
required
/>
<Tooltip
content={$i18n.t("Verify Connection")}
className="shrink-0 flex items-center mr-1"
>
<button
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg transition"
on:click={() => {
verifyHandler();
}}
aria-label={$i18n.t(
"Verify Connection",
)}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
<Tooltip
content={enable
? $i18n.t("Enabled")
: $i18n.t("Disabled")}
>
<Switch bind:state={enable} />
</Tooltip>
</div>
</div>
</div>
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class="flex justify-between items-center">
<div class="flex gap-2 items-center">
<div
for="select-bearer-or-session"
class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>
{$i18n.t("Auth")}
</div>
</div>
{#if ["oauth_2.1", "oauth_2.1_static"].includes(auth_type)}
<div class="flex items-center gap-2">
<div
class="flex flex-col justify-end items-center shrink-0"
>
<Tooltip
content={oauthClientInfo
? $i18n.t(
"Register Again",
)
: $i18n.t(
"Register Client",
)}
>
<button
class=" text-xs underline dark:text-gray-500 dark:hover:text-gray-200 text-gray-700 hover:text-gray-900 transition"
type="button"
on:click={() => {
registerOAuthClientHandler();
}}
>
{$i18n.t(
"Register Client",
)}
</button>
</Tooltip>
</div>
{#if !oauthClientInfo}
<div
class="text-xs font-medium px-1.5 rounded-md bg-yellow-500/20 text-yellow-700 dark:text-yellow-200"
>
{$i18n.t("Not Registered")}
</div>
{:else}
<div
class="text-xs font-medium px-1.5 rounded-md bg-green-500/20 text-green-700 dark:text-green-200"
>
{$i18n.t("Registered")}
</div>
{/if}
</div>
{/if}
</div>
<div class="flex gap-2">
<div class="flex-shrink-0 self-start">
<select
id="select-bearer-or-session"
class={`dark:bg-gray-900 w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? "placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"}`}
bind:value={auth_type}
>
<option value="none"
>{$i18n.t("None")}</option
>
<option value="bearer"
>{$i18n.t("Bearer")}</option
>
<option value="session"
>{$i18n.t("Session")}</option
>
{#if !direct}
<option value="system_oauth"
>{$i18n.t("OAuth")}</option
>
{#if type === "mcp"}
<option value="oauth_2.1"
>{$i18n.t(
"OAuth 2.1",
)}</option
>
<option
value="oauth_2.1_static"
>{$i18n.t(
"OAuth 2.1 (Static)",
)}</option
>
{/if}
{/if}
</select>
</div>
<div class="flex flex-1 items-center">
{#if auth_type === "bearer"}
<SensitiveInput
bind:value={key}
placeholder={$i18n.t("API Key")}
required={false}
/>
{:else if auth_type === "none"}
<div
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>
{$i18n.t("No authentication")}
</div>
{:else if auth_type === "session"}
<div
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>
{$i18n.t(
"Forwards system user session credentials to authenticate",
)}
</div>
{:else if auth_type === "system_oauth"}
<div
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>
{$i18n.t(
"Forwards system user OAuth access token to authenticate",
)}
</div>
{:else if auth_type === "oauth_2.1"}
<div
class={`flex items-center text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>
{$i18n.t(
"Uses OAuth 2.1 Dynamic Client Registration",
)}
</div>
{:else if auth_type === "oauth_2.1_static"}
<div
class="flex flex-col gap-1.5 w-full mt-0.5"
>
<SensitiveInput
bind:value={oauthClientId}
placeholder={$i18n.t(
"Client ID",
)}
required={false}
/>
<SensitiveInput
bind:value={
oauthClientSecret
}
placeholder={$i18n.t(
"Client Secret",
)}
required={false}
/>
<div
class="flex flex-1 items-center"
>
<input
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? "placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"}`}
type="text"
bind:value={
oauthServerUrl
}
placeholder={$i18n.t(
"OAuth Server URL",
)}
autocomplete="off"
/>
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between">
<button
type="button"
class="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition mt-2"
on:click={() => (showAdvanced = !showAdvanced)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-3 h-3 transition-transform {showAdvanced
? 'rotate-90'
: ''}"
>
<path
fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
{$i18n.t("Advanced")}
</button>
{#if !direct}
<button
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 object-cover rounded-full flex gap-1 items-center mt-2"
type="button"
on:click={() => {
showAccessControlModal = true;
}}
>
<LockClosed
strokeWidth="2.5"
className="size-3.5 shrink-0"
/>
<div class="text-xs font-medium shrink-0">
{$i18n.t("Access")}
</div>
</button>
{/if}
</div>
{#if showAdvanced}
{#if ["", "openapi"].includes(type)}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div
class="flex justify-between items-center mb-0.5"
>
<div
class="flex gap-2 items-center"
>
<div
for="select-bearer-or-session"
class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>
{$i18n.t("OpenAPI Spec")}
</div>
</div>
</div>
<div class="flex gap-2">
<div
class="flex-shrink-0 self-start"
>
<select
id="select-bearer-or-session"
class={`dark:bg-gray-900 w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? "placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"}`}
bind:value={spec_type}
>
<option value="url"
>{$i18n.t(
"URL",
)}</option
>
<option value="json"
>{$i18n.t(
"JSON",
)}</option
>
</select>
</div>
<div
class="flex flex-1 items-center"
>
{#if spec_type === "url"}
<div
class="flex-1 flex items-center"
>
<label
for="url-or-path"
class="sr-only"
>{$i18n.t(
"openapi.json URL or Path",
)}</label
>
<input
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? "placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"}`}
type="text"
id="url-or-path"
bind:value={path}
placeholder={$i18n.t(
"openapi.json URL or Path",
)}
autocomplete="off"
required
/>
</div>
{:else if spec_type === "json"}
<div
class={`text-xs w-full self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>
<label
for="url-or-path"
class="sr-only"
>{$i18n.t(
"JSON Spec",
)}</label
>
<textarea
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? "placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-black dark:text-white"}`}
bind:value={spec}
placeholder={$i18n.t(
"JSON Spec",
)}
autocomplete="off"
required
rows="5"
/>
</div>
{/if}
</div>
</div>
{#if ["", "url"].includes(spec_type)}
<div
class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`}
>
{$i18n.t(
`rexpro will make requests to "{{url}}"`,
{
url: path.includes(
"://",
)
? path
: `${url}${path.startsWith("/") ? "" : "/"}${path}`,
},
)}
</div>
{/if}
</div>
</div>
{/if}
{#if !direct}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<label
for="headers-input"
class={`mb-0.5 text-xs text-gray-500
${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : ""}`}
>{$i18n.t("Headers")}</label
>
<div class="flex-1">
<Tooltip
content={$i18n.t(
'Enter additional headers in JSON format (e.g. {"X-Custom-Header": "value"}',
)}
>
<Textarea
className="w-full text-sm outline-hidden"
bind:value={headers}
placeholder={$i18n.t(
"Enter additional headers in JSON format",
)}
required={false}
minSize={30}
/>
</Tooltip>
</div>
</div>
</div>
{/if}
{/if}
{#if !direct}
<hr
class=" border-gray-100/50 dark:border-gray-700/10 my-2.5 w-full"
/>
<div class="flex flex-col w-full mt-2">
<label
for="function-name-filter-list"
class={`mb-1 text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100 placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-gray-500"}`}
>{$i18n.t(
"Function Name Filter List",
)}</label
>
<div class="flex-1">
<input
id="function-name-filter-list"
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? "placeholder:text-gray-700 dark:placeholder:text-gray-100" : "outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"}`}
type="text"
bind:value={functionNameFilterList}
placeholder={$i18n.t(
"Enter function name filter list (e.g. func1, !func2)",
)}
autocomplete="off"
/>
</div>
</div>
{/if}
</div>
{#if type === "mcp"}
<div
class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-2xl text-xs px-4 py-3 mb-2 mt-1"
>
<span class="font-medium">
{$i18n.t("Warning")}:
</span>
{$i18n.t(
"MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the rexpro-ai team, making it the more reliable option for compatibility.",
)}
<a
class="font-medium underline"
href="https://docs.openrexpro.com/"
target="_blank">{$i18n.t("Read more →")}</a
>
</div>
{/if}
<div
class="flex justify-between items-center pt-3 text-sm font-medium"
>
<div>
{#if edit}
<button
class="px-1 py-1.5 text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:underline transition"
type="button"
on:click={() => {
showDeleteConfirmDialog = true;
}}
>
{$i18n.t("Delete")}
</button>
{/if}
</div>
<button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex items-center gap-2 whitespace-nowrap {loading
? ' cursor-not-allowed'
: ''}"
type="submit"
disabled={loading}
>
{$i18n.t("Save")}
{#if loading}
<span class="shrink-0">
<Spinner />
</span>
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
<AccessControlModal bind:show={showAccessControlModal} bind:accessGrants />
<ConfirmDialog
bind:show={showDeleteConfirmDialog}
message={$i18n.t(
"Are you sure you want to delete this connection? This action cannot be undone.",
)}
confirmLabel={$i18n.t("Delete")}
on:confirm={() => {
onDelete();
show = false;
}}
/>