-
Notifications
You must be signed in to change notification settings - Fork 125
docs(router): metrics #7696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
docs(router): metrics #7696
Changes from all commits
21c821c
02b1b0b
40e8fe3
fbadb6d
f66246f
1c6f7d9
45d3c3a
45c4300
5f1fe95
22520f2
d53624f
26bfd56
d31bf78
a7dc6de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| ); | ||
| } |
| 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> | ||
| <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> | ||
| ); | ||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The component includes a hardcoded |
||
| )} | ||
|
|
||
| {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> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.References
<a>tag (e.g., via a router'sLinkcomponent) instead of a<button>element to ensure semantic correctness and accessibility.