Pulse/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx
rcourtman 6eb1a10d9b Refactor: Code cleanup and localStorage consolidation
This commit includes comprehensive codebase cleanup and refactoring:

## Code Cleanup
- Remove dead TypeScript code (types/monitoring.ts - 194 lines duplicate)
- Remove unused Go functions (GetClusterNodes, MigratePassword, GetClusterHealthInfo)
- Clean up commented-out code blocks across multiple files
- Remove unused TypeScript exports (helpTextClass, private tag color helpers)
- Delete obsolete test files and components

## localStorage Consolidation
- Centralize all storage keys into STORAGE_KEYS constant
- Update 5 files to use centralized keys:
  * utils/apiClient.ts (AUTH, LEGACY_TOKEN)
  * components/Dashboard/Dashboard.tsx (GUEST_METADATA)
  * components/Docker/DockerHosts.tsx (DOCKER_METADATA)
  * App.tsx (PLATFORMS_SEEN)
  * stores/updates.ts (UPDATES)
- Benefits: Single source of truth, prevents typos, better maintainability

## Previous Work Committed
- Docker monitoring improvements and disk metrics
- Security enhancements and setup fixes
- API refactoring and cleanup
- Documentation updates
- Build system improvements

## Testing
- All frontend tests pass (29 tests)
- All Go tests pass (15 packages)
- Production build successful
- Zero breaking changes

Total: 186 files changed, 5825 insertions(+), 11602 deletions(-)
2025-11-04 21:50:46 +00:00

334 lines
12 KiB
TypeScript

