feat(email): add Send Email page and integrate with navigation

This commit is contained in:
Nawaz Dhandala
2026-03-24 09:05:33 +00:00
parent 296dc9c81e
commit 29c2bbbf57
7 changed files with 451 additions and 2 deletions

View File

@@ -12,6 +12,7 @@ import SettingsEmail from "./Pages/Settings/Email/Index";
import SettingsProbes from "./Pages/Settings/Probes/Index";
import SettingsAIAgents from "./Pages/Settings/AIAgents/Index";
import SettingsLlmProviders from "./Pages/Settings/LlmProviders/Index";
import SendEmail from "./Pages/SendEmail/Index";
import Users from "./Pages/Users/Index";
import PageMap from "./Utils/PageMap";
import RouteMap from "./Utils/RouteMap";
@@ -149,6 +150,11 @@ const App: () => JSX.Element = () => {
path={RouteMap[PageMap.SETTINGS_DATA_RETENTION]?.toString() || ""}
element={<SettingsDataRetention />}
/>
<PageRoute
path={RouteMap[PageMap.SEND_EMAIL]?.toString() || ""}
element={<SendEmail />}
/>
</Routes>
</MasterPage>
);

View File

@@ -2,7 +2,10 @@ import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import IconProp from "Common/Types/Icon/IconProp";
import NavBar, { NavItem } from "Common/UI/Components/Navbar/NavBar";
import NavBar, {
MoreMenuItem,
NavItem,
} from "Common/UI/Components/Navbar/NavBar";
import React, { FunctionComponent, ReactElement } from "react";
const DashboardNavbar: FunctionComponent = (): ReactElement => {
@@ -28,7 +31,21 @@ const DashboardNavbar: FunctionComponent = (): ReactElement => {
},
];
return <NavBar items={navItems} />;
const moreMenuItems: MoreMenuItem[] = [
{
title: "Send Email",
description: "Send announcement emails to all registered users.",
icon: IconProp.Email,
route: RouteUtil.populateRouteParams(
RouteMap[PageMap.SEND_EMAIL] as Route,
),
activeRoute: RouteUtil.populateRouteParams(
RouteMap[PageMap.SEND_EMAIL] as Route,
),
},
];
return <NavBar items={navItems} moreMenuItems={moreMenuItems} />;
};
export default DashboardNavbar;

View File

