Files
windmill/frontend/src/lib/components/InstanceSetting.svelte
T
Ruben FiszelandClaude Opus 5 a9b0d871a4 surface OTEL env vars in the settings page and startup log (#11074)
* feat: surface OTEL env vars in the settings page and startup log

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbLzVfFDZ9pjBGHSzCSrNZ

* chore: log OTEL compression and per-signal timeouts in the startup config

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbLzVfFDZ9pjBGHSzCSrNZ

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 22:54:54 +02:00

887 lines
29 KiB
Svelte

<script lang="ts">
import { isCloudHosted } from '$lib/cloud'
import { enterpriseLicense, isCriticalAlertsUIOpen } from '$lib/stores'
import { AlertCircle, BadgeCheck, BadgeX, Info } from 'lucide-svelte'
import type { Setting } from './instanceSettings'
import { OTEL_TRACING_PROXY_LANGUAGES } from './instanceSettings'
import { LanguageIcon } from './common/languageIcons'
import ObjectStoreConfigSettings from './ObjectStoreConfigSettings.svelte'
import { sendUserToast } from '$lib/toast'
import { ConfigService, SettingService, type ListAvailablePythonVersionsResponse } from '$lib/gen'
import { Button, SecondsInput, Skeleton } from './common'
import Password from './Password.svelte'
import { classNames } from '$lib/utils'
import Popover from './Popover.svelte'
import DropdownV2 from './DropdownV2.svelte'
import Toggle from './Toggle.svelte'
import type { Writable } from 'svelte/store'
import { createEventDispatcher, untrack } from 'svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import CriticalAlertChannels from './instanceSettings/CriticalAlertChannels.svelte'
import RetentionPeriodOverrides from './instanceSettings/RetentionPeriodOverrides.svelte'
import SmtpSettings from './instanceSettings/SmtpSettings.svelte'
import SecretBackendConfig from './instanceSettings/SecretBackendConfig.svelte'
import GhesAppSettings from './instanceSettings/GhesAppSettings.svelte'
import WebhookBaseUrlSetting from './instanceSettings/WebhookBaseUrlSetting.svelte'
import WsConnectivityTest from './instanceSettings/WsConnectivityTest.svelte'
import InstanceBannerSetting from './instanceSettings/InstanceBannerSetting.svelte'
import IndexerMemorySettings from './instanceSettings/IndexerMemorySettings.svelte'
import IndexerJobIndexSettings from './instanceSettings/IndexerJobIndexSettings.svelte'
import IndexerLogIndexSettings from './instanceSettings/IndexerLogIndexSettings.svelte'
import TextInput from './text_input/TextInput.svelte'
import Description from './Description.svelte'
import SettingCard from './instanceSettings/SettingCard.svelte'
interface Props {
setting: Setting
version: string
values: Writable<Record<string, any>>
loading?: boolean
openSmtpSettings?: () => void
oauths?: Record<string, any>
warning?: string
}
let {
setting,
version,
values,
loading = true,
openSmtpSettings,
oauths,
warning
}: Props = $props()
const dispatch = createEventDispatcher()
let latestKeyRenewalAttempt: {
result: string
attempted_at: string
} | null = $state(null)
let offlineCapStatus: {
seats_used: number
seats_cap: number
author_count: number
operator_count: number
current_cu: number
cu_cap: number
cu_over_cap: boolean
} | null = $state(null)
function showSetting(setting: string, values: Record<string, any>) {
if (setting == 'dev_instance') {
if (values['license_key'] == undefined) {
return false
}
}
// Hide the nsjail-only settings only when isolation is *explicitly* a
// non-nsjail mode. When `job_isolation` is unset, nsjail may still be
// enabled via the legacy env-driven path (`DISABLE_NSJAIL=false`), so
// keep the controls reachable.
if (setting == 'nsjail_tmp_backing' || setting == 'nsjail_tmpfs_size_mb') {
const isolation = values['job_isolation']
if (isolation === 'none' || isolation === 'unshare') {
return false
}
}
// The tmpfs size knob is meaningless when /tmp is disk-backed.
if (setting == 'nsjail_tmpfs_size_mb' && values['nsjail_tmp_backing'] === 'disk') {
return false
}
return true
}
let renewing = $state(false)
let opening = $state(false)
async function reloadKeyrenewalAttemptInfo() {
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
}
async function reloadLicenseStatus() {
try {
offlineCapStatus = (await SettingService.getOfflineLicenseStatus()) as any
} catch {
offlineCapStatus = null
}
}
async function reloadLicenseKey() {
$values['license_key'] = await SettingService.getGlobal({
key: 'license_key'
})
}
$effect(() => {
if (setting.key == 'license_key') {
untrack(() => {
reloadKeyrenewalAttemptInfo()
reloadLicenseStatus()
})
}
})
export async function renewLicenseKey() {
renewing = true
try {
await SettingService.renewLicenseKey({
licenseKey: $values['license_key'] || undefined
})
sendUserToast('Key renewal successful')
reloadLicenseKey()
} catch (err) {
throw err
} finally {
reloadKeyrenewalAttemptInfo()
renewing = false
}
}
export async function openCustomerPortal() {
opening = true
const newWindow = window.open('', '_blank')
try {
const url = await SettingService.createCustomerPortalSession({
licenseKey: $values['license_key'] || undefined
})
if (newWindow) {
newWindow.location.href = url
} else {
window.location.href = url
}
} catch (err) {
newWindow?.close()
throw err
} finally {
opening = false
}
}
function parseLicenseKey(key: string): {
valid: boolean
expiration?: string
} {
let splitted = key.split('.')
if (splitted.length >= 3) {
try {
let i = parseInt(splitted[1])
let date = new Date(i * 1000)
const stringDate = date.toLocaleDateString()
if (stringDate !== 'Invalid Date') {
return {
valid: date.getTime() > Date.now(),
expiration: date.toLocaleDateString()
}
}
} catch {}
}
return {
valid: false
}
}
$effect(() => {
if (setting.key === 'license_key') {
const key = $values['license_key'] ?? ''
const { valid } = parseLicenseKey(key)
if (valid) {
$enterpriseLicense = key.split('.')[0]
}
}
})
let pythonAvailableVersions: ListAvailablePythonVersionsResponse = $state([])
let isPyFetching = $state(false)
async function fetch_available_python_versions() {
if (isPyFetching) return
isPyFetching = true
try {
pythonAvailableVersions = await ConfigService.listAvailablePythonVersions()
} catch (error) {
console.error('Error fetching python versions:', error)
} finally {
isPyFetching = false
}
}
$effect(() => {
if (setting.fieldType == 'select_python') {
untrack(() => fetch_available_python_versions())
}
})
$effect(() => {
if (
(setting.fieldType == 'select' || setting.fieldType == 'select_python') &&
$values[setting.key] == undefined &&
setting.defaultValue
) {
untrack(() => {
if (setting.defaultValue) {
$values[setting.key] = setting.defaultValue()
}
})
}
})
</script>
<!-- {JSON.stringify($values, null, 2)} -->
{#if (!setting.cloudonly || isCloudHosted()) && showSetting(setting.key, $values) && !(setting.hiddenIfNull && $values[setting.key] == null) && !(setting.hiddenIfEmpty && !$values[setting.key]) && !(setting.hiddenInEe && $enterpriseLicense)}
{#if setting.fieldType == 'select'}
<SettingCard
label={setting.label}
description={setting.description}
ee_only={setting.ee_only}
settingKey={setting.key}
>
<ToggleButtonGroup bind:selected={$values[setting.key]}>
{#snippet children({ item: toggleButton })}
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButton}
/>
{/each}
{/snippet}
</ToggleButtonGroup>
</SettingCard>
{:else if setting.fieldType == 'select_python'}
<SettingCard
label={setting.label}
description={setting.description}
ee_only={setting.ee_only}
settingKey={setting.key}
>
<ToggleButtonGroup
bind:selected={() => $values[setting.key] ?? 'default', (v) => ($values[setting.key] = v)}
>
{#snippet children({ item: toggleButton })}
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButton}
/>
{/each}
<DropdownV2
items={() =>
pythonAvailableVersions.map((v) => ({
displayName: v,
action: () => {
$values[setting.key] = v
}
}))}
>
{#snippet buttonReplacement()}
{#if setting.select_items?.some((e) => e.label == $values[setting.key] || e.value == $values[setting.key])}
<Button variant="subtle" btnClasses="font-normal" nonCaptureEvent={true}
>Select Custom</Button
>
{:else}
<Button
variant="default"
btnClasses="font-normal bg-surface-input"
nonCaptureEvent={true}>Custom | {$values[setting.key]}</Button
>
{/if}
{/snippet}
</DropdownV2>
{/snippet}
</ToggleButtonGroup>
</SettingCard>
{:else if setting.fieldType == 'indexer_rates'}
{#if $values[setting.key]}
{@const fieldErrors = setting.validate?.($values[setting.key]) ?? {}}
<SettingCard
label="Memory"
description="Configure the memory budget for the indexer and manage index clearing."
ee_only=""
settingKey="indexer_settings_memory"
>
<div class="p-4 rounded-md border mt-2">
<IndexerMemorySettings {values} disabled={!$enterpriseLicense} errors={fieldErrors} />
</div>
</SettingCard>
<SettingCard
label="Completed Job Index"
description="Configure indexing parameters for completed jobs."
ee_only=""
settingKey="indexer_settings_jobs"
>
<div class="p-4 rounded-md border mt-2">
<IndexerJobIndexSettings {values} disabled={!$enterpriseLicense} errors={fieldErrors} />
</div>
</SettingCard>
<SettingCard
label="Service Logs Index"
description="Configure indexing parameters for service logs."
ee_only=""
settingKey="indexer_settings_logs"
>
<div class="p-4 rounded-md border mt-2">
<IndexerLogIndexSettings {values} disabled={!$enterpriseLicense} errors={fieldErrors} />
</div>
</SettingCard>
{/if}
{:else if setting.fieldType == 'retention_overrides'}
<!-- Discrete inline control (no section header) — sits right under the retention field.
Disabled until `loading` finishes so the editor can't be interacted with before
getInstanceConfig() has populated the persisted overrides (which would let a save drop
them). -->
<RetentionPeriodOverrides {values} disabled={!$enterpriseLicense || loading} />
{:else}
<SettingCard
label={setting.key === 'disable_stats'
? $enterpriseLicense
? 'Minimal telemetry'
: 'Disable telemetry'
: setting.fieldType != 'smtp_connect'
? setting.label
: undefined}
description={setting.key === 'disable_stats'
? $enterpriseLicense
? 'Reduces telemetry to only what is needed for license compliance (no job usage data).'
: 'Disables telemetry entirely.'
: setting.description}
ee_only={setting.ee_only}
tooltip={setting.tooltip}
settingKey={setting.key}
actionButton={setting.actionButton}
values={$values}
>
{#if $values}
{@const hasError = setting.isValid && !setting.isValid($values[setting.key])}
<div class="h-1"></div>
{#if loading}
<Skeleton layout={[[2.5]]} />
{:else if setting.fieldType == 'text'}
<TextInput
inputProps={{
type: 'text',
id: setting.key,
disabled: setting.ee_only != undefined && !$enterpriseLicense,
placeholder: setting.placeholder
}}
bind:value={$values[setting.key]}
class="max-w-lg"
/>
{#if warning}
<span class="text-yellow-600 dark:text-yellow-500 text-2xs">
{warning}
</span>
{/if}
{#if setting.advancedToggle}
<div class="mt-1">
<Toggle
size="xs"
options={{ right: setting.advancedToggle.label }}
checked={setting.advancedToggle.checked($values)}
on:change={() => {
if (setting.advancedToggle) {
$values = setting.advancedToggle.onChange($values)
}
}}
/>
</div>
{/if}
{:else if setting.fieldType == 'textarea'}
<textarea
id={setting.key}
disabled={!$enterpriseLicense}
rows="2"
placeholder={setting.placeholder}
bind:value={$values[setting.key]}
></textarea>
{#if setting.key == 'saml_metadata'}
<div class="flex mt-2">
<Button
disabled={!$enterpriseLicense}
on:click={async (e) => {
try {
const res = await SettingService.testMetadata({
requestBody: $values[setting.key]
})
sendUserToast(`Metadata valid: ${res}`)
} catch (error) {
sendUserToast(`Invalid metadata`, true, error.message)
}
}}>Test content/url</Button
>
</div>
{/if}
{:else if setting.fieldType == 'codearea'}
<SimpleEditor
autoHeight
class="editor"
lang={setting.codeAreaLang ?? 'txt'}
bind:code={$values[setting.key]}
fixedOverflowWidgets={false}
/>
{:else if setting.fieldType == 'license_key'}
{@const { valid, expiration } = parseLicenseKey($values[setting.key] ?? '')}
<div class="flex gap-2">
<Password
id={setting.key}
small
placeholder={setting.placeholder}
onBlur={() => {
if ($values[setting.key] && typeof $values[setting.key] === 'string') {
$values[setting.key] = $values[setting.key].trim()
}
}}
bind:password={$values[setting.key]}
/>
<Button
variant="default"
unifiedSize="md"
disabled={!$values[setting.key]}
on:click={async () => {
await SettingService.testLicenseKey({
requestBody: { license_key: $values[setting.key] }
})
sendUserToast('Valid key')
}}
>
Test key
</Button>
</div>
<div class="mt-1 flex flex-col gap-1 items-start">
{#if $values[setting.key]?.length > 0}
{#if valid}
<div class="flex flex-row gap-1 items-center">
<Info size={12} class="text-primary" />
<span class="text-primary text-xs">License key expires on {expiration ?? ''}</span
>
</div>
{:else if expiration}
<div class="flex flex-row gap-1 items-center">
<AlertCircle size={12} class="text-red-600" />
<span class="text-red-600 dark:text-red-400 text-xs">
{#if $values[setting.key]?.endsWith('__dev')}
Dev license key expired on {expiration}.<br />If even after successful
renewal, your dev license key is still expired, it means that your production
key has expired due to unpaid invoices or excessive use of your production
instance.
{:else}
License key expired on {expiration}.
{/if}
</span>
</div>
{:else}
<div class="flex flex-row gap-1 items-center">
<AlertCircle size={12} class="text-red-600" />
<span class="text-red-600 dark:text-red-400 text-xs"
>Invalid license key format</span
>
</div>
{/if}
{/if}
{#if latestKeyRenewalAttempt && !offlineCapStatus}
{@const attemptedAt = new Date(latestKeyRenewalAttempt.attempted_at).toLocaleString()}
{@const isTrial = latestKeyRenewalAttempt.result.startsWith('error: trial:')}
<div class="relative">
<Popover notClickable>
<div class="flex flex-row items-center gap-1">
{#if latestKeyRenewalAttempt.result === 'success'}
<BadgeCheck class="text-green-600" size={12} />
{:else}
<BadgeX class={isTrial ? 'text-yellow-600' : 'text-red-600'} size={12} />
{/if}
<span
class={classNames(
'text-xs',
latestKeyRenewalAttempt.result === 'success'
? 'text-green-600'
: isTrial
? 'text-yellow-600'
: 'text-red-600'
)}
>
{#if latestKeyRenewalAttempt.result === 'success' && $values[setting.key]?.endsWith('__dev')}
Latest dev key renewal succeeded on {attemptedAt}. The dev key expiry was
updated to align with your current production key's expiration date.
{:else}
{latestKeyRenewalAttempt.result === 'success'
? 'Latest key renewal succeeded'
: isTrial
? 'Latest key renewal ignored because in trial'
: 'Latest key renewal failed'}
on {attemptedAt}
{/if}
</span>
</div>
{#snippet text()}
<div>
{#if latestKeyRenewalAttempt?.result === 'success'}
<span class="text-green-300">
Latest key renewal succeeded on {attemptedAt}
</span>
{:else if isTrial}
<span class="text-yellow-300">
License key cannot be renewed during trial ({attemptedAt})
</span>
{:else}
<span class="text-red-600 dark:text-red-400">
Latest key renewal failed on {attemptedAt}: {latestKeyRenewalAttempt?.result.replace(
'error: ',
''
)}
</span>
{/if}
<br />
As long as invoices are paid and usage corresponds to the subscription, the key
is renewed daily with a validity of 35 days (grace period).
</div>
{/snippet}
</Popover>
</div>
{/if}
{#if $values[setting.key]?.length > 0 && version.includes('CE')}
<div class="flex flex-row gap-1 items-center">
<Info size={12} class="text-blue-600" />
<span class="text-blue-600 dark:text-blue-400 text-xs">
License key is set but the current image is Community Edition ({version}). Switch
to the EE image to finalize the upgrade.
</span>
</div>
{/if}
{#if offlineCapStatus}
{@const cap = offlineCapStatus}
{@const seatsOver = cap.seats_used > cap.seats_cap}
{@const cuOver = cap.cu_over_cap}
<div class="mt-1 flex flex-row items-center gap-2 text-xs">
<div class="flex flex-row items-center gap-1">
{#if seatsOver}
<BadgeX class="text-red-600" size={12} />
{:else}
<BadgeCheck class="text-green-600" size={12} />
{/if}
<span class={seatsOver ? 'text-red-600' : 'text-green-600'}>
Seats: {cap.seats_used.toFixed(1)} / {cap.seats_cap}
</span>
</div>
<div class="flex flex-row items-center gap-1">
{#if cuOver}
<BadgeX class="text-red-600" size={12} />
{:else}
<BadgeCheck class="text-green-600" size={12} />
{/if}
<span class={cuOver ? 'text-red-600' : 'text-green-600'}>
CUs: {cap.current_cu.toFixed(2)} / {cap.cu_cap.toFixed(2)}
</span>
</div>
</div>
{/if}
{#if valid || expiration}
<div class="flex flex-row gap-2 mt-1">
{#if !offlineCapStatus}
<Button on:click={renewLicenseKey} loading={renewing} size="xs" variant="accent"
>Renew key
</Button>
{/if}
<Button variant="accent" size="xs" loading={opening} on:click={openCustomerPortal}>
Open customer portal
</Button>
</div>
{/if}
</div>
{:else if setting.fieldType == 'email'}
<TextInput
inputProps={{
type: 'email',
placeholder: setting.placeholder,
id: setting.key
}}
bind:value={$values[setting.key]}
/>
{:else if setting.key == 'critical_alert_mute_ui'}
<div class="flex flex-col gap-y-2">
<Toggle
disabled={!$enterpriseLicense}
bind:checked={$values[setting.key]}
id={setting.key}
/>
<div class="flex flex-row">
<Button
variant="default"
disabled={!$enterpriseLicense}
size="xs"
on:click={() => {
isCriticalAlertsUIOpen.set(true)
dispatch('closeDrawer')
}}
>
Show critical alerts
</Button>
</div>
</div>
{:else if setting.fieldType == 'critical_error_channels'}
<CriticalAlertChannels {values} {openSmtpSettings} {oauths} />
{:else if setting.fieldType == 'otel'}
<div class="flex flex-col gap-4 p-4 rounded-md border">
{#if $values[setting.key]}
<div class="flex gap-8">
<Toggle
disabled={!$enterpriseLicense}
id="tracing_enabled"
bind:checked={$values[setting.key].tracing_enabled}
options={{ right: 'Tracing' }}
/>
<Toggle
disabled={!$enterpriseLicense}
id="logs_enabled"
bind:checked={$values[setting.key].logs_enabled}
options={{ right: 'Logs' }}
/>
<Toggle
disabled={!$enterpriseLicense}
id="metrics_enabled"
bind:checked={$values[setting.key].metrics_enabled}
options={{ right: 'Metrics' }}
/>
</div>
<div class="flex flex-col gap-1">
<label
for="OTEL_EXPORTER_OTLP_ENDPOINT"
class="block text-xs font-semibold text-emphasis">Endpoint</label
>
<TextInput
inputProps={{
type: 'text',
placeholder: 'http://otel-collector.example.com:4317',
id: 'OTEL_EXPORTER_OTLP_ENDPOINT',
disabled: !$enterpriseLicense
}}
bind:value={$values[setting.key].otel_exporter_otlp_endpoint}
/>
</div>
<div class="flex flex-col gap-1">
<label
for="OTEL_EXPORTER_OTLP_HEADERS"
class="block text-xs font-semibold text-emphasis">Headers</label
>
<TextInput
inputProps={{
type: 'text',
placeholder: 'Authorization=Bearer my-secret-token,Env=production',
id: 'OTEL_EXPORTER_OTLP_HEADERS',
disabled: !$enterpriseLicense
}}
bind:value={$values[setting.key].otel_exporter_otlp_headers}
/>
</div>
<div class="flex flex-col gap-1">
<label
for="OTEL_EXPORTER_OTLP_PROTOCOL"
class="block text-xs font-semibold text-emphasis">Protocol</label
>
<select
id="OTEL_EXPORTER_OTLP_PROTOCOL"
class="!text-xs"
disabled={!$enterpriseLicense}
bind:value={$values[setting.key].otel_exporter_otlp_protocol}
>
<option value={undefined}>grpc (default)</option>
<option value="http/protobuf">http/protobuf</option>
</select>
</div>
<Description
class="text-2xs text-secondary"
link="https://www.windmill.dev/docs/misc/guides/otel#environment-variables"
>
Service name, environment, resource attributes, metrics temporality and other
options are set with environment variables.
</Description>
{/if}
</div>
{:else if setting.fieldType == 'otel_tracing_proxy'}
{@const tracingProxyVal = $values[setting.key] ?? {
enabled: false,
enabled_languages: [...OTEL_TRACING_PROXY_LANGUAGES]
}}
<div class="flex flex-col gap-4">
<Toggle
id="otel_tracing_proxy_enabled"
checked={tracingProxyVal.enabled ?? false}
on:change={(e) => {
$values[setting.key] = { ...tracingProxyVal, enabled: e.detail }
}}
options={{ right: 'Enabled' }}
/>
{#if tracingProxyVal.enabled}
<div class="flex flex-wrap gap-2">
{#each OTEL_TRACING_PROXY_LANGUAGES as lang (lang)}
{@const isEnabled = (tracingProxyVal.enabled_languages ?? []).includes(lang)}
<button
class="flex flex-col items-center gap-1 p-2 rounded border transition-all {isEnabled
? 'border-blue-500 bg-blue-500/10'
: 'border-gray-300 opacity-40 hover:opacity-70'}"
onclick={() => {
const current = tracingProxyVal.enabled_languages ?? []
const newLangs = isEnabled
? current.filter((l) => l !== lang)
: [...current, lang]
$values[setting.key] = { ...tracingProxyVal, enabled_languages: newLangs }
}}
>
<LanguageIcon {lang} size={24} />
</button>
{/each}
</div>
<div class="flex flex-col gap-1">
<label
for="otel_tracing_proxy_no_proxy_hosts"
class="block text-xs font-semibold text-emphasis"
>
NO_PROXY hosts (bypass tracing)
</label>
<TextInput
inputProps={{
type: 'text',
placeholder: '*.eks.amazonaws.com,*.internal',
id: 'otel_tracing_proxy_no_proxy_hosts',
disabled: !$enterpriseLicense
}}
bind:value={$values[setting.key].no_proxy_hosts}
/>
<p class="text-xs text-tertiary">
Comma-separated host patterns that job HTTP clients should bypass the tracing
proxy for — those hosts will not be traced. Use this for clients that pin their
own CA (kubectl, helm, terraform providers, aws cli for EKS, etc.) which would
otherwise fail with <code>x509: certificate signed by unknown authority</code>.
Independent of the worker's own <code>NO_PROXY</code> env, which governs the proxy's
upstream relay (e.g. through a corporate proxy).
</p>
</div>
<div class="flex flex-col gap-1">
<label
for="otel_tracing_proxy_insecure_upstream_hosts"
class="block text-xs font-semibold text-emphasis"
>
Insecure upstream hosts (skip TLS verification)
</label>
<TextInput
inputProps={{
type: 'text',
placeholder: '10.0.0.5,*.internal,git.corp.example',
id: 'otel_tracing_proxy_insecure_upstream_hosts',
disabled: !$enterpriseLicense
}}
bind:value={$values[setting.key].insecure_upstream_hosts}
/>
<p class="text-xs text-tertiary">
Comma-separated host/IP patterns the proxy still traces but for which it skips
upstream TLS certificate verification. Use for internal endpoints with
self-signed or otherwise untrusted certificates — unlike NO_PROXY above, these
requests stay traced. Same matching as NO_PROXY (<code>example.com</code> matches
subdomains; <code>.example.com</code> matches subdomains only).
</p>
</div>
<div class="flex flex-col gap-1">
<label
for="otel_tracing_proxy_upstream_ca_certs"
class="block text-xs font-semibold text-emphasis"
>
Upstream CA certificates (PEM)
</label>
<textarea
id="otel_tracing_proxy_upstream_ca_certs"
disabled={!$enterpriseLicense}
rows="4"
placeholder={'-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----'}
bind:value={$values[setting.key].upstream_ca_certs}
></textarea>
<p class="text-xs text-tertiary">
Extra CA certificates added to the proxy's upstream trust store, on top of the
system roots. Use this to trace internal endpoints signed by a private CA while
keeping certificate verification enabled — preferred over the insecure list above
when you have the CA.
</p>
</div>
{/if}
</div>
{:else if setting.fieldType == 'object_store_config'}
<ObjectStoreConfigSettings bind:bucket_config={$values[setting.key]} />
{:else if setting.fieldType == 'critical_alerts_on_db_oversize'}
{#if $values[setting.key]}
<div class="flex flex-row flex-wrap gap-2 p-0 items-center">
<div class="p-1">
<Toggle
disabled={!$enterpriseLicense}
bind:checked={$values[setting.key].enabled}
id={setting.key}
/>
</div>
{#if $values[setting.key].enabled}
<label class="block shrink min-w-0">
<input
type="number"
placeholder={setting.placeholder}
bind:value={$values[setting.key].value}
/>
</label>
<span class="text-primary font-semibold text-sm">GB</span>
{/if}
</div>
{/if}
{:else if setting.fieldType == 'number'}
<TextInput
inputProps={{
type: 'number',
placeholder: setting.placeholder,
id: setting.key
}}
bind:value={$values[setting.key]}
class="max-w-lg"
/>
{:else if setting.fieldType == 'password'}
<Password small placeholder={setting.placeholder} bind:password={$values[setting.key]} />
{:else if setting.fieldType == 'boolean'}
<Toggle
disabled={setting.ee_only != undefined && !$enterpriseLicense}
bind:checked={$values[setting.key]}
id={setting.key}
/>
{:else if setting.fieldType == 'seconds'}
<div>
<SecondsInput
max={!$enterpriseLicense ? setting.ceMaxSeconds : undefined}
bind:seconds={$values[setting.key]}
clearable
/>
</div>
{:else if setting.fieldType == 'smtp_connect'}
<SmtpSettings {values} disabled={loading} />
{:else if setting.fieldType == 'secret_backend'}
<SecretBackendConfig {values} disabled={loading} />
{:else if setting.fieldType == 'github_enterprise_app'}
<GhesAppSettings {values} disabled={loading || !$enterpriseLicense} />
{:else if setting.fieldType == 'webhook_base_url'}
<WebhookBaseUrlSetting {values} disabled={loading || !$enterpriseLicense} />
{:else if setting.fieldType == 'ws_connectivity'}
<WsConnectivityTest {values} />
{:else if setting.fieldType == 'instance_banner'}
<InstanceBannerSetting {values} disabled={loading} />
{/if}
{#if hasError}
<span class="text-red-600 dark:text-red-400 text-xs">
{setting.error ?? ''}
</span>
{/if}
{:else}
<input disabled placeholder="Loading..." />
{/if}
</SettingCard>
{/if}
{/if}