Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ServiceMetricsPage } from "@/components/service/details/service-metrics-page";

export default function MetricsPage() {
return <ServiceMetricsPage />;
}
52 changes: 39 additions & 13 deletions web/components/service/details/service-details-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -101,7 +101,7 @@ type ChartRow = {
totalRequests: number;
} & Record<string, string | number | null>;

type ServiceChartMode = "requests" | "latency" | "traffic" | "resources";
export type ServiceChartMode = "requests" | "latency" | "traffic" | "resources";

type StatusSeries = {
status: string;
Expand Down Expand Up @@ -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<ServiceChartMode>("requests");
const [selectedMode, setSelectedMode] =
useState<ServiceChartMode>("requests");
const chartMode = fixedMode ?? selectedMode;
const chartRows = useMemo(() => buildChartRows(stats), [stats]);
const statusSeries = useMemo(() => buildStatusSeries(stats), [stats]);
const activeSeries = useMemo(
Expand All @@ -231,12 +239,23 @@ function ServiceMetricsPanel({
stats,
chartRows,
hasMetricData,
rangeLabel,
),
[chartMode, stats, chartRows, hasMetricData],
[chartMode, stats, chartRows, hasMetricData, rangeLabel],
);
return (
<div className="flex h-full min-h-72 flex-col gap-4 p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div
className={cn(
"flex flex-col gap-3",
fixedMode
? "items-start"
: "sm:flex-row sm:items-start sm:justify-between",
)}
>
{fixedMode ? (
<h2 className="text-lg font-semibold capitalize">{fixedMode}</h2>
) : null}
<div className="min-w-0">
{isLoading ? (
<div className="flex flex-nowrap items-end gap-x-5">
Expand All @@ -256,11 +275,13 @@ function ServiceMetricsPanel({
</div>
)}
</div>
<ServiceChartModeToggle
value={chartMode}
onChange={setChartMode}
disabled={isLoading || isUnavailable}
/>
{fixedMode ? null : (
<ServiceChartModeToggle
value={chartMode}
onChange={setSelectedMode}
disabled={isLoading || isUnavailable}
/>
)}
</div>

<div className="min-h-40 min-w-0 flex-1">
Expand Down Expand Up @@ -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"
/>
<YAxis
Expand Down Expand Up @@ -986,11 +1011,12 @@ function buildServiceMetricSummaryItems(
stats: ServiceMetricsResponse | undefined,
rows: ChartRow[],
hasMetricData: boolean,
rangeLabel = "24h",
): ServiceMetricSummaryItem[] {
if (mode === "requests") {
return [
{
label: "requests in 24h",
label: `requests in ${rangeLabel}`,
value:
hasMetricData && stats
? formatCompactNumber(stats.totalRequests)
Expand Down
90 changes: 90 additions & 0 deletions web/components/service/details/service-metrics-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import { ChevronDown } from "lucide-react";
import { parseAsStringLiteral, useQueryState } from "nuqs";
import useSWR from "swr";
import {
type ServiceChartMode,
ServiceMetricsPanel,
type ServiceMetricsResponse,
} from "@/components/service/details/service-details-overview";
import { useService } from "@/components/service/service-layout-client";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { fetcher } from "@/lib/fetcher";
import { LOG_TIME_RANGES, type LogTimeRange } from "@/lib/log-query";

const LABELS: Record<LogTimeRange, string> = {
"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("1h"),
);
const { data, error, isLoading } = useSWR<ServiceMetricsResponse>(
`/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 (
<div className="space-y-4">
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>
{LABELS[range]} <ChevronDown className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuRadioGroup
value={range}
onValueChange={(value) => setRange(value as typeof range)}
>
{LOG_TIME_RANGES.map((value) => (
<DropdownMenuRadioItem key={value} value={value}>
{LABELS[value]}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-4">
{MODES.map((mode) => (
<div key={mode} className="h-80 rounded-lg border border-border">
<ServiceMetricsPanel
hasPublicHttp={hasPublicHttp}
stats={data}
error={error}
isLoading={isLoading}
fixedMode={mode}
rangeLabel={range}
useRangeAwareTimeAxis
/>
</div>
))}
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions web/components/service/service-layout-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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` }]
Expand Down
Loading