@@ -0,0 +1,282 @@
import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import React, { FunctionComponent, ReactElement, useState } from "react";
import Page from "Common/UI/Components/Page/Page";
import Card from "Common/UI/Components/Card/Card";
import BasicForm from "Common/UI/Components/Forms/BasicForm";
import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import { JSONObject } from "Common/Types/JSON";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import API from "Common/UI/Utils/API/API";
import { APP_API_URL } from "Common/UI/Config";
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
const SendEmail: FunctionComponent = (): ReactElement => {
const [isSendingTest, setIsSendingTest] = useState<boolean>(false);
const [isSendingAll, setIsSendingAll] = useState<boolean>(false);
const [testError, setTestError] = useState<string>("");
const [testSuccess, setTestSuccess] = useState<string>("");
const [sendAllError, setSendAllError] = useState<string>("");
const [sendAllSuccess, setSendAllSuccess] = useState<string>("");
const [showConfirmModal, setShowConfirmModal] = useState<boolean>(false);
const [pendingSubject, setPendingSubject] = useState<string>("");
const [pendingMessage, setPendingMessage] = useState<string>("");
const sendToAllUsers: () => Promise<void> = async (): Promise<void> => {
setIsSendingAll(true);
setSendAllError("");
setSendAllSuccess("");
try {
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post({
url: URL.fromString(APP_API_URL.toString()).addRoute(
"/notification/broadcast-email/send-to-all-users",
),
data: {
subject: pendingSubject,
message: pendingMessage,
},
});
if (response instanceof HTTPErrorResponse) {
throw response;
}
if (response.isFailure()) {
throw new Error("Failed to send emails.");
}
const data: JSONObject = response.data as JSONObject;
setSendAllSuccess(
`Emails sent successfully. Total users: ${data["totalUsers"]}, Sent: ${data["sentCount"]}, Errors: ${data["errorCount"]}`,
);
} catch (err) {
setSendAllError(API.getFriendlyMessage(err));
} finally {
setIsSendingAll(false);
}
};
return (
<Page
title={"Send Announcement Email"}
breadcrumbLinks={[
{
title: "Admin Dashboard",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
},
{
title: "Send Email",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SEND_EMAIL] as Route,
),
},
]}
>
<Card
title="Send Test Email"
description="Send a test announcement email to a single email address to preview how it looks before sending to all users."
>
{testSuccess ? (
<Alert
type={AlertType.SUCCESS}
title={testSuccess}
className="mb-4"
/>
) : (
<></>
)}
<BasicForm
id="send-test-email-form"
name="Send Test Announcement Email"
isLoading={isSendingTest}
error={testError || ""}
submitButtonText="Send Test Email"
maxPrimaryButtonWidth={true}
initialValues={{
subject: "",
message: "",
testEmail: "",
}}
fields={[
{
field: {
subject: true,
},
title: "Subject",
description: "The subject line of the announcement email.",
placeholder: "Enter email subject",
required: true,
fieldType: FormFieldSchemaType.Text,
},
{
field: {
message: true,
},
title: "Message",
description:
"The body of the announcement email. This will be displayed in a branded OneUptime email template.",
placeholder: "Enter your announcement message here...",
required: true,
fieldType: FormFieldSchemaType.LongText,
},
{
field: {
testEmail: true,
},
title: "Test Email Address",
description:
"The email address where the test email will be sent.",
placeholder: "test@example.com",
required: true,
fieldType: FormFieldSchemaType.Email,
disableSpellCheck: true,
},
]}
onSubmit={async (
values: JSONObject,
onSubmitSuccessful?: () => void,
) => {
const subject: string = String(values["subject"] || "").trim();
const message: string = String(values["message"] || "").trim();
const testEmail: string = String(values["testEmail"] || "").trim();
if (!subject || !message || !testEmail) {
setTestSuccess("");
setTestError("Please fill in all fields.");
return;
}
setIsSendingTest(true);
setTestError("");
setTestSuccess("");
try {
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post({
url: URL.fromString(APP_API_URL.toString()).addRoute(
"/notification/broadcast-email/send-test",
),
data: {
subject,
message,
testEmail,
},
});
if (response instanceof HTTPErrorResponse) {
throw response;
}
if (response.isFailure()) {
throw new Error("Failed to send test email.");
}
setTestSuccess(
"Test email sent successfully. Please check your inbox.",
);
if (onSubmitSuccessful) {
onSubmitSuccessful();
}
} catch (err) {
setTestError(API.getFriendlyMessage(err));
} finally {
setIsSendingTest(false);
}
}}
/>
</Card>
<Card
title="Send Email to All Users"
description="Send an announcement email to all registered users. Please send a test email first to verify the content."
>
{sendAllSuccess ? (
<Alert
type={AlertType.SUCCESS}
title={sendAllSuccess}
className="mb-4"
/>
) : (
<></>
)}
<BasicForm
id="send-all-email-form"
name="Send Announcement to All Users"
isLoading={isSendingAll}
error={sendAllError || ""}
submitButtonText="Send to All Users"
maxPrimaryButtonWidth={true}
initialValues={{
subject: "",
message: "",
}}
fields={[
{
field: {
subject: true,
},
title: "Subject",
description: "The subject line of the announcement email.",
placeholder: "Enter email subject",
required: true,
fieldType: FormFieldSchemaType.Text,
},
{
field: {
message: true,
},
title: "Message",
description:
"The body of the announcement email. This will be sent to all registered users.",
placeholder: "Enter your announcement message here...",
required: true,
fieldType: FormFieldSchemaType.LongText,
},
]}
onSubmit={async (values: JSONObject) => {
const subject: string = String(values["subject"] || "").trim();
const message: string = String(values["message"] || "").trim();
if (!subject || !message) {
setSendAllSuccess("");
setSendAllError("Please fill in all fields.");
return;
}
setPendingSubject(subject);
setPendingMessage(message);
setShowConfirmModal(true);
}}
/>
</Card>
{showConfirmModal ? (
<ConfirmModal
title="Confirm Send to All Users"
description="Are you sure you want to send this announcement email to all registered users? This action cannot be undone."
submitButtonText="Yes, Send to All Users"
onSubmit={async () => {
setShowConfirmModal(false);
await sendToAllUsers();
}}
onClose={() => {
setShowConfirmModal(false);
}}
/>
) : (
<></>
)}
</Page>
);
};
export default SendEmail;

