fix: migrate smtp instance settings to global settings (#4416)

* all

* all

* all

* fix

* update ee private

* all
This commit is contained in:
Ruben Fiszel
2024-09-21 01:42:15 +02:00
committed by GitHub
parent d318f10f36
commit efbcb1ffdd
21 changed files with 428 additions and 338 deletions
@@ -1,7 +1,7 @@
<script lang="ts">
import { settings, settingsKeys, type SettingStorage } from './instanceSettings'
import { Button, Skeleton, Tab, TabContent, Tabs } from '$lib/components/common'
import { ConfigService, SettingService, SettingsService } from '$lib/gen'
import { SettingService, SettingsService } from '$lib/gen'
import Toggle from '$lib/components/Toggle.svelte'
import SecondsInput from '$lib/components/common/seconds/SecondsInput.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
@@ -54,7 +54,6 @@
attempted_at: string
} | null = null
let serverConfig = {}
let initialValues: Record<string, any> = {}
let loading = true
@@ -71,17 +70,10 @@
async function loadSettings() {
loading = true
try {
serverConfig = (await ConfigService.getConfig({ name: 'server' })) ?? {}
} catch (e) {
console.log("Sever config not found, assuming it's first setup")
}
async function getValue(key: string, storage: SettingStorage) {
if (storage == 'setting') {
return SettingService.getGlobal({ key })
} else if (storage == 'config') {
return serverConfig[key]
}
}
initialOauths = (await SettingService.getGlobal({ key: 'oauths' })) ?? {}
@@ -110,6 +102,9 @@
if (values['base_url'] == undefined) {
values['base_url'] = 'http://localhost'
}
if (values['smtp_connect'] == undefined) {
values['smtp_connect'] = {}
}
loading = false
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
@@ -118,18 +113,6 @@
export async function saveSettings() {
if (values) {
const allSettings = Object.values(settings).flatMap((x) => Object.entries(x))
const newServerConfig = Object.fromEntries(
allSettings
.filter((x) => x[1].storage == 'config' && values?.[x[1].key] && values?.[x[1].key] != '')
.map((x) => [x[1].key, values?.[x[1].key]])
)
if (!deepEqual(newServerConfig, serverConfig)) {
await ConfigService.updateConfig({
name: 'server',
requestBody: newServerConfig
})
serverConfig = JSON.parse(JSON.stringify(newServerConfig))
}
let licenseKeySet = false
await Promise.all(
allSettings
@@ -693,79 +676,102 @@
bind:value={values[setting.key]}
/>
{:else if setting.fieldType == 'critical_error_channels'}
<div class="w-full">
<div class="flex max-w-md mt-1 gap-2 w-full items-center">
<select disabled>
<option>Tracing</option>
</select>
<input disabled />
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover"
aria-label="Clear"
disabled
>
<X size={14} />
</button>
<div class="w-full flex gap-x-16 flex-wrap">
<div class="w-full max-w-lg">
<div class="flex w-full max-w-lg mt-1 gap-2 w-full items-center">
<input
type="text"
placeholder="Logs (critical errors are always logged)"
disabled
/>
</div>
{#if $enterpriseLicense && Array.isArray(values[setting.key])}
{#each values[setting.key] ?? [] as v, i}
<div class="flex w-full max-w-lg mt-1 gap-2 w-full items-center">
<select
class="w-20"
on:change={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
[e.target['value']]: ''
}
}
}}
value={v && 'slack_channel' in v ? 'slack_channel' : 'email'}
>
<option value="email">Email</option>
<option value="slack_channel">Slack</option>
</select>
{#if v && 'slack_channel' in v}
<input
type="text"
placeholder="Slack channel"
on:input={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
slack_channel: e.target['value']
}
}
}}
value={v?.slack_channel ?? ''}
/>
{:else}
<input
type="email"
placeholder="Email address"
on:input={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
email: e.target['value']
}
}
}}
value={v?.email ?? ''}
/>
{/if}
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover"
aria-label="Clear"
on:click={() => {
values[setting.key] = values[setting.key].filter(
(_, index) => index !== i
)
}}
>
<X size={14} />
</button>
</div>
{/each}
{/if}
</div>
{#if $enterpriseLicense && Array.isArray(values[setting.key])}
{#each values[setting.key] ?? [] as v, i}
<div class="flex max-w-md mt-1 gap-2 w-full items-center">
<select
on:change={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
[e.target['value']]: ''
}
}
}}
value={v && 'slack_channel' in v ? 'slack_channel' : 'email'}
>
<option value="email">Email</option>
<option value="slack_channel">Slack</option>
</select>
{#if v && 'slack_channel' in v}
<input
type="text"
placeholder="Slack channel"
on:input={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
slack_channel: e.target['value']
}
}
}}
value={v?.slack_channel ?? ''}
/>
{:else}
<input
type="email"
placeholder="Email address"
on:input={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
email: e.target['value']
}
}
}}
value={v?.email ?? ''}
/>
{/if}
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover"
aria-label="Clear"
on:click={() => {
values[setting.key] = values[setting.key].filter(
(_, index) => index !== i
<div
><div class="flex mt-1">
<Button
size="xs"
variant="contained"
on:click={async () => {
try {
await SettingService.testCriticalChannels({
requestBody: values[setting.key]
})
sendUserToast(
'Test message sent successfully to critical channels',
false
)
}}
>
<X size={14} />
</button>
</div>
{/each}
{/if}
} catch (error) {
sendUserToast(
'Failed to send test message: ' + error.message,
true
)
}
}}
>
Test Critical Channels
</Button>
</div>
</div>
</div>
<div class="flex mt-2 gap-20 items-baseline">
<Button
@@ -818,6 +824,65 @@
</Button>
{/if}
</div>
{:else if setting.fieldType == 'smtp_connect'}
<div class="flex flex-col gap-4 mt-4">
{#if values[setting.key]}
<div>
<label for="smtp_host" class="block text-sm font-medium">Host</label>
<input
type="text"
id="smtp_host"
placeholder="smtp.gmail.com"
bind:value={values[setting.key].smtp_host}
/>
</div>
<div>
<label for="smtp_port" class="block text-sm font-medium">Port</label>
<input
type="number"
id="smtp_port"
placeholder="587"
bind:value={values[setting.key].smtp_port}
/>
</div>
<div>
<label for="smtp_username" class="block text-sm font-medium"
>Username</label
>
<input
type="text"
id="smtp_username"
placeholder="ruben@windmill.dev"
bind:value={values[setting.key].smtp_username}
/>
</div>
<div>
<label for="smtp_password" class="block text-sm font-medium"
>Password</label
>
<Password bind:password={values[setting.key].smtp_password} />
</div>
<div>
<label for="smtp_from" class="block text-sm font-medium"
>From Address</label
>
<input
type="email"
id="smtp_from"
placeholder="noreply@windmill.dev"
bind:value={values[setting.key].smtp_from}
/>
</div>
<div>
<Toggle
id="smtp_tls_implicit"
bind:checked={values[setting.key].smtp_tls_implicit}
options={{ right: 'Implicit TLS' }}
label="Implicit TLS"
/>
</div>
{/if}
</div>
{:else if setting.fieldType == 'object_store_config'}
<ObjectStoreConfigSettings bind:bucket_config={values[setting.key]} />
<div class="mb-6" />
@@ -863,21 +928,22 @@
</div>
</div>
{#if category == 'SMTP'}
{@const smtp = values['smtp_settings']}
<div class="flex gap-4"
><input type="email" bind:value={to} placeholder="contact@windmill.dev" />
<Button
disabled={to == ''}
disabled={to == '' || !smtp}
on:click={async () => {
await SettingService.testSmtp({
requestBody: {
to,
smtp: {
host: values['smtp_host'],
username: values['smtp_username'],
password: values['smtp_password'],
port: values['smtp_port'],
from: values['smtp_from'],
tls_implicit: values['smtp_tls_implicit']
host: smtp['smtp_host'],
username: smtp['smtp_username'],
password: smtp['smtp_password'],
port: smtp['smtp_port'],
from: smtp['smtp_from'],
tls_implicit: smtp['smtp_tls_implicit']
}
}
})
@@ -72,7 +72,7 @@
/>
</div>
{#if bucket_config}
<div class="flex gap-2">
<div class="flex gap-2 py-1">
<Button
spacingSize="sm"
size="xs"
@@ -123,81 +123,83 @@
<Tab size="sm" value="S3">S3</Tab>
<Tab size="sm" value="Azure">Azure Blob</Tab>
</Tabs>
{#if bucket_config.type === 'S3'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Bucket</span>
<input type="text" placeholder="bucket-name" bind:value={bucket_config.bucket} />
</label>
<div class="flex flex-col gap-2 mt-2 p-2 border rounded-md">
{#if bucket_config.type === 'S3'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Bucket</span>
<input type="text" placeholder="bucket-name" bind:value={bucket_config.bucket} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Region</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_REGION</span
>
<input type="text" bind:value={bucket_config.region} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Access key ID</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_ACCESS_KEY_ID, pod or ec2 profile</span
>
<input type="text" bind:value={bucket_config.access_key} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Secret key</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_SECRET_KEY, pod or ec2 profile</span
>
<input type="password" autocomplete="new-password" bind:value={bucket_config.secret_key} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Endpoint</span>
<span class="text-tertiary text-2xs"
>Only needed for non AWS S3 providers like R2 or MinIo</span
>
<input type="text" bind:value={bucket_config.endpoint} />
</label>
<div class="block pb-2">
<span class="text-tertiary text-2xs">Disable if using https only policy</span>
<div>
<Toggle bind:checked={bucket_config.allow_http} options={{ right: 'Allow http' }} />
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Region</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_REGION</span
>
<input type="text" bind:value={bucket_config.region} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Access key ID</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_ACCESS_KEY_ID, pod or ec2 profile</span
>
<input type="text" bind:value={bucket_config.access_key} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Secret key</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_SECRET_KEY, pod or ec2 profile</span
>
<input type="password" autocomplete="new-password" bind:value={bucket_config.secret_key} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Endpoint</span>
<span class="text-tertiary text-2xs"
>Only needed for non AWS S3 providers like R2 or MinIo</span
>
<input type="text" bind:value={bucket_config.endpoint} />
</label>
<div class="block pb-2">
<span class="text-tertiary text-2xs">Disable if using https only policy</span>
<div>
<Toggle bind:checked={bucket_config.allow_http} options={{ right: 'Allow http' }} />
</div>
</div>
</div>
{:else if bucket_config.type === 'Azure'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Account name</span>
<input type="text" placeholder="account-name" bind:value={bucket_config.accountName} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Container name</span>
<input type="text" placeholder="container-name" bind:value={bucket_config.containerName} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Access key</span>
<input type="password" autocomplete="new-password" bind:value={bucket_config.accessKey} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Tenant ID <span class="text-2xs text-tertiary">(optional)</span></span
>
<input type="text" bind:value={bucket_config.tenantId} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Client ID <span class="text-2xs text-tertiary">(optional)</span></span
>
<input type="text" bind:value={bucket_config.clientId} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Endpoint <span class="text-2xs text-tertiary">(optional)</span></span
>
<span class="text-tertiary text-2xs"
>Only needed for non Azure Blob providers like Azurite</span
>
<input type="text" bind:value={bucket_config.endpoint} />
</label>
{:else}
<div>Unknown bucket type {bucket_config['type']}</div>
{/if}
{:else if bucket_config.type === 'Azure'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Account name</span>
<input type="text" placeholder="account-name" bind:value={bucket_config.accountName} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Container name</span>
<input type="text" placeholder="container-name" bind:value={bucket_config.containerName} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Access key</span>
<input type="password" autocomplete="new-password" bind:value={bucket_config.accessKey} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Tenant ID <span class="text-2xs text-tertiary">(optional)</span></span
>
<input type="text" bind:value={bucket_config.tenantId} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Client ID <span class="text-2xs text-tertiary">(optional)</span></span
>
<input type="text" bind:value={bucket_config.clientId} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Endpoint <span class="text-2xs text-tertiary">(optional)</span></span
>
<span class="text-tertiary text-2xs"
>Only needed for non Azure Blob providers like Azurite</span
>
<input type="text" bind:value={bucket_config.endpoint} />
</label>
{:else}
<div>Unknown bucket type {bucket_config['type']}</div>
{/if}
</div>
{/if}
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
// @ts-nocheck
import { onMount } from 'svelte'
export let password: string
export let password: string | undefined
export let placeholder = '******'
export let disabled = false
export let required = false
@@ -19,13 +19,14 @@ export interface Setting {
| 'object_store_config'
| 'critical_error_channels'
| 'slack_connect'
| 'smtp_connect'
storage: SettingStorage
isValid?: (value: any) => boolean
error?: string
defaultValue?: () => any
}
export type SettingStorage = 'setting' | 'config'
export type SettingStorage = 'setting'
export const settings: Record<string, Setting[]> = {
Core: [
@@ -76,7 +77,7 @@ export const settings: Record<string, Setting[]> = {
cloudonly: true,
fieldType: 'seconds',
placeholder: '60',
storage: 'config'
storage: 'setting'
},
{
label: 'License key',
@@ -188,45 +189,12 @@ export const settings: Record<string, Setting[]> = {
}
],
SMTP: [
{
label: 'Host',
key: 'smtp_host',
fieldType: 'text',
placeholder: 'smtp.gmail.com',
storage: 'config'
},
{
label: 'Port',
key: 'smtp_port',
fieldType: 'number',
placeholder: '587',
storage: 'config'
},
{
label: 'Username',
key: 'smtp_username',
fieldType: 'text',
placeholder: 'ruben@windmill.dev',
storage: 'config'
},
{
label: 'Password',
key: 'smtp_password',
fieldType: 'password',
storage: 'config'
},
{
label: 'From Address',
key: 'smtp_from',
placeholder: 'noreply@windmill.dev',
fieldType: 'email',
storage: 'config'
},
{
label: 'Implicit TLS',
key: 'smtp_tls_implicit',
fieldType: 'boolean',
storage: 'config'
{
label: 'SMTP',
key: 'smtp_settings',
fieldType: 'smtp_connect',
storage: 'setting',
ee_only: '',
}
],
Slack: [