diff --git a/server/routers/newt/buildConfiguration.ts b/server/routers/newt/buildConfiguration.ts index 46729f11d..cca69dd61 100644 --- a/server/routers/newt/buildConfiguration.ts +++ b/server/routers/newt/buildConfiguration.ts @@ -21,6 +21,7 @@ import { generateSubnetProxyTargetV2, SubnetProxyTargetV2 } from "@server/lib/ip"; +import { supportsTargetHealthChecksV2 } from "./targets"; export async function buildClientConfigurationForNewtClient( site: Site, @@ -86,7 +87,8 @@ export async function buildClientConfigurationForNewtClient( // ) // ); - if (!client.clientSitesAssociationsCache.isJitMode) { // if we are adding sites through jit then dont add the site to the olm + if (!client.clientSitesAssociationsCache.isJitMode) { + // if we are adding sites through jit then dont add the site to the olm // update the peer info on the olm // if the peer has not been added yet this will be a no-op await updatePeer(client.clients.clientId, { @@ -189,7 +191,10 @@ export async function buildClientConfigurationForNewtClient( }; } -export async function buildTargetConfigurationForNewtClient(siteId: number) { +export async function buildTargetConfigurationForNewtClient( + siteId: number, + version?: string | null +) { // Get all enabled targets with their resource protocol information const allTargets = await db .select({ @@ -201,7 +206,7 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) { internalPort: targets.internalPort, enabled: targets.enabled, protocol: resources.protocol, - hcId: targetHealthCheck.targetHealthCheckId, + targetHealthCheckId: targetHealthCheck.targetHealthCheckId, hcEnabled: targetHealthCheck.hcEnabled, hcPath: targetHealthCheck.hcPath, hcScheme: targetHealthCheck.hcScheme, @@ -273,8 +278,9 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) { } return { - id: target.targetId, - hcId: target.hcId, + id: supportsTargetHealthChecksV2(version) + ? target.targetId + : target.targetHealthCheckId, hcEnabled: target.hcEnabled, hcPath: target.hcPath, hcScheme: target.hcScheme, diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index fce42caa3..f3902a35d 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -192,7 +192,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { } const { tcpTargets, udpTargets, validHealthCheckTargets } = - await buildTargetConfigurationForNewtClient(siteId); + await buildTargetConfigurationForNewtClient(siteId, newtVersion); logger.debug( `Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}` diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index 572c63e98..a28ef4f91 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -2,6 +2,13 @@ import { Target, TargetHealthCheck } from "@server/db"; import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; import { canCompress } from "@server/lib/clientVersionChecks"; +import semver from "semver"; + +const NEWT_V2_TARGET_HEALTH_CHECK_VERSION = ">=1.12.0"; + +export function supportsTargetHealthChecksV2(version?: string | null) { + return version ? semver.satisfies(version, NEWT_V2_TARGET_HEALTH_CHECK_VERSION) : false; +} export async function addTargets( newtId: string, @@ -83,8 +90,7 @@ export async function addTargets( } return { - id: target.targetId, - hcId: hc.targetHealthCheckId, + id: supportsTargetHealthChecksV2(version) ? target.targetId : hc.targetHealthCheckId, hcEnabled: hc.hcEnabled, hcPath: hc.hcPath, hcScheme: hc.hcScheme, diff --git a/server/routers/target/handleHealthcheckStatusMessage.ts b/server/routers/target/handleHealthcheckStatusMessage.ts index a049e3224..47e4a771c 100644 --- a/server/routers/target/handleHealthcheckStatusMessage.ts +++ b/server/routers/target/handleHealthcheckStatusMessage.ts @@ -14,6 +14,7 @@ import { fireHealthCheckHealthyAlert, fireHealthCheckNotHealthyAlert } from "#dynamic/lib/alerts"; +import { supportsTargetHealthChecksV2 } from "@server/routers/newt/targets"; interface TargetHealthStatus { status: string; @@ -73,6 +74,8 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( let successCount = 0; let errorCount = 0; + const isV2 = supportsTargetHealthChecksV2(newt.version); + // Process each target status update for (const [targetId, healthStatus] of Object.entries(data.targets)) { logger.debug( @@ -88,34 +91,78 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( continue; } - const [targetCheck] = await db - .select({ - targetId: targets.targetId, - siteId: targets.siteId, - orgId: targetHealthCheck.orgId, - targetHealthCheckId: targetHealthCheck.targetHealthCheckId, - resourceOrgId: resources.orgId, - resourceId: resources.resourceId, - name: targetHealthCheck.name, - hcStatus: targetHealthCheck.hcHealth - }) - .from(targets) - .innerJoin( - resources, - eq(targets.resourceId, resources.resourceId) - ) - .innerJoin(sites, eq(targets.siteId, sites.siteId)) - .innerJoin( - targetHealthCheck, - eq(targets.targetId, targetHealthCheck.targetId) - ) - .where( - and( - eq(targets.targetId, targetIdNum), - eq(sites.siteId, newt.siteId) + let targetCheck: { + targetId: number; + siteId: number | null; + orgId: string | null; + targetHealthCheckId: number; + resourceOrgId: string | null; + resourceId: number | null; + name: string | null; + hcStatus: string | null; + } | undefined; + + if (isV2) { + // New newt (>= 1.12.0): the key is the targetId + [targetCheck] = await db + .select({ + targetId: targets.targetId, + siteId: targets.siteId, + orgId: targetHealthCheck.orgId, + targetHealthCheckId: targetHealthCheck.targetHealthCheckId, + resourceOrgId: resources.orgId, + resourceId: resources.resourceId, + name: targetHealthCheck.name, + hcStatus: targetHealthCheck.hcHealth + }) + .from(targets) + .innerJoin( + resources, + eq(targets.resourceId, resources.resourceId) ) - ) - .limit(1); + .innerJoin(sites, eq(targets.siteId, sites.siteId)) + .innerJoin( + targetHealthCheck, + eq(targets.targetId, targetHealthCheck.targetId) + ) + .where( + and( + eq(targets.targetId, targetIdNum), + eq(sites.siteId, newt.siteId) + ) + ) + .limit(1); + } else { + // Old newt (< 1.12.0): the key is the targetHealthCheckId + [targetCheck] = await db + .select({ + targetId: targets.targetId, + siteId: targets.siteId, + orgId: targetHealthCheck.orgId, + targetHealthCheckId: targetHealthCheck.targetHealthCheckId, + resourceOrgId: resources.orgId, + resourceId: resources.resourceId, + name: targetHealthCheck.name, + hcStatus: targetHealthCheck.hcHealth + }) + .from(targetHealthCheck) + .innerJoin( + targets, + eq(targetHealthCheck.targetId, targets.targetId) + ) + .innerJoin( + resources, + eq(targets.resourceId, resources.resourceId) + ) + .innerJoin(sites, eq(targets.siteId, sites.siteId)) + .where( + and( + eq(targetHealthCheck.targetHealthCheckId, targetIdNum), + eq(sites.siteId, newt.siteId) + ) + ) + .limit(1); + } if (!targetCheck) { logger.warn( @@ -142,7 +189,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( | "healthy" | "unhealthy" }) - .where(eq(targetHealthCheck.targetId, targetIdNum)); + .where(eq(targetHealthCheck.targetId, targetCheck.targetId)); // Log the state change to status history await db.insert(statusHistory).values({ @@ -170,7 +217,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( .where( and( eq(targets.resourceId, targetCheck.resourceId), - eq(targets.targetId, targetIdNum) // only check the other targets, not the one we just updated + eq(targets.targetId, targetCheck.targetId) // only check the other targets, not the one we just updated ) ); diff --git a/src/components/alert-rule-editor/AlertRuleFields.tsx b/src/components/alert-rule-editor/AlertRuleFields.tsx index 2b57724cc..aa004357b 100644 --- a/src/components/alert-rule-editor/AlertRuleFields.tsx +++ b/src/components/alert-rule-editor/AlertRuleFields.tsx @@ -46,7 +46,7 @@ import { orgQueries } from "@app/lib/queries"; import { useQuery } from "@tanstack/react-query"; import { ChevronsUpDown, Plus, Trash2 } from "lucide-react"; import { useTranslations } from "next-intl"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import type { Control, UseFormReturn } from "react-hook-form"; import { useFormContext, useWatch } from "react-hook-form"; import { useDebounce } from "use-debounce"; @@ -484,8 +484,8 @@ function NotifyActionFields({ number | null >(null); - const { data: orgUsers = [] } = useQuery(orgQueries.users({ orgId })); - const { data: orgRoles = [] } = useQuery(orgQueries.roles({ orgId })); + const { data: orgUsers = [], isLoading: isLoadingUsers } = useQuery(orgQueries.users({ orgId })); + const { data: orgRoles = [], isLoading: isLoadingRoles } = useQuery(orgQueries.roles({ orgId })); const allUsers = useMemo( () => @@ -508,6 +508,50 @@ function NotifyActionFields({ [orgRoles] ); + const hasResolvedTagsRef = useRef(false); + + useEffect(() => { + if (isLoadingUsers || isLoadingRoles) return; + if (hasResolvedTagsRef.current) return; + + const currentUserTags = form.getValues( + `actions.${index}.userTags` + ) as Tag[]; + const currentRoleTags = form.getValues( + `actions.${index}.roleTags` + ) as Tag[]; + + const resolvedUserTags = currentUserTags.map((tag) => { + const match = allUsers.find((u) => u.id === tag.id); + return match ? { id: tag.id, text: match.text } : tag; + }); + + const resolvedRoleTags = currentRoleTags.map((tag) => { + const match = allRoles.find((r) => r.id === tag.id); + return match ? { id: tag.id, text: match.text } : tag; + }); + + const userTagsNeedUpdate = resolvedUserTags.some( + (t, i) => t.text !== currentUserTags[i]?.text + ); + const roleTagsNeedUpdate = resolvedRoleTags.some( + (t, i) => t.text !== currentRoleTags[i]?.text + ); + + if (userTagsNeedUpdate) { + form.setValue(`actions.${index}.userTags`, resolvedUserTags, { + shouldDirty: false + }); + } + if (roleTagsNeedUpdate) { + form.setValue(`actions.${index}.roleTags`, resolvedRoleTags, { + shouldDirty: false + }); + } + + hasResolvedTagsRef.current = true; + }, [isLoadingUsers, isLoadingRoles, allUsers, allRoles]); + const userTags = (useWatch({ control, name: `actions.${index}.userTags` }) ?? []) as Tag[]; const roleTags = (useWatch({ control, name: `actions.${index}.roleTags` }) ?? []) as Tag[]; const emailTags = (useWatch({ control, name: `actions.${index}.emailTags` }) ?? []) as Tag[];