From bc1dcba362b98387be23867838cbf0b1cfc371c4 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 12 Jul 2026 19:16:42 +1000 Subject: [PATCH 1/2] Add service metrics page --- .../services/[serviceId]/metrics/page.tsx | 5 ++ .../details/service-details-overview.tsx | 52 ++++++++--- .../service/details/service-metrics-page.tsx | 90 +++++++++++++++++++ .../service/service-layout-client.tsx | 2 + 4 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 web/app/(dashboard)/dashboard/projects/[slug]/[env]/services/[serviceId]/metrics/page.tsx create mode 100644 web/components/service/details/service-metrics-page.tsx diff --git a/web/app/(dashboard)/dashboard/projects/[slug]/[env]/services/[serviceId]/metrics/page.tsx b/web/app/(dashboard)/dashboard/projects/[slug]/[env]/services/[serviceId]/metrics/page.tsx new file mode 100644 index 00000000..926a4121 --- /dev/null +++ b/web/app/(dashboard)/dashboard/projects/[slug]/[env]/services/[serviceId]/metrics/page.tsx @@ -0,0 +1,5 @@ +import { ServiceMetricsPage } from "@/components/service/details/service-metrics-page"; + +export default function MetricsPage() { + return ; +} diff --git a/web/components/service/details/service-details-overview.tsx b/web/components/service/details/service-details-overview.tsx index 479fc5dd..6eeb3449 100644 --- a/web/components/service/details/service-details-overview.tsx +++ b/web/components/service/details/service-details-overview.tsx @@ -33,7 +33,7 @@ import { isObservedStarting } from "@/lib/deployment-status"; import { fetcher } from "@/lib/fetcher"; import { cn } from "@/lib/utils"; -type ServiceMetricsResponse = { +export type ServiceMetricsResponse = { metricsEnabled: boolean; range: string; windowStart: string; @@ -101,7 +101,7 @@ type ChartRow = { totalRequests: number; } & Record; -type ServiceChartMode = "requests" | "latency" | "traffic" | "resources"; +export type ServiceChartMode = "requests" | "latency" | "traffic" | "resources"; type StatusSeries = { status: string; @@ -203,18 +203,26 @@ export function ServiceDetailsOverview({ service }: { service: Service }) { ); } -function ServiceMetricsPanel({ +export function ServiceMetricsPanel({ hasPublicHttp, stats, error, isLoading, + fixedMode, + rangeLabel = "24h", + useRangeAwareTimeAxis = false, }: { hasPublicHttp: boolean; stats?: ServiceMetricsResponse; error?: unknown; isLoading: boolean; + fixedMode?: ServiceChartMode; + rangeLabel?: string; + useRangeAwareTimeAxis?: boolean; }) { - const [chartMode, setChartMode] = useState("requests"); + const [selectedMode, setSelectedMode] = + useState("requests"); + const chartMode = fixedMode ?? selectedMode; const chartRows = useMemo(() => buildChartRows(stats), [stats]); const statusSeries = useMemo(() => buildStatusSeries(stats), [stats]); const activeSeries = useMemo( @@ -231,12 +239,23 @@ function ServiceMetricsPanel({ stats, chartRows, hasMetricData, + rangeLabel, ), - [chartMode, stats, chartRows, hasMetricData], + [chartMode, stats, chartRows, hasMetricData, rangeLabel], ); return (
-
+
+ {fixedMode ? ( +

{fixedMode}

+ ) : null}
{isLoading ? (
@@ -256,11 +275,13 @@ function ServiceMetricsPanel({
)}
- + {fixedMode ? null : ( + + )}
@@ -307,7 +328,11 @@ function ServiceMetricsPanel({ minTickGap={32} tickLine={false} axisLine={false} - tickFormatter={(value) => formatCompactDate(value)} + tickFormatter={(value) => + !useRangeAwareTimeAxis || rangeLabel === "7d" + ? formatCompactDate(value) + : formatCompactDateTime(value) + } className="text-xs" /> = { + "1h": "Last hour", + "6h": "Last 6 hours", + "24h": "Last 24 hours", + "7d": "Last 7 days", +}; +const MODES: ServiceChartMode[] = [ + "requests", + "latency", + "traffic", + "resources", +]; + +export function ServiceMetricsPage() { + const { service } = useService(); + const [range, setRange] = useQueryState( + "range", + parseAsStringLiteral(LOG_TIME_RANGES).withDefault("24h"), + ); + const { data, error, isLoading } = useSWR( + `/api/services/${service.id}/metrics?range=${range}`, + fetcher, + { refreshInterval: 60000, keepPreviousData: true }, + ); + const hasPublicHttp = + service.ports?.some( + (port) => port.isPublic && port.domain && port.protocol === "http", + ) ?? false; + + return ( +
+
+ + }> + {LABELS[range]} + + + setRange(value as typeof range)} + > + {LOG_TIME_RANGES.map((value) => ( + + {LABELS[value]} + + ))} + + + +
+
+ {MODES.map((mode) => ( +
+ +
+ ))} +
+
+ ); +} diff --git a/web/components/service/service-layout-client.tsx b/web/components/service/service-layout-client.tsx index 67fafb1c..7d4d0fe4 100644 --- a/web/components/service/service-layout-client.tsx +++ b/web/components/service/service-layout-client.tsx @@ -130,6 +130,7 @@ export function ServiceLayoutClient({ const basePath = `/dashboard/projects/${projectSlug}/${envName}/services/${service?.id}`; const isConstrainedTab = + pathname.includes("/metrics") || pathname.includes("/configuration") || pathname.includes("/builds") || pathname.includes("/backups"); @@ -139,6 +140,7 @@ export function ServiceLayoutClient({ const tabs = [ { name: "Deployments", href: basePath }, { name: "Configuration", href: `${basePath}/configuration` }, + { name: "Metrics", href: `${basePath}/metrics` }, { name: "Logs", href: `${basePath}/logs` }, ...(hasPublicPorts ? [{ name: "Requests", href: `${basePath}/requests` }] From 66f9779c67f22b3dab8954a5291a14e8f52ac369 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 12 Jul 2026 19:17:18 +1000 Subject: [PATCH 2/2] Default service metrics to one hour --- web/components/service/details/service-metrics-page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/service/details/service-metrics-page.tsx b/web/components/service/details/service-metrics-page.tsx index 49c4b0c0..98b33739 100644 --- a/web/components/service/details/service-metrics-page.tsx +++ b/web/components/service/details/service-metrics-page.tsx @@ -37,7 +37,7 @@ export function ServiceMetricsPage() { const { service } = useService(); const [range, setRange] = useQueryState( "range", - parseAsStringLiteral(LOG_TIME_RANGES).withDefault("24h"), + parseAsStringLiteral(LOG_TIME_RANGES).withDefault("1h"), ); const { data, error, isLoading } = useSWR( `/api/services/${service.id}/metrics?range=${range}`,