mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-29 20:22:59 +00:00
adjust email template for alerts
This commit is contained in:
@@ -8,9 +8,11 @@ import {
|
|||||||
EmailHeading,
|
EmailHeading,
|
||||||
EmailInfoSection,
|
EmailInfoSection,
|
||||||
EmailLetterHead,
|
EmailLetterHead,
|
||||||
|
EmailSection,
|
||||||
EmailSignature,
|
EmailSignature,
|
||||||
EmailText
|
EmailText
|
||||||
} from "./components/Email";
|
} from "./components/Email";
|
||||||
|
import ButtonLink from "./components/ButtonLink";
|
||||||
|
|
||||||
export type AlertEventType =
|
export type AlertEventType =
|
||||||
| "site_online"
|
| "site_online"
|
||||||
@@ -23,11 +25,38 @@ export type AlertEventType =
|
|||||||
| "resource_unhealthy"
|
| "resource_unhealthy"
|
||||||
| "resource_toggle";
|
| "resource_toggle";
|
||||||
|
|
||||||
interface Props {
|
// ---------------------------------------------------------------------------
|
||||||
|
// Local preview / layout testing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Set to `true` while running `npm run email` or otherwise rendering this
|
||||||
|
// template without real alert context. Uses `alertNotificationFixture` below
|
||||||
|
// and ignores props passed by callers (including real alert sends). Must be
|
||||||
|
// `false` before shipping or triggering real alert emails.
|
||||||
|
|
||||||
|
export const USE_FAKE_ALERT_NOTIFICATION_DATA = true;
|
||||||
|
|
||||||
|
export type AlertNotificationProps = {
|
||||||
eventType: AlertEventType;
|
eventType: AlertEventType;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
data: Record<string, unknown>;
|
data: Record<string, unknown>;
|
||||||
}
|
dashboardLink: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Sample props for previews; also used when `USE_FAKE_ALERT_NOTIFICATION_DATA` is true. */
|
||||||
|
export const alertNotificationFixture: AlertNotificationProps = {
|
||||||
|
eventType: "site_online",
|
||||||
|
orgId: "org_preview_7a3c2f91",
|
||||||
|
dashboardLink:
|
||||||
|
"https://app.pangolin.net/org_preview_7a3c2f91/settings/alerting/rules",
|
||||||
|
data: {
|
||||||
|
siteId: 42,
|
||||||
|
healthCheckName: "Edge API – readiness probe",
|
||||||
|
targetUrl: "https://api.example.com/internal/health",
|
||||||
|
lastFailureMessage: "Connection timed out after 5000ms",
|
||||||
|
consecutiveFailures: 3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function getEventMeta(eventType: AlertEventType): {
|
function getEventMeta(eventType: AlertEventType): {
|
||||||
heading: string;
|
heading: string;
|
||||||
@@ -51,7 +80,7 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
heading: "Site Offline",
|
heading: "Site Offline",
|
||||||
previewText: "A site in your organization has gone offline.",
|
previewText: "A site in your organization has gone offline.",
|
||||||
summary:
|
summary:
|
||||||
"A site in your organization has gone offline and is no longer reachable. Please investigate as soon as possible.",
|
"A site in your organization has gone offline and is no longer reachable.",
|
||||||
statusLabel: "Offline",
|
statusLabel: "Offline",
|
||||||
statusColor: "#dc2626"
|
statusColor: "#dc2626"
|
||||||
};
|
};
|
||||||
@@ -59,8 +88,7 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
return {
|
return {
|
||||||
heading: "Site Status Changed",
|
heading: "Site Status Changed",
|
||||||
previewText: "A site in your organization has changed status.",
|
previewText: "A site in your organization has changed status.",
|
||||||
summary:
|
summary: "A site in your organization has changed status.",
|
||||||
"A site in your organization has changed status. Please review the details below and take action if needed.",
|
|
||||||
statusLabel: "Status Changed",
|
statusLabel: "Status Changed",
|
||||||
statusColor: "#f59e0b"
|
statusColor: "#f59e0b"
|
||||||
};
|
};
|
||||||
@@ -80,7 +108,7 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
previewText:
|
previewText:
|
||||||
"A health check in your organization is not healthy.",
|
"A health check in your organization is not healthy.",
|
||||||
summary:
|
summary:
|
||||||
"A health check in your organization is currently failing. Please review the details below and take action if needed.",
|
"A health check in your organization is currently failing.",
|
||||||
statusLabel: "Not Healthy",
|
statusLabel: "Not Healthy",
|
||||||
statusColor: "#dc2626"
|
statusColor: "#dc2626"
|
||||||
};
|
};
|
||||||
@@ -90,7 +118,7 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
previewText:
|
previewText:
|
||||||
"A health check in your organization has changed status.",
|
"A health check in your organization has changed status.",
|
||||||
summary:
|
summary:
|
||||||
"A health check in your organization has changed status. Please review the details below and take action if needed.",
|
"A health check in your organization has changed status.",
|
||||||
statusLabel: "Status Changed",
|
statusLabel: "Status Changed",
|
||||||
statusColor: "#f59e0b"
|
statusColor: "#f59e0b"
|
||||||
};
|
};
|
||||||
@@ -108,7 +136,7 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
heading: "Resource Unhealthy",
|
heading: "Resource Unhealthy",
|
||||||
previewText: "A resource in your organization is not healthy.",
|
previewText: "A resource in your organization is not healthy.",
|
||||||
summary:
|
summary:
|
||||||
"A resource in your organization is currently unhealthy. Please review the details below and take action if needed.",
|
"A resource in your organization is currently unhealthy.",
|
||||||
statusLabel: "Unhealthy",
|
statusLabel: "Unhealthy",
|
||||||
statusColor: "#dc2626"
|
statusColor: "#dc2626"
|
||||||
};
|
};
|
||||||
@@ -117,17 +145,16 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
heading: "Resource Status Changed",
|
heading: "Resource Status Changed",
|
||||||
previewText:
|
previewText:
|
||||||
"A resource in your organization has changed status.",
|
"A resource in your organization has changed status.",
|
||||||
summary:
|
summary: "A resource in your organization has changed status.",
|
||||||
"A resource in your organization has changed status. Please review the details below and take action if needed.",
|
|
||||||
statusLabel: "Status Changed",
|
statusLabel: "Status Changed",
|
||||||
statusColor: "#f59e0b"
|
statusColor: "#f59e0b"
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
heading: "Alert Notification",
|
heading: "Alert Notification",
|
||||||
previewText: "An alert event has occurred in your organization.",
|
previewText:
|
||||||
summary:
|
"An alert event has occurred in your organization.",
|
||||||
"An alert event has occurred in your organization. Please review the details below and take action if needed.",
|
summary: "An alert event has occurred in your organization.",
|
||||||
statusLabel: "Alert",
|
statusLabel: "Alert",
|
||||||
statusColor: "#f59e0b"
|
statusColor: "#f59e0b"
|
||||||
};
|
};
|
||||||
@@ -148,17 +175,22 @@ function formatDataItems(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlertNotification = ({ eventType, orgId, data }: Props) => {
|
export const AlertNotification = (props: AlertNotificationProps) => {
|
||||||
|
const { eventType, orgId, data, dashboardLink } =
|
||||||
|
USE_FAKE_ALERT_NOTIFICATION_DATA ? alertNotificationFixture : props;
|
||||||
const meta = getEventMeta(eventType);
|
const meta = getEventMeta(eventType);
|
||||||
const dataItems = formatDataItems(data);
|
const dataItems = formatDataItems(data);
|
||||||
|
|
||||||
const allItems: { label: string; value: React.ReactNode }[] = [
|
const allItems: { label: string; value: React.ReactNode }[] = [
|
||||||
{ label: "Organization", value: orgId },
|
{ label: "Organization", value: orgId },
|
||||||
{ label: "Status", value: (
|
{
|
||||||
<span style={{ color: meta.statusColor, fontWeight: 600 }}>
|
label: "Status",
|
||||||
{meta.statusLabel}
|
value: (
|
||||||
</span>
|
<span style={{ color: meta.statusColor, fontWeight: 600 }}>
|
||||||
)},
|
{meta.statusLabel}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
{ label: "Time", value: new Date().toUTCString() },
|
{ label: "Time", value: new Date().toUTCString() },
|
||||||
...dataItems
|
...dataItems
|
||||||
];
|
];
|
||||||
@@ -184,10 +216,16 @@ export const AlertNotification = ({ eventType, orgId, data }: Props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<EmailText>
|
<EmailText>
|
||||||
Log in to your dashboard to view more details and
|
Open your dashboard to view more details and manage
|
||||||
manage your alert rules.
|
your alert rules.
|
||||||
</EmailText>
|
</EmailText>
|
||||||
|
|
||||||
|
<EmailSection>
|
||||||
|
<ButtonLink href={dashboardLink}>
|
||||||
|
Open Dashboard
|
||||||
|
</ButtonLink>
|
||||||
|
</EmailSection>
|
||||||
|
|
||||||
<EmailFooter>
|
<EmailFooter>
|
||||||
<EmailSignature />
|
<EmailSignature />
|
||||||
</EmailFooter>
|
</EmailFooter>
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export const NotifyTrialExpiring = ({
|
|||||||
: `Your trial for ${orgName} ends in ${daysRemaining} days.`;
|
: `Your trial for ${orgName} ends in ${daysRemaining} days.`;
|
||||||
|
|
||||||
const heading = hasEnded
|
const heading = hasEnded
|
||||||
? "Your Trial Has Ended"
|
? "Your Trial Ended"
|
||||||
: "Your Trial Is Ending Soon";
|
: "Your Trial is Ending Soon";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
@@ -124,4 +124,4 @@ export const NotifyTrialExpiring = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotifyTrialExpiring;
|
export default NotifyTrialExpiring;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { build } from "@server/build";
|
|||||||
// EmailContainer: Wraps the entire email layout
|
// EmailContainer: Wraps the entire email layout
|
||||||
export function EmailContainer({ children }: { children: React.ReactNode }) {
|
export function EmailContainer({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Container className="bg-white border border-solid border-gray-200 max-w-lg mx-auto my-8 rounded-lg overflow-hidden shadow-sm">
|
<Container className="bg-white border border-solid border-gray-200 max-w-lg mx-auto my-8 rounded-xl overflow-hidden">
|
||||||
{children}
|
{children}
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
@@ -18,7 +18,7 @@ export function EmailLetterHead() {
|
|||||||
<Img
|
<Img
|
||||||
src="https://fossorial-public-assets.s3.us-east-1.amazonaws.com/word_mark_black.png"
|
src="https://fossorial-public-assets.s3.us-east-1.amazonaws.com/word_mark_black.png"
|
||||||
alt="Pangolin Logo"
|
alt="Pangolin Logo"
|
||||||
width="120"
|
width="180"
|
||||||
height="auto"
|
height="auto"
|
||||||
className="mx-auto"
|
className="mx-auto"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export async function fireHealthCheckHealthyAlert(
|
|||||||
orgId,
|
orgId,
|
||||||
healthCheckId,
|
healthCheckId,
|
||||||
data: {
|
data: {
|
||||||
healthCheckId,
|
|
||||||
...(healthCheckName != null ? { healthCheckName } : {}),
|
...(healthCheckName != null ? { healthCheckName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
@@ -87,7 +86,6 @@ export async function fireHealthCheckNotHealthyAlert(
|
|||||||
orgId,
|
orgId,
|
||||||
healthCheckId,
|
healthCheckId,
|
||||||
data: {
|
data: {
|
||||||
healthCheckId,
|
|
||||||
...(healthCheckName != null ? { healthCheckName } : {}),
|
...(healthCheckName != null ? { healthCheckName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export async function fireResourceHealthyAlert(
|
|||||||
orgId,
|
orgId,
|
||||||
resourceId,
|
resourceId,
|
||||||
data: {
|
data: {
|
||||||
resourceId,
|
|
||||||
...(resourceName != null ? { resourceName } : {}),
|
...(resourceName != null ? { resourceName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
@@ -87,7 +86,6 @@ export async function fireResourceUnhealthyAlert(
|
|||||||
orgId,
|
orgId,
|
||||||
resourceId,
|
resourceId,
|
||||||
data: {
|
data: {
|
||||||
resourceId,
|
|
||||||
...(resourceName != null ? { resourceName } : {}),
|
...(resourceName != null ? { resourceName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
@@ -133,7 +131,6 @@ export async function fireResourceToggleAlert(
|
|||||||
orgId,
|
orgId,
|
||||||
resourceId,
|
resourceId,
|
||||||
data: {
|
data: {
|
||||||
resourceId,
|
|
||||||
...(resourceName != null ? { resourceName } : {}),
|
...(resourceName != null ? { resourceName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export async function fireSiteOnlineAlert(
|
|||||||
orgId,
|
orgId,
|
||||||
siteId,
|
siteId,
|
||||||
data: {
|
data: {
|
||||||
siteId,
|
|
||||||
...(siteName != null ? { siteName } : {}),
|
...(siteName != null ? { siteName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
@@ -87,7 +86,6 @@ export async function fireSiteOfflineAlert(
|
|||||||
orgId,
|
orgId,
|
||||||
siteId,
|
siteId,
|
||||||
data: {
|
data: {
|
||||||
siteId,
|
|
||||||
...(siteName != null ? { siteName } : {}),
|
...(siteName != null ? { siteName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,17 @@ export async function sendAlertEmail(
|
|||||||
const from = config.getNoReplyEmail();
|
const from = config.getNoReplyEmail();
|
||||||
const subject = buildSubject(context);
|
const subject = buildSubject(context);
|
||||||
|
|
||||||
|
const baseUrl = config.getRawConfig().app.dashboard_url!.replace(/\/$/, "");
|
||||||
|
const dashboardLink = `${baseUrl}/${context.orgId}/settings`;
|
||||||
|
|
||||||
for (const to of recipients) {
|
for (const to of recipients) {
|
||||||
try {
|
try {
|
||||||
await sendEmail(
|
await sendEmail(
|
||||||
AlertNotification({
|
AlertNotification({
|
||||||
eventType: context.eventType,
|
eventType: context.eventType,
|
||||||
orgId: context.orgId,
|
orgId: context.orgId,
|
||||||
data: context.data
|
data: context.data,
|
||||||
|
dashboardLink
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
from,
|
from,
|
||||||
|
|||||||
Reference in New Issue
Block a user