Skip to content
Open
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
58 changes: 58 additions & 0 deletions packages/web/docs/src/components/otel-metrics/label-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Info, Lightbulb, Tag } from 'lucide-react';

interface LabelCardProps {
name: string;
meaning: string;
typicalValues: string[];
notes?: string;
}

export function LabelCard({ name, meaning, typicalValues, notes }: LabelCardProps) {
return (
<div>
<div className="mb-3 flex items-start gap-3">
<div className="shrink-0 rounded-md border border-gray-200 bg-gray-100 p-1.5 dark:border-neutral-700 dark:bg-neutral-800">
<Tag className="h-4 w-4 text-gray-600 dark:text-slate-100" />
</div>
<div className="min-w-0 flex-1">
<code className="break-all text-sm font-semibold text-gray-900 dark:text-slate-100">
{name}
</code>
<p className="mt-1 text-sm leading-relaxed text-gray-600 dark:text-slate-100">
{meaning}
</p>
</div>
</div>

<div className="mt-4 space-y-3">
<div>
<div className="mb-2 flex items-center gap-1.5">
<Info className="h-3.5 w-3.5 text-gray-500 dark:text-slate-400" />
<span className="text-xs font-semibold uppercase text-gray-700 dark:text-slate-100">
Typical Values
</span>
</div>
<div className="flex flex-wrap gap-1.5">
{typicalValues.map(value => (
<code
key={value}
className="rounded-md border border-slate-200 bg-slate-50 px-2.5 py-1 text-xs font-medium text-slate-700 dark:border-neutral-700 dark:bg-neutral-800 dark:text-slate-200"
>
{value}
</code>
))}
</div>
</div>

{notes && (
<div className="pt-1">
<div className="flex items-start gap-2">
<Lightbulb className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-600 dark:text-amber-400" />
<p className="text-sm leading-relaxed text-gray-600 dark:text-slate-100">{notes}</p>
</div>
</div>
)}
</div>
</div>
);
}
163 changes: 163 additions & 0 deletions packages/web/docs/src/components/otel-metrics/metric-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { useEffect, useRef, useState } from 'react';
import { Activity, BarChart3, Gauge, TrendingUp } from 'lucide-react';

interface MetricCardProps {
name: string;
type: 'Counter' | 'Histogram' | 'UpDownCounter' | 'Gauge';
unit?: string;
description?: string;
labels?: string[];
}

const typeConfig = {
Counter: {
icon: TrendingUp,
color:
'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-700/50',
badge: 'bg-emerald-100 text-emerald-800',
},
Histogram: {
icon: BarChart3,
color:
'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700/50',
badge: 'bg-blue-100 text-blue-800',
},
UpDownCounter: {
icon: Activity,
color:
'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-300 dark:border-amber-700/50',
badge: 'bg-amber-100 text-amber-800',
},
Gauge: {
icon: Gauge,
color:
'bg-slate-50 text-slate-700 border-slate-200 dark:bg-slate-800/60 dark:text-slate-100 dark:border-slate-700',
badge: 'bg-slate-100 text-slate-800',
},
};

