mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +00:00
Merge aa4e9dc74e into cf6a1300ca
This commit is contained in:
commit
20c3c891be
7 changed files with 364 additions and 4 deletions
|
|
@ -147,6 +147,7 @@ class ToolServerConnection(BaseModel):
|
||||||
headers: Optional[dict | str] = None
|
headers: Optional[dict | str] = None
|
||||||
key: Optional[str]
|
key: Optional[str]
|
||||||
config: Optional[dict]
|
config: Optional[dict]
|
||||||
|
placeholders: Optional[list[str]] = None
|
||||||
|
|
||||||
model_config = ConfigDict(extra="allow")
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,11 @@ from open_webui.utils.misc import (
|
||||||
convert_logit_bias_input_to_json,
|
convert_logit_bias_input_to_json,
|
||||||
get_content_from_message,
|
get_content_from_message,
|
||||||
)
|
)
|
||||||
from open_webui.utils.tools import get_tools, get_updated_tool_function
|
from open_webui.utils.tools import (
|
||||||
|
get_tools,
|
||||||
|
get_updated_tool_function,
|
||||||
|
replace_placeholders_in_headers
|
||||||
|
)
|
||||||
from open_webui.utils.plugin import load_function_module_by_id
|
from open_webui.utils.plugin import load_function_module_by_id
|
||||||
from open_webui.utils.filter import (
|
from open_webui.utils.filter import (
|
||||||
get_sorted_filter_ids,
|
get_sorted_filter_ids,
|
||||||
|
|
@ -1139,10 +1143,16 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error getting OAuth token: {e}")
|
log.error(f"Error getting OAuth token: {e}")
|
||||||
|
|
||||||
|
__user__ = user.model_dump() if isinstance(user, UserModel) else {}
|
||||||
|
if isinstance(user, UserModel) and user.settings:
|
||||||
|
user_settings = user.settings.model_dump() if user.settings else {}
|
||||||
|
if "tool_server_placeholders" in user_settings:
|
||||||
|
__user__["tool_server_placeholders"] = user_settings["tool_server_placeholders"]
|
||||||
|
|
||||||
extra_params = {
|
extra_params = {
|
||||||
"__event_emitter__": event_emitter,
|
"__event_emitter__": event_emitter,
|
||||||
"__event_call__": event_caller,
|
"__event_call__": event_caller,
|
||||||
"__user__": user.model_dump() if isinstance(user, UserModel) else {},
|
"__user__": __user__,
|
||||||
"__metadata__": metadata,
|
"__metadata__": metadata,
|
||||||
"__oauth_token__": oauth_token,
|
"__oauth_token__": oauth_token,
|
||||||
"__request__": request,
|
"__request__": request,
|
||||||
|
|
@ -1401,6 +1411,16 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
|
|
||||||
connection_headers = mcp_server_connection.get("headers", None)
|
connection_headers = mcp_server_connection.get("headers", None)
|
||||||
if connection_headers and isinstance(connection_headers, dict):
|
if connection_headers and isinstance(connection_headers, dict):
|
||||||
|
user_placeholders = (
|
||||||
|
extra_params.get("__user__", {})
|
||||||
|
.get("tool_server_placeholders", {})
|
||||||
|
.get(server_id, {})
|
||||||
|
)
|
||||||
|
|
||||||
|
connection_headers = replace_placeholders_in_headers(
|
||||||
|
connection_headers, user_placeholders
|
||||||
|
)
|
||||||
|
|
||||||
for key, value in connection_headers.items():
|
for key, value in connection_headers.items():
|
||||||
headers[key] = value
|
headers[key] = value
|
||||||
|
|
||||||
|
|
@ -1993,10 +2013,16 @@ async def process_chat_response(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error getting OAuth token: {e}")
|
log.error(f"Error getting OAuth token: {e}")
|
||||||
|
|
||||||
|
__user__ = user.model_dump() if isinstance(user, UserModel) else {}
|
||||||
|
if isinstance(user, UserModel) and user.settings:
|
||||||
|
user_settings = user.settings.model_dump() if user.settings else {}
|
||||||
|
if "tool_server_placeholders" in user_settings:
|
||||||
|
__user__["tool_server_placeholders"] = user_settings["tool_server_placeholders"]
|
||||||
|
|
||||||
extra_params = {
|
extra_params = {
|
||||||
"__event_emitter__": event_emitter,
|
"__event_emitter__": event_emitter,
|
||||||
"__event_call__": event_caller,
|
"__event_call__": event_caller,
|
||||||
"__user__": user.model_dump() if isinstance(user, UserModel) else {},
|
"__user__": __user__,
|
||||||
"__metadata__": metadata,
|
"__metadata__": metadata,
|
||||||
"__oauth_token__": oauth_token,
|
"__oauth_token__": oauth_token,
|
||||||
"__request__": request,
|
"__request__": request,
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,28 @@ log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
|
|
||||||
|
def replace_placeholders_in_headers(
|
||||||
|
headers: dict, placeholders: dict[str, str]
|
||||||
|
) -> dict:
|
||||||
|
if not headers or not placeholders:
|
||||||
|
return headers
|
||||||
|
|
||||||
|
replaced_headers = {}
|
||||||
|
for key, value in headers.items():
|
||||||
|
if isinstance(value, str):
|
||||||
|
replaced_value = value
|
||||||
|
for placeholder_name, placeholder_value in placeholders.items():
|
||||||
|
placeholder_pattern = f"{{{{{placeholder_name}}}}}"
|
||||||
|
replaced_value = replaced_value.replace(
|
||||||
|
placeholder_pattern, placeholder_value
|
||||||
|
)
|
||||||
|
replaced_headers[key] = replaced_value
|
||||||
|
else:
|
||||||
|
replaced_headers[key] = value
|
||||||
|
|
||||||
|
return replaced_headers
|
||||||
|
|
||||||
|
|
||||||
def get_async_tool_function_and_apply_extra_params(
|
def get_async_tool_function_and_apply_extra_params(
|
||||||
function: Callable, extra_params: dict
|
function: Callable, extra_params: dict
|
||||||
) -> Callable[..., Awaitable]:
|
) -> Callable[..., Awaitable]:
|
||||||
|
|
@ -195,6 +217,17 @@ async def get_tools(
|
||||||
|
|
||||||
connection_headers = tool_server_connection.get("headers", None)
|
connection_headers = tool_server_connection.get("headers", None)
|
||||||
if connection_headers and isinstance(connection_headers, dict):
|
if connection_headers and isinstance(connection_headers, dict):
|
||||||
|
user_placeholders = (
|
||||||
|
extra_params.get("__user__", {})
|
||||||
|
.get("tool_server_placeholders", {})
|
||||||
|
.get(server_id, {})
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace placeholders in headers
|
||||||
|
connection_headers = replace_placeholders_in_headers(
|
||||||
|
connection_headers, user_placeholders
|
||||||
|
)
|
||||||
|
|
||||||
for key, value in connection_headers.items():
|
for key, value in connection_headers.items():
|
||||||
headers[key] = value
|
headers[key] = value
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
let auth_type = 'bearer';
|
let auth_type = 'bearer';
|
||||||
let key = '';
|
let key = '';
|
||||||
let headers = '';
|
let headers = '';
|
||||||
|
let placeholders: string[] = [];
|
||||||
|
|
||||||
let functionNameFilterList = '';
|
let functionNameFilterList = '';
|
||||||
let accessControl = {};
|
let accessControl = {};
|
||||||
|
|
@ -197,6 +198,7 @@
|
||||||
if (data.auth_type) auth_type = data.auth_type;
|
if (data.auth_type) auth_type = data.auth_type;
|
||||||
if (data.headers) headers = JSON.stringify(data.headers, null, 2);
|
if (data.headers) headers = JSON.stringify(data.headers, null, 2);
|
||||||
if (data.key) key = data.key;
|
if (data.key) key = data.key;
|
||||||
|
if (data.placeholders) placeholders = data.placeholders;
|
||||||
|
|
||||||
if (data.info) {
|
if (data.info) {
|
||||||
id = data.info.id ?? '';
|
id = data.info.id ?? '';
|
||||||
|
|
@ -231,6 +233,7 @@
|
||||||
auth_type,
|
auth_type,
|
||||||
headers: headers ? JSON.parse(headers) : undefined,
|
headers: headers ? JSON.parse(headers) : undefined,
|
||||||
key,
|
key,
|
||||||
|
placeholders: placeholders.length > 0 ? placeholders : undefined,
|
||||||
|
|
||||||
info: {
|
info: {
|
||||||
id: id,
|
id: id,
|
||||||
|
|
@ -302,6 +305,7 @@
|
||||||
headers: headers ? JSON.parse(headers) : undefined,
|
headers: headers ? JSON.parse(headers) : undefined,
|
||||||
|
|
||||||
key,
|
key,
|
||||||
|
placeholders: placeholders.length > 0 ? placeholders : undefined,
|
||||||
config: {
|
config: {
|
||||||
enable: enable,
|
enable: enable,
|
||||||
function_name_filter_list: functionNameFilterList,
|
function_name_filter_list: functionNameFilterList,
|
||||||
|
|
@ -330,6 +334,7 @@
|
||||||
|
|
||||||
key = '';
|
key = '';
|
||||||
auth_type = 'bearer';
|
auth_type = 'bearer';
|
||||||
|
placeholders = [];
|
||||||
|
|
||||||
id = '';
|
id = '';
|
||||||
name = '';
|
name = '';
|
||||||
|
|
@ -355,6 +360,7 @@
|
||||||
headers = connection?.headers ? JSON.stringify(connection.headers, null, 2) : '';
|
headers = connection?.headers ? JSON.stringify(connection.headers, null, 2) : '';
|
||||||
|
|
||||||
key = connection?.key ?? '';
|
key = connection?.key ?? '';
|
||||||
|
placeholders = connection?.placeholders ?? [];
|
||||||
|
|
||||||
id = connection.info?.id ?? '';
|
id = connection.info?.id ?? '';
|
||||||
name = connection.info?.name ?? '';
|
name = connection.info?.name ?? '';
|
||||||
|
|
@ -726,6 +732,66 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-2">
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class="flex justify-between items-center mb-0.5">
|
||||||
|
<label
|
||||||
|
for="placeholders-input"
|
||||||
|
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{$i18n.t('Placeholders')}
|
||||||
|
<span class="text-xs text-gray-200 dark:text-gray-800 ml-0.5">
|
||||||
|
{$i18n.t('Optional')}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Tooltip content={$i18n.t('Add Placeholder')}>
|
||||||
|
<button
|
||||||
|
class="px-1"
|
||||||
|
on:click={() => {
|
||||||
|
placeholders = [...placeholders, ''];
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Plus />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 mb-2">
|
||||||
|
{$i18n.t(
|
||||||
|
'Define placeholder names that users will need to fill in. Use them in headers as {{PLACEHOLDER_NAME}}'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if placeholders.length > 0}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each placeholders as placeholder, idx}
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
class={`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={placeholders[idx]}
|
||||||
|
placeholder={$i18n.t('e.g., API_KEY or USER_TOKEN')}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
||||||
|
on:click={() => {
|
||||||
|
placeholders = placeholders.filter((_, i) => i !== idx);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
aria-label={$i18n.t('Remove')}
|
||||||
|
>
|
||||||
|
<Minus />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { config, user, tools as _tools, mobile, settings, toolServers } from '$lib/stores';
|
import { config, user, tools as _tools, mobile, settings, toolServers } from '$lib/stores';
|
||||||
|
|
||||||
import { getOAuthClientAuthorizationUrl } from '$lib/apis/configs';
|
import { getOAuthClientAuthorizationUrl, getToolServerConnections } from '$lib/apis/configs';
|
||||||
import { getTools } from '$lib/apis/tools';
|
import { getTools } from '$lib/apis/tools';
|
||||||
|
|
||||||
import Knobs from '$lib/components/icons/Knobs.svelte';
|
import Knobs from '$lib/components/icons/Knobs.svelte';
|
||||||
|
|
@ -21,6 +21,8 @@
|
||||||
import Terminal from '$lib/components/icons/Terminal.svelte';
|
import Terminal from '$lib/components/icons/Terminal.svelte';
|
||||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||||
|
import PencilSolid from '$lib/components/icons/PencilSolid.svelte';
|
||||||
|
import PlaceholderConfigModal from './PlaceholderConfigModal.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -48,6 +50,10 @@
|
||||||
let tab = '';
|
let tab = '';
|
||||||
|
|
||||||
let tools = null;
|
let tools = null;
|
||||||
|
let toolServerPlaceholders = {};
|
||||||
|
|
||||||
|
let showPlaceholderModal = false;
|
||||||
|
let selectedServerForPlaceholder = null;
|
||||||
|
|
||||||
$: if (show) {
|
$: if (show) {
|
||||||
init();
|
init();
|
||||||
|
|
@ -88,6 +94,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getToolServerConnections(localStorage.token);
|
||||||
|
const adminServers = res.TOOL_SERVER_CONNECTIONS || [];
|
||||||
|
|
||||||
|
toolServerPlaceholders = {};
|
||||||
|
for (const server of adminServers) {
|
||||||
|
if (server.placeholders && server.placeholders.length > 0) {
|
||||||
|
const serverId = server.info?.id || server.url;
|
||||||
|
toolServerPlaceholders[serverId] = {
|
||||||
|
name: server.info?.name || server.url,
|
||||||
|
placeholders: server.placeholders,
|
||||||
|
serverId: serverId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch admin tool servers:', err);
|
||||||
|
}
|
||||||
|
|
||||||
selectedToolIds = selectedToolIds.filter((id) => Object.keys(tools).includes(id));
|
selectedToolIds = selectedToolIds.filter((id) => Object.keys(tools).includes(id));
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -392,6 +417,28 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if Object.keys(toolServerPlaceholders).find((serverId) => toolId.includes(serverId))}
|
||||||
|
{@const serverData = toolServerPlaceholders[
|
||||||
|
Object.keys(toolServerPlaceholders).find((serverId) => toolId.includes(serverId))
|
||||||
|
]}
|
||||||
|
<div class=" shrink-0">
|
||||||
|
<Tooltip content={$i18n.t('Configure Placeholders')}>
|
||||||
|
<button
|
||||||
|
class="self-center w-fit text-sm text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition rounded-full"
|
||||||
|
type="button"
|
||||||
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
selectedServerForPlaceholder = serverData;
|
||||||
|
showPlaceholderModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PencilSolid />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class=" shrink-0">
|
<div class=" shrink-0">
|
||||||
<Switch state={tools[toolId].enabled} />
|
<Switch state={tools[toolId].enabled} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -402,3 +449,10 @@
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
|
<PlaceholderConfigModal
|
||||||
|
bind:show={showPlaceholderModal}
|
||||||
|
serverName={selectedServerForPlaceholder?.name || ''}
|
||||||
|
serverId={selectedServerForPlaceholder?.serverId || ''}
|
||||||
|
placeholders={selectedServerForPlaceholder?.placeholders || []}
|
||||||
|
/>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
|
import { updateUserSettings } from '$lib/apis/users';
|
||||||
|
import { settings } from '$lib/stores';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let serverName: string = '';
|
||||||
|
export let serverId: string = '';
|
||||||
|
export let placeholders: string[] = [];
|
||||||
|
|
||||||
|
let placeholderValues: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
$: if (show && placeholders) {
|
||||||
|
// Initialize values from settings
|
||||||
|
const savedValues = $settings?.tool_server_placeholders?.[serverId] || {};
|
||||||
|
placeholderValues = placeholders.reduce((acc, placeholder) => {
|
||||||
|
acc[placeholder] = savedValues[placeholder] || '';
|
||||||
|
return acc;
|
||||||
|
}, {} as { [key: string]: string });
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveHandler = async () => {
|
||||||
|
try {
|
||||||
|
const currentPlaceholders = $settings?.tool_server_placeholders || {};
|
||||||
|
const updatedPlaceholders = {
|
||||||
|
...currentPlaceholders,
|
||||||
|
[serverId]: placeholderValues
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateUserSettings(localStorage.token, {
|
||||||
|
tool_server_placeholders: updatedPlaceholders
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update local settings
|
||||||
|
settings.set({
|
||||||
|
...$settings,
|
||||||
|
tool_server_placeholders: updatedPlaceholders
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success($i18n.t('Placeholder values saved'));
|
||||||
|
show = false;
|
||||||
|
} catch (error) {
|
||||||
|
toast.error($i18n.t('Failed to save placeholder values'));
|
||||||
|
console.error('Error saving placeholder values:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal size="sm" bind:show>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center dark:text-gray-100 px-5 pt-4 pb-2">
|
||||||
|
<h1 class="text-lg font-medium self-center font-primary">
|
||||||
|
{$i18n.t('Configure Placeholders')}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col px-5 pb-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="text-sm font-medium mb-1">{serverName}</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t(
|
||||||
|
'These values will be used to replace placeholders in the server headers. Your values are private and only visible to you.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
on:submit|preventDefault={saveHandler}
|
||||||
|
class="flex flex-col gap-3"
|
||||||
|
>
|
||||||
|
{#each placeholders as placeholder}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
for={`placeholder-${placeholder}`}
|
||||||
|
class="text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
{placeholder}
|
||||||
|
</label>
|
||||||
|
<SensitiveInput
|
||||||
|
id={`placeholder-${placeholder}`}
|
||||||
|
bind:value={placeholderValues[placeholder]}
|
||||||
|
placeholder={$i18n.t(`Enter value for {{placeholder}}`, { placeholder })}
|
||||||
|
required={false}
|
||||||
|
/>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('Used in headers as')} <code class="text-xs">{'{{' + placeholder + '}}'}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 gap-2">
|
||||||
|
<button
|
||||||
|
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Cancel')}
|
||||||
|
</button>
|
||||||
|
<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"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{$i18n.t('Save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let serverName: string = '';
|
||||||
|
export let serverId: string = '';
|
||||||
|
export let placeholders: string[] = [];
|
||||||
|
export let values: { [key: string]: string } = {};
|
||||||
|
export let onChange: (values: { [key: string]: string }) => void = () => {};
|
||||||
|
|
||||||
|
// Initialize values if not set
|
||||||
|
$: if (placeholders && !Object.keys(values).length) {
|
||||||
|
values = placeholders.reduce((acc, placeholder) => {
|
||||||
|
acc[placeholder] = '';
|
||||||
|
return acc;
|
||||||
|
}, {} as { [key: string]: string });
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValueChange = (placeholder: string, value: string) => {
|
||||||
|
values[placeholder] = value;
|
||||||
|
onChange(values);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if placeholders && placeholders.length > 0}
|
||||||
|
<div class="flex flex-col gap-2 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="font-medium text-sm">{serverName}</div>
|
||||||
|
{#if serverId}
|
||||||
|
<div class="text-xs text-gray-500">{serverId}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each placeholders as placeholder}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for={`${serverId}-${placeholder}`} class="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{placeholder}
|
||||||
|
</label>
|
||||||
|
<SensitiveInput
|
||||||
|
id={`${serverId}-${placeholder}`}
|
||||||
|
value={values[placeholder] || ''}
|
||||||
|
on:input={(e) => handleValueChange(placeholder, e.target.value)}
|
||||||
|
placeholder={$i18n.t(`Enter value for {{placeholder}}`, { placeholder })}
|
||||||
|
required={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t(
|
||||||
|
'These values will be used to replace placeholders in the server headers (e.g., {{{{PLACEHOLDER_NAME}}}})'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span class="underline cursor-help">{$i18n.t('What are placeholders?')}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
Loading…
Reference in a new issue