import { useEffect, useMemo, useState } from "react" import { Activity, CheckCircle2, CloudCog, RefreshCw, Rocket, XCircle } from "lucide-react" import { useRollouts } from "../hooks/useRollouts" import { useObservabilityData } from "../hooks/useObservabilityData" import { formatDuration, formatRelativeTime } from "../lib/timeUtils" import type { Rollout, RolloutImpact } from "../lib/types" import { FilterBar, type FilterChip } from "./FilterBar" import { MasterDetail } from "./MasterDetail" import { Segmented } from "./common/Segmented" import { DetailField, SectionLabel } from "./common/DetailField" import { useSubRoute } from "../lib/routes" type PhaseFilter = "progressing" | "completed" | "failed" type SortMode = "recent" | "active" const phaseMeta: Record = { completed: { Icon: CheckCircle2, pill: "text-ds-ok bg-ds-ok/15 border-ds-ok/20", dot: "bg-ds-ok", bar: "bg-ds-ok" }, failed: { Icon: XCircle, pill: "text-ds-critical bg-ds-critical-bg border-ds-critical/20", dot: "bg-ds-critical", bar: "bg-ds-critical" }, progressing: { Icon: Activity, pill: "text-ds-warn bg-ds-warn-bg border-ds-warn/20", dot: "bg-ds-warn", bar: "bg-ds-warn" }, } const phaseOf = (rollout: Rollout) => phaseMeta[rollout.phase] ?? phaseMeta.progressing const PhasePill = ({ rollout }: { rollout: Rollout }) => { const { Icon, pill } = phaseOf(rollout) return ( ) } /** Real replica progress: available of desired, tinted by the rollout's actual phase. */ const ReplicaBar = ({ rollout }: { rollout: Rollout }) => { const { bar } = phaseOf(rollout) const ratio = rollout.desiredReplicas > 0 ? Math.min(rollout.availableReplicas / rollout.desiredReplicas, 1) : 0 return ( {rollout.availableReplicas}/{rollout.desiredReplicas} ) } const RolloutListItem = ({ rollout }: { rollout: Rollout }) => { const { dot } = phaseOf(rollout) return (
rev {rollout.revision || "?"}
) } const formatDateTime = (ms: number) => new Date(ms).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) /** * Error rate around the rollout, from Prometheus. A nonzero 5xx rate is drawn * in the critical tone; the hairline marks when this revision started rolling. */ const ImpactChart = ({ impact, rollout }: { impact: RolloutImpact; rollout: Rollout }) => { const samples = useMemo( () => impact.points .map((point) => ({ t: point.timestamp, v: Number.parseFloat(point.value) })) .filter((sample) => Number.isFinite(sample.v)), [impact.points], ) if (samples.length === 0) { return

No matching Prometheus series for this workload.

} const max = Math.max(...samples.map((s) => s.v)) const first = samples[0].t const span = Math.max(samples[samples.length - 1].t - first, 1) const startPct = ((rollout.startedAt / 1e9 - first) / span) * 100 const formatRate = (v: number) => (v >= 100 ? String(Math.round(v)) : String(Number(v.toFixed(3)))) return (
5xx/s · peak {formatRate(max)} {formatDateTime(first * 1000)} → {formatDateTime(samples[samples.length - 1].t * 1000)}
{startPct > 0 && startPct < 100 && ( )} {samples.map((sample) => ( 0 ? "bg-ds-critical/70" : "bg-ds-line-strong/60"}`} style={{ height: max > 0 ? `${Math.max((sample.v / max) * 100, 2)}%` : "2%" }} title={`${formatDateTime(sample.t * 1000)} — ${formatRate(sample.v)} 5xx/s`} /> ))}
) } const RolloutDetail = ({ rollout, impactState, impactLoading, }: { rollout: Rollout impactState?: RolloutImpact impactLoading: boolean }) => { const short = rollout.generation > 0 ? ` · generation ${rollout.generation}` : "" const replicas = [ { label: "Desired", value: rollout.desiredReplicas }, { label: "Updated", value: rollout.updatedReplicas }, { label: "Available", value: rollout.availableReplicas, warn: rollout.availableReplicas < rollout.desiredReplicas }, { label: "Ready", value: rollout.readyReplicas, warn: rollout.readyReplicas < rollout.desiredReplicas }, ] return (
{rollout.namespace}/{rollout.workload} revision {rollout.revision || "unknown"} {short}
{replicas.map((stat) => (
{stat.label} {stat.value}
))}
Timeline
Started {formatDateTime(rollout.startedAt / 1e6)} · {formatRelativeTime(rollout.startedAt / 1e6)}
Completed {rollout.completedAt ? ( {formatDateTime(rollout.completedAt / 1e6)} · took{" "} {formatDuration(Math.max(rollout.completedAt - rollout.startedAt, 0))} ) : ( not yet )}
Prometheus error rate {impactLoading &&
{!impactState ? (

Loading impact data…

) : !impactState.configured ? (

Prometheus is not configured.

) : ( )}
) } const Tile = ({ label, value, detail, tone, }: { label: string value: string detail: string tone?: "critical" | "warn" | "ok" }) => (
{label} {value} {detail}
) const phaseRank: Record = { progressing: 0, failed: 1, completed: 2 } export function RolloutsVisualizer() { const { rollouts, loading, error, refresh, impact } = useRollouts() const { searchQuery, setSearchQuery } = useObservabilityData() const { segments, go } = useSubRoute() const [phaseFilter, setPhaseFilter] = useState(null) const [namespaceFilter, setNamespaceFilter] = useState(null) const [sortMode, setSortMode] = useState("recent") const [impactById, setImpactById] = useState>({}) const [impactPending, setImpactPending] = useState>({}) const [impactFailed, setImpactFailed] = useState>({}) const [impactError, setImpactError] = useState(null) const namespaces = useMemo(() => [...new Set(rollouts.map((r) => r.namespace))].sort(), [rollouts]) const filtered = useMemo(() => { return rollouts.filter((r) => { if (phaseFilter && r.phase !== phaseFilter) return false if (namespaceFilter && r.namespace !== namespaceFilter) return false if (searchQuery) { const haystack = [r.workload, r.namespace, r.service, r.release, r.image, r.revision] .filter(Boolean) .join(" ") .toLowerCase() if (!haystack.includes(searchQuery.toLowerCase())) return false } return true }) }, [rollouts, phaseFilter, namespaceFilter, searchQuery]) const sorted = useMemo(() => { const byStart = (a: Rollout, b: Rollout) => b.startedAt - a.startedAt if (sortMode === "active") return [...filtered].sort((a, b) => (phaseRank[a.phase] ?? 3) - (phaseRank[b.phase] ?? 3) || byStart(a, b)) return [...filtered].sort(byStart) }, [filtered, sortMode]) const selected = sorted.find((r) => r.id === segments[0]) ?? sorted[0] ?? null // Fetch Prometheus impact for whatever the user selects; cached per rollout, // failures recorded so a dead Prometheus isn't re-polled on every render. const selectedId = selected?.id ?? null useEffect(() => { if (!selectedId || impactById[selectedId] || impactFailed[selectedId] || impactPending[selectedId]) return setImpactPending((prev) => ({ ...prev, [selectedId]: true })) impact(selectedId) .then((result) => { setImpactById((prev) => ({ ...prev, [selectedId]: result })) setImpactError(null) }) .catch((cause) => { setImpactFailed((prev) => ({ ...prev, [selectedId]: true })) setImpactError(cause instanceof Error ? cause.message : String(cause)) }) .finally(() => setImpactPending((prev) => ({ ...prev, [selectedId]: false }))) }, [selectedId, impactById, impactFailed, impactPending, impact]) const stats = useMemo(() => { const dayAgoMs = Date.now() - 24 * 3600 * 1000 const progressing = rollouts.filter((r) => r.phase === "progressing").length const failed = rollouts.filter((r) => r.phase === "failed").length const recent = rollouts.filter((r) => r.startedAt / 1e6 >= dayAgoMs).length const workloads = new Set(rollouts.map((r) => `${r.namespace}/${r.workload}`)).size return { progressing, failed, recent, workloads } }, [rollouts]) const activeFilterCount = (phaseFilter ? 1 : 0) + (namespaceFilter ? 1 : 0) + (searchQuery ? 1 : 0) const chips: FilterChip[] = [ ...(phaseFilter ? [ { id: "phase", filterKey: "phase", value: phaseFilter, tone: phaseFilter === "failed" ? ("critical" as const) : phaseFilter === "progressing" ? ("warn" as const) : ("ok" as const), onRemove: () => setPhaseFilter(null), }, ] : []), ...(namespaceFilter ? [{ id: "namespace", filterKey: "namespace", value: namespaceFilter, tone: "neutral" as const, onRemove: () => setNamespaceFilter(null) }] : []), ...(searchQuery ? [{ id: "query", filterKey: "query", value: searchQuery, tone: "neutral" as const, onRemove: () => setSearchQuery("") }] : []), ] const addFilterOptions = [ ...(["progressing", "failed", "completed"] as PhaseFilter[]) .filter((phase) => phase !== phaseFilter) .map((phase) => ({ value: `phase:${phase}`, label: `phase=${phase}` })), ...namespaces.filter((ns) => ns !== namespaceFilter).map((ns) => ({ value: `ns:${ns}`, label: `namespace=${ns}` })), ] const onAddFilter = (value: string) => { if (value.startsWith("phase:")) setPhaseFilter(value.slice(6) as PhaseFilter) else if (value.startsWith("ns:")) setNamespaceFilter(value.slice(3)) } const clearAll = () => { setPhaseFilter(null) setNamespaceFilter(null) setSearchQuery("") } const matchText = `Matching ${filtered.length} of ${rollouts.length} rollouts · ${activeFilterCount} filter${activeFilterCount === 1 ? "" : "s"}` if (error) { return (
) } if (loading && rollouts.length === 0) { return
Loading cluster rollouts…
} if (rollouts.length === 0) { return (
) } return (
0 ? addFilterOptions : undefined} onAddFilter={addFilterOptions.length > 0 ? onAddFilter : undefined} matchText={matchText} onClearAll={activeFilterCount > 0 ? clearAll : undefined} />
0 ? "rolling out right now" : "cluster is settled"} tone={stats.progressing > 0 ? "warn" : "ok"} /> 0 ? "deadline exceeded or unavailable" : "no failing rollouts"} tone={stats.failed > 0 ? "critical" : "ok"} />
{sorted.length === 0 ? (
) : ( r.id} selectedKey={selected?.id ?? null} onSelect={(id) => go([id])} label="Cluster rollouts" storageKey="rollouts" listSize={{ default: 400, min: 300, max: 620 }} listHeader={

Rollouts · {sortMode === "active" ? "active first" : "newest first"}

} renderItem={(rollout) => } renderDetail={(rollout) => ( )} /> )} {impactError && (

Impact query failed: {impactError}

)}
) } export default RolloutsVisualizer