export function MetricCard({ name, type, unit, description, labels }: MetricCardProps) {
const config = typeConfig[type];
const Icon = config.icon;
const [isCopied, setIsCopied] = useState(false);
const copiedTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const metricId = `metric-${name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')}`;

useEffect(() => {
return () => {
if (copiedTimeoutRef.current) {
clearTimeout(copiedTimeoutRef.current);
}
};
}, []);

function showCopiedState() {
setIsCopied(true);

if (copiedTimeoutRef.current) {
clearTimeout(copiedTimeoutRef.current);
}

copiedTimeoutRef.current = setTimeout(() => {
setIsCopied(false);
}, 1200);
}

async function copyMetricLink() {
if (typeof window === 'undefined') {
return;
}

const metricUrl = `${window.location.origin}${window.location.pathname}${window.location.search}#${metricId}`;

try {
await navigator.clipboard.writeText(metricUrl);
showCopiedState();
} catch {
window.location.hash = metricId;
}
}

return (
<div
id={metricId}
className="group scroll-mt-20 overflow-hidden rounded-lg border border-gray-200 bg-white transition-shadow duration-200 hover:shadow-md dark:border-neutral-800 dark:bg-neutral-900 dark:hover:shadow-black/30"
>
<div className="p-5">
<div className="mb-3 flex items-center justify-between gap-4">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<code className="break-all text-sm font-semibold text-gray-900 dark:text-slate-100">
{name}
</code>
<button
type="button"
onClick={() => {
void copyMetricLink();
}}
className={`hive-focus inline-flex items-center justify-center rounded font-mono text-sm font-semibold leading-none transition-all duration-200 ${isCopied ? 'translate-y-0 text-gray-500 opacity-100 dark:text-slate-500' : 'translate-y-0 text-gray-500 opacity-0 hover:text-gray-700 focus:text-gray-700 group-focus-within:opacity-100 group-hover:opacity-100 dark:text-slate-500 dark:hover:text-slate-200 dark:focus:text-slate-200'}`}
aria-label={`Copy link to ${name}`}
title="Copy metric link"
>
{isCopied ? (
<>
<span>✓</span>
<span className="ml-1 text-xs">copied</span>
</>
) : (
'#'
)}
</button>
Comment on lines +96 to +113
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For semantic correctness and better accessibility, consider changing this <button> to an <a> tag. The button's content is # and its fallback behavior involves navigation, which are characteristics of a link. Using an <a> tag aligns the element's semantics with its behavior and appearance, adhering to web standards and improving user experience, especially for users of assistive technologies.

              <a
                href={`#${metricId}`}
                onClick={e => {
                  e.preventDefault();
                  void copyMetricLink();
                }}
                className={`hive-focus inline-flex items-center justify-center rounded font-mono text-sm font-semibold leading-none transition-all duration-200 ${isCopied ? 'translate-y-0 text-gray-500 opacity-100 dark:text-slate-500' : 'translate-y-0 text-gray-500 opacity-0 hover:text-gray-700 focus:text-gray-700 group-focus-within:opacity-100 group-hover:opacity-100 dark:text-slate-500 dark:hover:text-slate-200 dark:focus:text-slate-200'}`}
                aria-label={`Copy link to ${name}`}
                title="Copy metric link"
              >
                {isCopied ? (
                  <>
                    <span>✓</span>
                    <span className="ml-1 text-xs">copied</span>
                  </>
                ) : (
                  '#'
                )}
              </a>
References
  1. For navigation, use an <a> tag (e.g., via a router's Link component) instead of a <button> element to ensure semantic correctness and accessibility.

<span className="sr-only" aria-live="polite">
{isCopied ? `Copied link to ${name}` : ''}
</span>
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
{unit && (
<div className="flex items-center gap-1.5 rounded-md border border-gray-200 bg-gray-100 px-2.5 py-1 text-xs text-gray-700 dark:border-neutral-700 dark:bg-neutral-800 dark:text-slate-200">
<span className="font-medium text-gray-500 dark:text-slate-300">Unit:</span>
<code>{unit}</code>
</div>
)}
<div
className={`flex items-center gap-1.5 rounded-md border px-2.5 py-1 ${config.color}`}
>
<Icon className="h-3.5 w-3.5" />
<span className="text-xs font-medium">{type}</span>
</div>
</div>
</div>

{description && (
<p className="mb-4 text-sm leading-relaxed text-gray-600 dark:text-slate-100">
{description}
</p>
)}

{labels && labels.length > 0 && (
<div className="mt-4 border-t border-gray-100 pt-4 dark:border-neutral-800">
<div className="mb-2 flex items-center gap-2">
<span className="text-xs font-semibold uppercase text-gray-700 dark:text-slate-100">
Labels
</span>
</div>
<div className="flex flex-wrap gap-1.5">
{labels.map(label => (
<code
key={label}
className="rounded border border-gray-200 bg-gray-50 px-2 py-1 text-xs text-gray-700 transition-colors hover:border-gray-300 dark:border-neutral-700 dark:bg-neutral-800 dark:text-slate-200 dark:hover:border-neutral-600"
>
{label}
</code>
))}
</div>
</div>
)}
</div>
</div>
);
}
80 changes: 80 additions & 0 deletions packages/web/docs/src/components/otel-metrics/metrics-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';

import { useId, useState } from 'react';
import { ChevronDown } from 'lucide-react';
import { LabelCard } from './label-card';
import { MetricCard } from './metric-card';

interface Metric {
name: string;
type: 'Counter' | 'Histogram' | 'UpDownCounter' | 'Gauge';
unit?: string;
description?: string;
labels?: string[];
}

interface Label {
name: string;
meaning: string;
typicalValues: string[];
notes?: string;
}

interface MetricsSectionProps {
title?: string;
description?: string;
Comment on lines +24 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The props title and description are defined in MetricsSectionProps but are not used within the MetricsSection component. They should be removed to keep the component's API clean and avoid confusion for future developers.

metrics?: Metric[];
labels?: Label[];
}
export function MetricsSection({ metrics, labels }: MetricsSectionProps) {
const [isLabelsOpen, setIsLabelsOpen] = useState(false);
const labelsRegionId = useId();

return (
<div className="space-y-6">
{metrics && metrics.length > 0 && (
<div className="space-y-4">
<h4 className="mt-8 text-xl font-semibold tracking-tight text-slate-900 dark:text-slate-100">
Metrics
</h4>
<div className="grid gap-4">
{metrics.map(metric => (
<MetricCard key={metric.name} {...metric} />
))}
</div>
</div>
Comment on lines +36 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The component includes a hardcoded <h4>Metrics</h4> heading. This can lead to incorrect heading hierarchies when used within metrics.mdx (e.g., an h4 as a direct child of another h4) and reduces the component's flexibility. It's better to remove this heading and let the consumer of the component (the MDX file) control the document structure and headings.

        <div className="grid gap-4">
          {metrics.map(metric => (
            <MetricCard key={metric.name} {...metric} />
          ))}
        </div>

)}

{labels && labels.length > 0 && (
<div className="overflow-hidden rounded-lg border border-gray-200 bg-white dark:border-neutral-800 dark:bg-neutral-900">
<button
type="button"
onClick={() => setIsLabelsOpen(current => !current)}
aria-expanded={isLabelsOpen}
aria-controls={labelsRegionId}
className="hive-focus flex w-full items-center justify-between px-5 py-4 text-left text-xl font-semibold tracking-tight text-slate-900 dark:text-slate-100"
>
<span>Labels Reference</span>
<ChevronDown
className={`h-5 w-5 transition-transform duration-200 ${isLabelsOpen ? 'rotate-180' : ''}`}
/>
</button>
<div
id={labelsRegionId}
className={`overflow-hidden transition-[max-height,opacity] duration-300 ease-out ${isLabelsOpen ? 'max-h-[4000px] opacity-100' : 'max-h-0 opacity-90'}`}
>
<div className="border-t border-gray-100 px-5 pb-5 dark:border-neutral-800">
<div className="divide-y divide-gray-100 pt-2 dark:divide-neutral-800">
{labels.map(label => (
<div key={label.name} className="py-6">
<LabelCard {...label} />
</div>
))}
</div>
</div>
</div>
</div>
)}
</div>
);
}
Loading
Loading