make alerts and health checks table server side

This commit is contained in:
miloschwartz
2026-04-21 16:49:45 -07:00
parent b22ac17178
commit db2942447a
11 changed files with 333 additions and 142 deletions

View File

@@ -33,7 +33,7 @@ export default function EditAlertRulePage() {
useEffect(() => {
if (isNaN(alertRuleId)) {
router.replace(`/${orgId}/settings/alerting`);
router.replace(`/${orgId}/settings/alerting/rules`);
return;
}
@@ -56,7 +56,7 @@ export default function EditAlertRulePage() {
useEffect(() => {
if (formValues === null) {
router.replace(`/${orgId}/settings/alerting`);
router.replace(`/${orgId}/settings/alerting/rules`);
}
}, [formValues, orgId, router]);

View File

@@ -0,0 +1,86 @@
import HealthChecksTable from "@app/components/HealthChecksTable";
import DismissableBanner from "@app/components/DismissableBanner";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
import { AxiosResponse } from "axios";
import { HeartPulse } from "lucide-react";
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
export const metadata: Metadata = {
title: "Health checks"
};
type AlertingHealthChecksPageProps = {
params: Promise<{ orgId: string }>;
searchParams: Promise<Record<string, string>>;
};
export const dynamic = "force-dynamic";
function parsePositiveInt(s: string | undefined): number | undefined {
if (!s) return undefined;
const n = Number(s);
if (!Number.isInteger(n) || n <= 0) return undefined;
return n;
}
export default async function AlertingHealthChecksPage(
props: AlertingHealthChecksPageProps
) {
const params = await props.params;
const searchParams = new URLSearchParams(await props.searchParams);
const page = Math.max(1, parsePositiveInt(searchParams.get("page") ?? undefined) ?? 1);
const pageSize = Math.max(
1,
parsePositiveInt(searchParams.get("pageSize") ?? undefined) ?? 20
);
const pageIndex = page - 1;
const query = searchParams.get("query") ?? undefined;
const apiSp = new URLSearchParams();
apiSp.set("limit", String(pageSize));
apiSp.set("offset", String(pageIndex * pageSize));
if (query) apiSp.set("query", query);
let healthChecks: ListHealthChecksResponse["healthChecks"] = [];
let pagination: ListHealthChecksResponse["pagination"] = {
total: 0,
limit: pageSize,
offset: pageIndex * pageSize
};
try {
const res = await internal.get<AxiosResponse<ListHealthChecksResponse>>(
`/org/${params.orgId}/health-checks?${apiSp.toString()}`,
await authCookieHeader()
);
const responseData = res.data.data;
healthChecks = responseData.healthChecks;
pagination = responseData.pagination;
} catch {
// leave defaults
}
const t = await getTranslations();
return (
<div className="space-y-6">
<DismissableBanner
storageKey="alerting-health-checks-banner-dismissed"
version={1}
title={t("alertingHealthChecksBannerTitle")}
titleIcon={
<HeartPulse className="w-5 h-5 text-primary shrink-0" />
}
description={t("alertingHealthChecksBannerDescription")}
/>
<HealthChecksTable
orgId={params.orgId}
healthChecks={healthChecks}
rowCount={pagination.total}
/>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import { getTranslations } from "next-intl/server";
type AlertingLayoutProps = {
children: React.ReactNode;
params: Promise<{ orgId: string }>;
};
export default async function AlertingLayout({
children,
params
}: AlertingLayoutProps) {
const { orgId } = await params;
const t = await getTranslations();
const navItems = [
{
title: t("alertingTabRules"),
href: `/${orgId}/settings/alerting/rules`,
activePrefix: `/${orgId}/settings/alerting`
},
{
title: t("alertingTabHealthChecks"),
href: `/${orgId}/settings/alerting/health-checks`
}
];
return (
<>
<SettingsSectionTitle
title={t("alertingTitle")}
description={t("alertingDescription")}
/>
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
</>
);
}

View File

@@ -1,58 +1,15 @@
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import AlertingRulesTable from "@app/components/AlertingRulesTable";
import HealthChecksTable from "@app/components/HealthChecksTable";
import DismissableBanner from "@app/components/DismissableBanner";
import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs";
import { BellRing, HeartPulse } from "lucide-react";
import { getTranslations } from "next-intl/server";
import type { Metadata } from "next";
import { redirect } from "next/navigation";
type AlertingPageProps = {
export const metadata: Metadata = {
title: "Alerting"
};
type AlertingIndexPageProps = {
params: Promise<{ orgId: string }>;
};
export const dynamic = "force-dynamic";
export default async function AlertingPage(props: AlertingPageProps) {
export default async function AlertingIndexPage(props: AlertingIndexPageProps) {
const params = await props.params;
const t = await getTranslations();
const tabs: TabItem[] = [
{ title: t("alertingTabRules"), href: "" },
{ title: t("alertingTabHealthChecks"), href: "" }
];
return (
<>
<SettingsSectionTitle
title={t("alertingTitle")}
description={t("alertingDescription")}
/>
<HorizontalTabs items={tabs} clientSide>
<div className="space-y-6">
<DismissableBanner
storageKey="alerting-rules-banner-dismissed"
version={1}
title={t("alertingRulesBannerTitle")}
titleIcon={
<BellRing className="w-5 h-5 text-primary shrink-0" />
}
description={t("alertingRulesBannerDescription")}
/>
<AlertingRulesTable orgId={params.orgId} />
</div>
<div className="space-y-6">
<DismissableBanner
storageKey="alerting-health-checks-banner-dismissed"
version={1}
title={t("alertingHealthChecksBannerTitle")}
titleIcon={
<HeartPulse className="w-5 h-5 text-primary shrink-0" />
}
description={t("alertingHealthChecksBannerDescription")}
/>
<HealthChecksTable orgId={params.orgId} />
</div>
</HorizontalTabs>
</>
);
redirect(`/${params.orgId}/settings/alerting/rules`);
}

View File

@@ -0,0 +1,100 @@
import AlertingRulesTable from "@app/components/AlertingRulesTable";
import DismissableBanner from "@app/components/DismissableBanner";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import type { ListAlertRulesResponse } from "@server/private/routers/alertRule";
import { AxiosResponse } from "axios";
import { BellRing } from "lucide-react";
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
export const metadata: Metadata = {
title: "Alerting"
};
type AlertingRulesPageProps = {
params: Promise<{ orgId: string }>;
searchParams: Promise<Record<string, string>>;
};
export const dynamic = "force-dynamic";
function parsePositiveInt(s: string | undefined): number | undefined {
if (!s) return undefined;
const n = Number(s);
if (!Number.isInteger(n) || n <= 0) return undefined;
return n;
}
export default async function AlertingRulesPage(props: AlertingRulesPageProps) {
const params = await props.params;
const searchParams = new URLSearchParams(await props.searchParams);
const page = Math.max(1, parsePositiveInt(searchParams.get("page") ?? undefined) ?? 1);
const pageSize = Math.max(
1,
parsePositiveInt(searchParams.get("pageSize") ?? undefined) ?? 20
);
const pageIndex = page - 1;
const query = searchParams.get("query") ?? undefined;
const sortBy = searchParams.get("sort_by") ?? undefined;
const order = searchParams.get("order") ?? undefined;
const enabled = searchParams.get("enabled");
const enabledParam =
enabled === "true" || enabled === "false" ? enabled : undefined;
const siteId = parsePositiveInt(searchParams.get("siteId") ?? undefined);
const resourceId = parsePositiveInt(
searchParams.get("resourceId") ?? undefined
);
const apiSp = new URLSearchParams();
apiSp.set("limit", String(pageSize));
apiSp.set("offset", String(pageIndex * pageSize));
if (query) apiSp.set("query", query);
if (siteId != null) apiSp.set("siteId", String(siteId));
if (resourceId != null) apiSp.set("resourceId", String(resourceId));
if (sortBy) {
apiSp.set("sort_by", sortBy);
if (order) apiSp.set("order", order);
}
if (enabledParam) apiSp.set("enabled", enabledParam);
let alertRules: ListAlertRulesResponse["alertRules"] = [];
let pagination: ListAlertRulesResponse["pagination"] = {
total: 0,
limit: pageSize,
offset: pageIndex * pageSize
};
try {
const res = await internal.get<AxiosResponse<ListAlertRulesResponse>>(
`/org/${params.orgId}/alert-rules?${apiSp.toString()}`,
await authCookieHeader()
);
const responseData = res.data.data;
alertRules = responseData.alertRules;
pagination = responseData.pagination;
} catch {
// leave defaults
}
const t = await getTranslations();
return (
<div className="space-y-6">
<DismissableBanner
storageKey="alerting-rules-banner-dismissed"
version={1}
title={t("alertingRulesBannerTitle")}
titleIcon={
<BellRing className="w-5 h-5 text-primary shrink-0" />
}
description={t("alertingRulesBannerDescription")}
/>
<AlertingRulesTable
orgId={params.orgId}
alertRules={alertRules}
rowCount={pagination.total}
/>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
title: "Health checks"
};
type LegacyHealthChecksPageProps = {
params: Promise<{ orgId: string }>;
};
/** @deprecated Use `/settings/alerting/health-checks` */
export default async function LegacyHealthChecksRedirect(
props: LegacyHealthChecksPageProps
) {
const params = await props.params;
redirect(`/${params.orgId}/settings/alerting/health-checks`);
}