|
| 1 | +"use client" |
| 2 | + |
| 3 | +import * as React from "react" |
| 4 | +import { Badge } from "@/components/ui/badge" |
| 5 | +import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card" |
| 6 | +import { Separator } from "@/components/ui/separator" |
| 7 | +import type { ClusterDiagnostics, OperationalStatus, StatusDiagnostic } from "@/lib/performance-data" |
| 8 | + |
| 9 | +type Translate = (key: string) => string |
| 10 | + |
| 11 | +function getStatusLabel(state: OperationalStatus, t: Translate) { |
| 12 | + if (state === "healthy") return t("Healthy") |
| 13 | + if (state === "degraded") return t("Degraded") |
| 14 | + if (state === "stale") return t("Stale") |
| 15 | + if (state === "not_reported") return t("Not reported") |
| 16 | + return t("Unknown") |
| 17 | +} |
| 18 | + |
| 19 | +function getStatusVariant(state: OperationalStatus): "secondary" | "destructive" | "default" | "outline" | "ghost" { |
| 20 | + if (state === "healthy") return "secondary" |
| 21 | + if (state === "degraded") return "destructive" |
| 22 | + if (state === "stale") return "default" |
| 23 | + if (state === "not_reported") return "ghost" |
| 24 | + return "outline" |
| 25 | +} |
| 26 | + |
| 27 | +function getDefaultDescription(state: OperationalStatus, t: Translate) { |
| 28 | + if (state === "healthy") return t("No issue was reported by this source.") |
| 29 | + if (state === "degraded") return t("This status source requires attention.") |
| 30 | + if (state === "stale") return t("Previously reported data may be out of date.") |
| 31 | + if (state === "not_reported") return t("This status source was not reported by the server.") |
| 32 | + return t("The server reported this status source with an unknown condition.") |
| 33 | +} |
| 34 | + |
| 35 | +function formatTimestamp(value: string, locale: string | undefined) { |
| 36 | + const timestamp = Date.parse(value) |
| 37 | + if (!Number.isFinite(timestamp)) return value |
| 38 | + return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "medium" }).format(timestamp) |
| 39 | +} |
| 40 | + |
| 41 | +function DiagnosticRow({ |
| 42 | + label, |
| 43 | + diagnostic, |
| 44 | + troubleshooting, |
| 45 | + troubleshootingHref, |
| 46 | + showLastSuccessfulUpdate, |
| 47 | + t, |
| 48 | + locale, |
| 49 | +}: { |
| 50 | + label: string |
| 51 | + diagnostic: StatusDiagnostic |
| 52 | + troubleshooting?: string |
| 53 | + troubleshootingHref?: string |
| 54 | + showLastSuccessfulUpdate?: boolean |
| 55 | + t: Translate |
| 56 | + locale?: string |
| 57 | +}) { |
| 58 | + const scope = diagnostic.scope |
| 59 | + const scopeParts = [ |
| 60 | + scope?.bucket ? `${t("Bucket")}: ${scope.bucket}` : undefined, |
| 61 | + scope?.prefix ? `${t("Prefix")}: ${scope.prefix}` : undefined, |
| 62 | + scope?.set ? `${t("Set")}: ${scope.set}` : undefined, |
| 63 | + scope?.timeout ? `${t("Timeout")}: ${scope.timeout}` : undefined, |
| 64 | + ].filter(Boolean) |
| 65 | + |
| 66 | + return ( |
| 67 | + <div className="grid gap-2 py-4 sm:grid-cols-[minmax(0,12rem)_minmax(0,1fr)] sm:gap-6"> |
| 68 | + <dt className="flex min-w-0 items-center justify-between gap-3 sm:flex-col sm:items-start sm:justify-start"> |
| 69 | + <span className="font-medium text-foreground">{label}</span> |
| 70 | + <Badge variant={getStatusVariant(diagnostic.state)}>{getStatusLabel(diagnostic.state, t)}</Badge> |
| 71 | + </dt> |
| 72 | + <dd className="flex min-w-0 flex-col gap-2 text-muted-foreground"> |
| 73 | + <p className="break-words text-foreground [overflow-wrap:anywhere]"> |
| 74 | + {diagnostic.reason ?? getDefaultDescription(diagnostic.state, t)} |
| 75 | + </p> |
| 76 | + {diagnostic.lastSuccessfulUpdate || showLastSuccessfulUpdate ? ( |
| 77 | + <p className="text-xs"> |
| 78 | + {t("Last successful update")}:{" "} |
| 79 | + {diagnostic.lastSuccessfulUpdate ? formatTimestamp(diagnostic.lastSuccessfulUpdate, locale) : t("Unknown")} |
| 80 | + </p> |
| 81 | + ) : null} |
| 82 | + {diagnostic.lastError ? ( |
| 83 | + <p className="break-words text-xs [overflow-wrap:anywhere]"> |
| 84 | + {t("Last error")}: {diagnostic.lastError} |
| 85 | + </p> |
| 86 | + ) : null} |
| 87 | + {scopeParts.length ? ( |
| 88 | + <p className="break-words text-xs [overflow-wrap:anywhere]">{scopeParts.join(" · ")}</p> |
| 89 | + ) : null} |
| 90 | + {diagnostic.source ? ( |
| 91 | + <p className="break-words text-xs [overflow-wrap:anywhere]"> |
| 92 | + {t("Source")}: {diagnostic.source} |
| 93 | + </p> |
| 94 | + ) : null} |
| 95 | + {diagnostic.historicalStallTimeouts !== undefined ? ( |
| 96 | + <div className="space-y-1 text-xs"> |
| 97 | + <p className="text-foreground"> |
| 98 | + {t("Historical internode stall timeouts")}: {diagnostic.historicalStallTimeouts} |
| 99 | + </p> |
| 100 | + <p> |
| 101 | + {t("This lifetime counter has no sampling window and does not indicate current degradation by itself.")} |
| 102 | + </p> |
| 103 | + </div> |
| 104 | + ) : null} |
| 105 | + {diagnostic.hint ? ( |
| 106 | + <p className="break-words text-xs [overflow-wrap:anywhere]"> |
| 107 | + {t("Backend guidance")}: {diagnostic.hint} |
| 108 | + </p> |
| 109 | + ) : null} |
| 110 | + {troubleshooting ? ( |
| 111 | + <p className="text-xs text-foreground"> |
| 112 | + {t("Troubleshooting")}: {troubleshooting} |
| 113 | + </p> |
| 114 | + ) : null} |
| 115 | + {troubleshootingHref ? ( |
| 116 | + <a |
| 117 | + className="w-fit text-xs font-medium text-foreground underline underline-offset-4" |
| 118 | + href={troubleshootingHref} |
| 119 | + target="_blank" |
| 120 | + rel="noreferrer" |
| 121 | + > |
| 122 | + {t("Open the real multi-node metrics verification guide")} |
| 123 | + </a> |
| 124 | + ) : null} |
| 125 | + </dd> |
| 126 | + </div> |
| 127 | + ) |
| 128 | +} |
| 129 | + |
| 130 | +export function PerformanceStatusSources({ |
| 131 | + diagnostics, |
| 132 | + usageFreshness, |
| 133 | + t, |
| 134 | + locale, |
| 135 | +}: { |
| 136 | + diagnostics?: ClusterDiagnostics |
| 137 | + usageFreshness?: StatusDiagnostic |
| 138 | + t: Translate |
| 139 | + locale?: string |
| 140 | +}) { |
| 141 | + const notReported: StatusDiagnostic = { state: "not_reported" } |
| 142 | + const peerHealth = diagnostics?.peerHealth ?? notReported |
| 143 | + const storageReadiness = diagnostics?.storageReadiness ?? notReported |
| 144 | + const resolvedUsageFreshness = usageFreshness ?? diagnostics?.usageFreshness ?? notReported |
| 145 | + const listingHealth = diagnostics?.listingHealth ?? notReported |
| 146 | + const workloadAdmission = diagnostics?.workloadAdmission ?? notReported |
| 147 | + const rows = [ |
| 148 | + { label: t("Peer Health"), diagnostic: peerHealth }, |
| 149 | + { label: t("Storage Readiness"), diagnostic: storageReadiness }, |
| 150 | + { label: t("Usage Freshness"), diagnostic: resolvedUsageFreshness, showLastSuccessfulUpdate: true }, |
| 151 | + { |
| 152 | + label: t("Listing and Metacache"), |
| 153 | + diagnostic: listingHealth, |
| 154 | + troubleshooting: t( |
| 155 | + "Correlate time-windowed walk_dir metrics and metacache logs before treating listing symptoms as a disk failure.", |
| 156 | + ), |
| 157 | + troubleshootingHref: "https://github.com/rustfs/backlog/issues/1392#issuecomment-5040442761", |
| 158 | + }, |
| 159 | + { label: t("Workload Admission"), diagnostic: workloadAdmission }, |
| 160 | + ] |
| 161 | + |
| 162 | + return ( |
| 163 | + <Card className="shadow-none"> |
| 164 | + <CardHeader> |
| 165 | + <h2 id="status-sources-title" className="text-base font-semibold"> |
| 166 | + {t("Status Sources")} |
| 167 | + </h2> |
| 168 | + <CardDescription> |
| 169 | + {t("Review peer, storage, usage, listing, and workload admission health independently.")} |
| 170 | + </CardDescription> |
| 171 | + </CardHeader> |
| 172 | + <CardContent> |
| 173 | + <dl aria-labelledby="status-sources-title"> |
| 174 | + {rows.map((row, index) => ( |
| 175 | + <React.Fragment key={row.label}> |
| 176 | + {index ? <Separator /> : null} |
| 177 | + <DiagnosticRow |
| 178 | + label={row.label} |
| 179 | + diagnostic={row.diagnostic} |
| 180 | + troubleshooting={row.troubleshooting} |
| 181 | + troubleshootingHref={row.troubleshootingHref} |
| 182 | + showLastSuccessfulUpdate={row.showLastSuccessfulUpdate} |
| 183 | + t={t} |
| 184 | + locale={locale} |
| 185 | + /> |
| 186 | + </React.Fragment> |
| 187 | + ))} |
| 188 | + </dl> |
| 189 | + </CardContent> |
| 190 | + </Card> |
| 191 | + ) |
| 192 | +} |
0 commit comments