diff --git a/server/private/routers/alertRule/listAlertRules.ts b/server/private/routers/alertRule/listAlertRules.ts index e6a2e6381..73f11ca03 100644 --- a/server/private/routers/alertRule/listAlertRules.ts +++ b/server/private/routers/alertRule/listAlertRules.ts @@ -14,14 +14,19 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { alertRules, alertSites, alertHealthChecks, alertResources } from "@server/db"; +import { + alertRules, + alertSites, + alertHealthChecks, + alertResources +} from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { and, asc, desc, eq, inArray, like, sql } from "drizzle-orm"; +import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty() @@ -51,11 +56,34 @@ const querySchema = z.strictObject({ .optional() .transform((v) => (v !== undefined ? Number(v) : undefined)) .pipe(z.number().int().positive().optional()), + healthCheckId: z + .string() + .optional() + .transform((v) => (v !== undefined ? Number(v) : undefined)) + .pipe(z.number().int().positive().optional()), sort_by: z.enum(["name", "last_triggered_at"]).optional(), order: z.enum(["asc", "desc"]).optional().default("asc"), enabled: z.enum(["true", "false"]).optional() }); +const SITE_ALERT_EVENT_TYPES = [ + "site_online", + "site_offline", + "site_toggle" +] as const; + +const RESOURCE_ALERT_EVENT_TYPES = [ + "resource_healthy", + "resource_unhealthy", + "resource_toggle" +] as const; + +const HEALTH_CHECK_ALERT_EVENT_TYPES = [ + "health_check_healthy", + "health_check_unhealthy", + "health_check_toggle" +] as const; + export type ListAlertRulesResponse = { alertRules: { alertRuleId: number; @@ -122,66 +150,110 @@ export async function listAlertRules( query, siteId, resourceId, + healthCheckId, sort_by, order, enabled: enabledFilter } = parsedQuery.data; - // Resolve siteId filter → matching alertRuleIds - let siteFilterRuleIds: number[] | null = null; - if (siteId !== undefined) { - const rows = await db - .select({ alertRuleId: alertSites.alertRuleId }) - .from(alertSites) - .where(eq(alertSites.siteId, siteId)); - siteFilterRuleIds = rows.map((r) => r.alertRuleId); - if (siteFilterRuleIds.length === 0) { - return response(res, { - data: { - alertRules: [], - pagination: { total: 0, limit, offset } - }, - success: true, - error: false, - message: "Alert rules retrieved successfully", - status: HttpCode.OK - }); - } - } + const explicitSiteRuleIds: number[] = + siteId !== undefined + ? ( + await db + .select({ alertRuleId: alertSites.alertRuleId }) + .from(alertSites) + .where(eq(alertSites.siteId, siteId)) + ).map((r) => r.alertRuleId) + : []; - // Resolve resourceId filter → matching alertRuleIds - let resourceFilterRuleIds: number[] | null = null; - if (resourceId !== undefined) { - const rows = await db - .select({ alertRuleId: alertResources.alertRuleId }) - .from(alertResources) - .where(eq(alertResources.resourceId, resourceId)); - resourceFilterRuleIds = rows.map((r) => r.alertRuleId); - if (resourceFilterRuleIds.length === 0) { - return response(res, { - data: { - alertRules: [], - pagination: { total: 0, limit, offset } - }, - success: true, - error: false, - message: "Alert rules retrieved successfully", - status: HttpCode.OK - }); - } - } + const explicitResourceRuleIds: number[] = + resourceId !== undefined + ? ( + await db + .select({ + alertRuleId: alertResources.alertRuleId + }) + .from(alertResources) + .where(eq(alertResources.resourceId, resourceId)) + ).map((r) => r.alertRuleId) + : []; + + const explicitHealthCheckRuleIds: number[] = + healthCheckId !== undefined + ? ( + await db + .select({ + alertRuleId: alertHealthChecks.alertRuleId + }) + .from(alertHealthChecks) + .where( + eq(alertHealthChecks.healthCheckId, healthCheckId) + ) + ).map((r) => r.alertRuleId) + : []; + + const allSitesWildcardClause = and( + eq(alertRules.allSites, true), + inArray(alertRules.eventType, SITE_ALERT_EVENT_TYPES) + ); + + const siteScopeClause = + siteId !== undefined + ? explicitSiteRuleIds.length > 0 + ? or( + allSitesWildcardClause, + inArray(alertRules.alertRuleId, explicitSiteRuleIds) + ) + : allSitesWildcardClause + : undefined; + + const allResourcesWildcardClause = and( + eq(alertRules.allResources, true), + inArray(alertRules.eventType, RESOURCE_ALERT_EVENT_TYPES) + ); + + const resourceScopeClause = + resourceId !== undefined + ? explicitResourceRuleIds.length > 0 + ? or( + allResourcesWildcardClause, + inArray( + alertRules.alertRuleId, + explicitResourceRuleIds + ) + ) + : allResourcesWildcardClause + : undefined; + + const allHealthChecksWildcardClause = and( + eq(alertRules.allHealthChecks, true), + inArray(alertRules.eventType, HEALTH_CHECK_ALERT_EVENT_TYPES) + ); + + const healthCheckScopeClause = + healthCheckId !== undefined + ? explicitHealthCheckRuleIds.length > 0 + ? or( + allHealthChecksWildcardClause, + inArray( + alertRules.alertRuleId, + explicitHealthCheckRuleIds + ) + ) + : allHealthChecksWildcardClause + : undefined; const whereClause = and( eq(alertRules.orgId, orgId), query - ? like(sql`LOWER(${alertRules.name})`, `%${query.toLowerCase()}%`) - : undefined, - siteFilterRuleIds !== null - ? inArray(alertRules.alertRuleId, siteFilterRuleIds) - : undefined, - resourceFilterRuleIds !== null - ? inArray(alertRules.alertRuleId, resourceFilterRuleIds) + ? like( + sql`LOWER(${alertRules.name})`, + `%${query.toLowerCase()}%` + ) : undefined, + siteScopeClause, + resourceScopeClause, + healthCheckScopeClause, enabledFilter !== undefined ? eq(alertRules.enabled, enabledFilter === "true") : undefined @@ -228,9 +300,7 @@ export async function listAlertRules( ? await db .select() .from(alertHealthChecks) - .where( - inArray(alertHealthChecks.alertRuleId, ruleIds) - ) + .where(inArray(alertHealthChecks.alertRuleId, ruleIds)) : []; const resourceRows = @@ -297,4 +367,4 @@ export async function listAlertRules( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/alerting/(list)/rules/page.tsx b/src/app/[orgId]/settings/alerting/(list)/rules/page.tsx index 0bdcd817c..ee2a561bf 100644 --- a/src/app/[orgId]/settings/alerting/(list)/rules/page.tsx +++ b/src/app/[orgId]/settings/alerting/(list)/rules/page.tsx @@ -46,6 +46,9 @@ export default async function AlertingRulesPage(props: AlertingRulesPageProps) { const resourceId = parsePositiveInt( searchParams.get("resourceId") ?? undefined ); + const healthCheckId = parsePositiveInt( + searchParams.get("healthCheckId") ?? undefined + ); const apiSp = new URLSearchParams(); apiSp.set("limit", String(pageSize)); @@ -53,6 +56,8 @@ export default async function AlertingRulesPage(props: AlertingRulesPageProps) { if (query) apiSp.set("query", query); if (siteId != null) apiSp.set("siteId", String(siteId)); if (resourceId != null) apiSp.set("resourceId", String(resourceId)); + if (healthCheckId != null) + apiSp.set("healthCheckId", String(healthCheckId)); if (sortBy) { apiSp.set("sort_by", sortBy); if (order) apiSp.set("order", order); diff --git a/src/components/UptimeAlertSection.tsx b/src/components/UptimeAlertSection.tsx index f7647c977..a43ef858b 100644 --- a/src/components/UptimeAlertSection.tsx +++ b/src/components/UptimeAlertSection.tsx @@ -51,7 +51,9 @@ export default function UptimeAlertSection({ const queryClient = useQueryClient(); const [open, setOpen] = useState(false); - const [name, setName] = useState(`${siteId ? "Site" : "Resource"} ${startingName} Alert`); + const [name, setName] = useState( + `${siteId ? "Site" : "Resource"} ${startingName} Alert` + ); const [userTags, setUserTags] = useState([]); const [roleTags, setRoleTags] = useState([]); const [emailTags, setEmailTags] = useState([]); @@ -129,8 +131,7 @@ export default function UptimeAlertSection({ toast({ title: "Alert created", - description: - "You will be notified when this changes status." + description: "You will be notified when this changes status." }); setOpen(false); @@ -156,11 +157,17 @@ export default function UptimeAlertSection({ setLoading(false); } + const rulesListSearch = new URLSearchParams(); + if (siteId != null) rulesListSearch.set("siteId", String(siteId)); + if (resourceId != null) + rulesListSearch.set("resourceId", String(resourceId)); + const rulesListHref = `/${orgId}/settings/alerting/rules${ + rulesListSearch.toString() ? `?${rulesListSearch}` : "" + }`; + const alertButton = alertRulesLoading ? null : hasRules ? (