mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 08:01:25 +00:00
31bfccc745
* clean layout * remove useless toggle * improve layout * nit * Fix input type seconds * convert secondInputs to svelte 5 * redesign seconds input * Adapt layout where time input is used * nit
947 lines
31 KiB
Svelte
947 lines
31 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 Tooltip from './Tooltip.svelte'
|
|
import ObjectStoreConfigSettings from './ObjectStoreConfigSettings.svelte'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
|
|
import {
|
|
ConfigService,
|
|
IndexSearchService,
|
|
SettingService,
|
|
type ListAvailablePythonVersionsResponse
|
|
} from '$lib/gen'
|
|
import { Button, SecondsInput, Section, Skeleton } from './common'
|
|
import Password from './Password.svelte'
|
|
import { classNames } from '$lib/utils'
|
|
import Popover from './Popover.svelte'
|
|
import PopoverMelt from './meltComponents/Popover.svelte'
|
|
import Toggle from './Toggle.svelte'
|
|
import type { Writable } from 'svelte/store'
|
|
import { createEventDispatcher } from 'svelte'
|
|
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
|
import SimpleEditor from './SimpleEditor.svelte'
|
|
import LoadingIcon from './apps/svelte-select/lib/LoadingIcon.svelte'
|
|
import EEOnly from './EEOnly.svelte'
|
|
import CriticalAlertChannels from './instanceSettings/CriticalAlertChannels.svelte'
|
|
import SmtpSettings from './instanceSettings/SmtpSettings.svelte'
|
|
import SecretBackendConfig from './instanceSettings/SecretBackendConfig.svelte'
|
|
import TextInput from './text_input/TextInput.svelte'
|
|
import Label from './Label.svelte'
|
|
|
|
interface Props {
|
|
setting: Setting
|
|
version: string
|
|
values: Writable<Record<string, any>>
|
|
loading?: boolean
|
|
openSmtpSettings?: () => void
|
|
oauths?: Record<string, any>
|
|
}
|
|
|
|
let { setting, version, values, loading = true, openSmtpSettings, oauths }: Props = $props()
|
|
const dispatch = createEventDispatcher()
|
|
|
|
if (
|
|
(setting.fieldType == 'select' || setting.fieldType == 'select_python') &&
|
|
$values[setting.key] == undefined
|
|
) {
|
|
$values[setting.key] = 'default'
|
|
}
|
|
|
|
let latestKeyRenewalAttempt: {
|
|
result: string
|
|
attempted_at: string
|
|
} | null = $state(null)
|
|
|
|
function showSetting(setting: string, values: Record<string, any>) {
|
|
if (setting == 'dev_instance') {
|
|
if (values['license_key'] == undefined) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
let licenseKeyChanged = $state(false)
|
|
let renewing = $state(false)
|
|
let opening = $state(false)
|
|
|
|
async function reloadKeyrenewalAttemptInfo() {
|
|
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
|
|
}
|
|
|
|
async function reloadLicenseKey() {
|
|
$values['license_key'] = await SettingService.getGlobal({
|
|
key: 'license_key'
|
|
})
|
|
}
|
|
|
|
if (setting.key == 'license_key') {
|
|
reloadKeyrenewalAttemptInfo()
|
|
}
|
|
|
|
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
|
|
try {
|
|
const url = await SettingService.createCustomerPortalSession({
|
|
licenseKey: $values['license_key'] || undefined
|
|
})
|
|
window.open(url, '_blank')
|
|
} 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
|
|
}
|
|
}
|
|
|
|
let pythonAvailableVersions: ListAvailablePythonVersionsResponse = $state([])
|
|
|
|
let isPyFetching = $state(false)
|
|
let clearJobsIndexModalOpen = $state(false)
|
|
let clearServiceLogsIndexModalOpen = $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
|
|
}
|
|
}
|
|
if (setting.fieldType == 'select_python') {
|
|
fetch_available_python_versions()
|
|
}
|
|
</script>
|
|
|
|
{#snippet LabelSnippet()}
|
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
<label class="flex flex-col gap-1 mb-1">
|
|
<div class="flex gap-1">
|
|
<span class="text-emphasis font-semibold text-xs">{setting.label}</span>
|
|
{#if setting.ee_only != undefined && !$enterpriseLicense}
|
|
<EEOnly>
|
|
{#if setting.ee_only != ''}{setting.ee_only}{/if}
|
|
</EEOnly>
|
|
{/if}
|
|
</div>
|
|
{#if setting.description}
|
|
<span class="text-secondary text-xs font-normal">
|
|
{@html setting.description}
|
|
</span>
|
|
{/if}
|
|
</label>
|
|
{/snippet}
|
|
|
|
<!-- {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'}
|
|
<div>
|
|
{@render LabelSnippet()}
|
|
<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>
|
|
</div>
|
|
{:else if setting.fieldType == 'select_python'}
|
|
<div>
|
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
{@render LabelSnippet()}
|
|
|
|
<ToggleButtonGroup bind:selected={$values[setting.key]}>
|
|
{#snippet children({ item: toggleButtonn })}
|
|
{#each setting.select_items ?? [] as item}
|
|
<ToggleButton
|
|
value={item.value ?? item.label}
|
|
label={item.label}
|
|
tooltip={item.tooltip}
|
|
item={toggleButtonn}
|
|
/>
|
|
{/each}
|
|
<PopoverMelt closeButton={!isPyFetching} contentClasses="max-w-md">
|
|
{#snippet trigger()}
|
|
{#if setting.select_items?.some((e) => e.label == $values[setting.key] || e.value == $values[setting.key])}
|
|
<Button
|
|
variant="default"
|
|
btnClasses="px-1.5 py-1.5 text-2xs bg-surface-secondary border-0"
|
|
nonCaptureEvent={true}>Select Custom</Button
|
|
>
|
|
{:else}
|
|
<Button
|
|
variant="default"
|
|
btnClasses="px-1.5 py-1.5 text-2xs border-0 shadow-md"
|
|
nonCaptureEvent={true}>Custom | {$values[setting.key]}</Button
|
|
>
|
|
{/if}
|
|
{/snippet}
|
|
{#snippet content()}
|
|
{#if isPyFetching}
|
|
<div class="p-4">
|
|
<LoadingIcon />
|
|
</div>
|
|
{:else}
|
|
<ToggleButtonGroup
|
|
bind:selected={$values[setting.key]}
|
|
class="mr-10 h-full"
|
|
tabListClass="flex-wrap p-2"
|
|
>
|
|
{#snippet children({ item: toggleButtonn })}
|
|
{#each pythonAvailableVersions as item}
|
|
<ToggleButton value={item} label={item} tooltip={item} item={toggleButtonn} />
|
|
{/each}
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
{/if}
|
|
{/snippet}
|
|
</PopoverMelt>
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</div>
|
|
{:else}
|
|
{#snippet settingContent()}
|
|
<div class="flex flex-col gap-2 mb-1">
|
|
<div class="flex items-center justify-between">
|
|
<div class="text-emphasis font-semibold text-xs flex flex-col gap-1 w-full">
|
|
<div class="flex items-center justify-between gap-2 w-full">
|
|
{#if setting.fieldType != 'smtp_connect'}
|
|
<div class="flex gap-1 items-baseline">
|
|
<span class="text-emphasis font-semibold text-xs pb-1">{setting.label}</span>
|
|
{#if setting.ee_only != undefined && !$enterpriseLicense}
|
|
{#if setting.ee_only != ''}
|
|
<EEOnly>{setting.ee_only}</EEOnly>
|
|
{:else}
|
|
<EEOnly />
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{#if setting.actionButton}
|
|
<Button
|
|
disabled={setting.ee_only != undefined && !$enterpriseLicense}
|
|
variant={setting.actionButton.variant ?? 'default'}
|
|
unifiedSize="sm"
|
|
onclick={async () => await setting.actionButton?.onclick($values)}
|
|
>
|
|
{setting.actionButton.label}
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
{#if setting.description}
|
|
<span class="text-secondary font-normal text-xs">
|
|
{@html setting.description}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{#if setting.tooltip}
|
|
<Tooltip>{setting.tooltip}</Tooltip>
|
|
{/if}
|
|
{#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'}
|
|
<input
|
|
id={setting.key}
|
|
disabled={setting.ee_only != undefined && !$enterpriseLicense}
|
|
type="text"
|
|
placeholder={setting.placeholder}
|
|
class={hasError
|
|
? 'border !border-red-700 !border-opacity-30 !focus:border-red-700 !focus:border-opacity-30'
|
|
: ''}
|
|
bind:value={$values[setting.key]}
|
|
/>
|
|
{#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}
|
|
onKeyDown={() => {
|
|
licenseKeyChanged = true
|
|
}}
|
|
onBlur={() => {
|
|
if ($values[setting.key] && typeof $values[setting.key] === 'string') {
|
|
$values[setting.key] = $values[setting.key].trim()
|
|
}
|
|
}}
|
|
bind:password={$values[setting.key]}
|
|
/>
|
|
<Button
|
|
variant="accent"
|
|
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}
|
|
{@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-300">
|
|
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 licenseKeyChanged && !$enterpriseLicense}
|
|
{#if version.startsWith('CE')}
|
|
<div class="text-red-400"
|
|
>License key is set but image used is the Community Edition {version}. Switch
|
|
image to EE.</div
|
|
>
|
|
{/if}
|
|
{/if}
|
|
|
|
{#if valid || expiration}
|
|
<div class="flex flex-row gap-2 mt-1">
|
|
<Button on:click={renewLicenseKey} loading={renewing} size="xs" variant="accent"
|
|
>Renew key
|
|
</Button>
|
|
<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 == 'indexer_rates'}
|
|
<div class="flex flex-col gap-16 mt-4">
|
|
{#if $values[setting.key]}
|
|
<Section label="Memory" class="space-y-6">
|
|
<div class="flex flex-col gap-1">
|
|
<label
|
|
for="writer_memory_budget"
|
|
class="block text-xs font-semibold text-emphasis"
|
|
>
|
|
Index writer memory budget (MB)
|
|
<Tooltip>
|
|
The allocated memory arena for the indexer. A bigger value means less writing
|
|
to disk and potentially higher indexing throughput
|
|
</Tooltip>
|
|
</label>
|
|
<TextInput
|
|
inputProps={{
|
|
type: 'number',
|
|
placeholder: '300',
|
|
id: 'writer_memory_budget',
|
|
disabled: !$enterpriseLicense,
|
|
oninput: (e) => {
|
|
if (e.target instanceof HTMLInputElement) {
|
|
if (e.target.valueAsNumber) {
|
|
$values[setting.key].writer_memory_budget =
|
|
e.target.valueAsNumber * (1024 * 1024)
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
value={$values[setting.key].writer_memory_budget / (1024 * 1024)}
|
|
/>
|
|
</div>
|
|
<Label label="Clear index">
|
|
<span class="text-xs text-secondary"
|
|
>This buttons will clear the whole index, and the service will start reindexing
|
|
from scratch. Full text search might be down during this time.</span
|
|
>
|
|
<div class="flex flex-row gap-2">
|
|
<Button
|
|
variant="default"
|
|
unifiedSize="sm"
|
|
on:click={() => {
|
|
clearJobsIndexModalOpen = true
|
|
}}
|
|
>
|
|
Clear jobs index
|
|
</Button>
|
|
<Button
|
|
variant="default"
|
|
unifiedSize="sm"
|
|
on:click={() => {
|
|
clearServiceLogsIndexModalOpen = true
|
|
}}
|
|
>
|
|
Clear service logs index
|
|
</Button>
|
|
</div>
|
|
</Label>
|
|
<ConfirmationModal
|
|
title="Clear jobs index"
|
|
confirmationText="Clear"
|
|
open={clearJobsIndexModalOpen}
|
|
type="danger"
|
|
on:canceled={() => {
|
|
clearJobsIndexModalOpen = false
|
|
}}
|
|
on:confirmed={async () => {
|
|
const r = await IndexSearchService.clearIndex({
|
|
idxName: 'JobIndex'
|
|
})
|
|
sendUserToast(r)
|
|
clearJobsIndexModalOpen = false
|
|
}}
|
|
>
|
|
Are you sure you want to clear the jobs index? The service will start reindexing
|
|
from scratch. Full text search might be down during this time.
|
|
</ConfirmationModal>
|
|
<ConfirmationModal
|
|
title="Clear service logs index"
|
|
confirmationText="Clear"
|
|
open={clearServiceLogsIndexModalOpen}
|
|
type="danger"
|
|
on:canceled={() => {
|
|
clearServiceLogsIndexModalOpen = false
|
|
}}
|
|
on:confirmed={async () => {
|
|
const r = await IndexSearchService.clearIndex({
|
|
idxName: 'ServiceLogIndex'
|
|
})
|
|
sendUserToast(r)
|
|
clearServiceLogsIndexModalOpen = false
|
|
}}
|
|
>
|
|
Are you sure you want to clear the service logs index? The service will start
|
|
reindexing from scratch. Full text search might be down during this time.
|
|
</ConfirmationModal>
|
|
</Section>
|
|
<hr class="border-t -my-6" />
|
|
<Section label="Completed Job Index" class="space-y-6">
|
|
<div class="flex flex-col gap-1">
|
|
<label
|
|
for="commit_job_max_batch_size"
|
|
class="block text-xs font-semibold text-emphasis"
|
|
>
|
|
Commit max batch size <Tooltip>
|
|
The max amount of documents (here jobs) per commit. To optimize indexing
|
|
throughput, it is best to keep this as high as possible. However, especially
|
|
when reindexing the whole instance, it can be useful to have a limit on how
|
|
many jobs can be written without being committed. A commit will make the jobs
|
|
available for search, constitute a "checkpoint" state in the indexing and will
|
|
be logged.
|
|
</Tooltip>
|
|
</label>
|
|
<TextInput
|
|
inputProps={{
|
|
type: 'number',
|
|
placeholder: '100000',
|
|
id: 'commit_job_max_batch_size',
|
|
disabled: !$enterpriseLicense
|
|
}}
|
|
bind:value={$values[setting.key].commit_job_max_batch_size}
|
|
/>
|
|
</div>
|
|
<div class="flex flex-col gap-1">
|
|
<label
|
|
for="refresh_index_period"
|
|
class="block text-xs font-semibold text-emphasis"
|
|
>
|
|
Refresh index period (s) <Tooltip>
|
|
The index will query new jobs periodically and write them on the index. This
|
|
setting sets that period.
|
|
</Tooltip></label
|
|
>
|
|
<TextInput
|
|
inputProps={{
|
|
type: 'number',
|
|
placeholder: '300',
|
|
id: 'refresh_index_period',
|
|
disabled: !$enterpriseLicense
|
|
}}
|
|
bind:value={$values[setting.key].refresh_index_period}
|
|
/>
|
|
</div>
|
|
<div class="flex flex-col gap-1">
|
|
<label
|
|
for="max_indexed_job_log_size"
|
|
class="block text-xs font-semibold text-emphasis"
|
|
>
|
|
Max indexed job log size (KB) <Tooltip>
|
|
Job logs are included when indexing, but to avoid the index size growing
|
|
artificially, the logs will be truncated after a size has been reached.
|
|
</Tooltip>
|
|
</label>
|
|
<TextInput
|
|
inputProps={{
|
|
type: 'number',
|
|
placeholder: '1024',
|
|
id: 'max_indexed_job_log_size',
|
|
disabled: !$enterpriseLicense,
|
|
oninput: (e) => {
|
|
if (e.target instanceof HTMLInputElement) {
|
|
if (e.target.valueAsNumber) {
|
|
$values[setting.key].max_indexed_job_log_size =
|
|
e.target.valueAsNumber * 1024
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
value={$values[setting.key].max_indexed_job_log_size / 1024}
|
|
/>
|
|
</div>
|
|
</Section>
|
|
<hr class="border-t -my-6" />
|
|
<Section label="Service logs index" class="space-y-6">
|
|
<div class="flex flex-col gap-1">
|
|
<label
|
|
for="commit_log_max_batch_size"
|
|
class="block text-xs font-semibold text-emphasis"
|
|
>Commit max batch size <Tooltip>
|
|
The max amount of documents per commit. In this case 1 document is one log
|
|
file representing all logs during 1 minute for a specific host. To optimize
|
|
indexing throughput, it is best to keep this as high as possible. However,
|
|
especially when reindexing the whole instance, it can be useful to have a
|
|
limit on how many logs can be written without being committed. A commit will
|
|
make the logs available for search, appear as a log line, and be a
|
|
"checkpoint" of the indexing progress.
|
|
</Tooltip>
|
|
</label>
|
|
<input
|
|
disabled={!$enterpriseLicense}
|
|
type="number"
|
|
id="commit_log_max_batch_size"
|
|
placeholder="10000"
|
|
bind:value={$values[setting.key].commit_log_max_batch_size}
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-1">
|
|
<label
|
|
for="refresh_log_index_period"
|
|
class="block text-xs font-semibold text-emphasis"
|
|
>
|
|
Refresh index period (s) <Tooltip>
|
|
The index will query new service logs peridically and write them on the index.
|
|
This setting sets that period.
|
|
</Tooltip>
|
|
</label>
|
|
<TextInput
|
|
inputProps={{
|
|
type: 'number',
|
|
placeholder: '300',
|
|
id: 'refresh_log_index_period',
|
|
disabled: !$enterpriseLicense
|
|
}}
|
|
bind:value={$values[setting.key].refresh_log_index_period}
|
|
/>
|
|
</div>
|
|
</Section>
|
|
{/if}
|
|
</div>
|
|
{:else if setting.fieldType == 'otel'}
|
|
<div class="flex flex-col gap-4 border rounded p-4">
|
|
{#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
|
|
id="metrics_enabled"
|
|
bind:checked={$values[setting.key].logs_enabled}
|
|
options={{ right: 'Metrics (coming soon)' }}
|
|
/>
|
|
</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
|
|
>
|
|
<span class="text-primary font-normal text-xs">gRPC</span>
|
|
</div>
|
|
<!-- <div>
|
|
<label for="OTEL_EXPORTER_OTLP_PROTOCOL" class="block text-sm font-semibold"
|
|
>Protocol<span class="text-2xs text-primary ml-4"
|
|
>grpc, http/protobuf, http/json</span
|
|
></label
|
|
>
|
|
<input
|
|
type="text"
|
|
id="OTEL_EXPORTER_OTLP_PROTOCOL"
|
|
placeholder="grpc"
|
|
bind:value={$values[setting.key].otel_exporter_otlp_protocol}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label for="OTEL_EXPORTER_OTLP_COMPRESSION" class="block text-sm font-semibold"
|
|
>Compression <span class="text-2xs text-primary ml-4">none, gzip</span></label
|
|
>
|
|
<input
|
|
type="text"
|
|
id="OTEL_EXPORTER_OTLP_COMPRESSION"
|
|
placeholder="none"
|
|
bind:value={$values[setting.key].otel_exporter_otlp_compression}
|
|
/>
|
|
</div> -->
|
|
{/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>
|
|
{/if}
|
|
</div>
|
|
{:else if setting.fieldType == 'object_store_config'}
|
|
<ObjectStoreConfigSettings bind:bucket_config={$values[setting.key]} />
|
|
<div class="mb-6"></div>
|
|
{: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>
|
|
<div class="mb-6"></div>
|
|
{/if}
|
|
{:else if setting.fieldType == 'number'}
|
|
<TextInput
|
|
inputProps={{
|
|
type: 'number',
|
|
placeholder: setting.placeholder,
|
|
id: setting.key
|
|
}}
|
|
bind:value={$values[setting.key]}
|
|
/>
|
|
{: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={setting.ee_only != undefined && !$enterpriseLicense
|
|
? 60 * 60 * 24 * 30
|
|
: undefined}
|
|
bind:seconds={$values[setting.key]}
|
|
clearable
|
|
/>
|
|
</div>
|
|
{:else if setting.fieldType == 'select'}
|
|
TODO
|
|
{:else if setting.fieldType == 'smtp_connect'}
|
|
<SmtpSettings {values} disabled={loading} />
|
|
{:else if setting.fieldType == 'secret_backend'}
|
|
<SecretBackendConfig {values} disabled={loading} />
|
|
{/if}
|
|
{#if hasError}
|
|
<span class="text-red-500 dark:text-red-400 text-sm">
|
|
{setting.error ?? ''}
|
|
</span>
|
|
{/if}
|
|
{:else}
|
|
<input disabled placeholder="Loading..." />
|
|
{/if}
|
|
{/snippet}
|
|
|
|
<div class="block">
|
|
{@render settingContent()}
|
|
</div>
|
|
{/if}
|
|
{/if}
|