This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
import { personalAccountAtCapacity } from "@/lib/personal-org";
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
import { WarningBadge } from "@/components/ui/warning-badge";
|
||||
import { OnboardingFlow } from "@/components/onboarding/onboarding-flow";
|
||||
import { ProvisioningStatus } from "@/components/onboarding/provisioning-status";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
@@ -348,7 +349,10 @@ export default async function DashboardPage() {
|
||||
{tenant.metadata.name}
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge phase={tenant.status?.phase ?? "Pending"} />
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<StatusBadge phase={tenant.status?.phase ?? "Pending"} />
|
||||
<WarningBadge warnings={tenant.status?.warnings ?? []} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tenant.spec.agentName && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { redirect, notFound } from "next/navigation";
|
||||
import { getTenant } from "@/lib/k8s";
|
||||
import { canUserSeeTenant } from "@/lib/visibility";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
import { WarningBadge } from "@/components/ui/warning-badge";
|
||||
import { UsageDisplay } from "@/components/dashboard/usage-display";
|
||||
import { PackageList } from "@/components/packages/package-list";
|
||||
import { WorkspaceEditor } from "@/components/packages/workspace-editor";
|
||||
@@ -88,6 +89,7 @@ export default async function TenantDetailPage({
|
||||
{tenant.spec.displayName || name}
|
||||
</h1>
|
||||
<StatusBadge phase={tenant.status?.phase ?? "Pending"} />
|
||||
<WarningBadge warnings={tenant.status?.warnings ?? []} />
|
||||
</div>
|
||||
{tenant.spec.agentName && (
|
||||
<p className="text-sm text-text-secondary mt-3">
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -361,5 +361,9 @@
|
||||
"Error": "Fehler",
|
||||
"Deleting": "Wird gelöscht",
|
||||
"Reconfiguring": "Wird neu konfiguriert"
|
||||
},
|
||||
"warnings": {
|
||||
"oneTooltip": "1 Warnung",
|
||||
"manyTooltip": "{count} Warnungen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,5 +361,9 @@
|
||||
"Error": "Error",
|
||||
"Deleting": "Deleting",
|
||||
"Reconfiguring": "Reconfiguring"
|
||||
},
|
||||
"warnings": {
|
||||
"oneTooltip": "1 warning",
|
||||
"manyTooltip": "{count} warnings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,5 +361,9 @@
|
||||
"Error": "Erreur",
|
||||
"Deleting": "Suppression",
|
||||
"Reconfiguring": "Reconfiguration"
|
||||
},
|
||||
"warnings": {
|
||||
"oneTooltip": "1 avertissement",
|
||||
"manyTooltip": "{count} avertissements"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,5 +361,9 @@
|
||||
"Error": "Errore",
|
||||
"Deleting": "Eliminazione",
|
||||
"Reconfiguring": "Riconfigurazione"
|
||||
},
|
||||
"warnings": {
|
||||
"oneTooltip": "1 avviso",
|
||||
"manyTooltip": "{count} avvisi"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,21 @@ export interface PiecedTenantStatus {
|
||||
litellmKeyAlias?: string;
|
||||
tenantNamespace?: string;
|
||||
enabledPackages?: string[];
|
||||
/**
|
||||
* Non-fatal issues from downstream resources surfaced by the operator
|
||||
* (e.g. an OpenClawInstance sub-condition reporting failure). The
|
||||
* tenant is still usable — these are informational, rendered as a
|
||||
* warning badge alongside the phase.
|
||||
*
|
||||
* `source` is "<Kind>/<ConditionType>" e.g. "OpenClawInstance/SkillPacksReady".
|
||||
* `message` is shown in the tooltip when the user hovers the badge.
|
||||
*/
|
||||
warnings?: Array<{
|
||||
source: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
since?: string;
|
||||
}>;
|
||||
conditions?: Array<{
|
||||
type: string;
|
||||
status: string;
|
||||
|
||||
Reference in New Issue
Block a user