Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 31 additions & 49 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,25 @@ import {
} from './components/nodes/shared/index';
import useClusterStatus from './hooks/useClusterStatus';
import useDashboardData from './hooks/useDashboardData';
import useNodeDetails from './hooks/useNodeDetails';
import type { NodeStatus } from './types';

const NodeDetailModal = React.lazy(() => import('./components/nodes/NodeDetailModal'));
const NodeDetailModal = React.lazy(() => import('./components/nodes/NodeDetailDialog'));
const noFullNodes: NodeStatus[] = [];
const noLegacyDetail = () => undefined;

export default function App() {
const {
summary, status, loading, error, wsConnected, sendWsMessage,
nodeDetail, requestNodeDetail, subscribeNodeDetail, unsubscribeNodeDetail
summary, loading, error, wsConnected, sendWsMessage
} = useClusterStatus();
const nodes = status?.nodes || [];
const sites = summary?.sites || status?.sites || [];
const gatewayPools = summary?.gatewayPools || status?.gatewayPools || [];
const nodes = noFullNodes;
const sites = summary?.sites || [];
const gatewayPools = summary?.gatewayPools || [];
const nodeSummaries = summary?.nodeSummaries || [];
const [hiddenSites, setHiddenSites] = useState<Set<string>>(new Set());
const [hiddenGatewayPools, setHiddenGatewayPools] = useState<Set<string>>(new Set());
const [selectedNodeName, setSelectedNodeName] = useState<string | null>(null);
const { detail, load: loadNodeDetail } = useNodeDetails(selectedNodeName);
const [selectedNodeDetailTab, setSelectedNodeDetailTab] = useState<'peerings' | 'routes' | 'bpf'>('peerings');
const [pullEnabledOptimistic, setPullEnabledOptimistic] = useState<boolean | null>(null);
const [selectedNodeTypesFilter, setSelectedNodeTypesFilter] = useState<Set<string>>(new Set(['Gateway', 'Worker']));
Expand Down Expand Up @@ -82,23 +86,13 @@ export default function App() {
setInfoOpen(false);
}, [statusJsonOpen]);

// Request and subscribe to node detail when a node is selected
useEffect(() => {
if (!selectedNodeName) return;
requestNodeDetail(selectedNodeName);
subscribeNodeDetail(selectedNodeName);
return () => {
unsubscribeNodeDetail(selectedNodeName);
};
}, [selectedNodeName, requestNodeDetail, subscribeNodeDetail, unsubscribeNodeDetail]);

useEffect(() => {
if (pullEnabledOptimistic === null) return;
const pullEnabled = summary?.pullEnabled ?? status?.pullEnabled;
const pullEnabled = summary?.pullEnabled;
if (typeof pullEnabled === 'boolean' && pullEnabled === pullEnabledOptimistic) {
setPullEnabledOptimistic(null);
}
}, [summary?.pullEnabled, status?.pullEnabled, pullEnabledOptimistic]);
}, [summary?.pullEnabled, pullEnabledOptimistic]);

useEffect(() => {
if (pullEnabledOptimistic === null) return;
Expand All @@ -112,31 +106,20 @@ export default function App() {
const activeErrors = useMemo(() => {
const items: string[] = [];
if (error) items.push(`controller: ${error}`);
const errors = summary?.errors || status?.errors || [];
const errors = summary?.errors || [];
for (const msg of errors) {
items.push(`controller: ${msg}`);
}
items.sort();
return items;
}, [error, summary?.errors, status?.errors]);
}, [error, summary?.errors]);
const activeWarnings = useMemo(() => {
const items: string[] = [];
const warnings = summary?.warnings || status?.warnings || [];
const warnings = summary?.warnings || [];
for (const msg of warnings) {
items.push(`controller: ${msg}`);
}
// Use full node data for detailed error messages when available
const fullNodes = status?.nodes || [];
if (fullNodes.length > 0) {
for (const node of fullNodes) {
const name = node.nodeInfo?.name || 'unknown';
for (const ne of node.nodeErrors || []) {
const msg = (ne.message || '').trim();
if (msg) items.push(`node ${name}: ${msg}`);
}
}
} else {
// Fall back to summary error counts / first error message
// Summary errors stay independent of explicitly loaded diagnostics.
for (const ns of nodeSummaries) {
const count = ns.errorCount || 0;
if (count === 1 && ns.firstError) {
Expand All @@ -145,10 +128,9 @@ export default function App() {
items.push(`node ${ns.name || 'unknown'}: ${count} error(s)`);
}
}
}
items.sort();
return items;
}, [summary?.warnings, status?.warnings, status?.nodes, nodeSummaries]);
}, [summary?.warnings, nodeSummaries]);