View File

@@ -23,6 +23,8 @@ enum PageMap {
SETTINGS_AUTHENTICATION = "SETTINGS_AUTHENTICATION",
SETTINGS_API_KEY = "SETTINGS_API_KEY",
SETTINGS_DATA_RETENTION = "SETTINGS_DATA_RETENTION",
SEND_EMAIL = "SEND_EMAIL",
}
export default PageMap;

View File

@@ -39,6 +39,8 @@ const RouteMap: Dictionary<Route> = {
[PageMap.SETTINGS_DATA_RETENTION]: new Route(
`/admin/settings/data-retention`,
),
[PageMap.SEND_EMAIL]: new Route(`/admin/send-email`),
};
export class RouteUtil {

View File

@@ -0,0 +1,135 @@
import MailService from "../Services/MailService";
import Email from "Common/Types/Email";
import EmailMessage from "Common/Types/Email/EmailMessage";
import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONObject } from "Common/Types/JSON";
import LIMIT_MAX from "Common/Types/Database/LimitMax";
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
import UserService from "Common/Server/Services/UserService";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import logger from "Common/Server/Utils/Logger";
import User from "Common/Models/DatabaseModels/User";
const router: ExpressRouter = Express.getRouter();
router.post(
"/send-test",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const subject: string = body["subject"] as string;
const message: string = body["message"] as string;
const testEmail: string = body["testEmail"] as string;
if (!subject) {
throw new BadDataException("Subject is required");
}
if (!message) {
throw new BadDataException("Message is required");
}
if (!testEmail) {
throw new BadDataException("Test email address is required");
}
const mail: EmailMessage = {
templateType: EmailTemplateType.SimpleMessage,
toEmail: new Email(testEmail),
subject: subject,
vars: {
subject: subject,
message: message,
},
body: "",
};
await MailService.send(mail);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.post(
"/send-to-all-users",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const subject: string = body["subject"] as string;
const message: string = body["message"] as string;
if (!subject) {
throw new BadDataException("Subject is required");
}
if (!message) {
throw new BadDataException("Message is required");
}
const users: Array<User> = await UserService.findAllBy({
query: {},
select: {
email: true,
},
skip: 0,
props: {
isRoot: true,
},
});
let sentCount: number = 0;
let errorCount: number = 0;
for (const user of users) {
if (!user.email) {
continue;
}
try {
const mail: EmailMessage = {
templateType: EmailTemplateType.SimpleMessage,
toEmail: user.email,
subject: subject,
vars: {
subject: subject,
message: message,
},
body: "",
};
await MailService.send(mail);
sentCount++;
} catch (err) {
errorCount++;
logger.error(
`Failed to send broadcast email to ${user.email.toString()}: ${err}`,
);
}
}
return Response.sendJsonObjectResponse(req, res, {
totalUsers: users.length,
sentCount: sentCount,
errorCount: errorCount,
});
} catch (err) {
return next(err);
}
},
);
export default router;

View File

@@ -1,3 +1,4 @@
import BroadcastEmailAPI from "./API/BroadcastEmail";
import CallAPI from "./API/Call";
// API
import MailAPI from "./API/Mail";
@@ -27,6 +28,10 @@ const NotificationFeatureSet: FeatureSet = {
app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);
app.use([`/${APP_NAME}/phone-number`, "/phone-number"], PhoneNumberAPI);
app.use([`/${APP_NAME}/incoming-call`, "/incoming-call"], IncomingCallAPI);
app.use(
[`/${APP_NAME}/broadcast-email`, "/broadcast-email"],
BroadcastEmailAPI,
);
},
};