@skyhook-io/radar-app 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -4,7 +4,8 @@ import { clsx } from 'clsx'
|
|
|
4
4
|
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
5
5
|
import { openExternal } from '../../utils/navigation'
|
|
6
6
|
import { useDiagnostics } from '../../api/client'
|
|
7
|
-
import type { DiagnosticsSnapshot, DiagMetricsSourceHealth, DiagDropRecord, DiagErrorEntry } from '../../api/client'
|
|
7
|
+
import type { DiagnosticsSnapshot, DiagMetricsSourceHealth, DiagDropRecord, DiagErrorEntry, DiagCacheSyncStatus, DiagInformerSyncStatus, DiagSyncPhase, DiagSampleWindow } from '../../api/client'
|
|
8
|
+
import { getK8sUIPerfSnapshot, type K8sUIPerfSnapshot } from '@skyhook-io/k8s-ui'
|
|
8
9
|
|
|
9
10
|
interface DiagnosticsOverlayProps {
|
|
10
11
|
onClose: () => void
|
|
@@ -31,9 +32,10 @@ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverla
|
|
|
31
32
|
|
|
32
33
|
const copyToClipboard = useCallback(async (type: 'json' | 'formatted') => {
|
|
33
34
|
if (!data) return
|
|
35
|
+
const frontendPerf = getK8sUIPerfSnapshot()
|
|
34
36
|
const text = type === 'json'
|
|
35
|
-
? JSON.stringify(data, null, 2)
|
|
36
|
-
: formatForGitHub(data)
|
|
37
|
+
? JSON.stringify({ ...data, frontendPerf }, null, 2)
|
|
38
|
+
: formatForGitHub(data, frontendPerf)
|
|
37
39
|
try {
|
|
38
40
|
await navigator.clipboard.writeText(text)
|
|
39
41
|
setCopied(type)
|
|
@@ -46,7 +48,7 @@ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverla
|
|
|
46
48
|
|
|
47
49
|
const openBugReport = useCallback(() => {
|
|
48
50
|
if (!data) return
|
|
49
|
-
const body = formatForBugReport(data)
|
|
51
|
+
const body = formatForBugReport(data, getK8sUIPerfSnapshot())
|
|
50
52
|
const url = `https://github.com/skyhook-io/radar/issues/new?labels=bug&body=${encodeURIComponent(body)}`
|
|
51
53
|
if (url.length > 8000) {
|
|
52
54
|
// URL too long for GitHub — copy diagnostics to clipboard and open blank issue
|
|
@@ -97,7 +99,7 @@ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverla
|
|
|
97
99
|
{/* Content */}
|
|
98
100
|
<div className="overflow-y-auto flex-1 px-5 py-4 space-y-4">
|
|
99
101
|
{isLoading && (
|
|
100
|
-
<div className="text-sm text-theme-text-tertiary text-center py-8">Loading diagnostics
|
|
102
|
+
<div className="text-sm text-theme-text-tertiary text-center py-8">Loading diagnostics…</div>
|
|
101
103
|
)}
|
|
102
104
|
{error && (
|
|
103
105
|
<div className="text-sm text-red-400 text-center py-8">Failed to load diagnostics: {(error as Error).message}</div>
|
|
@@ -116,6 +118,7 @@ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverla
|
|
|
116
118
|
<TrafficSection data={data} />
|
|
117
119
|
<PermissionsSection data={data} />
|
|
118
120
|
<APIDiscoverySection data={data} />
|
|
121
|
+
<PerfSection data={data} />
|
|
119
122
|
<RuntimeSection data={data} />
|
|
120
123
|
<ConfigSection data={data} />
|
|
121
124
|
{data.errors && data.errors.length > 0 && (
|
|
@@ -315,10 +318,41 @@ function EventPipelineSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
|
315
318
|
function InformersSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
316
319
|
if (!data.informers) return null
|
|
317
320
|
const inf = data.informers
|
|
321
|
+
const sync = inf.syncStatus
|
|
322
|
+
const phaseWarn = sync ? sync.phase !== 'complete' : false
|
|
323
|
+
const criticalWarn = sync ? sync.criticalSynced < sync.criticalTotal : false
|
|
324
|
+
const promoted = sync?.promotedKinds ?? []
|
|
325
|
+
const pendingCritical = sync?.pendingCritical ?? []
|
|
326
|
+
const pendingDeferred = sync?.pendingDeferred ?? []
|
|
327
|
+
const sectionWarn = phaseWarn || criticalWarn || promoted.length > 0
|
|
318
328
|
return (
|
|
319
|
-
<Section title="Informers">
|
|
329
|
+
<Section title="Informers" warn={sectionWarn}>
|
|
320
330
|
<Row label="Typed" value={inf.typedCount} />
|
|
321
331
|
<Row label="Dynamic (CRDs)" value={inf.dynamicCount} />
|
|
332
|
+
{sync && (
|
|
333
|
+
<>
|
|
334
|
+
<Row
|
|
335
|
+
label="Sync Phase"
|
|
336
|
+
value={`${formatSyncPhase(sync.phase)} (${formatElapsed(sync.elapsedSec)})`}
|
|
337
|
+
warn={phaseWarn}
|
|
338
|
+
/>
|
|
339
|
+
<Row
|
|
340
|
+
label="Critical Synced"
|
|
341
|
+
value={`${sync.criticalSynced} / ${sync.criticalTotal}`}
|
|
342
|
+
warn={criticalWarn}
|
|
343
|
+
/>
|
|
344
|
+
<Row
|
|
345
|
+
label="Deferred Synced"
|
|
346
|
+
value={`${sync.deferredSynced} / ${sync.deferredTotal}`}
|
|
347
|
+
/>
|
|
348
|
+
{promoted.length > 0 && (
|
|
349
|
+
<Row label="Promoted to Deferred" value={promoted.join(', ')} warn />
|
|
350
|
+
)}
|
|
351
|
+
{(pendingCritical.length > 0 || pendingDeferred.length > 0) && (
|
|
352
|
+
<PendingInformers sync={sync} />
|
|
353
|
+
)}
|
|
354
|
+
</>
|
|
355
|
+
)}
|
|
322
356
|
{inf.watchedCRDs && inf.watchedCRDs.length > 0 && (
|
|
323
357
|
<Row label="Watched CRDs" value={inf.watchedCRDs.join(', ')} />
|
|
324
358
|
)}
|
|
@@ -326,6 +360,53 @@ function InformersSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
|
326
360
|
)
|
|
327
361
|
}
|
|
328
362
|
|
|
363
|
+
function PendingInformers({ sync }: { sync: DiagCacheSyncStatus }) {
|
|
364
|
+
const pending = getPendingInformers(sync)
|
|
365
|
+
if (pending.length === 0) return null
|
|
366
|
+
return (
|
|
367
|
+
<div className="mt-1.5 pt-1.5 border-t border-theme-border-light">
|
|
368
|
+
<span className="text-[10px] text-theme-text-tertiary uppercase">Pending Informers ({pending.length})</span>
|
|
369
|
+
{pending.map((i: DiagInformerSyncStatus) => (
|
|
370
|
+
<Row
|
|
371
|
+
key={i.kind}
|
|
372
|
+
label={`${i.kind} (${i.deferred ? 'deferred' : 'critical'})`}
|
|
373
|
+
value={`${i.items.toLocaleString()} items so far`}
|
|
374
|
+
warn={!i.deferred}
|
|
375
|
+
/>
|
|
376
|
+
))}
|
|
377
|
+
</div>
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function getPendingInformers(sync: DiagCacheSyncStatus): DiagInformerSyncStatus[] {
|
|
382
|
+
const pendingNames = new Set([
|
|
383
|
+
...(sync.pendingCritical ?? []),
|
|
384
|
+
...(sync.pendingDeferred ?? []),
|
|
385
|
+
])
|
|
386
|
+
return sync.informers
|
|
387
|
+
.filter((i) => pendingNames.has(i.kind))
|
|
388
|
+
.sort((a, b) => Number(a.deferred) - Number(b.deferred) || a.kind.localeCompare(b.kind))
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function formatSyncPhase(phase: DiagSyncPhase): string {
|
|
392
|
+
switch (phase) {
|
|
393
|
+
case 'not_started': return 'not started'
|
|
394
|
+
case 'syncing_critical': return 'syncing critical'
|
|
395
|
+
case 'syncing_deferred': return 'syncing deferred'
|
|
396
|
+
case 'complete': return 'complete'
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function formatElapsed(sec: number): string {
|
|
401
|
+
const s = Math.max(0, sec)
|
|
402
|
+
if (s < 1) return `${Math.round(s * 1000)}ms`
|
|
403
|
+
if (s < 60) return `${s.toFixed(1)}s`
|
|
404
|
+
const total = Math.round(s)
|
|
405
|
+
const m = Math.floor(total / 60)
|
|
406
|
+
const rem = total - m * 60
|
|
407
|
+
return `${m}m ${rem}s`
|
|
408
|
+
}
|
|
409
|
+
|
|
329
410
|
function PrometheusSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
330
411
|
if (!data.prometheus) return null
|
|
331
412
|
const p = data.prometheus
|
|
@@ -381,6 +462,73 @@ function APIDiscoverySection({ data }: { data: DiagnosticsSnapshot }) {
|
|
|
381
462
|
)
|
|
382
463
|
}
|
|
383
464
|
|
|
465
|
+
function PerfSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
466
|
+
const backend = data.perf
|
|
467
|
+
const frontend = getK8sUIPerfSnapshot()
|
|
468
|
+
if (!backend && frontend.totalLayouts === 0 && frontend.totalStructureKeyComputes === 0) return null
|
|
469
|
+
// Warn when SSE has dropped frames, the topology payload window's p95 exceeds
|
|
470
|
+
// 5 MB, or the frontend ELK layout p95 exceeds 1s — these are the load-bearing
|
|
471
|
+
// thresholds for "the tab is going to feel bad."
|
|
472
|
+
const warn =
|
|
473
|
+
(backend?.sse.totalDrops ?? 0) > 0 ||
|
|
474
|
+
(backend?.topology.payloadBytes.p95 ?? 0) > 5 * 1024 * 1024 ||
|
|
475
|
+
frontend.layoutMs.p95 > 1000
|
|
476
|
+
return (
|
|
477
|
+
<Section title="Performance" warn={warn}>
|
|
478
|
+
{backend && (
|
|
479
|
+
<>
|
|
480
|
+
<Row label="Topology Builds" value={backend.topology.totalBuilds.toLocaleString()} />
|
|
481
|
+
<Row label=" Duration" value={formatSampleDuration(backend.topology.durationUs)} />
|
|
482
|
+
<Row label=" Node Count" value={formatSampleCount(backend.topology.nodeCount)} />
|
|
483
|
+
<Row label=" Edge Count" value={formatSampleCount(backend.topology.edgeCount)} />
|
|
484
|
+
<Row label=" Payload" value={formatSampleBytes(backend.topology.payloadBytes)} warn={backend.topology.payloadBytes.p95 > 5 * 1024 * 1024} />
|
|
485
|
+
<Row label=" Estimated Nodes" value={formatSampleCount(backend.topology.estimatedNodes)} />
|
|
486
|
+
<Row label="SSE Broadcasts" value={backend.sse.totalBroadcasts.toLocaleString()} />
|
|
487
|
+
<Row label="SSE Drops" value={backend.sse.totalDrops.toLocaleString()} warn={backend.sse.totalDrops > 0} />
|
|
488
|
+
</>
|
|
489
|
+
)}
|
|
490
|
+
{(frontend.totalLayouts > 0 || frontend.totalStructureKeyComputes > 0) && (
|
|
491
|
+
<>
|
|
492
|
+
<Row label="Frontend Layouts" value={`${frontend.totalLayouts.toLocaleString()} (skipped ${frontend.totalLayoutsSkipped.toLocaleString()})`} />
|
|
493
|
+
<Row label=" ELK Duration" value={formatFrontendMs(frontend.layoutMs)} warn={frontend.layoutMs.p95 > 1000} />
|
|
494
|
+
<Row label=" Last Rendered" value={`${frontend.lastLayoutNodeCount.toLocaleString()} nodes / ${frontend.lastLayoutEdgeCount.toLocaleString()} edges`} />
|
|
495
|
+
<Row label="Frontend structureKey" value={`${frontend.totalStructureKeyComputes.toLocaleString()} computes`} />
|
|
496
|
+
<Row label=" Duration" value={formatFrontendUs(frontend.structureKeyUs)} />
|
|
497
|
+
</>
|
|
498
|
+
)}
|
|
499
|
+
</Section>
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function formatSampleDuration(w: DiagSampleWindow): string {
|
|
504
|
+
if (w.count === 0) return 'no samples'
|
|
505
|
+
const ms = (us: number) => (us / 1000).toFixed(us < 1000 ? 2 : 1)
|
|
506
|
+
return `last ${ms(w.last)}ms · p50 ${ms(w.p50)} · p95 ${ms(w.p95)} · max ${ms(w.max)}ms (n=${w.count})`
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function formatSampleCount(w: DiagSampleWindow): string {
|
|
510
|
+
if (w.count === 0) return 'no samples'
|
|
511
|
+
return `last ${w.last.toLocaleString()} · p50 ${w.p50.toLocaleString()} · p95 ${w.p95.toLocaleString()} · max ${w.max.toLocaleString()}`
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function formatSampleBytes(w: DiagSampleWindow): string {
|
|
515
|
+
if (w.count === 0) return 'no samples'
|
|
516
|
+
const kb = (b: number) => b < 1024 * 1024 ? `${(b / 1024).toFixed(1)}KB` : `${(b / 1024 / 1024).toFixed(2)}MB`
|
|
517
|
+
return `last ${kb(w.last)} · p50 ${kb(w.p50)} · p95 ${kb(w.p95)} · max ${kb(w.max)}`
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function formatFrontendMs(w: { count: number; last: number; p50: number; p95: number; max: number }): string {
|
|
521
|
+
if (w.count === 0) return 'no samples'
|
|
522
|
+
const fmt = (v: number) => v < 100 ? v.toFixed(1) : Math.round(v).toString()
|
|
523
|
+
return `last ${fmt(w.last)}ms · p50 ${fmt(w.p50)} · p95 ${fmt(w.p95)} · max ${fmt(w.max)}ms (n=${w.count})`
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function formatFrontendUs(w: { count: number; last: number; p50: number; p95: number; max: number }): string {
|
|
527
|
+
if (w.count === 0) return 'no samples'
|
|
528
|
+
const fmt = (v: number) => v < 1000 ? `${v.toFixed(0)}μs` : `${(v / 1000).toFixed(2)}ms`
|
|
529
|
+
return `last ${fmt(w.last)} · p50 ${fmt(w.p50)} · p95 ${fmt(w.p95)} · max ${fmt(w.max)} (n=${w.count})`
|
|
530
|
+
}
|
|
531
|
+
|
|
384
532
|
function RuntimeSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
385
533
|
if (!data.runtime) return null
|
|
386
534
|
const rt = data.runtime
|
|
@@ -406,6 +554,7 @@ function ConfigSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
|
406
554
|
<Row label="History Limit" value={cfg.historyLimit.toLocaleString()} />
|
|
407
555
|
<Row label="MCP Enabled" value={cfg.mcpEnabled ? 'Yes' : 'No'} />
|
|
408
556
|
<Row label="Prometheus URL" value={cfg.hasPrometheusURL ? 'Set' : 'Auto-discover'} />
|
|
557
|
+
<Row label="Prometheus Headers" value={cfg.hasPrometheusHeaders ? 'Set' : 'None'} />
|
|
409
558
|
</Section>
|
|
410
559
|
)
|
|
411
560
|
}
|
|
@@ -431,7 +580,7 @@ function CopyButton({ label, onClick, copied }: { label: string; onClick: () =>
|
|
|
431
580
|
|
|
432
581
|
// --- GitHub-friendly formatting ---
|
|
433
582
|
|
|
434
|
-
function formatForGitHub(data: DiagnosticsSnapshot, includeRawJson = true): string {
|
|
583
|
+
function formatForGitHub(data: DiagnosticsSnapshot, frontendPerf?: K8sUIPerfSnapshot, includeRawJson = true): string {
|
|
435
584
|
const lines: string[] = []
|
|
436
585
|
lines.push(`## Radar Diagnostics`)
|
|
437
586
|
lines.push(``)
|
|
@@ -512,6 +661,36 @@ function formatForGitHub(data: DiagnosticsSnapshot, includeRawJson = true): stri
|
|
|
512
661
|
const inf = data.informers
|
|
513
662
|
lines.push(`### Informers`)
|
|
514
663
|
lines.push(`- Typed: ${inf.typedCount} | Dynamic: ${inf.dynamicCount}`)
|
|
664
|
+
if (inf.syncStatus) {
|
|
665
|
+
const sync = inf.syncStatus
|
|
666
|
+
lines.push(`- Sync Phase: \`${sync.phase}\` (${formatElapsed(sync.elapsedSec)})`)
|
|
667
|
+
lines.push(`- Critical: ${sync.criticalSynced}/${sync.criticalTotal} synced | Deferred: ${sync.deferredSynced}/${sync.deferredTotal} synced`)
|
|
668
|
+
if (sync.promotedKinds && sync.promotedKinds.length > 0) {
|
|
669
|
+
lines.push(`- **Promoted to Deferred:** ${sync.promotedKinds.join(', ')}`)
|
|
670
|
+
}
|
|
671
|
+
const pending = getPendingInformers(sync)
|
|
672
|
+
if (pending.length > 0) {
|
|
673
|
+
const parts = pending.map((i) => {
|
|
674
|
+
const flags = [i.deferred ? 'deferred' : 'critical', `${i.items.toLocaleString()} items`]
|
|
675
|
+
if (i.forbiddenSeen) flags.push('forbidden')
|
|
676
|
+
if (i.lastError) flags.push(`err: ${i.lastError}`)
|
|
677
|
+
return `${i.kind}(${flags.join(', ')})`
|
|
678
|
+
})
|
|
679
|
+
lines.push(`- **Pending:** ${parts.join(', ')}`)
|
|
680
|
+
}
|
|
681
|
+
// Synced informers that have since hit a watch error or 403 — a count of 0
|
|
682
|
+
// from one of these is a stale/forbidden lister, not an empty cluster.
|
|
683
|
+
const errored = sync.informers.filter((i) => !pending.includes(i) && (i.lastError || i.forbiddenSeen))
|
|
684
|
+
if (errored.length > 0) {
|
|
685
|
+
const parts = errored.map((i) => {
|
|
686
|
+
const flags: string[] = []
|
|
687
|
+
if (i.forbiddenSeen) flags.push('forbidden')
|
|
688
|
+
if (i.lastError) flags.push(`err: ${i.lastError}`)
|
|
689
|
+
return `${i.kind}(${flags.join(', ')})`
|
|
690
|
+
})
|
|
691
|
+
lines.push(`- **Informer errors:** ${parts.join(', ')}`)
|
|
692
|
+
}
|
|
693
|
+
}
|
|
515
694
|
if (inf.watchedCRDs && inf.watchedCRDs.length > 0) {
|
|
516
695
|
lines.push(`- CRDs: ${inf.watchedCRDs.join(', ')}`)
|
|
517
696
|
}
|
|
@@ -548,6 +727,37 @@ function formatForGitHub(data: DiagnosticsSnapshot, includeRawJson = true): stri
|
|
|
548
727
|
lines.push(``)
|
|
549
728
|
}
|
|
550
729
|
|
|
730
|
+
if (data.perf || (frontendPerf && (frontendPerf.totalLayouts > 0 || frontendPerf.totalStructureKeyComputes > 0))) {
|
|
731
|
+
lines.push(`### Performance`)
|
|
732
|
+
if (data.perf) {
|
|
733
|
+
const p = data.perf
|
|
734
|
+
const fmtMs = (us: number) => (us / 1000).toFixed(us < 1000 ? 2 : 1)
|
|
735
|
+
const fmtKB = (b: number) => b < 1024 * 1024 ? `${(b / 1024).toFixed(1)}KB` : `${(b / 1024 / 1024).toFixed(2)}MB`
|
|
736
|
+
lines.push(`- Topology Builds: ${p.topology.totalBuilds.toLocaleString()}`)
|
|
737
|
+
if (p.topology.durationUs.count > 0) {
|
|
738
|
+
lines.push(` - Duration (ms): last ${fmtMs(p.topology.durationUs.last)} · p50 ${fmtMs(p.topology.durationUs.p50)} · p95 ${fmtMs(p.topology.durationUs.p95)} · max ${fmtMs(p.topology.durationUs.max)}`)
|
|
739
|
+
lines.push(` - Nodes: last ${p.topology.nodeCount.last} · p95 ${p.topology.nodeCount.p95} · max ${p.topology.nodeCount.max}`)
|
|
740
|
+
lines.push(` - Edges: last ${p.topology.edgeCount.last} · p95 ${p.topology.edgeCount.p95} · max ${p.topology.edgeCount.max}`)
|
|
741
|
+
lines.push(` - Payload: last ${fmtKB(p.topology.payloadBytes.last)} · p95 ${fmtKB(p.topology.payloadBytes.p95)} · max ${fmtKB(p.topology.payloadBytes.max)}`)
|
|
742
|
+
lines.push(` - Estimated Nodes: last ${p.topology.estimatedNodes.last} · p95 ${p.topology.estimatedNodes.p95}`)
|
|
743
|
+
}
|
|
744
|
+
lines.push(`- SSE: ${p.sse.totalBroadcasts.toLocaleString()} broadcasts, ${p.sse.totalDrops.toLocaleString()} drops`)
|
|
745
|
+
}
|
|
746
|
+
if (frontendPerf && (frontendPerf.totalLayouts > 0 || frontendPerf.totalStructureKeyComputes > 0)) {
|
|
747
|
+
const fmt = (v: number) => v < 100 ? v.toFixed(1) : Math.round(v).toString()
|
|
748
|
+
lines.push(`- Frontend Layouts: ${frontendPerf.totalLayouts.toLocaleString()} (${frontendPerf.totalLayoutsSkipped.toLocaleString()} skipped)`)
|
|
749
|
+
if (frontendPerf.layoutMs.count > 0) {
|
|
750
|
+
lines.push(` - ELK (ms): last ${fmt(frontendPerf.layoutMs.last)} · p50 ${fmt(frontendPerf.layoutMs.p50)} · p95 ${fmt(frontendPerf.layoutMs.p95)} · max ${fmt(frontendPerf.layoutMs.max)}`)
|
|
751
|
+
lines.push(` - Last rendered: ${frontendPerf.lastLayoutNodeCount.toLocaleString()} nodes / ${frontendPerf.lastLayoutEdgeCount.toLocaleString()} edges`)
|
|
752
|
+
}
|
|
753
|
+
if (frontendPerf.structureKeyUs.count > 0) {
|
|
754
|
+
const fmtUs = (v: number) => v < 1000 ? `${Math.round(v)}μs` : `${(v / 1000).toFixed(2)}ms`
|
|
755
|
+
lines.push(` - structureKey: ${frontendPerf.totalStructureKeyComputes.toLocaleString()} computes · p50 ${fmtUs(frontendPerf.structureKeyUs.p50)} · p95 ${fmtUs(frontendPerf.structureKeyUs.p95)} · max ${fmtUs(frontendPerf.structureKeyUs.max)}`)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
lines.push(``)
|
|
759
|
+
}
|
|
760
|
+
|
|
551
761
|
if (data.runtime) {
|
|
552
762
|
const rt = data.runtime
|
|
553
763
|
lines.push(`### Runtime`)
|
|
@@ -591,8 +801,8 @@ function formatForGitHub(data: DiagnosticsSnapshot, includeRawJson = true): stri
|
|
|
591
801
|
return lines.join('\n')
|
|
592
802
|
}
|
|
593
803
|
|
|
594
|
-
function formatForBugReport(data: DiagnosticsSnapshot): string {
|
|
595
|
-
const diagnostics = formatForGitHub(data, false)
|
|
804
|
+
function formatForBugReport(data: DiagnosticsSnapshot, frontendPerf?: K8sUIPerfSnapshot): string {
|
|
805
|
+
const diagnostics = formatForGitHub(data, frontendPerf, false)
|
|
596
806
|
|
|
597
807
|
const lines: string[] = []
|
|
598
808
|
lines.push(`## Describe the bug`)
|
|
@@ -39,10 +39,10 @@ export function Markdown({ children, className }: MarkdownProps) {
|
|
|
39
39
|
</a>
|
|
40
40
|
),
|
|
41
41
|
ul: ({ children }) => (
|
|
42
|
-
<ul className="list-disc list-
|
|
42
|
+
<ul className="list-disc list-outside pl-4 my-2 space-y-1 text-theme-text-secondary">{children}</ul>
|
|
43
43
|
),
|
|
44
44
|
ol: ({ children }) => (
|
|
45
|
-
<ol className="list-decimal list-
|
|
45
|
+
<ol className="list-decimal list-outside pl-4 my-2 space-y-1 text-theme-text-secondary">{children}</ol>
|
|
46
46
|
),
|
|
47
47
|
li: ({ children }) => (
|
|
48
48
|
<li className="leading-relaxed">{children}</li>
|
|
@@ -51,7 +51,7 @@ export function Markdown({ children, className }: MarkdownProps) {
|
|
|
51
51
|
const isInline = !className
|
|
52
52
|
if (isInline) {
|
|
53
53
|
return (
|
|
54
|
-
<code className="
|
|
54
|
+
<code className="inline-code">
|
|
55
55
|
{children}
|
|
56
56
|
</code>
|
|
57
57
|
)
|