useEffect(() => {
const key = activeErrors.join('\n');
Expand Down Expand Up @@ -208,7 +190,6 @@ export default function App() {
}, []);

const {
activeSelectedNode,
effectivePullEnabled,
gatewayByNode,
nodeK8sStatusMap,
Expand All @@ -220,7 +201,7 @@ export default function App() {
visibleNodeSummaries
} = useDashboardData({
summary,
status,
status: null,
nodes,
nodeSummaries,
gatewayPools,
Expand All @@ -229,7 +210,7 @@ export default function App() {
selectedNodeTypesFilter,
pullEnabledOptimistic,
selectedNodeName,
nodeDetail
nodeDetail: noLegacyDetail
});

// All known node names for the detail modal's peer navigation
Expand All @@ -251,10 +232,10 @@ export default function App() {
}
}, [nodeSummaries, nodes, selectedNodeName]);

const wsState = wsConnected ? 'ok' : (summary || status) ? 'warn' : 'err';
const wsState = wsConnected ? 'ok' : summary ? 'warn' : 'err';
const wsLabel = wsConnected
? 'WebSocket connected'
: (summary || status)
: summary
? 'Polling only'
: 'No data';

Expand Down Expand Up @@ -285,10 +266,9 @@ export default function App() {
return <div className="card-maximized">{content}</div>;
};

// Build info from summary or status
const buildInfo = summary?.buildInfo || status?.buildInfo;
const leaderInfo = summary?.leaderInfo || status?.leaderInfo;
const timestamp = summary?.timestamp || status?.timestamp;
const buildInfo = summary?.buildInfo;
const leaderInfo = summary?.leaderInfo;
const timestamp = summary?.timestamp;

if (maximizedPanel) {
return (
Expand All @@ -298,11 +278,12 @@ export default function App() {
<Suspense fallback={null}>
<NodeDetailModal
nodeName={selectedNodeName}
node={activeSelectedNode}
detail={detail}
onLoad={loadNodeDetail}
allNodeNames={allNodeNames}
gatewayByNode={gatewayByNode}
nodeK8sStatusMap={nodeK8sStatusMap}
azureTenantId={summary?.azureTenantId || status?.azureTenantId}
azureTenantId={summary?.azureTenantId}
pullEnabled={effectivePullEnabled}
theme={theme}
detailTab={selectedNodeDetailTab}
Expand Down Expand Up @@ -488,7 +469,7 @@ export default function App() {
<div className="overview-grid">
<div className="card overview-metric-card">
<div className="section-title overview-metric-title">Sites</div>
<div className="overview-metric-value">{(summary?.siteCount ?? status?.siteCount ?? sites.length).toLocaleString()}</div>
<div className="overview-metric-value">{(summary?.siteCount ?? sites.length).toLocaleString()}</div>
</div>
<div className="card overview-metric-card">
<div className="section-title overview-metric-title">Gateway Pools</div>
Expand Down Expand Up @@ -519,11 +500,12 @@ export default function App() {
<Suspense fallback={null}>
<NodeDetailModal
nodeName={selectedNodeName}
node={activeSelectedNode}
detail={detail}
onLoad={loadNodeDetail}
allNodeNames={allNodeNames}
gatewayByNode={gatewayByNode}
nodeK8sStatusMap={nodeK8sStatusMap}
azureTenantId={summary?.azureTenantId || status?.azureTenantId}
azureTenantId={summary?.azureTenantId}
pullEnabled={effectivePullEnabled}
theme={theme}
detailTab={selectedNodeDetailTab}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0

import { ClusterStatus, ClusterStatusDelta, ClusterSummary, ClusterSummaryDelta, NodeStatus, NodeDetailResult } from './types';
import type { ClusterStatus, ClusterStatusDelta, ClusterSummary, ClusterSummaryDelta, NodeStatus, NodeDetailResult } from './types';

export type StatusEvent = {
type: 'cluster_status' | 'cluster_status_delta' | 'cluster_summary' | 'cluster_summary_delta' | 'node_detail_response' | 'node_detail_update';
Expand Down Expand Up @@ -46,10 +46,10 @@ export function pollNodeDetails(name: string, requestId: string, signal: AbortSi
});
}

export async function fetchClusterStatus(): Promise<ClusterStatus> {
export async function fetchClusterStatus(signal?: AbortSignal): Promise<ClusterSummary | ClusterStatus> {
const url = buildControllerUrl('/status/json');
try {
const res = await fetch(url);
const res = await fetch(url, { signal, cache: 'no-store', credentials: 'same-origin' });
if (!res.ok) {
let details = '';
try {
Expand Down
Loading