From fc50852085eb97c8c4d56515d343b7dee072d7f9 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Mon, 9 Feb 2026 11:34:55 -0800 Subject: [PATCH] chore: add prometheus verification ui --- .../release-targets/Verifications.tsx | 10 +- .../release-targets/prometheus/Prometheus.tsx | 119 ++++++++++++++++++ .../prometheus/PrometheusIcon.tsx | 12 ++ .../prometheus/prometheus-metric.ts | 51 ++++++++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/Prometheus.tsx create mode 100644 apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/PrometheusIcon.tsx create mode 100644 apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/prometheus-metric.ts diff --git a/apps/web/app/routes/ws/deployments/_components/release-targets/Verifications.tsx b/apps/web/app/routes/ws/deployments/_components/release-targets/Verifications.tsx index 074330021..f75410e2b 100644 --- a/apps/web/app/routes/ws/deployments/_components/release-targets/Verifications.tsx +++ b/apps/web/app/routes/ws/deployments/_components/release-targets/Verifications.tsx @@ -19,6 +19,9 @@ import { import { cn } from "~/lib/utils"; import { ArgoCDVerificationDisplay } from "./argocd/ArgoCD"; import { isArgoCDMeasurement } from "./argocd/argocd-metric"; +import { PrometheusVerificationDisplay } from "./prometheus/Prometheus"; +import { PrometheusIcon } from "./prometheus/PrometheusIcon"; +import { isPrometheusProvider } from "./prometheus/prometheus-metric"; import { VerificationMetricStatus } from "./VerificationMetricStatus"; type JobVerification = WorkspaceEngine["schemas"]["JobVerification"]; @@ -152,6 +155,7 @@ function MetricDisplay({ metric }: { metric: VerificationMetricStatus }) { const isArgoCD = latestMeasurement != null && isArgoCDMeasurement(latestMeasurement.data); + const isPrometheus = isPrometheusProvider(metric.provider); return ( @@ -163,6 +167,9 @@ function MetricDisplay({ metric }: { metric: VerificationMetricStatus }) { open ? "rotate-90" : "", )} /> + {isPrometheus && ( + + )} {metric.name}
@@ -171,7 +178,8 @@ function MetricDisplay({ metric }: { metric: VerificationMetricStatus }) { {isArgoCD && } - {!isArgoCD && ( + {isPrometheus && } + {!isArgoCD && !isPrometheus && ( <> {sortedMeasurements.map((measurement, idx) => ( diff --git a/apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/Prometheus.tsx b/apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/Prometheus.tsx new file mode 100644 index 000000000..b1bbc6e1a --- /dev/null +++ b/apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/Prometheus.tsx @@ -0,0 +1,119 @@ +import type { WorkspaceEngine } from "@ctrlplane/workspace-engine-sdk"; +import { formatDistanceToNowStrict } from "date-fns"; + +import { cn } from "~/lib/utils"; +import { + parsePrometheusMeasurement, + parsePrometheusProvider, +} from "./prometheus-metric"; + +type VerificationMetricStatus = + WorkspaceEngine["schemas"]["VerificationMetricStatus"]; +type MetricMeasurement = VerificationMetricStatus["measurements"][number]; + +export function PrometheusVerificationDisplay({ + metric, +}: { + metric: VerificationMetricStatus; +}) { + const provider = parsePrometheusProvider(metric.provider); + const sortedMeasurements = [...metric.measurements].sort( + (a, b) => + new Date(b.measuredAt).getTime() - new Date(a.measuredAt).getTime(), + ); + + return ( +
+ + {sortedMeasurements.length > 0 && ( + + )} +
+ ); +} + +function ProviderInfo({ + query, + address, + successCondition, +}: { + query?: string; + address?: string; + successCondition: string; +}) { + return ( +
+ {query != null && ( +
+ Query + + {query} + +
+ )} +
+ Condition + + {successCondition} + +
+ {address != null && ( +
+ Server + {address} +
+ )} +
+ ); +} + +function MeasurementTrend({ + measurements, +}: { + measurements: MetricMeasurement[]; +}) { + return ( +
+ Measurements +
+ {measurements.map((m, i) => ( + + ))} +
+
+ ); +} + +function MeasurementRow({ measurement }: { measurement: MetricMeasurement }) { + const parsed = parsePrometheusMeasurement(measurement.data); + const isPassed = measurement.status === "passed"; + const isFailed = measurement.status === "failed"; + + const timeAgo = formatDistanceToNowStrict( + new Date(measurement.measuredAt), + { addSuffix: true }, + ); + + return ( +
+
+ + {timeAgo} +
+ + {parsed?.value != null ? parsed.value : "—"} + +
+ ); +} diff --git a/apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/PrometheusIcon.tsx b/apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/PrometheusIcon.tsx new file mode 100644 index 000000000..453644831 --- /dev/null +++ b/apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/PrometheusIcon.tsx @@ -0,0 +1,12 @@ +export function PrometheusIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/prometheus-metric.ts b/apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/prometheus-metric.ts new file mode 100644 index 000000000..9db4ce67b --- /dev/null +++ b/apps/web/app/routes/ws/deployments/_components/release-targets/prometheus/prometheus-metric.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; + +export const prometheusProviderConfig = z.object({ + type: z.literal("prometheus"), + address: z.string(), + query: z.string(), + timeout: z.number().optional(), +}); + +export type PrometheusProviderConfig = z.infer; + +const prometheusResultEntry = z.object({ + metric: z.record(z.string()).optional(), + value: z.number(), +}); + +export const prometheusMeasurementData = z.object({ + ok: z.boolean(), + statusCode: z.number(), + duration: z.number().optional(), + value: z.number().nullable().optional(), + results: z.array(prometheusResultEntry).nullable().optional(), + error: z.string().optional(), + errorType: z.string().optional(), +}); + +export type PrometheusMeasurementData = z.infer< + typeof prometheusMeasurementData +>; + +export function parsePrometheusProvider( + provider: unknown, +): PrometheusProviderConfig | null { + const result = prometheusProviderConfig.safeParse(provider); + return result.success ? result.data : null; +} + +export function parsePrometheusMeasurement( + data: unknown, +): PrometheusMeasurementData | null { + const result = prometheusMeasurementData.safeParse(data); + return result.success ? result.data : null; +} + +export function isPrometheusMeasurement(data: unknown): boolean { + return prometheusMeasurementData.safeParse(data).success; +} + +export function isPrometheusProvider(provider: unknown): boolean { + return prometheusProviderConfig.safeParse(provider).success; +}