Add template variables display and SMTP settings UI updates
All checks were successful
PostgreSQL Compatibility Matrix / PG14 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG15 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG16 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG17 smoke (push) Successful in 8s
PostgreSQL Compatibility Matrix / PG18 smoke (push) Successful in 7s
All checks were successful
PostgreSQL Compatibility Matrix / PG14 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG15 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG16 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG17 smoke (push) Successful in 8s
PostgreSQL Compatibility Matrix / PG18 smoke (push) Successful in 7s
This commit introduces a new section for template variables in the Admin Users page, improving clarity by listing placeholders available for email templates. It also enhances the SMTP settings interface with clearer organization and additional features like clearing stored passwords. Associated styling updates include new visual elements for template variables and subcards.
This commit is contained in:
@@ -2,6 +2,21 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { apiFetch } from "../api";
|
import { apiFetch } from "../api";
|
||||||
import { useAuth } from "../state";
|
import { useAuth } from "../state";
|
||||||
|
|
||||||
|
const TEMPLATE_VARIABLES = [
|
||||||
|
"target_name",
|
||||||
|
"target_id",
|
||||||
|
"alert_name",
|
||||||
|
"severity",
|
||||||
|
"category",
|
||||||
|
"description",
|
||||||
|
"message",
|
||||||
|
"value",
|
||||||
|
"warning_threshold",
|
||||||
|
"alert_threshold",
|
||||||
|
"checked_at",
|
||||||
|
"alert_key",
|
||||||
|
];
|
||||||
|
|
||||||
export function AdminUsersPage() {
|
export function AdminUsersPage() {
|
||||||
const { tokens, refresh, me } = useAuth();
|
const { tokens, refresh, me } = useAuth();
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
@@ -223,148 +238,164 @@ export function AdminUsersPage() {
|
|||||||
</div>
|
</div>
|
||||||
{smtpInfo && <div className="test-connection-result ok">{smtpInfo}</div>}
|
{smtpInfo && <div className="test-connection-result ok">{smtpInfo}</div>}
|
||||||
<form className="grid two admin-smtp-form" onSubmit={saveSmtp}>
|
<form className="grid two admin-smtp-form" onSubmit={saveSmtp}>
|
||||||
<label className="toggle-check field-full">
|
<div className="admin-subcard field-full">
|
||||||
<input
|
<h4>SMTP Settings</h4>
|
||||||
type="checkbox"
|
<div className="grid two">
|
||||||
checked={emailSettings.enabled}
|
<label className="toggle-check field-full">
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, enabled: e.target.checked })}
|
<input
|
||||||
/>
|
type="checkbox"
|
||||||
<span className="toggle-ui" />
|
checked={emailSettings.enabled}
|
||||||
<span>
|
onChange={(e) => setEmailSettings({ ...emailSettings, enabled: e.target.checked })}
|
||||||
<strong>Enable alert emails</strong>
|
/>
|
||||||
</span>
|
<span className="toggle-ui" />
|
||||||
</label>
|
<span>
|
||||||
|
<strong>Enable alert emails</strong>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div className="admin-field">
|
<div className="admin-field">
|
||||||
<label>SMTP host</label>
|
<label>SMTP host</label>
|
||||||
<input
|
<input
|
||||||
value={emailSettings.smtp_host}
|
value={emailSettings.smtp_host}
|
||||||
placeholder="smtp.example.com"
|
placeholder="smtp.example.com"
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_host: e.target.value })}
|
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_host: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-field">
|
<div className="admin-field">
|
||||||
<label>SMTP port</label>
|
<label>SMTP port</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={emailSettings.smtp_port}
|
value={emailSettings.smtp_port}
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_port: Number(e.target.value || 587) })}
|
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_port: Number(e.target.value || 587) })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="admin-field">
|
<div className="admin-field">
|
||||||
<label>SMTP username</label>
|
<label>SMTP username</label>
|
||||||
<input
|
<input
|
||||||
value={emailSettings.smtp_username}
|
value={emailSettings.smtp_username}
|
||||||
placeholder="alerts@example.com"
|
placeholder="alerts@example.com"
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_username: e.target.value })}
|
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_username: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-field">
|
<div className="admin-field">
|
||||||
<label>SMTP password</label>
|
<label>SMTP password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={emailSettings.smtp_password}
|
value={emailSettings.smtp_password}
|
||||||
placeholder={smtpState.has_password ? "Stored (enter to replace)" : "Set password"}
|
placeholder={smtpState.has_password ? "Stored (enter to replace)" : "Set password"}
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_password: e.target.value, clear_smtp_password: false })}
|
onChange={(e) => setEmailSettings({ ...emailSettings, smtp_password: e.target.value, clear_smtp_password: false })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="admin-field">
|
<div className="admin-field">
|
||||||
<label>From name</label>
|
<label>From name</label>
|
||||||
<input
|
<input
|
||||||
value={emailSettings.from_name}
|
value={emailSettings.from_name}
|
||||||
placeholder="NexaPG Alerts"
|
placeholder="NexaPG Alerts"
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, from_name: e.target.value })}
|
onChange={(e) => setEmailSettings({ ...emailSettings, from_name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-field">
|
<div className="admin-field">
|
||||||
<label>From email</label>
|
<label>From email</label>
|
||||||
<input
|
<input
|
||||||
value={emailSettings.from_email}
|
value={emailSettings.from_email}
|
||||||
placeholder="noreply@example.com"
|
placeholder="noreply@example.com"
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, from_email: e.target.value })}
|
onChange={(e) => setEmailSettings({ ...emailSettings, from_email: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-field">
|
|
||||||
<label>Transport mode</label>
|
<div className="admin-field field-full">
|
||||||
<div className="smtp-mode-picker" role="radiogroup" aria-label="SMTP transport mode">
|
<label>Transport mode</label>
|
||||||
<button
|
<div className="smtp-mode-picker" role="radiogroup" aria-label="SMTP transport mode">
|
||||||
type="button"
|
<button
|
||||||
className={`smtp-mode-btn ${protocolMode === "starttls" ? "active" : ""}`}
|
type="button"
|
||||||
aria-pressed={protocolMode === "starttls"}
|
className={`smtp-mode-btn ${protocolMode === "starttls" ? "active" : ""}`}
|
||||||
onClick={() => setProtocolMode("starttls")}
|
aria-pressed={protocolMode === "starttls"}
|
||||||
>
|
onClick={() => setProtocolMode("starttls")}
|
||||||
STARTTLS
|
>
|
||||||
</button>
|
STARTTLS
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
className={`smtp-mode-btn ${protocolMode === "ssl" ? "active" : ""}`}
|
type="button"
|
||||||
aria-pressed={protocolMode === "ssl"}
|
className={`smtp-mode-btn ${protocolMode === "ssl" ? "active" : ""}`}
|
||||||
onClick={() => setProtocolMode("ssl")}
|
aria-pressed={protocolMode === "ssl"}
|
||||||
>
|
onClick={() => setProtocolMode("ssl")}
|
||||||
SSL/TLS (SMTPS)
|
>
|
||||||
</button>
|
SSL/TLS (SMTPS)
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
className={`smtp-mode-btn ${protocolMode === "plain" ? "active" : ""}`}
|
type="button"
|
||||||
aria-pressed={protocolMode === "plain"}
|
className={`smtp-mode-btn ${protocolMode === "plain" ? "active" : ""}`}
|
||||||
onClick={() => setProtocolMode("plain")}
|
aria-pressed={protocolMode === "plain"}
|
||||||
>
|
onClick={() => setProtocolMode("plain")}
|
||||||
No TLS
|
>
|
||||||
</button>
|
No TLS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small className="muted">Select exactly one mode to avoid STARTTLS/SSL conflicts.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="toggle-check field-full">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={emailSettings.clear_smtp_password}
|
||||||
|
onChange={(e) => setEmailSettings({ ...emailSettings, clear_smtp_password: e.target.checked, smtp_password: e.target.checked ? "" : emailSettings.smtp_password })}
|
||||||
|
/>
|
||||||
|
<span className="toggle-ui" />
|
||||||
|
<span>Clear stored SMTP password</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<small className="muted">Select exactly one mode to avoid STARTTLS/SSL conflicts.</small>
|
|
||||||
</div>
|
|
||||||
<div className="admin-field">
|
|
||||||
<label>Template variables</label>
|
|
||||||
<small className="muted">
|
|
||||||
Use placeholders like: {"{target_name}"}, {"{alert_name}"}, {"{severity}"}, {"{description}"}, {"{message}"}, {"{value}"}, {"{warning_threshold}"}, {"{alert_threshold}"}, {"{checked_at}"}, {"{alert_key}"}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="admin-field field-full">
|
<div className="admin-subcard field-full">
|
||||||
<label>Warning subject template</label>
|
<h4>Template Settings</h4>
|
||||||
<input
|
<p className="muted template-help-text">
|
||||||
value={emailSettings.warning_subject_template}
|
If a template field is left empty, NexaPG automatically uses the built-in default template.
|
||||||
placeholder="[NexaPG][WARNING] {target_name} - {alert_name}"
|
</p>
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, warning_subject_template: e.target.value })}
|
<div className="template-vars-grid">
|
||||||
/>
|
{TEMPLATE_VARIABLES.map((item) => (
|
||||||
</div>
|
<code key={item} className="template-var-pill">
|
||||||
<div className="admin-field field-full">
|
{"{" + item + "}"}
|
||||||
<label>Alert subject template</label>
|
</code>
|
||||||
<input
|
))}
|
||||||
value={emailSettings.alert_subject_template}
|
</div>
|
||||||
placeholder="[NexaPG][ALERT] {target_name} - {alert_name}"
|
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, alert_subject_template: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-field field-full">
|
|
||||||
<label>Warning body template</label>
|
|
||||||
<textarea
|
|
||||||
value={emailSettings.warning_body_template}
|
|
||||||
placeholder={"Severity: {severity}\nTarget: {target_name} (id={target_id})\nAlert: {alert_name}\nMessage: {message}\nChecked At: {checked_at}"}
|
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, warning_body_template: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-field field-full">
|
|
||||||
<label>Alert body template</label>
|
|
||||||
<textarea
|
|
||||||
value={emailSettings.alert_body_template}
|
|
||||||
placeholder={"Severity: {severity}\nTarget: {target_name} (id={target_id})\nAlert: {alert_name}\nMessage: {message}\nCurrent Value: {value}\nWarning Threshold: {warning_threshold}\nAlert Threshold: {alert_threshold}\nChecked At: {checked_at}\nAlert Key: {alert_key}"}
|
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, alert_body_template: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="toggle-check field-full">
|
<div className="grid two">
|
||||||
<input
|
<div className="admin-field field-full">
|
||||||
type="checkbox"
|
<label>Warning subject template</label>
|
||||||
checked={emailSettings.clear_smtp_password}
|
<input
|
||||||
onChange={(e) => setEmailSettings({ ...emailSettings, clear_smtp_password: e.target.checked, smtp_password: e.target.checked ? "" : emailSettings.smtp_password })}
|
value={emailSettings.warning_subject_template}
|
||||||
/>
|
placeholder="[NexaPG][WARNING] {target_name} - {alert_name}"
|
||||||
<span className="toggle-ui" />
|
onChange={(e) => setEmailSettings({ ...emailSettings, warning_subject_template: e.target.value })}
|
||||||
<span>Clear stored SMTP password</span>
|
/>
|
||||||
</label>
|
</div>
|
||||||
|
<div className="admin-field field-full">
|
||||||
|
<label>Alert subject template</label>
|
||||||
|
<input
|
||||||
|
value={emailSettings.alert_subject_template}
|
||||||
|
placeholder="[NexaPG][ALERT] {target_name} - {alert_name}"
|
||||||
|
onChange={(e) => setEmailSettings({ ...emailSettings, alert_subject_template: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="admin-field field-full">
|
||||||
|
<label>Warning body template</label>
|
||||||
|
<textarea
|
||||||
|
value={emailSettings.warning_body_template}
|
||||||
|
placeholder={"Severity: {severity}\nTarget: {target_name} (id={target_id})\nAlert: {alert_name}\nMessage: {message}\nChecked At: {checked_at}"}
|
||||||
|
onChange={(e) => setEmailSettings({ ...emailSettings, warning_body_template: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="admin-field field-full">
|
||||||
|
<label>Alert body template</label>
|
||||||
|
<textarea
|
||||||
|
value={emailSettings.alert_body_template}
|
||||||
|
placeholder={"Severity: {severity}\nTarget: {target_name} (id={target_id})\nAlert: {alert_name}\nMessage: {message}\nCurrent Value: {value}\nWarning Threshold: {warning_threshold}\nAlert Threshold: {alert_threshold}\nChecked At: {checked_at}\nAlert Key: {alert_key}"}
|
||||||
|
onChange={(e) => setEmailSettings({ ...emailSettings, alert_body_template: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-actions field-full">
|
<div className="form-actions field-full">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1208,6 +1208,40 @@ td {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-subcard {
|
||||||
|
border: 1px solid #2c598f;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(180deg, #102748, #0e2342);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-subcard h4 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-help-text {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-vars-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-var-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #4d81bc;
|
||||||
|
background: #15365f;
|
||||||
|
color: #d9ebff;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-test-recipient {
|
.admin-test-recipient {
|
||||||
min-width: 260px;
|
min-width: 260px;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
|
|||||||
Reference in New Issue
Block a user