@skyhook-io/radar-app 1.3.2 → 1.3.4
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/package.json +1 -1
- package/src/App.tsx +111 -58
- package/src/api/client.ts +29 -1
- package/src/components/ConnectionErrorView.tsx +2 -2
- package/src/components/helm/ChartBrowser.tsx +7 -3
- package/src/components/helm/InstallWizard.tsx +1 -1
- package/src/components/helm/RoleGatedPanel.tsx +2 -2
- package/src/components/home/ClusterHealthCard.tsx +1 -1
- package/src/components/home/HomeView.tsx +14 -3
- package/src/components/home/MCPSetupDialog.tsx +4 -4
- package/src/components/issues/IssuesPane.tsx +78 -0
- package/src/components/portforward/PortForwardButton.tsx +1 -1
- package/src/components/portforward/PortForwardManager.tsx +1 -1
- package/src/components/resource/PrometheusCharts.tsx +18 -159
- package/src/components/resources/ImageFilesystemModal.tsx +1 -2
- package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -2
- package/src/components/settings/MyPermissionsDialog.tsx +1 -1
- package/src/components/settings/SettingsDialog.tsx +22 -2
- package/src/components/timeline/TimelineSwimlanes.tsx +8 -1311
- package/src/components/ui/Markdown.tsx +1 -1
- package/src/components/ui/UpdateNotification.tsx +1 -1
- package/src/components/workload/WorkloadView.tsx +188 -6
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { useQueryClient } from '@tanstack/react-query'
|
|
|
6
6
|
import { useNavigate, useLocation, useSearchParams, useNavigationType, NavigationType } from 'react-router-dom'
|
|
7
7
|
import { HomeView } from './components/home/HomeView'
|
|
8
8
|
import { DebugOverlay } from './components/DebugOverlay'
|
|
9
|
-
import { TopologyGraph, TopologyFilterSidebar, TopologyControls, gitOpsRouteForKind } from '@skyhook-io/k8s-ui'
|
|
9
|
+
import { TopologyGraph, TopologySearch, TopologyFilterSidebar, TopologyControls, gitOpsRouteForKind } from '@skyhook-io/k8s-ui'
|
|
10
10
|
import { TimelineView } from './components/timeline/TimelineView'
|
|
11
11
|
import { ResourcesView } from './components/resources/ResourcesView'
|
|
12
12
|
import { serializeColumnFilters } from './components/resources/resource-utils'
|
|
@@ -17,6 +17,7 @@ import { HelmView } from './components/helm/HelmView'
|
|
|
17
17
|
import { TrafficView } from './components/traffic/TrafficView'
|
|
18
18
|
import { CostView } from './components/cost/CostView'
|
|
19
19
|
import { AuditView } from './components/audit/AuditView'
|
|
20
|
+
import { IssuesPane } from './components/issues/IssuesPane'
|
|
20
21
|
import { GitOpsView } from './components/gitops/GitOpsView'
|
|
21
22
|
import { HelmReleaseDrawer } from './components/helm/HelmReleaseDrawer'
|
|
22
23
|
import { PortForwardProvider, PortForwardIndicator, PortForwardPanel } from './components/portforward/PortForwardManager'
|
|
@@ -41,7 +42,7 @@ import { routePath, apiUrl, getAuthHeaders, getCredentialsMode } from './api/con
|
|
|
41
42
|
import { KeyboardShortcutProvider, useRegisterShortcut, useRegisterShortcuts } from './hooks/useKeyboardShortcuts'
|
|
42
43
|
import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
|
|
43
44
|
import radarLoadingIcon from '@skyhook-io/k8s-ui/assets/radar/radar-icon-loading.svg'
|
|
44
|
-
import { RefreshCw, Network, List, Clock, Package, Sun, Moon, Activity, Home, Star, Search, Bug, Settings, SquareTerminal, ShieldCheck, GitBranch
|
|
45
|
+
import { RefreshCw, Network, List, Clock, Package, Sun, Moon, Activity, Home, Star, Search, Bug, Settings, SquareTerminal, ShieldCheck, GitBranch } from 'lucide-react'
|
|
45
46
|
import { useTheme } from './context/ThemeContext'
|
|
46
47
|
import { Tooltip } from './components/ui/Tooltip'
|
|
47
48
|
import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
|
|
@@ -91,34 +92,8 @@ const FLEET_MODE_KINDS = new Set<NodeKind>([
|
|
|
91
92
|
])
|
|
92
93
|
|
|
93
94
|
// Convert API resource name back to topology node ID prefix
|
|
94
|
-
function apiResourceToNodeIdPrefix(apiResource: string): string {
|
|
95
|
-
const prefixMap: Record<string, string> = {
|
|
96
|
-
'pods': 'pod',
|
|
97
|
-
'services': 'service',
|
|
98
|
-
'deployments': 'deployment',
|
|
99
|
-
'daemonsets': 'daemonset',
|
|
100
|
-
'statefulsets': 'statefulset',
|
|
101
|
-
'replicasets': 'replicaset',
|
|
102
|
-
'ingresses': 'ingress',
|
|
103
|
-
'gateways': 'gateway',
|
|
104
|
-
'httproutes': 'httproute',
|
|
105
|
-
'grpcroutes': 'grpcroute',
|
|
106
|
-
'tcproutes': 'tcproute',
|
|
107
|
-
'tlsroutes': 'tlsroute',
|
|
108
|
-
'configmaps': 'configmap',
|
|
109
|
-
'secrets': 'secret',
|
|
110
|
-
'horizontalpodautoscalers': 'horizontalpodautoscaler',
|
|
111
|
-
'jobs': 'job',
|
|
112
|
-
'cronjobs': 'cronjob',
|
|
113
|
-
'persistentvolumeclaims': 'persistentvolumeclaim',
|
|
114
|
-
'namespaces': 'namespace',
|
|
115
|
-
'httpproxies': 'httpproxy', // Contour
|
|
116
|
-
}
|
|
117
|
-
return prefixMap[apiResource] || apiResource.replace(/s$/, '')
|
|
118
|
-
}
|
|
119
|
-
|
|
120
95
|
// Extended MainView type that includes traffic and cost
|
|
121
|
-
type ExtendedMainView = MainView | 'traffic' | 'cost' | 'workload' | 'audit' | 'gitops' | 'compare'
|
|
96
|
+
type ExtendedMainView = MainView | 'traffic' | 'cost' | 'workload' | 'audit' | 'gitops' | 'compare' | 'issues'
|
|
122
97
|
|
|
123
98
|
// Extract view from URL path
|
|
124
99
|
function getViewFromPath(pathname: string): ExtendedMainView {
|
|
@@ -134,6 +109,7 @@ function getViewFromPath(pathname: string): ExtendedMainView {
|
|
|
134
109
|
if (path === 'audit') return 'audit'
|
|
135
110
|
if (path === 'gitops') return 'gitops'
|
|
136
111
|
if (path === 'compare') return 'compare'
|
|
112
|
+
if (path === 'issues') return 'issues'
|
|
137
113
|
return 'home'
|
|
138
114
|
}
|
|
139
115
|
|
|
@@ -301,6 +277,8 @@ function AppInner() {
|
|
|
301
277
|
// Topology filter state
|
|
302
278
|
const [visibleKinds, setVisibleKinds] = useState<Set<NodeKind>>(() => new Set(DEFAULT_VISIBLE_KINDS))
|
|
303
279
|
const [filterSidebarCollapsed, setFilterSidebarCollapsed] = useState(false)
|
|
280
|
+
// Topology node-search → canvas focus request (nonce lets the same node re-focus)
|
|
281
|
+
const [topologyFocus, setTopologyFocus] = useState<{ id: string; nonce: number } | null>(null)
|
|
304
282
|
// Track CRD kinds that have been auto-added to visibleKinds so we don't override user toggles
|
|
305
283
|
const seededCRDKindsRef = useRef<Set<string>>(new Set())
|
|
306
284
|
|
|
@@ -344,6 +322,14 @@ function AppInner() {
|
|
|
344
322
|
// Suppress the mainView-change clear effect during controlled expand/collapse transitions.
|
|
345
323
|
const suppressViewClearRef = useRef(false)
|
|
346
324
|
|
|
325
|
+
// On a history Pop (back/forward) the URL is authoritative. The URL-write
|
|
326
|
+
// effect, running with not-yet-synced state, would otherwise write the stale
|
|
327
|
+
// state back and revert the Pop — and oscillate with the URL→state read
|
|
328
|
+
// effect (infinite re-render, React #185, blank page). Suppress the writer
|
|
329
|
+
// for the synchronous reconciliation burst after a Pop, then auto-clear (see
|
|
330
|
+
// the arming effect) so later user-driven writes are never affected.
|
|
331
|
+
const skipUrlWriteAfterPopRef = useRef(false)
|
|
332
|
+
|
|
347
333
|
// Close resource drawer when the /resources route no longer matches the
|
|
348
334
|
// selected drawer resource. This covers both in-view kind switches and
|
|
349
335
|
// cross-kind navigations from expanded drawers (for example Node -> View Pods).
|
|
@@ -409,6 +395,27 @@ function AppInner() {
|
|
|
409
395
|
}
|
|
410
396
|
}, [selectedResource])
|
|
411
397
|
|
|
398
|
+
// Navigate from a detector finding (Audit / Issues) to the resources list for
|
|
399
|
+
// its kind, opening the resource. Shared by both queues — the body was
|
|
400
|
+
// duplicated verbatim at each render site. Encodes the opened resource in the
|
|
401
|
+
// URL (?resource=ns/name) — the same deep-link shape the resources view
|
|
402
|
+
// round-trips — so refresh/share keeps the drawer open instead of dropping it.
|
|
403
|
+
const navigateToResourceList = useCallback((resource: SelectedResource) => {
|
|
404
|
+
const pluralKind = kindToPlural(resource.kind)
|
|
405
|
+
setSelectedResource({ ...resource, kind: pluralKind })
|
|
406
|
+
const newParams = new URLSearchParams(searchParams)
|
|
407
|
+
newParams.delete('kind')
|
|
408
|
+
newParams.delete('mode')
|
|
409
|
+
newParams.delete('group')
|
|
410
|
+
newParams.set('resource', resource.namespace ? `${resource.namespace}/${resource.name}` : resource.name)
|
|
411
|
+
if (resource.group) {
|
|
412
|
+
newParams.set('apiGroup', resource.group)
|
|
413
|
+
} else {
|
|
414
|
+
newParams.delete('apiGroup')
|
|
415
|
+
}
|
|
416
|
+
navigate({ pathname: `/resources/${pluralKind}`, search: newParams.toString() })
|
|
417
|
+
}, [searchParams, navigate])
|
|
418
|
+
|
|
412
419
|
// Collapse from expanded WorkloadView back to drawer
|
|
413
420
|
const handleCollapseFromExpanded = useCallback(() => {
|
|
414
421
|
suppressViewClearRef.current = true
|
|
@@ -843,10 +850,35 @@ function AppInner() {
|
|
|
843
850
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- namespaces and setActiveNamespace are intentionally excluded; we only react to server-side changes.
|
|
844
851
|
}, [namespaceScope, namespaceScopeKey])
|
|
845
852
|
|
|
853
|
+
// Arm the skip on every history Pop (location.key changes per nav), then
|
|
854
|
+
// clear it on the next macrotask. The revert/oscillation is a synchronous
|
|
855
|
+
// re-render burst, so a macrotask-deferred clear covers it; clearing
|
|
856
|
+
// afterward means a stale arm can't survive into an unrelated later write
|
|
857
|
+
// (e.g. a Pop that changes none of the write effect's deps would otherwise
|
|
858
|
+
// leave the flag set and silently drop the next user-driven URL write).
|
|
859
|
+
useEffect(() => {
|
|
860
|
+
if (navigationType !== NavigationType.Pop) {
|
|
861
|
+
// Any non-Pop navigation clears the guard. Without this, a Push/Replace
|
|
862
|
+
// that lands before the macrotask fires would run this cleanup (cancelling
|
|
863
|
+
// the timeout) and re-run as a no-op, leaving the flag stuck true and
|
|
864
|
+
// silently suppressing all later URL writes.
|
|
865
|
+
skipUrlWriteAfterPopRef.current = false
|
|
866
|
+
return
|
|
867
|
+
}
|
|
868
|
+
skipUrlWriteAfterPopRef.current = true
|
|
869
|
+
const id = setTimeout(() => { skipUrlWriteAfterPopRef.current = false }, 0)
|
|
870
|
+
return () => clearTimeout(id)
|
|
871
|
+
}, [location.key, navigationType])
|
|
872
|
+
|
|
846
873
|
// Update URL query params when state changes (path is handled by setMainView)
|
|
847
874
|
// Read from window.location.search (not React Router's searchParams) to preserve
|
|
848
875
|
// params set by child components via window.history.replaceState (e.g., kind from ResourcesView).
|
|
849
876
|
useEffect(() => {
|
|
877
|
+
// Don't write (and revert) the URL while state is still catching up to a
|
|
878
|
+
// Pop — the read effect below owns syncing state from the popped URL. The
|
|
879
|
+
// flag auto-clears on the next macrotask, so this never blocks a later
|
|
880
|
+
// user-driven write.
|
|
881
|
+
if (skipUrlWriteAfterPopRef.current) return
|
|
850
882
|
const currentSearch = window.location.search
|
|
851
883
|
const params = new URLSearchParams(currentSearch)
|
|
852
884
|
|
|
@@ -1045,6 +1077,21 @@ function AppInner() {
|
|
|
1045
1077
|
}
|
|
1046
1078
|
}, [displayedTopology, visibleKinds, namespaces, topologyMode])
|
|
1047
1079
|
|
|
1080
|
+
// The graph node id of the currently open resource, used to highlight it on
|
|
1081
|
+
// the canvas. Looked up from the topology (not reconstructed) because node
|
|
1082
|
+
// ids are `<lowercaseKind>/<ns>/<name>` with special prefixes for CRD
|
|
1083
|
+
// collisions — rebuilding the string can't match those reliably.
|
|
1084
|
+
const selectedNodeId = useMemo(() => {
|
|
1085
|
+
if (!selectedResource) return undefined
|
|
1086
|
+
const ns = selectedResource.namespace || ''
|
|
1087
|
+
const match = topology?.nodes.find(n =>
|
|
1088
|
+
((n.data.namespace as string) || '') === ns &&
|
|
1089
|
+
n.name === selectedResource.name &&
|
|
1090
|
+
(kindToPlural(n.kind) === selectedResource.kind || n.kind === selectedResource.kind)
|
|
1091
|
+
)
|
|
1092
|
+
return match?.id
|
|
1093
|
+
}, [selectedResource, topology])
|
|
1094
|
+
|
|
1048
1095
|
// Filter handlers
|
|
1049
1096
|
const handleToggleKind = useCallback((kind: NodeKind) => {
|
|
1050
1097
|
setVisibleKinds(prev => {
|
|
@@ -1253,18 +1300,6 @@ function AppInner() {
|
|
|
1253
1300
|
</button>
|
|
1254
1301
|
)}
|
|
1255
1302
|
|
|
1256
|
-
{/* My Permissions — what the current user can do in the cluster,
|
|
1257
|
-
computed live by the apiserver via SelfSubjectRulesReview.
|
|
1258
|
-
Available in embedded mode too — Radar Hub users still benefit
|
|
1259
|
-
from "why can't I do X" debugging. */}
|
|
1260
|
-
<button
|
|
1261
|
-
onClick={() => setShowMyPermissions(true)}
|
|
1262
|
-
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1263
|
-
title="My permissions in this cluster"
|
|
1264
|
-
>
|
|
1265
|
-
<ShieldIcon className="w-4 h-4" />
|
|
1266
|
-
</button>
|
|
1267
|
-
|
|
1268
1303
|
{/* User menu (when auth enabled) — hidden in embedded mode;
|
|
1269
1304
|
host app typically provides its own via rightExtras. */}
|
|
1270
1305
|
{!navCustomization.embedded && <UserMenu />}
|
|
@@ -1497,13 +1532,27 @@ function AppInner() {
|
|
|
1497
1532
|
groupingMode={effectiveGroupingMode}
|
|
1498
1533
|
hideGroupHeader={hideGroupHeader}
|
|
1499
1534
|
onNodeClick={handleNodeClick}
|
|
1500
|
-
selectedNodeId={
|
|
1535
|
+
selectedNodeId={selectedNodeId}
|
|
1501
1536
|
paused={topologyPaused}
|
|
1502
1537
|
onTogglePause={handleTogglePause}
|
|
1503
1538
|
onMaximizeNamespace={(ns) => setActiveNamespace.mutate({ namespaces: [ns] })}
|
|
1504
1539
|
namespaceBreadcrumb={namespaces.length === 1 ? namespaces[0] : undefined}
|
|
1505
1540
|
onClearNamespace={namespaces.length >= 1 ? () => setActiveNamespace.mutate({ namespaces: [] }) : undefined}
|
|
1506
1541
|
namespacesKey={namespaces.join(',')}
|
|
1542
|
+
focusNodeId={topologyFocus?.id}
|
|
1543
|
+
focusNonce={topologyFocus?.nonce}
|
|
1544
|
+
/>
|
|
1545
|
+
|
|
1546
|
+
{/* Topology node search overlay - top left */}
|
|
1547
|
+
<TopologySearch
|
|
1548
|
+
nodes={filteredTopology?.nodes ?? []}
|
|
1549
|
+
allNodes={topology?.nodes}
|
|
1550
|
+
viewModeLabel={topologyMode === 'fleet' ? 'Fleet' : topologyMode === 'traffic' ? 'Traffic' : 'Resources'}
|
|
1551
|
+
onNodeSelect={handleNodeClick}
|
|
1552
|
+
onZoomToNode={(id) => setTopologyFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 }))}
|
|
1553
|
+
// Stack below the namespace breadcrumb (shown only for a single
|
|
1554
|
+
// namespace) so the two don't overlap in the top-left corner.
|
|
1555
|
+
triggerClassName={namespaces.length === 1 ? 'top-12 left-3' : 'top-3 left-3'}
|
|
1507
1556
|
/>
|
|
1508
1557
|
|
|
1509
1558
|
{/* Topology controls overlay - top right */}
|
|
@@ -1612,21 +1661,18 @@ function AppInner() {
|
|
|
1612
1661
|
<AuditView
|
|
1613
1662
|
namespaces={namespaces}
|
|
1614
1663
|
onBack={() => setMainView('home')}
|
|
1615
|
-
onNavigateToResource={
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
}
|
|
1628
|
-
navigate({ pathname: `/resources/${pluralKind}`, search: newParams.toString() })
|
|
1629
|
-
}}
|
|
1664
|
+
onNavigateToResource={navigateToResourceList}
|
|
1665
|
+
/>
|
|
1666
|
+
)}
|
|
1667
|
+
|
|
1668
|
+
{/* Issues — per-cluster live triage queue (hidden route: not yet in the
|
|
1669
|
+
nav `views` list; reachable at /issues). Same shared <IssuesView> the
|
|
1670
|
+
Hub fleet uses; resource clicks open the standard resource drawer. */}
|
|
1671
|
+
{mainView === 'issues' && (
|
|
1672
|
+
<IssuesPane
|
|
1673
|
+
namespaces={namespaces}
|
|
1674
|
+
onBack={() => setMainView('home')}
|
|
1675
|
+
onNavigateToResource={navigateToResourceList}
|
|
1630
1676
|
/>
|
|
1631
1677
|
)}
|
|
1632
1678
|
|
|
@@ -1749,7 +1795,14 @@ function AppInner() {
|
|
|
1749
1795
|
{diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} onClose={() => setShowDiagnostics(false)} />}
|
|
1750
1796
|
|
|
1751
1797
|
{/* Settings dialog */}
|
|
1752
|
-
<SettingsDialog
|
|
1798
|
+
<SettingsDialog
|
|
1799
|
+
open={showSettings}
|
|
1800
|
+
onClose={() => setShowSettings(false)}
|
|
1801
|
+
onShowMyPermissions={() => {
|
|
1802
|
+
setShowSettings(false)
|
|
1803
|
+
setShowMyPermissions(true)
|
|
1804
|
+
}}
|
|
1805
|
+
/>
|
|
1753
1806
|
<MyPermissionsDialog open={showMyPermissions} onClose={() => setShowMyPermissions(false)} />
|
|
1754
1807
|
|
|
1755
1808
|
{/* Debug overlay - only in dev mode */}
|
package/src/api/client.ts
CHANGED
|
@@ -237,7 +237,7 @@ export interface DashboardCRDCount {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
// Re-export shared types from k8s-ui — single source of truth
|
|
240
|
-
import type { AuditCardData, AuditFinding, ResourceGroup, CheckMeta, Check } from '@skyhook-io/k8s-ui'
|
|
240
|
+
import type { AuditCardData, AuditFinding, ResourceGroup, CheckMeta, Check, Issue } from '@skyhook-io/k8s-ui'
|
|
241
241
|
export type DashboardAudit = AuditCardData
|
|
242
242
|
export type { AuditFinding, ResourceGroup, CheckMeta, Check }
|
|
243
243
|
|
|
@@ -338,6 +338,34 @@ export function useAudit(namespaces: string[] = []) {
|
|
|
338
338
|
})
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
// Live cluster Issues — the grouped triage queue (radar's /api/issues =
|
|
342
|
+
// internal/issues.Compose+Classify+Group). Single-cluster here; the Hub fleet
|
|
343
|
+
// view fans the same shape across clusters. keepPreviousData semantics via
|
|
344
|
+
// placeholderData so the queue doesn't flash empty on the 30s refresh.
|
|
345
|
+
// total = rows returned (after the cap); total_matched = rows that matched
|
|
346
|
+
// before the cap. total_matched > total means the queue was truncated — surface
|
|
347
|
+
// that honestly rather than presenting a capped list as if it were complete.
|
|
348
|
+
export interface IssuesResponse {
|
|
349
|
+
issues: Issue[]
|
|
350
|
+
total?: number
|
|
351
|
+
total_matched?: number
|
|
352
|
+
// Present only when RBAC visibility is incomplete (absent = full access).
|
|
353
|
+
// state 'degraded' means core workload reads are denied, so an empty list may
|
|
354
|
+
// mean "can't see" rather than "nothing broken" — the UI must say so.
|
|
355
|
+
visibility?: { state?: string; impact?: string }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function useIssues(namespaces: string[] = []) {
|
|
359
|
+
const params = namespaces.length > 0 ? `?namespaces=${namespaces.join(',')}` : ''
|
|
360
|
+
return useQuery<IssuesResponse>({
|
|
361
|
+
queryKey: ['issues', namespaces],
|
|
362
|
+
queryFn: () => fetchJSON(`/issues${params}`),
|
|
363
|
+
staleTime: 30000,
|
|
364
|
+
refetchInterval: 30000,
|
|
365
|
+
placeholderData: (prev) => prev,
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
341
369
|
export function useResourceAudit(kind: string, namespace: string, name: string) {
|
|
342
370
|
return useQuery<AuditFinding[]>({
|
|
343
371
|
queryKey: ['audit', 'resource', kind, namespace, name],
|
|
@@ -197,13 +197,13 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
|
|
|
197
197
|
Context: {connection.context ? (
|
|
198
198
|
<ClusterName name={connection.context} />
|
|
199
199
|
) : (
|
|
200
|
-
<span className="
|
|
200
|
+
<span className="inline-code">(none)</span>
|
|
201
201
|
)}
|
|
202
202
|
</p>
|
|
203
203
|
|
|
204
204
|
{connection.clusterName && (
|
|
205
205
|
<p className="text-sm text-theme-text-secondary mb-4">
|
|
206
|
-
Cluster: <span className="
|
|
206
|
+
Cluster: <span className="inline-code">{connection.clusterName}</span>
|
|
207
207
|
</p>
|
|
208
208
|
)}
|
|
209
209
|
|
|
@@ -307,7 +307,7 @@ export function ChartBrowser({ onChartSelect }: ChartBrowserProps) {
|
|
|
307
307
|
<div className="text-sm mt-1">
|
|
308
308
|
<p>No Helm repositories configured.</p>
|
|
309
309
|
<p className="mt-1">
|
|
310
|
-
Add repositories using <code className="
|
|
310
|
+
Add repositories using <code className="inline-code">helm repo add</code>
|
|
311
311
|
</p>
|
|
312
312
|
<p className="mt-2">
|
|
313
313
|
Or try searching on <button onClick={() => setChartSource('artifacthub')} className="text-accent-text hover:underline">ArtifactHub</button>
|
|
@@ -476,7 +476,9 @@ function LocalChartCard({ chart, onSelect }: LocalChartCardProps) {
|
|
|
476
476
|
)}
|
|
477
477
|
<div className="flex-1 min-w-0">
|
|
478
478
|
<div className="flex items-center gap-2">
|
|
479
|
-
<
|
|
479
|
+
<Tooltip content={chart.name} wrapperClassName="min-w-0 flex-1">
|
|
480
|
+
<h4 className="text-sm font-medium text-theme-text-primary truncate">{chart.name}</h4>
|
|
481
|
+
</Tooltip>
|
|
480
482
|
{chart.deprecated && (
|
|
481
483
|
<span className={clsx('px-1 py-0.5 text-[10px] rounded', SEVERITY_BADGE.warning)}>
|
|
482
484
|
deprecated
|
|
@@ -540,7 +542,9 @@ function ArtifactHubChartCard({ chart, onSelect }: ArtifactHubChartCardProps) {
|
|
|
540
542
|
|
|
541
543
|
{/* Name and org */}
|
|
542
544
|
<div className="flex-1 min-w-0">
|
|
543
|
-
<
|
|
545
|
+
<Tooltip content={chart.name} wrapperClassName="w-full">
|
|
546
|
+
<h4 className="text-sm font-medium text-theme-text-primary truncate">{chart.name}</h4>
|
|
547
|
+
</Tooltip>
|
|
544
548
|
<div className="flex items-center gap-2 mt-0.5 text-xs text-theme-text-tertiary">
|
|
545
549
|
<span className="flex items-center gap-1">
|
|
546
550
|
<Building2 className="w-3 h-3" />
|
|
@@ -893,7 +893,7 @@ function ReviewStep({
|
|
|
893
893
|
<div>
|
|
894
894
|
<p className="text-sm font-medium text-amber-400">Installing from ArtifactHub</p>
|
|
895
895
|
<p className="text-xs text-theme-text-secondary mt-1">
|
|
896
|
-
This chart will be installed from: <code className="
|
|
896
|
+
This chart will be installed from: <code className="inline-code">{artifactHubRepoUrl || repo}</code>
|
|
897
897
|
</p>
|
|
898
898
|
</div>
|
|
899
899
|
</div>
|
|
@@ -38,8 +38,8 @@ export function RoleGatedPanel({ min, feature, children }: RoleGatedPanelProps)
|
|
|
38
38
|
Your role can't view {feature}
|
|
39
39
|
</div>
|
|
40
40
|
<div className="mt-1 max-w-md text-xs text-theme-text-secondary">
|
|
41
|
-
You're signed in as <span className="
|
|
42
|
-
This view requires <span className="
|
|
41
|
+
You're signed in as <span className="inline-code">{role ?? 'viewer'}</span>.
|
|
42
|
+
This view requires <span className="inline-code">{min}</span> or higher.
|
|
43
43
|
Ask a {min} or owner if you need access.
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
@@ -52,7 +52,7 @@ function MetricsUnavailableHint({ platform, metricsServerAvailable }: { platform
|
|
|
52
52
|
content={
|
|
53
53
|
<div className="space-y-1">
|
|
54
54
|
<div className="font-medium">How to fix</div>
|
|
55
|
-
<div>{isPreInstalled ? hint : <>Install by running:<br /><code className="text-[10px] opacity-80">{hint}</code></>}</div>
|
|
55
|
+
<div>{isPreInstalled ? hint : <>Install by running:<br /><code className="inline-code text-[10px] opacity-80">{hint}</code></>}</div>
|
|
56
56
|
</div>
|
|
57
57
|
}
|
|
58
58
|
position="bottom"
|
|
@@ -194,6 +194,7 @@ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToR
|
|
|
194
194
|
{hasProblems && (
|
|
195
195
|
<ProblemsPanel
|
|
196
196
|
problems={data.problems}
|
|
197
|
+
onNavigateToIssues={() => onNavigateToView('issues')}
|
|
197
198
|
onResourceClick={onNavigateToResource}
|
|
198
199
|
/>
|
|
199
200
|
)}
|
|
@@ -216,19 +217,29 @@ function BandItem({ children }: { children: ReactNode }) {
|
|
|
216
217
|
|
|
217
218
|
interface ProblemsPanelProps {
|
|
218
219
|
problems: DashboardResponse['problems']
|
|
220
|
+
onNavigateToIssues: () => void
|
|
219
221
|
onResourceClick: (resource: SelectedResource) => void
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
|
|
223
|
-
function ProblemsPanel({ problems, onResourceClick }: ProblemsPanelProps) {
|
|
225
|
+
function ProblemsPanel({ problems, onNavigateToIssues, onResourceClick }: ProblemsPanelProps) {
|
|
224
226
|
return (
|
|
225
227
|
<div className="rounded-xl bg-theme-surface shadow-theme-sm flex flex-col lg:max-h-[calc(100vh-280px)] lg:sticky lg:top-0">
|
|
226
228
|
<div className="flex items-center justify-between px-5 py-3 border-b border-theme-border/50 shrink-0">
|
|
227
229
|
<div className="flex items-center gap-2">
|
|
228
230
|
<AlertTriangle className="w-4 h-4 text-red-500" />
|
|
229
|
-
<span className="text-xs font-semibold uppercase tracking-wider text-red-500">
|
|
231
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-red-500">Active Issues</span>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="flex items-center gap-2">
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
className="rounded-md px-2 py-1 text-xs font-medium text-accent-text transition-colors hover:bg-accent-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-radar-accent)]/40"
|
|
237
|
+
onClick={onNavigateToIssues}
|
|
238
|
+
>
|
|
239
|
+
View all
|
|
240
|
+
</button>
|
|
241
|
+
<span className="badge status-unhealthy rounded-full">{problems.length}</span>
|
|
230
242
|
</div>
|
|
231
|
-
<span className="badge status-unhealthy rounded-full">{problems.length}</span>
|
|
232
243
|
</div>
|
|
233
244
|
<div className="overflow-y-auto flex-1 min-h-0">
|
|
234
245
|
<div className="divide-y divide-theme-border">
|
|
@@ -221,7 +221,7 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
|
|
|
221
221
|
<div className="relative">
|
|
222
222
|
<div className="flex items-center gap-3 bg-theme-base rounded-md px-3 py-2.5">
|
|
223
223
|
<span className="badge text-purple-400 bg-purple-500/10">HTTP</span>
|
|
224
|
-
<code className="
|
|
224
|
+
<code className="inline-code text-sm">{mcpUrl}</code>
|
|
225
225
|
</div>
|
|
226
226
|
<CopyButton text={mcpUrl} />
|
|
227
227
|
</div>
|
|
@@ -306,7 +306,7 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
|
|
|
306
306
|
{MCP_TOOL_CATALOG.map((tool) => (
|
|
307
307
|
<div key={tool.name} className="card-inner space-y-1.5">
|
|
308
308
|
<div className="flex items-center gap-2">
|
|
309
|
-
<code className="text-[11px]
|
|
309
|
+
<code className="inline-code text-[11px]">{tool.name}</code>
|
|
310
310
|
{tool.write && (
|
|
311
311
|
<span className="badge-sm bg-amber-500/10 text-amber-600 dark:text-amber-400" title="Write tool — annotated as destructive">
|
|
312
312
|
write
|
|
@@ -317,8 +317,8 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
|
|
|
317
317
|
{tool.params.length > 0 && (
|
|
318
318
|
<div className="flex flex-wrap gap-1.5 pt-0.5">
|
|
319
319
|
{tool.params.map((p) => (
|
|
320
|
-
<span key={p.arg} className="
|
|
321
|
-
<span
|
|
320
|
+
<span key={p.arg} className="inline-code text-[11px]" title={p.desc}>
|
|
321
|
+
<span>{p.arg}</span>
|
|
322
322
|
{p.required && <span className="text-red-400">*</span>}
|
|
323
323
|
</span>
|
|
324
324
|
))}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useIssues } from '../../api/client'
|
|
2
|
+
import type { SelectedResource } from '../../types'
|
|
3
|
+
import { IssuesView, PaneLoader, type IssueResourceRef } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { AlertTriangle, ArrowLeft } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
interface IssuesPaneProps {
|
|
7
|
+
namespaces: string[]
|
|
8
|
+
onBack: () => void
|
|
9
|
+
onNavigateToResource: (resource: SelectedResource) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// The per-cluster Issues surface. Renders the same shared triage queue
|
|
13
|
+
// (IssuesView) the Hub fleet view uses — single cluster here, so no cluster
|
|
14
|
+
// label and in-app (client-side) resource navigation. Classification +
|
|
15
|
+
// owner-grouping come pre-computed from radar's /api/issues
|
|
16
|
+
// (internal/issues.Compose → Classify → Group).
|
|
17
|
+
export function IssuesPane({ namespaces, onBack, onNavigateToResource }: IssuesPaneProps) {
|
|
18
|
+
const { data, isLoading, error } = useIssues(namespaces)
|
|
19
|
+
|
|
20
|
+
const onResourceClick = (ref: IssueResourceRef) =>
|
|
21
|
+
onNavigateToResource({ kind: ref.kind, namespace: ref.namespace ?? '', name: ref.name, group: ref.group ?? '' })
|
|
22
|
+
|
|
23
|
+
if (isLoading) {
|
|
24
|
+
return <PaneLoader label="Loading issues…" className="flex-1" />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (error) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex-1 flex items-center justify-center text-theme-text-secondary">
|
|
30
|
+
<p>Failed to load issues</p>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex-1 flex flex-col min-h-0 p-6 gap-6 overflow-auto">
|
|
37
|
+
<div className="flex items-center gap-4">
|
|
38
|
+
<button
|
|
39
|
+
onClick={onBack}
|
|
40
|
+
className="p-1.5 rounded-lg hover:bg-theme-hover transition-colors"
|
|
41
|
+
>
|
|
42
|
+
<ArrowLeft className="w-5 h-5 text-theme-text-secondary" />
|
|
43
|
+
</button>
|
|
44
|
+
<div className="flex-1">
|
|
45
|
+
<div className="flex items-center gap-2">
|
|
46
|
+
<AlertTriangle className="w-5 h-5 text-theme-text-secondary" />
|
|
47
|
+
<h1 className="text-lg font-semibold text-theme-text-primary">Issues</h1>
|
|
48
|
+
</div>
|
|
49
|
+
<p className="text-sm text-theme-text-tertiary mt-1 ml-7">
|
|
50
|
+
Live cluster problems — crashes, scheduling failures, bad references — grouped by the resource they affect.
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Visibility honesty: when RBAC reads are incomplete, an empty queue may
|
|
56
|
+
mean "can't see" rather than "nothing broken" — say so up front so the
|
|
57
|
+
empty state isn't mistaken for a clean bill of health. */}
|
|
58
|
+
{data?.visibility?.impact && (
|
|
59
|
+
<div className="-mt-3 flex items-start gap-2 rounded-lg border border-theme-border bg-theme-elevated px-3 py-2 text-xs text-theme-text-secondary">
|
|
60
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-500" />
|
|
61
|
+
<span>Limited visibility — {data.visibility.impact} Results may be incomplete.</span>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{/* Truncation honesty: when more issues matched than were returned, say
|
|
66
|
+
so — don't present a capped list as the complete picture. */}
|
|
67
|
+
{data?.total_matched != null && data.total_matched > (data.issues?.length ?? 0) && (
|
|
68
|
+
<p className="-mt-3 text-xs text-theme-text-tertiary">
|
|
69
|
+
Showing {data.issues?.length ?? 0} of {data.total_matched} issues (capped) — narrow by namespace to see the rest.
|
|
70
|
+
</p>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{/* anyData = the query resolved, i.e. the cluster is reachable; an empty
|
|
74
|
+
list then means "nothing broken" rather than "not connected". */}
|
|
75
|
+
<IssuesView issues={data?.issues ?? []} anyData={!!data} onResourceClick={onResourceClick} />
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -308,7 +308,7 @@ export function PortForwardButton({
|
|
|
308
308
|
className="w-full px-3 py-2 text-left text-sm text-theme-text-primary hover:bg-theme-elevated flex items-center justify-between"
|
|
309
309
|
>
|
|
310
310
|
<span className="flex items-center gap-2 shrink-0">
|
|
311
|
-
<code className="
|
|
311
|
+
<code className="inline-code">{port.port}</code>
|
|
312
312
|
<span className="text-theme-text-disabled">/{port.protocol || 'TCP'}</span>
|
|
313
313
|
</span>
|
|
314
314
|
{port.name && (
|
|
@@ -794,7 +794,7 @@ export function PortForwardPanel() {
|
|
|
794
794
|
<Tooltip content="Click to change local port" delay={300} position="bottom" disabled={!isPanelOpen}>
|
|
795
795
|
<code
|
|
796
796
|
className={clsx(
|
|
797
|
-
'group/port text-xs
|
|
797
|
+
'inline-code group/port text-xs transition-all inline-flex items-center gap-1',
|
|
798
798
|
changingPortId === session.id
|
|
799
799
|
? 'opacity-50'
|
|
800
800
|
: 'cursor-pointer hover:ring-1 hover:ring-blue-500/50'
|