mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-28 08:02:43 +00:00
fix: migrate smtp instance settings to global settings (#4416)
* all * all * all * fix * update ee private * all
This commit is contained in:
@@ -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,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: [
|
||||
|
||||
Reference in New Issue
Block a user