fix(diagnosis): prevent progress stream blocking and refine tunnel type chips (#230)

* fix(diagnosis): avoid result channel deadlock in progress stream

Close the diagnosis result channel asynchronously after workers complete so progress can stream without blocking, and improve tunnel card type chip contrast for clearer protocol distinction.

* feat(diagnosis): stream pending items and render in-progress states

Pre-populate diagnosis stream with pending targets so tunnel and forward dialogs can show per-item diagnosing status immediately. Update result typing and UI states to distinguish in-progress, success, and failure rows/cards consistently.
This commit is contained in:
sagit
2026-03-01 14:49:38 +08:00
committed by GitHub
parent 9ffeb83753
commit cf51b305b0
6 changed files with 237 additions and 93 deletions
@@ -58,6 +58,55 @@ type diagnosisProgress struct {
type diagnosisItemEmitter func(index int, item map[string]interface{}, progress diagnosisProgress) type diagnosisItemEmitter func(index int, item map[string]interface{}, progress diagnosisProgress)
func (h *Handler) buildDiagnosisStreamStartItems(workItems []diagnosisWorkItem) []map[string]interface{} {
if len(workItems) == 0 {
return []map[string]interface{}{}
}
nodeCache := map[int64]*nodeRecord{}
items := make([]map[string]interface{}, 0, len(workItems))
for _, workItem := range workItems {
targetIP := strings.TrimSpace(workItem.targetIP)
targetPort := workItem.targetPort
if workItem.hasChainHop {
fromNode, _ := h.cachedNode(nodeCache, workItem.fromNodeID)
targetNode, err := h.cachedNode(nodeCache, workItem.toNode.NodeID)
if err == nil {
resolvedIP, resolvedPort, resolveErr := resolveChainProbeTarget(fromNode, targetNode, workItem.toNode.Port, workItem.ipPreference)
if resolveErr == nil {
targetIP = resolvedIP
targetPort = resolvedPort
}
}
}
if targetPort <= 0 {
targetPort = 443
}
nodeName := fmt.Sprintf("node_%d", workItem.fromNodeID)
if node, err := h.cachedNode(nodeCache, workItem.fromNodeID); err == nil && strings.TrimSpace(node.Name) != "" {
nodeName = node.Name
}
item := map[string]interface{}{
"success": false,
"diagnosing": true,
"description": workItem.description,
"nodeName": nodeName,
"nodeId": strconv.FormatInt(workItem.fromNodeID, 10),
"targetIp": targetIP,
"targetPort": targetPort,
"message": "诊断中...",
}
for key, value := range workItem.metadata {
item[key] = value
}
items = append(items, item)
}
return items
}
const diagnosisMaxConcurrency = 8 const diagnosisMaxConcurrency = 8
const ( const (
@@ -923,8 +972,10 @@ enqueueLoop:
} }
} }
close(jobs) close(jobs)
wg.Wait() go func() {
close(resultCh) wg.Wait()
close(resultCh)
}()
progress := diagnosisProgress{Total: len(workItems)} progress := diagnosisProgress{Total: len(workItems)}
for result := range resultCh { for result := range resultCh {
@@ -81,6 +81,7 @@ func (h *Handler) streamDiagnosisRuntime(ctx context.Context, cancel context.Can
payload := map[string]interface{}{ payload := map[string]interface{}{
"total": len(workItems), "total": len(workItems),
"timestamp": time.Now().UnixMilli(), "timestamp": time.Now().UnixMilli(),
"items": h.buildDiagnosisStreamStartItems(workItems),
} }
for key, value := range startPayload { for key, value := range startPayload {
payload[key] = value payload[key] = value
+87 -44
View File
@@ -890,10 +890,13 @@ export default function ForwardPage() {
? payload.forwardName ? payload.forwardName
: forward.name; : forward.name;
const startTotal = Number(payload.total); const startTotal = Number(payload.total);
const startItems = Array.isArray(payload.items)
? (payload.items as ForwardDiagnosisResult["results"])
: [];
setDiagnosisResult((prev) => ({ setDiagnosisResult((prev) => ({
forwardName: startForwardName, forwardName: startForwardName,
timestamp: Date.now(), timestamp: Date.now(),
results: prev?.results || [], results: startItems.length > 0 ? startItems : prev?.results || [],
})); }));
if (Number.isFinite(startTotal) && startTotal >= 0) { if (Number.isFinite(startTotal) && startTotal >= 0) {
setDiagnosisProgress((prev) => ({ setDiagnosisProgress((prev) => ({
@@ -919,9 +922,15 @@ export default function ForwardPage() {
); );
if (existingIndex >= 0) { if (existingIndex >= 0) {
nextResults[existingIndex] = result; nextResults[existingIndex] = {
...result,
diagnosing: false,
};
} else { } else {
nextResults.push(result); nextResults.push({
...result,
diagnosing: false,
});
} }
return { return {
...base, ...base,
@@ -3460,15 +3469,7 @@ export default function ForwardPage() {
)} )}
</ModalHeader> </ModalHeader>
<ModalBody className="bg-content1"> <ModalBody className="bg-content1">
{diagnosisLoading && {diagnosisResult ? (
(!diagnosisResult || diagnosisResult.results.length === 0) ? (
<div className="flex items-center justify-center py-16">
<div className="flex items-center gap-3">
<Spinner size="sm" />
<span className="text-default-600">正在诊断...</span>
</div>
</div>
) : diagnosisResult ? (
<div className="space-y-4"> <div className="space-y-4">
{diagnosisLoading && ( {diagnosisLoading && (
<div className="flex items-center justify-between rounded-lg border border-primary/20 bg-primary/5 px-3 py-2"> <div className="flex items-center justify-between rounded-lg border border-primary/20 bg-primary/5 px-3 py-2">
@@ -3599,6 +3600,10 @@ export default function ForwardPage() {
</thead> </thead>
<tbody className="divide-y divide-divider bg-white dark:bg-gray-800"> <tbody className="divide-y divide-divider bg-white dark:bg-gray-800">
{results.map((result, index) => { {results.map((result, index) => {
const isDiagnosing = Boolean(
result.diagnosing,
);
const isSuccess = result.success === true;
const quality = const quality =
getForwardDiagnosisQualityDisplay( getForwardDiagnosisQualityDisplay(
result.averageTime, result.averageTime,
@@ -3609,22 +3614,28 @@ export default function ForwardPage() {
<tr <tr
key={index} key={index}
className={`hover:bg-default-50 dark:hover:bg-gray-700/50 ${ className={`hover:bg-default-50 dark:hover:bg-gray-700/50 ${
result.success isDiagnosing
? "bg-warning-50 dark:bg-warning-900/20"
: isSuccess
? "bg-white dark:bg-gray-800" ? "bg-white dark:bg-gray-800"
: "bg-danger-50 dark:bg-danger-900/30" : "bg-danger-50 dark:bg-danger-900/30"
}`} }`}
> >
<td className="px-3 py-2"> <td className="px-3 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span {isDiagnosing ? (
className={`w-5 h-5 rounded-full flex items-center justify-center text-xs ${ <Spinner size="sm" />
result.success ) : (
? "bg-success text-white" <span
: "bg-danger text-white" className={`w-5 h-5 rounded-full flex items-center justify-center text-xs ${
}`} isSuccess
> ? "bg-success text-white"
{result.success ? "✓" : "✗"} : "bg-danger text-white"
</span> }`}
>
{isSuccess ? "✓" : "✗"}
</span>
)}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-medium text-foreground truncate"> <div className="font-medium text-foreground truncate">
{result.description} {result.description}
@@ -3639,18 +3650,24 @@ export default function ForwardPage() {
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
<Chip <Chip
color={ color={
result.success isDiagnosing
? "warning"
: isSuccess
? "success" ? "success"
: "danger" : "danger"
} }
size="sm" size="sm"
variant="flat" variant="flat"
> >
{result.success ? "成功" : "失败"} {isDiagnosing
? "诊断中"
: isSuccess
? "成功"
: "失败"}
</Chip> </Chip>
</td> </td>
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
{result.success ? ( {isSuccess ? (
<span className="font-semibold text-primary"> <span className="font-semibold text-primary">
{result.averageTime?.toFixed(0)} {result.averageTime?.toFixed(0)}
</span> </span>
@@ -3661,7 +3678,7 @@ export default function ForwardPage() {
)} )}
</td> </td>
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
{result.success ? ( {isSuccess ? (
<span <span
className={`font-semibold ${ className={`font-semibold ${
(result.packetLoss || 0) > 0 (result.packetLoss || 0) > 0
@@ -3678,7 +3695,7 @@ export default function ForwardPage() {
)} )}
</td> </td>
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
{result.success && quality ? ( {isSuccess && quality ? (
<Chip <Chip
className="text-xs whitespace-nowrap" className="text-xs whitespace-nowrap"
color={quality.color as any} color={quality.color as any}
@@ -3772,6 +3789,8 @@ export default function ForwardPage() {
</h3> </h3>
</div> </div>
{results.map((result, index) => { {results.map((result, index) => {
const isDiagnosing = Boolean(result.diagnosing);
const isSuccess = result.success === true;
const quality = const quality =
getForwardDiagnosisQualityDisplay( getForwardDiagnosisQualityDisplay(
result.averageTime, result.averageTime,
@@ -3782,21 +3801,27 @@ export default function ForwardPage() {
<div <div
key={index} key={index}
className={`border rounded-lg p-3 ${ className={`border rounded-lg p-3 ${
result.success isDiagnosing
? "border-warning-200 dark:border-warning-300/30 bg-warning-50 dark:bg-warning-900/20"
: isSuccess
? "border-divider bg-white dark:bg-gray-800" ? "border-divider bg-white dark:bg-gray-800"
: "border-danger-200 dark:border-danger-300/30 bg-danger-50 dark:bg-danger-900/30" : "border-danger-200 dark:border-danger-300/30 bg-danger-50 dark:bg-danger-900/30"
}`} }`}
> >
<div className="flex items-start gap-2 mb-2"> <div className="flex items-start gap-2 mb-2">
<span {isDiagnosing ? (
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs flex-shrink-0 ${ <Spinner size="sm" />
result.success ) : (
? "bg-success text-white" <span
: "bg-danger text-white" className={`w-6 h-6 rounded-full flex items-center justify-center text-xs flex-shrink-0 ${
}`} isSuccess
> ? "bg-success text-white"
{result.success ? "✓" : "✗"} : "bg-danger text-white"
</span> }`}
>
{isSuccess ? "✓" : "✗"}
</span>
)}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-semibold text-sm text-foreground break-words"> <div className="font-semibold text-sm text-foreground break-words">
{result.description} {result.description}
@@ -3808,16 +3833,24 @@ export default function ForwardPage() {
<Chip <Chip
className="flex-shrink-0" className="flex-shrink-0"
color={ color={
result.success ? "success" : "danger" isDiagnosing
? "warning"
: isSuccess
? "success"
: "danger"
} }
size="sm" size="sm"
variant="flat" variant="flat"
> >
{result.success ? "成功" : "失败"} {isDiagnosing
? "诊断中"
: isSuccess
? "成功"
: "失败"}
</Chip> </Chip>
</div> </div>
{result.success ? ( {isSuccess ? (
<div className="grid grid-cols-3 gap-2 mt-2 pt-2 border-t border-divider"> <div className="grid grid-cols-3 gap-2 mt-2 pt-2 border-t border-divider">
<div className="text-center"> <div className="text-center">
<div className="text-lg font-bold text-primary"> <div className="text-lg font-bold text-primary">
@@ -3861,8 +3894,16 @@ export default function ForwardPage() {
</div> </div>
) : ( ) : (
<div className="mt-2 pt-2 border-t border-divider"> <div className="mt-2 pt-2 border-t border-divider">
<div className="text-xs text-danger"> <div
{result.message || "连接失败"} className={`text-xs ${
isDiagnosing
? "text-warning"
: "text-danger"
}`}
>
{isDiagnosing
? result.message || "诊断中..."
: result.message || "连接失败"}
</div> </div>
</div> </div>
)} )}
@@ -3903,14 +3944,16 @@ export default function ForwardPage() {
</div> </div>
{/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */} {/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */}
{diagnosisResult.results.some((r) => !r.success) && ( {diagnosisResult.results.some(
(r) => r.success === false && !r.diagnosing,
) && (
<div className="space-y-2 hidden md:block"> <div className="space-y-2 hidden md:block">
<h4 className="text-sm font-semibold text-danger"> <h4 className="text-sm font-semibold text-danger">
失败详情 失败详情
</h4> </h4>
<div className="space-y-2"> <div className="space-y-2">
{diagnosisResult.results {diagnosisResult.results
.filter((r) => !r.success) .filter((r) => r.success === false && !r.diagnosing)
.map((result, index) => ( .map((result, index) => (
<Alert <Alert
key={index} key={index}
+2 -1
View File
@@ -1,5 +1,6 @@
export interface ForwardDiagnosisEntry { export interface ForwardDiagnosisEntry {
success: boolean; success?: boolean;
diagnosing?: boolean;
description: string; description: string;
nodeName: string; nodeName: string;
nodeId: string; nodeId: string;
+92 -45
View File
@@ -490,11 +490,14 @@ export default function TunnelPage() {
? "端口转发" ? "端口转发"
: "隧道转发"; : "隧道转发";
const startTotal = Number(payload.total); const startTotal = Number(payload.total);
const startItems = Array.isArray(payload.items)
? (payload.items as DiagnosisResult["results"])
: [];
setDiagnosisResult((prev) => ({ setDiagnosisResult((prev) => ({
tunnelName: startTunnelName, tunnelName: startTunnelName,
tunnelType: startTunnelType, tunnelType: startTunnelType,
timestamp: Date.now(), timestamp: Date.now(),
results: prev?.results || [], results: startItems.length > 0 ? startItems : prev?.results || [],
})); }));
if (Number.isFinite(startTotal) && startTotal >= 0) { if (Number.isFinite(startTotal) && startTotal >= 0) {
setDiagnosisProgress((prev) => ({ setDiagnosisProgress((prev) => ({
@@ -521,9 +524,15 @@ export default function TunnelPage() {
); );
if (existingIndex >= 0) { if (existingIndex >= 0) {
nextResults[existingIndex] = result; nextResults[existingIndex] = {
...result,
diagnosing: false,
};
} else { } else {
nextResults.push(result); nextResults.push({
...result,
diagnosing: false,
});
} }
return { return {
...base, ...base,
@@ -970,6 +979,10 @@ export default function TunnelPage() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{sortedTunnels.map((tunnel) => { {sortedTunnels.map((tunnel) => {
const typeDisplay = getTunnelTypeDisplay(tunnel.type); const typeDisplay = getTunnelTypeDisplay(tunnel.type);
const tunnelTypeChipClassName =
tunnel.type === 1
? "text-xs bg-primary-100 text-primary-800 border-primary-300 dark:bg-primary-900/45 dark:text-primary-200 dark:border-primary-700"
: "text-xs bg-success-100 text-success-800 border-success-300 dark:bg-success-900/35 dark:text-success-200 dark:border-success-700";
return ( return (
<SortableItem key={tunnel.id} id={tunnel.id}> <SortableItem key={tunnel.id} id={tunnel.id}>
@@ -993,7 +1006,7 @@ export default function TunnelPage() {
</h3> </h3>
<div className="flex items-center gap-1.5 mt-1"> <div className="flex items-center gap-1.5 mt-1">
<Chip <Chip
className="text-xs" className={tunnelTypeChipClassName}
color={typeDisplay.color as any} color={typeDisplay.color as any}
size="sm" size="sm"
variant="flat" variant="flat"
@@ -2106,15 +2119,7 @@ export default function TunnelPage() {
)} )}
</ModalHeader> </ModalHeader>
<ModalBody className="bg-content1"> <ModalBody className="bg-content1">
{diagnosisLoading && {diagnosisResult ? (
(!diagnosisResult || diagnosisResult.results.length === 0) ? (
<div className="flex items-center justify-center py-16">
<div className="flex items-center gap-3">
<Spinner size="sm" />
<span className="text-default-600">正在诊断...</span>
</div>
</div>
) : diagnosisResult ? (
<div className="space-y-4"> <div className="space-y-4">
{diagnosisLoading && ( {diagnosisLoading && (
<div className="flex items-center justify-between rounded-lg border border-primary/20 bg-primary/5 px-3 py-2"> <div className="flex items-center justify-between rounded-lg border border-primary/20 bg-primary/5 px-3 py-2">
@@ -2245,6 +2250,10 @@ export default function TunnelPage() {
</thead> </thead>
<tbody className="divide-y divide-divider bg-white dark:bg-gray-800"> <tbody className="divide-y divide-divider bg-white dark:bg-gray-800">
{results.map((result, index) => { {results.map((result, index) => {
const isDiagnosing = Boolean(
result.diagnosing,
);
const isSuccess = result.success === true;
const quality = getDiagnosisQualityDisplay( const quality = getDiagnosisQualityDisplay(
result.averageTime, result.averageTime,
result.packetLoss, result.packetLoss,
@@ -2254,22 +2263,28 @@ export default function TunnelPage() {
<tr <tr
key={index} key={index}
className={`hover:bg-default-50 dark:hover:bg-gray-700/50 ${ className={`hover:bg-default-50 dark:hover:bg-gray-700/50 ${
result.success isDiagnosing
? "bg-warning-50 dark:bg-warning-900/20"
: isSuccess
? "bg-white dark:bg-gray-800" ? "bg-white dark:bg-gray-800"
: "bg-danger-50 dark:bg-danger-900/30" : "bg-danger-50 dark:bg-danger-900/30"
}`} }`}
> >
<td className="px-3 py-2"> <td className="px-3 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span {isDiagnosing ? (
className={`w-5 h-5 rounded-full flex items-center justify-center text-xs ${ <Spinner size="sm" />
result.success ) : (
? "bg-success text-white" <span
: "bg-danger text-white" className={`w-5 h-5 rounded-full flex items-center justify-center text-xs ${
}`} isSuccess
> ? "bg-success text-white"
{result.success ? "✓" : "✗"} : "bg-danger text-white"
</span> }`}
>
{isSuccess ? "✓" : "✗"}
</span>
)}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-medium text-foreground truncate"> <div className="font-medium text-foreground truncate">
{result.description} {result.description}
@@ -2284,18 +2299,24 @@ export default function TunnelPage() {
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
<Chip <Chip
color={ color={
result.success isDiagnosing
? "warning"
: isSuccess
? "success" ? "success"
: "danger" : "danger"
} }
size="sm" size="sm"
variant="flat" variant="flat"
> >
{result.success ? "成功" : "失败"} {isDiagnosing
? "诊断中"
: isSuccess
? "成功"
: "失败"}
</Chip> </Chip>
</td> </td>
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
{result.success ? ( {isSuccess ? (
<span className="font-semibold text-primary"> <span className="font-semibold text-primary">
{result.averageTime?.toFixed(0)} {result.averageTime?.toFixed(0)}
</span> </span>
@@ -2306,7 +2327,7 @@ export default function TunnelPage() {
)} )}
</td> </td>
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
{result.success ? ( {isSuccess ? (
<span <span
className={`font-semibold ${ className={`font-semibold ${
(result.packetLoss || 0) > 0 (result.packetLoss || 0) > 0
@@ -2323,7 +2344,7 @@ export default function TunnelPage() {
)} )}
</td> </td>
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
{result.success && quality ? ( {isSuccess && quality ? (
<Chip <Chip
className="text-xs whitespace-nowrap" className="text-xs whitespace-nowrap"
color={quality.color as any} color={quality.color as any}
@@ -2417,6 +2438,8 @@ export default function TunnelPage() {
</h3> </h3>
</div> </div>
{results.map((result, index) => { {results.map((result, index) => {
const isDiagnosing = Boolean(result.diagnosing);
const isSuccess = result.success === true;
const quality = getDiagnosisQualityDisplay( const quality = getDiagnosisQualityDisplay(
result.averageTime, result.averageTime,
result.packetLoss, result.packetLoss,
@@ -2426,21 +2449,27 @@ export default function TunnelPage() {
<div <div
key={index} key={index}
className={`border rounded-lg p-3 ${ className={`border rounded-lg p-3 ${
result.success isDiagnosing
? "border-warning-200 dark:border-warning-300/30 bg-warning-50 dark:bg-warning-900/20"
: isSuccess
? "border-divider bg-white dark:bg-gray-800" ? "border-divider bg-white dark:bg-gray-800"
: "border-danger-200 dark:border-danger-300/30 bg-danger-50 dark:bg-danger-900/30" : "border-danger-200 dark:border-danger-300/30 bg-danger-50 dark:bg-danger-900/30"
}`} }`}
> >
<div className="flex items-start gap-2 mb-2"> <div className="flex items-start gap-2 mb-2">
<span {isDiagnosing ? (
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs flex-shrink-0 ${ <Spinner size="sm" />
result.success ) : (
? "bg-success text-white" <span
: "bg-danger text-white" className={`w-6 h-6 rounded-full flex items-center justify-center text-xs flex-shrink-0 ${
}`} isSuccess
> ? "bg-success text-white"
{result.success ? "✓" : "✗"} : "bg-danger text-white"
</span> }`}
>
{isSuccess ? "✓" : "✗"}
</span>
)}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-semibold text-sm text-foreground break-words"> <div className="font-semibold text-sm text-foreground break-words">
{result.description} {result.description}
@@ -2452,16 +2481,24 @@ export default function TunnelPage() {
<Chip <Chip
className="flex-shrink-0" className="flex-shrink-0"
color={ color={
result.success ? "success" : "danger" isDiagnosing
? "warning"
: isSuccess
? "success"
: "danger"
} }
size="sm" size="sm"
variant="flat" variant="flat"
> >
{result.success ? "成功" : "失败"} {isDiagnosing
? "诊断中"
: isSuccess
? "成功"
: "失败"}
</Chip> </Chip>
</div> </div>
{result.success ? ( {isSuccess ? (
<div className="grid grid-cols-3 gap-2 mt-2 pt-2 border-t border-divider"> <div className="grid grid-cols-3 gap-2 mt-2 pt-2 border-t border-divider">
<div className="text-center"> <div className="text-center">
<div className="text-lg font-bold text-primary"> <div className="text-lg font-bold text-primary">
@@ -2505,8 +2542,16 @@ export default function TunnelPage() {
</div> </div>
) : ( ) : (
<div className="mt-2 pt-2 border-t border-divider"> <div className="mt-2 pt-2 border-t border-divider">
<div className="text-xs text-danger"> <div
{result.message || "连接失败"} className={`text-xs ${
isDiagnosing
? "text-warning"
: "text-danger"
}`}
>
{isDiagnosing
? result.message || "诊断中..."
: result.message || "连接失败"}
</div> </div>
</div> </div>
)} )}
@@ -2547,14 +2592,16 @@ export default function TunnelPage() {
</div> </div>
{/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */} {/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */}
{diagnosisResult.results.some((r) => !r.success) && ( {diagnosisResult.results.some(
(r) => r.success === false && !r.diagnosing,
) && (
<div className="space-y-2 hidden md:block"> <div className="space-y-2 hidden md:block">
<h4 className="text-sm font-semibold text-danger"> <h4 className="text-sm font-semibold text-danger">
失败详情 失败详情
</h4> </h4>
<div className="space-y-2"> <div className="space-y-2">
{diagnosisResult.results {diagnosisResult.results
.filter((r) => !r.success) .filter((r) => r.success === false && !r.diagnosing)
.map((result, index) => ( .map((result, index) => (
<Alert <Alert
key={index} key={index}
+2 -1
View File
@@ -1,5 +1,6 @@
export interface DiagnosisEntry { export interface DiagnosisEntry {
success: boolean; success?: boolean;
diagnosing?: boolean;
description: string; description: string;
nodeName: string; nodeName: string;
nodeId: string; nodeId: string;