import { createSignal, createEffect, Show, For } from 'solid-js';
import { NotificationsAPI } from '@/api/notifications';
import { logger } from '@/utils/logger';
import {
formField,
labelClass,
controlClass,
formHelpText,
formCheckbox,
} from '@/components/shared/Form';
interface EmailProvider {
name: string;
smtpHost: string;
smtpPort: number;
tls: boolean;
startTLS: boolean;
authRequired: boolean;
instructions: string;
}
interface EmailConfig {
enabled: boolean;
provider: string;
server: string;
port: number;
from: string;
username: string;
password: string;
to: string[];
tls: boolean;
startTLS: boolean;
replyTo: string;
maxRetries: number;
retryDelay: number;
rateLimit: number;
}
interface EmailProviderSelectProps {
config: EmailConfig;
onChange: (config: EmailConfig) => void;
onTest: () => void;
testing?: boolean;
}
export function EmailProviderSelect(props: EmailProviderSelectProps) {
const [providers, setProviders] = createSignal<EmailProvider[]>([]);
const [showAdvanced, setShowAdvanced] = createSignal(false);
const [showInstructions, setShowInstructions] = createSignal(false);
// Load email providers once
createEffect(async () => {
try {
const data = await NotificationsAPI.getEmailProviders();
setProviders(data);
} catch (err) {
logger.error('Failed to load email providers', err);
}
});
const applyProvider = (provider: EmailProvider | undefined) => {
if (!provider) {
props.onChange({ ...props.config, provider: '' });
setShowInstructions(false);
return;
}
props.onChange({
...props.config,
provider: provider.name,
server: provider.smtpHost,
port: provider.smtpPort,
tls: provider.tls,
startTLS: provider.startTLS,
username: provider.name === 'SendGrid' ? 'apikey' : props.config.username,
});
setShowInstructions(true);
};
const handleProviderChange = (value: string) => {
if (!value) {
applyProvider(undefined);
return;
}
const provider = providers().find((p) => p.name === value);
applyProvider(provider);
};
const currentProvider = () => providers().find((p) => p.name === props.config.provider);
const instructionBoxClass = "mt-2 rounded border border-blue-200 bg-blue-50 px-3 py-2 text-xs leading-relaxed text-blue-900 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-200";
return (
<div class="space-y-4 text-sm overflow-hidden">
<div class={formField}>
<label class={labelClass()}>Email provider</label>
<div class="flex w-full flex-wrap items-center gap-2 sm:flex-nowrap">
<select
value={props.config.provider}
onChange={(e) => handleProviderChange(e.currentTarget.value)}
class={`${controlClass('px-2 py-1.5')} sm:w-auto sm:min-w-[180px]`}
>
<option value="">Manual configuration</option>
<For each={providers()}>
{(provider) => (
<option value={provider.name}>
{provider.name} ({provider.smtpHost}:{provider.smtpPort})
</option>
)}
</For>
</select>
<Show when={props.config.provider}>
<button
type="button"
onClick={() => {
const provider = currentProvider();
if (provider) applyProvider(provider);
}}
class="text-xs font-medium text-blue-600 hover:underline dark:text-blue-400"
>
Reapply defaults
</button>
</Show>
</div>
</div>
<Show when={currentProvider()}>
<div class="sm:hidden w-full">
<button
type="button"
onClick={() => setShowInstructions(!showInstructions())}
class="text-xs font-medium text-blue-600 hover:underline dark:text-blue-300"
>
{showInstructions() ? 'Hide setup instructions' : 'Show setup instructions'}
</button>
<Show when={showInstructions()}>
<div class={instructionBoxClass}>
{currentProvider()!.instructions}
</div>
</Show>
</div>
<div class="hidden w-full sm:block">
<div class={instructionBoxClass}>
{currentProvider()!.instructions}
</div>
</div>
</Show>
<div class="grid w-full gap-3 sm:grid-cols-2">
<div class={formField}>
<label class={labelClass()}>SMTP server</label>
<input
type="text"
value={props.config.server}
onInput={(e) => props.onChange({ ...props.config, server: e.currentTarget.value })}
placeholder="smtp.example.com"
class={controlClass('px-2 py-1.5')}
/>
</div>
<div class={formField}>
<label class={labelClass()}>SMTP port</label>
<input
type="number"
value={props.config.port}
onInput={(e) =>
props.onChange({ ...props.config, port: parseInt(e.currentTarget.value) || 587 })
}
placeholder="587"
class={controlClass('px-2 py-1.5')}
/>
</div>
<div class={formField}>
<label class={labelClass()}>From address</label>
<input
type="email"
value={props.config.from}
onInput={(e) => props.onChange({ ...props.config, from: e.currentTarget.value })}
placeholder="noreply@example.com"
class={controlClass('px-2 py-1.5')}
/>
</div>
<div class={formField}>
<label class={labelClass()}>Reply-to address</label>
<input
type="email"
value={props.config.replyTo || ''}
onInput={(e) => props.onChange({ ...props.config, replyTo: e.currentTarget.value })}
placeholder="admin@example.com"
class={controlClass('px-2 py-1.5')}
/>
</div>
<div class={formField}>
<label class={labelClass()}>Username</label>
<input
type="text"
value={props.config.username}
onInput={(e) => props.onChange({ ...props.config, username: e.currentTarget.value })}
placeholder={props.config.provider === 'SendGrid' ? 'apikey' : 'username@example.com'}
class={controlClass('px-2 py-1.5')}
/>
</div>
<div class={formField}>
<label class={labelClass()}>Password / API key</label>
<input
type="password"
value={props.config.password}
onInput={(e) => props.onChange({ ...props.config, password: e.currentTarget.value })}
placeholder="••••••••"
class={controlClass('px-2 py-1.5')}
/>
</div>
</div>
<div class={formField}>
<label class={labelClass()}>Recipients (one per line)</label>
<textarea
value={props.config.to.join('\n')}
onInput={(e) => {
const recipients = e.currentTarget.value
.split('\n')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
props.onChange({ ...props.config, to: recipients });
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.stopPropagation();
}
}}
rows={3}
class={controlClass('px-2 py-1.5 font-mono leading-snug')}
placeholder={`Leave empty to use ${props.config.from || 'the from address'}\nOr add one recipient per line`}
/>
</div>
<div class="border-t border-gray-200 pt-3 dark:border-gray-700">
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced())}
class="text-xs font-semibold uppercase tracking-wide text-gray-600 transition-colors hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
{showAdvanced() ? 'Hide advanced options' : 'Show advanced options'}
</button>
<Show when={showAdvanced()}>
<div class="mt-3 space-y-3 text-xs text-gray-700 dark:text-gray-300">
<div class="grid gap-3 sm:grid-cols-3">
<label class="flex items-center gap-2">
<input
type="checkbox"
checked={props.config.tls}
onChange={(e) =>
props.onChange({ ...props.config, tls: e.currentTarget.checked })
}
class={`${formCheckbox} h-4 w-4`}
/>
<span>Use TLS</span>
</label>
<label class="flex items-center gap-2">
<input
type="checkbox"
checked={props.config.startTLS}
onChange={(e) =>
props.onChange({ ...props.config, startTLS: e.currentTarget.checked })
}
class={`${formCheckbox} h-4 w-4`}
/>
<span>Use STARTTLS</span>
</label>
<div class="flex w-full flex-wrap items-center gap-2 sm:flex-nowrap">
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>Rate limit</label>
<input
type="number"
value={props.config.rateLimit || 60}
onInput={(e) =>
props.onChange({ ...props.config, rateLimit: parseInt(e.currentTarget.value) })
}
class={`${controlClass('px-2 py-1 text-sm')} w-20`}
/>
<span class={formHelpText}>/min</span>
</div>
</div>
<div class="grid w-full gap-3 sm:grid-cols-2">
<div class={formField}>
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>Max retries</label>
<input
type="number"
value={props.config.maxRetries || 3}
min={0}
max={5}
onInput={(e) =>
props.onChange({ ...props.config, maxRetries: parseInt(e.currentTarget.value) })
}
class={controlClass('px-2 py-1 text-sm')}
/>
</div>
<div class={formField}>
<label class={labelClass('text-xs uppercase tracking-[0.08em]')}>
Retry delay (seconds)
</label>
<input
type="number"
value={props.config.retryDelay || 5}
min={1}
max={60}
onInput={(e) =>
props.onChange({ ...props.config, retryDelay: parseInt(e.currentTarget.value) })
}
class={controlClass('px-2 py-1 text-sm')}
/>
</div>
</div>
</div>
</Show>
</div>
<div class="flex justify-end pt-2">
<button
type="button"
onClick={props.onTest}
disabled={props.testing || !props.config.enabled}
class="rounded border border-blue-500 px-3 py-1.5 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-blue-400 dark:text-blue-300 dark:hover:bg-blue-900/30"
>
{props.testing ? 'Sending test email…' : 'Send test email'}
</button>
</div>
</div>
);
}