This commit is contained in:
Timothy Jaeryang Baek
2026-02-28 02:05:22 -06:00
parent f5bf2a2ed7
commit c71beb0a7d
2 changed files with 236 additions and 223 deletions
+233 -220
View File
@@ -19,7 +19,8 @@
import Tags from './common/Tags.svelte';
import { getToolServerData } from '$lib/apis';
import { verifyToolServerConnection, registerOAuthClient } from '$lib/apis/configs';
import AccessControl from './workspace/common/AccessControl.svelte';
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';
@@ -59,6 +60,7 @@
let enable = true;
let loading = false;
let showAdvanced = false;
let showAccessControlModal = false;
const registerOAuthClientHandler = async () => {
if (url === '') {
@@ -440,30 +442,94 @@
}}
>
<div class="px-1">
{#if !direct}
<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="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="">
<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>
</div>
<div class="">
<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>
</div>
</div>
{/if}
</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">
@@ -521,103 +587,6 @@
</div>
</div>
<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 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(`WebUI will make requests to "{{url}}"`, {
url: path.includes('://')
? path
: `${url}${path.startsWith('/') ? '' : '/'}${path}`
})}
</div>
{/if}
</div>
</div>
{/if}
{/if}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class="flex justify-between items-center">
@@ -725,104 +694,152 @@
</div>
</div>
{#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>
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<div class="flex gap-2">
<div class="flex flex-col w-full">
<label
for="enter-id"
class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>{$i18n.t('ID')}
{#if type !== 'mcp'}
<span class="text-xs text-gray-200 dark:text-gray-800 ml-0.5"
>{$i18n.t('Optional')}</span
>
{/if}
</label>
<div class="flex-1">
<input
id="enter-id"
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={id}
placeholder={$i18n.t('Enter ID')}
autocomplete="off"
required={type === 'mcp'}
/>
</div>
</div>
</div>
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<label
for="enter-name"
class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>{$i18n.t('Name')}
</label>
<div class="flex-1">
<input
id="enter-name"
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={name}
placeholder={$i18n.t('Enter name')}
autocomplete="off"
required
/>
</div>
</div>
</div>
<div class="flex flex-col w-full mt-2">
<label
for="description"
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('Description')}</label
<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' : ''}"
>
<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"
<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(`WebUI will make requests to "{{url}}"`, {
url: path.includes('://')
? path
: `${url}${path.startsWith('/') ? '' : '/'}${path}`
})}
</div>
{/if}
</div>
</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 dark:border-gray-700/10 my-2.5 w-full" />
<div class="flex flex-col w-full mt-2">
<label
@@ -842,12 +859,6 @@
/>
</div>
</div>
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<div class="my-2">
<AccessControl bind:accessGrants />
</div>
{/if}
</div>
@@ -908,3 +919,5 @@
</div>
</div>
</Modal>
<AccessControlModal bind:show={showAccessControlModal} bind:accessGrants />