This commit is contained in:
118
src/components/ui/warning-badge.tsx
Normal file
118
src/components/ui/warning-badge.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* Tenant warning shape received from the operator's status.warnings.
|
||||
* Mirror of the operator's `TenantWarning` type. See
|
||||
* pieced-operator/api/v1alpha1/piecedtenant_types.go.
|
||||
*/
|
||||
export interface TenantWarning {
|
||||
source: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
since?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
warnings: TenantWarning[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a small amber warning badge if there are any non-fatal
|
||||
* warnings on the tenant. The badge sits visually next to the phase
|
||||
* StatusBadge — they're separate concepts (phase = lifecycle, warnings
|
||||
* = observed sub-issues) and may both be present at once (e.g. tenant
|
||||
* is `Ready` but has a SkillPacksReady=False warning).
|
||||
*
|
||||
* Hover/focus reveals the warning detail. We don't truncate the message
|
||||
* inside the tooltip; OCI/CRD condition messages tend to be short and
|
||||
* include the actionable detail (which skill, which secret, which
|
||||
* resolver). If a future warning source has a 5-line stacktrace as a
|
||||
* message we'll need a different treatment; cross that bridge then.
|
||||
*
|
||||
* Returns null when there are no warnings — keep render-call sites
|
||||
* simple, they don't have to gate on length themselves.
|
||||
*/
|
||||
export function WarningBadge({ warnings }: Props) {
|
||||
const t = useTranslations("warnings");
|
||||
if (!warnings || warnings.length === 0) return null;
|
||||
|
||||
const tooltipLabel = (() => {
|
||||
try {
|
||||
return warnings.length === 1
|
||||
? t("oneTooltip")
|
||||
: t("manyTooltip", { count: warnings.length });
|
||||
} catch {
|
||||
return warnings.length === 1
|
||||
? "1 warning"
|
||||
: `${warnings.length} warnings`;
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<span className="relative group inline-flex">
|
||||
<button
|
||||
type="button"
|
||||
// Button is non-actionable in itself — it exists purely to get
|
||||
// keyboard focus for screen readers and keyboard users, so the
|
||||
// tooltip isn't pointer-only. `aria-label` carries the summary;
|
||||
// the full content is in the tooltip below for sighted users.
|
||||
aria-label={tooltipLabel}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-400 hover:bg-amber-500/20 focus:outline-none focus:ring-1 focus:ring-amber-400 cursor-help"
|
||||
// No onClick — this is informational, not actionable. Pure
|
||||
// hover/focus widget. tabIndex defaults to 0 for buttons.
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width={12}
|
||||
height={12}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
|
||||
</svg>
|
||||
<span>{warnings.length}</span>
|
||||
</button>
|
||||
|
||||
{/*
|
||||
Tooltip. Hidden by default; shown on hover OR focus of the
|
||||
sibling button. Positioned below-right so it doesn't collide with
|
||||
the StatusBadge that typically sits left of this. Constrained
|
||||
width so long messages wrap.
|
||||
z-50 keeps it above table rows / cards.
|
||||
*/}
|
||||
<div
|
||||
role="tooltip"
|
||||
className="invisible group-hover:visible group-focus-within:visible absolute left-0 top-full mt-1 z-50 w-72 rounded-lg border border-border bg-surface-1 p-3 shadow-lg text-left"
|
||||
>
|
||||
<div className="text-[10px] uppercase tracking-wider text-text-muted mb-2">
|
||||
{tooltipLabel}
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{warnings.map((w, i) => (
|
||||
<li key={i} className="text-xs">
|
||||
<div className="font-mono text-amber-400 break-all">
|
||||
{w.source}
|
||||
</div>
|
||||
{w.reason && (
|
||||
<div className="text-text-secondary">{w.reason}</div>
|
||||
)}
|
||||
{w.message && (
|
||||
<div className="text-text-secondary mt-0.5 break-words">
|
||||
{w.message}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user