Refactor SettingsPage component for improved readability and consistency
Update formatting in SettingsPage.tsx to enhance code clarity by standardizing quotation marks and restructuring object definitions. Maintain functionality while improving overall code style and organization.
This commit is contained in:
parent
ed3ec86025
commit
7f47ec4809
1 changed files with 183 additions and 126 deletions
|
|
@ -1,149 +1,177 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Loader2, Play, KeyRound } from 'lucide-react';
|
||||
import { apiGet, apiPatch, apiPost } from '../lib/api';
|
||||
import { useAuth } from '../lib/auth';
|
||||
import type { Settings, JobStatus } from '../lib/types';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import { Label } from '../components/ui/Label';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../components/ui/Card';
|
||||
import { Alert } from '../components/ui/Alert';
|
||||
import { Skeleton } from '../components/ui/Skeleton';
|
||||
import { useEffect, useState } from "react"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { Loader2, Play, KeyRound } from "lucide-react"
|
||||
import { apiGet, apiPatch, apiPost } from "../lib/api"
|
||||
import { useAuth } from "../lib/auth"
|
||||
import type { Settings, JobStatus } from "../lib/types"
|
||||
import { PageHeader } from "../components/PageHeader"
|
||||
import { Button } from "../components/ui/Button"
|
||||
import { Input } from "../components/ui/Input"
|
||||
import { Label } from "../components/ui/Label"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "../components/ui/Card"
|
||||
import { Alert } from "../components/ui/Alert"
|
||||
import { Skeleton } from "../components/ui/Skeleton"
|
||||
|
||||
const JOBS = [
|
||||
{ id: 'dispatcher', label: 'Campaign Dispatcher', path: '/api/jobs/dispatcher/run' },
|
||||
{ id: 'rollover', label: 'Campaign Rollover', path: '/api/jobs/rollover/run' },
|
||||
{ id: 'rescuer', label: 'Spam Rescuer', path: '/api/jobs/rescuer/run' },
|
||||
{ id: 'rollup', label: 'Stats Rollup', path: '/api/jobs/rollup/run' },
|
||||
] as const;
|
||||
{
|
||||
id: "dispatcher",
|
||||
label: "Campaign Dispatcher",
|
||||
path: "/api/jobs/dispatcher/run",
|
||||
},
|
||||
{
|
||||
id: "rollover",
|
||||
label: "Campaign Rollover",
|
||||
path: "/api/jobs/rollover/run",
|
||||
},
|
||||
{ id: "rescuer", label: "Spam Rescuer", path: "/api/jobs/rescuer/run" },
|
||||
{ id: "rollup", label: "Stats Rollup", path: "/api/jobs/rollup/run" },
|
||||
] as const
|
||||
|
||||
type PostmasterIntegrationStatus = {
|
||||
configured: boolean;
|
||||
connected: boolean;
|
||||
domains: string[];
|
||||
message: string;
|
||||
hasClientId: boolean;
|
||||
};
|
||||
configured: boolean
|
||||
connected: boolean
|
||||
domains: string[]
|
||||
message: string
|
||||
hasClientId: boolean
|
||||
}
|
||||
|
||||
export function SettingsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { apiKey, baseUrl, login } = useAuth();
|
||||
const [localKey, setLocalKey] = useState(apiKey);
|
||||
const [localUrl, setLocalUrl] = useState(baseUrl);
|
||||
const [form, setForm] = useState<Settings>({});
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [runningJob, setRunningJob] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient()
|
||||
const { apiKey, baseUrl, login } = useAuth()
|
||||
const [localKey, setLocalKey] = useState(apiKey)
|
||||
const [localUrl, setLocalUrl] = useState(baseUrl)
|
||||
const [form, setForm] = useState<Settings>({})
|
||||
const [message, setMessage] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [runningJob, setRunningJob] = useState<string | null>(null)
|
||||
|
||||
const [googleClientId, setGoogleClientId] = useState('');
|
||||
const [googleClientSecret, setGoogleClientSecret] = useState('');
|
||||
const [microsoftClientId, setMicrosoftClientId] = useState('');
|
||||
const [microsoftClientSecret, setMicrosoftClientSecret] = useState('');
|
||||
const [googleClientId, setGoogleClientId] = useState("")
|
||||
const [googleClientSecret, setGoogleClientSecret] = useState("")
|
||||
const [microsoftClientId, setMicrosoftClientId] = useState("")
|
||||
const [microsoftClientSecret, setMicrosoftClientSecret] = useState("")
|
||||
|
||||
const { data: postmasterStatus, refetch: refetchPostmaster } = useQuery({
|
||||
queryKey: ['integrations', 'google', 'status'],
|
||||
queryFn: () => apiGet<PostmasterIntegrationStatus>('/api/integrations/google/status'),
|
||||
});
|
||||
queryKey: ["integrations", "google", "status"],
|
||||
queryFn: () =>
|
||||
apiGet<PostmasterIntegrationStatus>("/api/integrations/google/status"),
|
||||
})
|
||||
|
||||
const { data: microsoftStatus, refetch: refetchMicrosoft } = useQuery({
|
||||
queryKey: ['integrations', 'microsoft', 'status'],
|
||||
queryFn: () => apiGet<{ configured: boolean; hasClientId: boolean }>(
|
||||
'/api/integrations/microsoft/status',
|
||||
queryKey: ["integrations", "microsoft", "status"],
|
||||
queryFn: () =>
|
||||
apiGet<{ configured: boolean; hasClientId: boolean }>(
|
||||
"/api/integrations/microsoft/status",
|
||||
),
|
||||
});
|
||||
})
|
||||
|
||||
const saveMicrosoftCredsMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
apiPost('/api/integrations/microsoft/credentials', {
|
||||
apiPost("/api/integrations/microsoft/credentials", {
|
||||
microsoft_oauth_client_id: microsoftClientId,
|
||||
microsoft_oauth_client_secret: microsoftClientSecret,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
setMessage('Microsoft OAuth credentials saved');
|
||||
refetchMicrosoft();
|
||||
setMessage("Microsoft OAuth credentials saved")
|
||||
refetchMicrosoft()
|
||||
},
|
||||
onError: (err: Error) => setError(err.message),
|
||||
});
|
||||
})
|
||||
|
||||
const saveGoogleCredsMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
apiPost<PostmasterIntegrationStatus>('/api/integrations/google/credentials', {
|
||||
apiPost<PostmasterIntegrationStatus>(
|
||||
"/api/integrations/google/credentials",
|
||||
{
|
||||
google_oauth_client_id: googleClientId,
|
||||
google_oauth_client_secret: googleClientSecret,
|
||||
}),
|
||||
},
|
||||
),
|
||||
onSuccess: () => {
|
||||
setMessage('Google OAuth credentials saved');
|
||||
refetchPostmaster();
|
||||
setMessage("Google OAuth credentials saved")
|
||||
refetchPostmaster()
|
||||
},
|
||||
onError: (err: Error) => setError(err.message),
|
||||
});
|
||||
})
|
||||
|
||||
async function connectPostmaster() {
|
||||
setError('');
|
||||
setError("")
|
||||
try {
|
||||
const { url } = await apiGet<{ url: string }>('/api/integrations/google/auth-url');
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
setMessage('Complete Google authorization in the new tab, then refresh this page.');
|
||||
const { url } = await apiGet<{ url: string }>(
|
||||
"/api/integrations/google/auth-url",
|
||||
)
|
||||
window.open(url, "_blank", "noopener,noreferrer")
|
||||
setMessage(
|
||||
"Complete Google authorization in the new tab, then refresh this page.",
|
||||
)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to start Google OAuth');
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to start Google OAuth",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (window.location.hash === '#google-postmaster') {
|
||||
document.getElementById('google-postmaster')?.scrollIntoView({ behavior: 'smooth' });
|
||||
if (window.location.hash === "#google-postmaster") {
|
||||
document
|
||||
.getElementById("google-postmaster")
|
||||
?.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['settings'],
|
||||
queryFn: () => apiGet<Settings>('/api/settings'),
|
||||
});
|
||||
queryKey: ["settings"],
|
||||
queryFn: () => apiGet<Settings>("/api/settings"),
|
||||
})
|
||||
|
||||
const { data: jobStatus } = useQuery({
|
||||
queryKey: ['jobs', 'status'],
|
||||
queryFn: () => apiGet<{ jobs: JobStatus[] }>('/api/jobs/status'),
|
||||
queryKey: ["jobs", "status"],
|
||||
queryFn: () => apiGet<{ jobs: JobStatus[] }>("/api/jobs/status"),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (data) setForm(data);
|
||||
}, [data]);
|
||||
if (data) setForm(data)
|
||||
}, [data])
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (payload: Settings) => apiPatch<Settings>('/api/settings', payload),
|
||||
mutationFn: (payload: Settings) =>
|
||||
apiPatch<Settings>("/api/settings", payload),
|
||||
onSuccess: (updated) => {
|
||||
setForm(updated);
|
||||
queryClient.setQueryData(['settings'], updated);
|
||||
setMessage('Settings saved');
|
||||
setError('');
|
||||
setForm(updated)
|
||||
queryClient.setQueryData(["settings"], updated)
|
||||
setMessage("Settings saved")
|
||||
setError("")
|
||||
},
|
||||
onError: (err: Error) => setError(err.message),
|
||||
});
|
||||
})
|
||||
|
||||
function handleSaveSettings(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
saveMutation.mutate(form);
|
||||
e.preventDefault()
|
||||
saveMutation.mutate(form)
|
||||
}
|
||||
|
||||
function handleSaveApiConfig(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
login(localKey.trim(), localUrl.trim());
|
||||
setMessage('API configuration updated');
|
||||
e.preventDefault()
|
||||
login(localKey.trim(), localUrl.trim())
|
||||
setMessage("API configuration updated")
|
||||
}
|
||||
|
||||
async function runJob(path: string, id: string) {
|
||||
setRunningJob(id);
|
||||
setError('');
|
||||
setRunningJob(id)
|
||||
setError("")
|
||||
try {
|
||||
await apiPost(path);
|
||||
setMessage(`Job "${id}" completed`);
|
||||
await apiPost(path)
|
||||
setMessage(`Job "${id}" completed`)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Job failed');
|
||||
setError(err instanceof Error ? err.message : "Job failed")
|
||||
} finally {
|
||||
setRunningJob(null);
|
||||
setRunningJob(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +188,9 @@ export function SettingsPage() {
|
|||
<KeyRound className="h-5 w-5" />
|
||||
API Configuration
|
||||
</CardTitle>
|
||||
<CardDescription>Connection settings for this dashboard session</CardDescription>
|
||||
<CardDescription>
|
||||
Connection settings for this dashboard session
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSaveApiConfig} className="space-y-4 max-w-md">
|
||||
|
|
@ -201,8 +231,10 @@ export function SettingsPage() {
|
|||
<Label htmlFor="dispatcher_cron">Dispatcher Cron</Label>
|
||||
<Input
|
||||
id="dispatcher_cron"
|
||||
value={form.dispatcher_cron ?? ''}
|
||||
onChange={(e) => setForm({ ...form, dispatcher_cron: e.target.value })}
|
||||
value={form.dispatcher_cron ?? ""}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, dispatcher_cron: e.target.value })
|
||||
}
|
||||
placeholder="*/5 * * * *"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -210,8 +242,10 @@ export function SettingsPage() {
|
|||
<Label htmlFor="rescuer_cron">Rescuer Cron</Label>
|
||||
<Input
|
||||
id="rescuer_cron"
|
||||
value={form.rescuer_cron ?? ''}
|
||||
onChange={(e) => setForm({ ...form, rescuer_cron: e.target.value })}
|
||||
value={form.rescuer_cron ?? ""}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, rescuer_cron: e.target.value })
|
||||
}
|
||||
placeholder="*/10 * * * *"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -219,28 +253,36 @@ export function SettingsPage() {
|
|||
<Label htmlFor="ramp_increment">Ramp Increment</Label>
|
||||
<Input
|
||||
id="ramp_increment"
|
||||
value={form.ramp_increment ?? ''}
|
||||
onChange={(e) => setForm({ ...form, ramp_increment: e.target.value })}
|
||||
value={form.ramp_increment ?? ""}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, ramp_increment: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ai_model">AI Model</Label>
|
||||
<Input
|
||||
id="ai_model"
|
||||
value={form.ai_model ?? ''}
|
||||
onChange={(e) => setForm({ ...form, ai_model: e.target.value })}
|
||||
value={form.ai_model ?? ""}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, ai_model: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="industries">Industries (JSON array)</Label>
|
||||
<Input
|
||||
id="industries"
|
||||
value={form.industries ?? ''}
|
||||
onChange={(e) => setForm({ ...form, industries: e.target.value })}
|
||||
value={form.industries ?? ""}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, industries: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={saveMutation.isPending}>
|
||||
{saveMutation.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{saveMutation.isPending && (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Save Settings
|
||||
</Button>
|
||||
</form>
|
||||
|
|
@ -250,17 +292,18 @@ export function SettingsPage() {
|
|||
|
||||
<Card id="microsoft-outlook">
|
||||
<CardHeader>
|
||||
<CardTitle>Microsoft Outlook (@live.com)</CardTitle>
|
||||
<CardTitle>Microsoft Outlook</CardTitle>
|
||||
<CardDescription>
|
||||
Required for consumer Outlook accounts — app passwords no longer work for SMTP/IMAP
|
||||
Required for consumer Outlook accounts — app passwords no longer
|
||||
work for SMTP/IMAP
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 max-w-2xl">
|
||||
{microsoftStatus && (
|
||||
<Alert variant={microsoftStatus.configured ? 'success' : 'warning'}>
|
||||
<Alert variant={microsoftStatus.configured ? "success" : "warning"}>
|
||||
{microsoftStatus.configured
|
||||
? 'Microsoft OAuth credentials are configured. Connect mailboxes from Mailboxes → Add.'
|
||||
: 'Add Azure app credentials below before connecting @live.com mailboxes.'}
|
||||
? "Microsoft OAuth credentials are configured. Connect mailboxes from Mailboxes → Add."
|
||||
: "Add Azure app credentials below before connecting @live.com mailboxes."}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
|
@ -268,7 +311,7 @@ export function SettingsPage() {
|
|||
<p className="font-medium">Setup (one-time)</p>
|
||||
<ol className="list-decimal pl-5 space-y-1">
|
||||
<li>
|
||||
In{' '}
|
||||
In{" "}
|
||||
<a
|
||||
href="https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade"
|
||||
target="_blank"
|
||||
|
|
@ -277,20 +320,24 @@ export function SettingsPage() {
|
|||
>
|
||||
Azure App registrations
|
||||
</a>
|
||||
, create a Web app. Supported account types: personal Microsoft accounts.
|
||||
, create a Web app. Supported account types: personal Microsoft
|
||||
accounts.
|
||||
</li>
|
||||
<li>
|
||||
Add redirect URI:{' '}
|
||||
Add redirect URI:{" "}
|
||||
<code className="text-xs bg-white px-1 py-0.5 rounded">
|
||||
http://localhost:3000/api/integrations/microsoft/callback
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
Under API permissions, add delegated{' '}
|
||||
<strong>IMAP.AccessAsUser.All</strong> and <strong>SMTP.Send</strong> for Office
|
||||
365 / Outlook.
|
||||
Under API permissions, add delegated{" "}
|
||||
<strong>IMAP.AccessAsUser.All</strong> and{" "}
|
||||
<strong>SMTP.Send</strong> for Office 365 / Outlook.
|
||||
</li>
|
||||
<li>
|
||||
Create a client secret, paste ID + secret below, then add
|
||||
mailboxes via Connect Microsoft.
|
||||
</li>
|
||||
<li>Create a client secret, paste ID + secret below, then add mailboxes via Connect Microsoft.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
|
@ -316,7 +363,9 @@ export function SettingsPage() {
|
|||
type="button"
|
||||
variant="outline"
|
||||
disabled={
|
||||
!microsoftClientId || !microsoftClientSecret || saveMicrosoftCredsMutation.isPending
|
||||
!microsoftClientId ||
|
||||
!microsoftClientSecret ||
|
||||
saveMicrosoftCredsMutation.isPending
|
||||
}
|
||||
onClick={() => saveMicrosoftCredsMutation.mutate()}
|
||||
>
|
||||
|
|
@ -330,16 +379,17 @@ export function SettingsPage() {
|
|||
<CardHeader>
|
||||
<CardTitle>Google Postmaster Tools</CardTitle>
|
||||
<CardDescription>
|
||||
Connect once to auto-verify Gmail/Workspace domains in campaign compliance checks
|
||||
Connect once to auto-verify Gmail/Workspace domains in campaign
|
||||
compliance checks
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 max-w-2xl">
|
||||
{postmasterStatus && (
|
||||
<Alert variant={postmasterStatus.connected ? 'success' : 'warning'}>
|
||||
<Alert variant={postmasterStatus.connected ? "success" : "warning"}>
|
||||
{postmasterStatus.message}
|
||||
{postmasterStatus.domains.length > 0 && (
|
||||
<span className="block mt-1 text-sm">
|
||||
Domains: {postmasterStatus.domains.join(', ')}
|
||||
Domains: {postmasterStatus.domains.join(", ")}
|
||||
</span>
|
||||
)}
|
||||
</Alert>
|
||||
|
|
@ -349,7 +399,7 @@ export function SettingsPage() {
|
|||
<p className="font-medium">Setup (one-time)</p>
|
||||
<ol className="list-decimal pl-5 space-y-1">
|
||||
<li>
|
||||
In{' '}
|
||||
In{" "}
|
||||
<a
|
||||
href="https://console.cloud.google.com/apis/library/gmailpostmastertools.googleapis.com"
|
||||
target="_blank"
|
||||
|
|
@ -361,13 +411,14 @@ export function SettingsPage() {
|
|||
, enable the <strong>Gmail Postmaster Tools API</strong>.
|
||||
</li>
|
||||
<li>
|
||||
Create OAuth 2.0 credentials (Web application). Add redirect URI:{' '}
|
||||
Create OAuth 2.0 credentials (Web application). Add redirect
|
||||
URI:{" "}
|
||||
<code className="text-xs bg-white px-1 py-0.5 rounded">
|
||||
http://localhost:3000/api/integrations/google/callback
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
Add your sending domain at{' '}
|
||||
Add your sending domain at{" "}
|
||||
<a
|
||||
href="https://postmaster.google.com"
|
||||
target="_blank"
|
||||
|
|
@ -375,10 +426,12 @@ export function SettingsPage() {
|
|||
className="text-primary-600 underline"
|
||||
>
|
||||
postmaster.google.com
|
||||
</a>{' '}
|
||||
</a>{" "}
|
||||
and complete DNS verification there.
|
||||
</li>
|
||||
<li>Paste Client ID and Secret below, save, then click Connect.</li>
|
||||
<li>
|
||||
Paste Client ID and Secret below, save, then click Connect.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
|
@ -405,7 +458,11 @@ export function SettingsPage() {
|
|||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={!googleClientId || !googleClientSecret || saveGoogleCredsMutation.isPending}
|
||||
disabled={
|
||||
!googleClientId ||
|
||||
!googleClientSecret ||
|
||||
saveGoogleCredsMutation.isPending
|
||||
}
|
||||
onClick={() => saveGoogleCredsMutation.mutate()}
|
||||
>
|
||||
Save OAuth Credentials
|
||||
|
|
@ -439,10 +496,10 @@ export function SettingsPage() {
|
|||
<span className="text-slate-500">
|
||||
{job.lastRunAt
|
||||
? `${new Date(job.lastRunAt).toLocaleString()} — ${
|
||||
job.success ? 'OK' : 'Failed'
|
||||
job.success ? "OK" : "Failed"
|
||||
}`
|
||||
: 'Never run'}
|
||||
{job.message ? ` (${job.message})` : ''}
|
||||
: "Never run"}
|
||||
{job.message ? ` (${job.message})` : ""}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -469,5 +526,5 @@ export function SettingsPage() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue