Spaces:
Runtime error
Runtime error
| <script lang="ts"> | |
| 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 SensitiveInput from "$lib/components/common/SensitiveInput.svelte"; | |
| import XMark from "$lib/components/icons/XMark.svelte"; | |
| import AccessControlModal from "$lib/components/workspace/common/AccessControlModal.svelte"; | |
| import LockClosed from "$lib/components/icons/LockClosed.svelte"; | |
| import Tooltip from "$lib/components/common/Tooltip.svelte"; | |
| import ConfirmDialog from "$lib/components/common/ConfirmDialog.svelte"; | |
| import { | |
| detectTerminalServerType, | |
| verifyTerminalServerConnection, | |
| putOrchestratorPolicy, | |
| } from "$lib/apis/configs"; | |
| import { getTerminalConfig } from "$lib/apis/terminal"; | |
| export let show = false; | |
| export let edit = false; | |
| export let direct = false; | |
| export let connection = null; | |
| export let onSubmit: Function = () => {}; | |
| export let onDelete: () => void = () => {}; | |
| let url = ""; | |
| let key = ""; | |
| let name = ""; | |
| let id = ""; | |
| let auth_type = "bearer"; | |
| let path = "/openapi.json"; | |
| let enabled = false; | |
| let showAdvanced = false; | |
| let showAccessControlModal = false; | |
| let showDeleteConfirmDialog = false; | |
| let accessGrants: any[] = []; | |
| // Policy / auto-detect state | |
| let serverType: "orchestrator" | "terminal" | null = null; | |
| let verifying = false; | |
| let policyId = ""; | |
| let policyImage = ""; | |
| let policyEnvPairs: { key: string; value: string }[] = []; | |
| let policyCpu = "1"; | |
| let policyMemory = "1Gi"; | |
| let policyStorage = "ephemeral"; | |
| let policyStorageSize = "5Gi"; | |
| let policyIdleTimeout = 30; | |
| const init = () => { | |
| if (connection) { | |
| id = connection?.id ?? ""; | |
| url = connection.url; | |
| key = connection?.key ?? ""; | |
| name = connection?.name ?? ""; | |
| auth_type = connection?.auth_type ?? "bearer"; | |
| path = connection?.path ?? "/openapi.json"; | |
| enabled = connection?.enabled ?? true; | |
| accessGrants = connection?.config?.access_grants ?? []; | |
| // Restore policy state | |
| serverType = connection?.server_type ?? null; | |
| policyId = connection?.policy_id ?? ""; | |
| const p = connection?.policy ?? {}; | |
| policyImage = p.image ?? ""; | |
| policyIdleTimeout = p.idle_timeout_minutes ?? 30; | |
| policyStorage = p.storage ? "persistent" : "ephemeral"; | |
| policyStorageSize = p.storage ?? "5Gi"; | |
| // Restore env pairs | |
| const env = p.env ?? {}; | |
| policyEnvPairs = Object.entries(env).map(([k, v]) => ({ | |
| key: k, | |
| value: v as string, | |
| })); | |
| // Restore resources | |
| policyCpu = p.cpu_limit ?? "1"; | |
| policyMemory = p.memory_limit ?? "1Gi"; | |
| } else { | |
| id = ""; | |
| url = ""; | |
| key = ""; | |
| name = ""; | |
| auth_type = "bearer"; | |
| path = "/openapi.json"; | |
| enabled = false; | |
| accessGrants = []; | |
| serverType = null; | |
| policyId = ""; | |
| policyImage = ""; | |
| policyEnvPairs = []; | |
| policyCpu = "1"; | |
| policyMemory = "1Gi"; | |
| policyStorage = "ephemeral"; | |
| policyStorageSize = "5Gi"; | |
| policyIdleTimeout = 30; | |
| } | |
| }; | |
| $: if (show) { | |
| init(); | |
| } | |
| const verifyHandler = async () => { | |
| const _url = url.replace(/\/$/, ""); | |
| if (!_url) { | |
| toast.error($i18n.t("Please enter a valid URL")); | |
| return; | |
| } | |
| verifying = true; | |
| try { | |
| if (!direct) { | |
| // System connection: proxy through backend to avoid CORS / key exposure | |
| const result = await verifyTerminalServerConnection( | |
| localStorage.token, | |
| { | |
| url: _url, | |
| key, | |
| auth_type, | |
| }, | |
| ); | |
| const type = result?.type ?? null; | |
| if (type) { | |
| serverType = type; | |
| toast.success( | |
| $i18n.t("Connected ({{type}})", { | |
| type: | |
| type === "orchestrator" | |
| ? "Orchestrator" | |
| : "Terminal", | |
| }), | |
| ); | |
| // Default policy_id to connection id when orchestrator detected | |
| if (type === "orchestrator" && !policyId) { | |
| policyId = | |
| id || | |
| name | |
| .toLowerCase() | |
| .replace(/[^a-z0-9-]/g, "-") | |
| .replace(/-+/g, "-") | |
| .replace(/^-|-$/g, "") || | |
| "default"; | |
| } | |
| } else { | |
| serverType = null; | |
| toast.error($i18n.t("Server connection failed")); | |
| } | |
| } else { | |
| // Direct connection: verify from browser | |
| const res = await getTerminalConfig(_url, key); | |
| if (res) { | |
| toast.success($i18n.t("Server connection verified")); | |
| } else { | |
| toast.error($i18n.t("Server connection failed")); | |
| } | |
| } | |
| } catch { | |
| serverType = null; | |
| toast.error($i18n.t("Server connection failed")); | |
| } finally { | |
| verifying = false; | |
| } | |
| }; | |
| const buildPolicyData = (): object => { | |
| const data: Record<string, any> = {}; | |
| if (policyImage) data.image = policyImage; | |
| if (policyCpu) data.cpu_limit = policyCpu; | |
| if (policyMemory) data.memory_limit = policyMemory; | |
| if (policyStorage === "persistent") { | |
| data.storage = policyStorageSize; | |
| } | |
| if (policyIdleTimeout > 0) { | |
| data.idle_timeout_minutes = policyIdleTimeout; | |
| } | |
| // Env vars | |
| const env: Record<string, string> = {}; | |
| for (const pair of policyEnvPairs) { | |
| if (pair.key.trim()) { | |
| env[pair.key.trim()] = pair.value; | |
| } | |
| } | |
| if (Object.keys(env).length > 0) { | |
| data.env = env; | |
| } | |
| return data; | |
| }; | |
| const submitHandler = async () => { | |
| if (url === "") { | |
| toast.error($i18n.t("Please enter a valid URL")); | |
| return; | |
| } | |
| // Remove trailing slash | |
| url = url.replace(/\/$/, ""); | |
| // Save policy to orchestrator if applicable | |
| if (serverType === "orchestrator" && !direct && policyId) { | |
| try { | |
| await putOrchestratorPolicy( | |
| localStorage.token, | |
| url, | |
| key, | |
| policyId, | |
| buildPolicyData(), | |
| ); | |
| } catch (err) { | |
| toast.error( | |
| $i18n.t("Failed to save policy: {{error}}", { error: err }), | |
| ); | |
| return; | |
| } | |
| } | |
| const result = { | |
| ...(!direct && id.trim() ? { id: id.trim() } : {}), | |
| url, | |
| key, | |
| name, | |
| path, | |
| auth_type, | |
| enabled: enabled, | |
| config: { | |
| ...(!direct ? { access_grants: accessGrants } : {}), | |
| }, | |
| // Policy fields | |
| ...(serverType ? { server_type: serverType } : {}), | |
| ...(serverType === "orchestrator" && policyId | |
| ? { policy_id: policyId } | |
| : {}), | |
| ...(serverType === "orchestrator" | |
| ? { policy: buildPolicyData() } | |
| : {}), | |
| }; | |
| onSubmit(result); | |
| show = false; | |
| }; | |
| </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 Terminal Connection")} | |
| {:else} | |
| {$i18n.t("Add Terminal Connection")} | |
| {/if} | |
| </h1> | |
| <button | |
| class="self-center" | |
| aria-label={$i18n.t("Close")} | |
| on:click={() => { | |
| show = false; | |
| }} | |
| > | |
| <XMark className={"size-5"} /> | |
| </button> | |
| </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" | |
| > | |
| <form | |
| class="flex flex-col w-full" | |
| on:submit|preventDefault={submitHandler} | |
| > | |
| <div class="px-1"> | |
| <div class="flex gap-2"> | |
| <div class="flex flex-col flex-1"> | |
| <div class="flex justify-between mb-0.5"> | |
| <label | |
| for="terminal-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="terminal-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("My Terminal")} | |
| autocomplete="off" | |
| /> | |
| </div> | |
| </div> | |
| {#if !direct} | |
| <div class="flex flex-col flex-1"> | |
| <div class="flex justify-between mb-0.5"> | |
| <label | |
| for="terminal-id" | |
| class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`} | |
| >{$i18n.t("ID")} | |
| <span class="opacity-50" | |
| >({$i18n.t("optional")})</span | |
| ></label | |
| > | |
| </div> | |
| <div class="flex flex-1 items-center"> | |
| <input | |
| id="terminal-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" | |
| /> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <div class="flex gap-2 mt-2"> | |
| <div class="flex flex-col w-full"> | |
| <div class="flex justify-between mb-0.5"> | |
| <label | |
| for="terminal-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="terminal-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="http://localhost:9900" | |
| required | |
| autocomplete="off" | |
| /> | |
| </div> | |
| </div> | |
| <Tooltip | |
| content={$i18n.t("Verify Connection")} | |
| className="self-end -mb-1" | |
| > | |
| <button | |
| class="self-center p-1 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg transition" | |
| on:click={() => { | |
| verifyHandler(); | |
| }} | |
| type="button" | |
| disabled={verifying} | |
| aria-label={$i18n.t("Verify Connection")} | |
| > | |
| {#if verifying} | |
| <svg | |
| class="w-4 h-4 animate-spin" | |
| xmlns="http://www.w3.org/2000/svg" | |
| fill="none" | |
| viewBox="0 0 24 24" | |
| > | |
| <circle | |
| class="opacity-25" | |
| cx="12" | |
| cy="12" | |
| r="10" | |
| stroke="currentColor" | |
| stroke-width="4" | |
| ></circle> | |
| <path | |
| class="opacity-75" | |
| fill="currentColor" | |
| d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" | |
| ></path> | |
| </svg> | |
| {:else} | |
| <svg | |
| xmlns="http://www.w3.org/2000/svg" | |
| viewBox="0 0 20 20" | |
| fill="currentColor" | |
| aria-hidden="true" | |
| class="w-4 h-4" | |
| > | |
| <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> | |
| {/if} | |
| </button> | |
| </Tooltip> | |
| </div> | |
| <!-- Policy section (orchestrator only, admin only) --> | |
| {#if serverType === "orchestrator" && !direct} | |
| <div class="flex gap-2 mt-2"> | |
| <div class="flex flex-col w-full"> | |
| <div class="flex justify-between mb-0.5"> | |
| <div | |
| class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`} | |
| > | |
| {$i18n.t("Policy ID")} | |
| </div> | |
| </div> | |
| <div class="flex flex-1 items-center"> | |
| <input | |
| id="policy-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={policyId} | |
| placeholder="python-ds" | |
| autocomplete="off" | |
| disabled={edit && | |
| !!connection?.policy_id} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex gap-2 mt-2"> | |
| <div class="flex flex-col w-full"> | |
| <div class="flex justify-between mb-0.5"> | |
| <div | |
| class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`} | |
| > | |
| {$i18n.t("Image")} | |
| <span class="opacity-50" | |
| >({$i18n.t("optional")})</span | |
| > | |
| </div> | |
| </div> | |
| <div class="flex flex-1 items-center"> | |
| <input | |
| id="policy-image" | |
| 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={policyImage} | |
| placeholder="ghcr.io/rexpro-ai/open-terminal:latest" | |
| autocomplete="off" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex gap-2 mt-2"> | |
| <div class="flex flex-col flex-1"> | |
| <div class="flex justify-between mb-0.5"> | |
| <div | |
| class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`} | |
| > | |
| {$i18n.t("CPU")} | |
| </div> | |
| </div> | |
| <div class="flex flex-1 items-center"> | |
| <input | |
| id="policy-cpu" | |
| 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={policyCpu} | |
| placeholder="1" | |
| autocomplete="off" | |
| /> | |
| </div> | |
| </div> | |
| <div class="flex flex-col flex-1"> | |
| <div class="flex justify-between mb-0.5"> | |
| <div | |
| class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`} | |
| > | |
| {$i18n.t("Memory")} | |
| </div> | |
| </div> | |
| <div class="flex flex-1 items-center"> | |
| <input | |
| id="policy-memory" | |
| 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={policyMemory} | |
| placeholder="1Gi" | |
| autocomplete="off" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex gap-2 mt-2"> | |
| <div class="flex flex-col flex-1"> | |
| <div class="flex justify-between mb-0.5"> | |
| <div | |
| class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`} | |
| > | |
| {$i18n.t("Storage")} | |
| </div> | |
| </div> | |
| <div class="flex gap-2"> | |
| <div class="flex-shrink-0 self-start"> | |
| <select | |
| 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={policyStorage} | |
| > | |
| <option value="ephemeral" | |
| >{$i18n.t( | |
| "Ephemeral", | |
| )}</option | |
| > | |
| <option value="persistent" | |
| >{$i18n.t( | |
| "Persistent", | |
| )}</option | |
| > | |
| </select> | |
| </div> | |
| {#if policyStorage === "persistent"} | |
| <div | |
| class="flex flex-1 items-center" | |
| > | |
| <input | |
| id="policy-storage-size" | |
| 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={ | |
| policyStorageSize | |
| } | |
| placeholder="5Gi" | |
| autocomplete="off" | |
| /> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <div class="flex flex-col flex-1"> | |
| <div class="flex justify-between mb-0.5"> | |
| <div | |
| class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`} | |
| > | |
| {$i18n.t("Idle Timeout")} | |
| <span class="opacity-50" | |
| >({$i18n.t("min")})</span | |
| > | |
| </div> | |
| </div> | |
| <div class="flex flex-1 items-center"> | |
| <input | |
| id="idle-timeout" | |
| 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="number" | |
| min="0" | |
| bind:value={policyIdleTimeout} | |
| placeholder="30" | |
| autocomplete="off" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Env Vars --> | |
| <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={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`} | |
| > | |
| {$i18n.t("Environment Variables")} | |
| </div> | |
| <button | |
| type="button" | |
| class="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition" | |
| on:click={() => | |
| (policyEnvPairs = [ | |
| ...policyEnvPairs, | |
| { key: "", value: "" }, | |
| ])} | |
| > | |
| + {$i18n.t("Add")} | |
| </button> | |
| </div> | |
| {#each policyEnvPairs as pair, idx} | |
| <div class="flex gap-1.5 mb-1"> | |
| <input | |
| class={`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={pair.key} | |
| placeholder="KEY" | |
| /> | |
| <input | |
| class={`flex-[2] 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={pair.value} | |
| placeholder="value" | |
| /> | |
| <button | |
| type="button" | |
| class="text-xs text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition px-1" | |
| on:click={() => | |
| (policyEnvPairs = | |
| policyEnvPairs.filter( | |
| (_, i) => i !== idx, | |
| ))} | |
| > | |
| <XMark className={"size-3"} /> | |
| </button> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| <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} | |
| <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 | |
| 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 flex-1 items-center"> | |
| <div | |
| class="flex-1 flex items-center" | |
| > | |
| <label | |
| for="openapi-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="openapi-path" | |
| bind:value={path} | |
| placeholder={$i18n.t( | |
| "openapi.json URL or Path", | |
| )} | |
| autocomplete="off" | |
| required | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <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> | |
| </div> | |
| </div> | |
| {/if} | |
| <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 | |
| class={`text-xs ${($settings?.highContrastMode ?? false) ? "text-gray-800 dark:text-gray-100" : "text-gray-500"}`} | |
| > | |
| {$i18n.t("Auth")} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex gap-2"> | |
| <div class="flex-shrink-0 self-start"> | |
| <select | |
| 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 | |
| > | |
| {#if !direct} | |
| <option value="session" | |
| >{$i18n.t( | |
| "Session", | |
| )}</option | |
| > | |
| <option value="system_oauth" | |
| >{$i18n.t("OAuth")}</option | |
| > | |
| {/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> | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <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 flex-row space-x-1 items-center" | |
| type="submit" | |
| > | |
| {$i18n.t("Save")} | |
| </button> | |
| </div> | |
| </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; | |
| }} | |
| /> | |