@skyhook-io/radar-app 1.8.7 → 1.8.8

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.
Files changed (48) hide show
  1. package/package.json +4 -4
  2. package/src/App.tsx +58 -50
  3. package/src/api/client.argoResourceSync.test.ts +69 -0
  4. package/src/api/client.rightsizing.test.ts +32 -0
  5. package/src/api/client.ts +1222 -234
  6. package/src/api/timelineSource.ts +4 -2
  7. package/src/components/applications/ApplicationsView.tsx +613 -219
  8. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  9. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  10. package/src/components/cost/CostTrendChart.tsx +103 -72
  11. package/src/components/cost/CostView.test.ts +12 -0
  12. package/src/components/cost/CostView.tsx +494 -229
  13. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  14. package/src/components/cost/CostViewTabs.tsx +40 -0
  15. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  16. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  17. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  18. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  19. package/src/components/cost/cloud-console.test.ts +39 -0
  20. package/src/components/cost/cloud-console.ts +81 -0
  21. package/src/components/cost/errors.ts +8 -0
  22. package/src/components/cost/format.test.ts +27 -0
  23. package/src/components/cost/format.ts +46 -0
  24. package/src/components/cost/kinds.ts +5 -0
  25. package/src/components/diagnose/AISettings.tsx +7 -12
  26. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  27. package/src/components/gitops/GitOpsView.tsx +81 -14
  28. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  29. package/src/components/helm/HelmCompareRoute.tsx +1 -2
  30. package/src/components/helm/ManifestDiffViewer.tsx +1 -31
  31. package/src/components/helm/ValuesDiffPreview.tsx +2 -3
  32. package/src/components/home/CostCard.tsx +21 -36
  33. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  34. package/src/components/resource/RightsizingStrip.tsx +319 -123
  35. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  36. package/src/components/rightsizing/copy.test.ts +56 -0
  37. package/src/components/rightsizing/model.test.ts +227 -0
  38. package/src/components/rightsizing/model.ts +158 -0
  39. package/src/components/rightsizing/presentation.test.ts +104 -0
  40. package/src/components/rightsizing/presentation.ts +94 -0
  41. package/src/components/settings/MyPermissionsDialog.tsx +66 -116
  42. package/src/components/settings/SettingsDialog.tsx +1268 -318
  43. package/src/components/timeline/TimelineList.tsx +35 -8
  44. package/src/components/timeline/TimelineView.tsx +156 -26
  45. package/src/components/timeline/TimelineView.urlparams.test.ts +43 -2
  46. package/src/components/workload/WorkloadView.tsx +711 -328
  47. package/src/index.css +5 -1
  48. package/src/main.tsx +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/radar-app",
3
- "version": "1.8.7",
3
+ "version": "1.8.8",
4
4
  "description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -63,16 +63,16 @@
63
63
  "@types/react": "^19.2.17",
64
64
  "@types/react-dom": "^19.2.3",
65
65
  "@vitejs/plugin-react": "^6.0.2",
66
- "@xyflow/react": "^12.10.2",
66
+ "@xyflow/react": "^12.11.2",
67
67
  "clsx": "^2.1.1",
68
68
  "elkjs": "^0.11.1",
69
69
  "eslint": "^10.6.0",
70
70
  "eslint-plugin-react-hooks": "^7.1.1",
71
71
  "eslint-plugin-react-refresh": "^0.5.3",
72
72
  "globals": "^17.7.0",
73
- "lucide-react": "^1.23.0",
73
+ "lucide-react": "^1.24.0",
74
74
  "postcss": "^8.5.16",
75
- "prettier": "^3.9.4",
75
+ "prettier": "^3.9.5",
76
76
  "react": "^19.2.7",
77
77
  "react-dom": "^19.2.7",
78
78
  "react-router-dom": "^7.18.1",
package/src/App.tsx CHANGED
@@ -9,7 +9,7 @@ import { DebugOverlay } from './components/DebugOverlay'
9
9
  import { GlobalDiagnoseButton } from './components/diagnose/LocalDiagnoseAction'
10
10
  import { useDiagnoseLayout } from './components/diagnose/DiagnoseContext'
11
11
  import { DiagnoseSurface } from './components/diagnose/DiagnoseSurface'
12
- import { TopologyGraph, TopologySearch, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader } from '@skyhook-io/k8s-ui'
12
+ import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader } from '@skyhook-io/k8s-ui'
13
13
  import { initNavigationMap } from '@skyhook-io/k8s-ui/utils/navigation'
14
14
  import { useAPIResources, CORE_RESOURCES } from './api/apiResources'
15
15
  import { TimelineView } from './components/timeline/TimelineView'
@@ -61,7 +61,6 @@ import { useTheme } from './context/ThemeContext'
61
61
  import { Tooltip } from './components/ui/Tooltip'
62
62
  import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
63
63
  import { SettingsDialog } from './components/settings/SettingsDialog'
64
- import { MyPermissionsDialog } from './components/settings/MyPermissionsDialog'
65
64
  import type { TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
66
65
  import { kindToPlural, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource } from './utils/navigation'
67
66
  import { type OmnibarHandle } from './components/ui/Omnibar'
@@ -153,7 +152,10 @@ function namespaceFilterDisabled(
153
152
  pathname: string,
154
153
  apiResources?: { name: string; kind: string; namespaced: boolean }[],
155
154
  ): { disabled: boolean; tooltip?: string } {
156
- if (view === 'cost') {
155
+ if (
156
+ view === 'cost' &&
157
+ !pathname.startsWith('/cost/rightsizing')
158
+ ) {
157
159
  return {
158
160
  disabled: true,
159
161
  tooltip: 'Cost is reported per namespace across the whole cluster — the namespace filter doesn’t apply here.',
@@ -485,6 +487,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
485
487
  const [filterSidebarCollapsed, setFilterSidebarCollapsed] = useState(false)
486
488
  // Topology node-search → canvas focus request (nonce lets the same node re-focus)
487
489
  const [topologyFocus, setTopologyFocus] = useState<{ id: string; nonce: number } | null>(null)
490
+ // The topology pane element — the search overlay portals into it so its
491
+ // backdrop dims only the pane (not the app) and stays clickable. Callback-ref
492
+ // state so it updates once the pane mounts.
493
+ const [topologyPane, setTopologyPane] = useState<HTMLDivElement | null>(null)
488
494
  // Track CRD kinds that have been auto-added to visibleKinds so we don't override user toggles
489
495
  const seededCRDKindsRef = useRef<Set<string>>(new Set())
490
496
 
@@ -501,7 +507,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
501
507
 
502
508
  // Settings dialog state
503
509
  const [showSettings, setShowSettings] = useState(false)
504
- const [showMyPermissions, setShowMyPermissions] = useState(false)
505
510
 
506
511
  // Listen for "open-settings" DOM event (used by MCPSetupDialog etc.)
507
512
  useEffect(() => {
@@ -2057,7 +2062,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2057
2062
  }}
2058
2063
  />
2059
2064
 
2060
- <div className="flex-1 relative">
2065
+ <div ref={setTopologyPane} className="flex-1 relative">
2061
2066
  <TopologyGraph
2062
2067
  topology={topologyWithAudit}
2063
2068
  viewMode={topologyMode}
@@ -2068,48 +2073,55 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2068
2073
  paused={topologyPaused}
2069
2074
  onTogglePause={handleTogglePause}
2070
2075
  onMaximizeNamespace={(ns) => setActiveNamespace.mutate({ namespaces: [ns] })}
2071
- namespaceBreadcrumb={namespaces.length === 1 ? namespaces[0] : undefined}
2072
- onClearNamespace={namespaces.length >= 1 ? () => setActiveNamespace.mutate({ namespaces: [] }) : undefined}
2073
2076
  namespacesKey={namespaces.join(',')}
2074
2077
  focusNodeId={topologyFocus?.id}
2075
2078
  focusNonce={topologyFocus?.nonce}
2076
- />
2077
-
2078
- {/* Topology node search overlay - top left */}
2079
- <TopologySearch
2080
- nodes={filteredTopology?.nodes ?? []}
2081
- allNodes={topology?.nodes}
2082
- viewModeLabel={topologyMode === 'fleet' ? 'Fleet' : topologyMode === 'traffic' ? 'Network Flow' : 'Resources'}
2083
- onNodeSelect={handleNodeClick}
2084
- onZoomToNode={(id) => setTopologyFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 }))}
2085
- // Stack below the namespace breadcrumb (shown only for a single
2086
- // namespace) so the two don't overlap in the top-left corner.
2087
- triggerClassName={namespaces.length === 1 ? 'top-12 left-3' : 'top-3 left-3'}
2088
- />
2089
-
2090
- {/* Topology controls overlay - top right */}
2091
- <TopologyControls
2092
- viewMode={topologyMode}
2093
- onViewModeChange={(mode) => {
2094
- setTopologyMode(mode)
2095
- // Fleet mode: namespace grouping for structure, but expanded (not collapsed chips)
2096
- if (mode === 'fleet') setGroupingMode('namespace')
2097
- }}
2098
- groupingMode={groupingMode}
2099
- onGroupingModeChange={setGroupingMode}
2100
- showNoGrouping={hasNamespaceFilter}
2101
- showPolicyEffect={showPolicyEffect}
2102
- onShowPolicyEffectChange={setShowPolicyEffect}
2103
- showFleetMode={displayedTopology?.nodes?.some(n => FLEET_MODE_KINDS.has(n.kind as NodeKind)) ?? false}
2104
- onNavigateToTraffic={() => setMainView('traffic')}
2105
- leadingSlot={
2106
- <FreshnessControl
2107
- mode="auto"
2108
- paused={topologyPaused}
2109
- connectionState={connection.state}
2079
+ >
2080
+ {/* Overlay row: left column (namespace breadcrumb over search)
2081
+ + controls. items-start pins the controls to the top even
2082
+ when the breadcrumb grows the left column; w-full so
2083
+ justify-between spans the canvas. */}
2084
+ <div className="flex w-full items-start justify-between gap-2">
2085
+ <div className="flex flex-col items-start gap-2">
2086
+ {namespaces.length === 1 && (
2087
+ <TopologyBreadcrumb
2088
+ namespace={namespaces[0]}
2089
+ onClear={() => setActiveNamespace.mutate({ namespaces: [] })}
2090
+ />
2091
+ )}
2092
+ <TopologySearch
2093
+ nodes={filteredTopology?.nodes ?? []}
2094
+ allNodes={topology?.nodes}
2095
+ viewModeLabel={topologyMode === 'fleet' ? 'Fleet' : topologyMode === 'traffic' ? 'Network Flow' : 'Resources'}
2096
+ onNodeSelect={handleNodeClick}
2097
+ onZoomToNode={(id) => setTopologyFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 }))}
2098
+ overlayContainer={topologyPane}
2099
+ />
2100
+ </div>
2101
+ <TopologyControls
2102
+ viewMode={topologyMode}
2103
+ onViewModeChange={(mode) => {
2104
+ setTopologyMode(mode)
2105
+ // Fleet mode: namespace grouping for structure, but expanded (not collapsed chips)
2106
+ if (mode === 'fleet') setGroupingMode('namespace')
2107
+ }}
2108
+ groupingMode={groupingMode}
2109
+ onGroupingModeChange={setGroupingMode}
2110
+ showNoGrouping={hasNamespaceFilter}
2111
+ showPolicyEffect={showPolicyEffect}
2112
+ onShowPolicyEffectChange={setShowPolicyEffect}
2113
+ showFleetMode={displayedTopology?.nodes?.some(n => FLEET_MODE_KINDS.has(n.kind as NodeKind)) ?? false}
2114
+ onNavigateToTraffic={() => setMainView('traffic')}
2115
+ leadingSlot={
2116
+ <FreshnessControl
2117
+ mode="auto"
2118
+ paused={topologyPaused}
2119
+ connectionState={connection.state}
2120
+ />
2121
+ }
2110
2122
  />
2111
- }
2112
- />
2123
+ </div>
2124
+ </TopologyGraph>
2113
2125
  </div>
2114
2126
  </>
2115
2127
  )}
@@ -2171,6 +2183,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2171
2183
  navigateToResource(resource)
2172
2184
  }}
2173
2185
  onClearNamespaces={clearAllNamespaces}
2186
+ onOpenSettings={() => setShowSettings(true)}
2174
2187
  />
2175
2188
  )}
2176
2189
 
@@ -2202,7 +2215,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2202
2215
 
2203
2216
  {/* Cost detail view */}
2204
2217
  {mainView === 'cost' && (
2205
- <CostView onBack={() => setMainView('home')} />
2218
+ <CostView namespaces={namespaces} onBack={() => setMainView('home')} onOpenResource={navigateToResource} />
2206
2219
  )}
2207
2220
 
2208
2221
  {/* Takeover splash. When the host claims the current view via
@@ -2398,16 +2411,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2398
2411
  {/* Diagnostics overlay */}
2399
2412
  {diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} onClose={() => setShowDiagnostics(false)} />}
2400
2413
 
2401
- {/* Settings dialog */}
2414
+ {/* Settings dialog — My permissions is rendered inline in its own section */}
2402
2415
  <SettingsDialog
2403
2416
  open={showSettings}
2404
2417
  onClose={() => setShowSettings(false)}
2405
- onShowMyPermissions={() => {
2406
- setShowSettings(false)
2407
- setShowMyPermissions(true)
2408
- }}
2409
2418
  />
2410
- <MyPermissionsDialog open={showMyPermissions} onClose={() => setShowMyPermissions(false)} />
2411
2419
 
2412
2420
  {/* Debug overlay — dev mode, standalone only. Embedded hosts (Radar Hub)
2413
2421
  own their own dev tooling; ours would collide with theirs bottom-right. */}
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildArgoResourceSyncVars } from './client'
3
+
4
+ const options = {
5
+ revision: 'should-not-survive',
6
+ prune: true,
7
+ dryRun: false,
8
+ force: true,
9
+ applyOnly: true,
10
+ syncOptions: ['ServerSideApply=true'],
11
+ }
12
+
13
+ describe('buildArgoResourceSyncVars', () => {
14
+ it('preserves the complete Argo status ref and forces resource-safe options', () => {
15
+ expect(
16
+ buildArgoResourceSyncVars(
17
+ 'argocd',
18
+ 'guestbook',
19
+ {
20
+ group: 'apps',
21
+ kind: 'Deployment',
22
+ namespace: 'guestbook',
23
+ name: 'guestbook-ui',
24
+ },
25
+ options,
26
+ ),
27
+ ).toEqual({
28
+ namespace: 'argocd',
29
+ name: 'guestbook',
30
+ resources: [
31
+ {
32
+ group: 'apps',
33
+ kind: 'Deployment',
34
+ namespace: 'guestbook',
35
+ name: 'guestbook-ui',
36
+ },
37
+ ],
38
+ revision: undefined,
39
+ prune: false,
40
+ dryRun: false,
41
+ force: true,
42
+ applyOnly: false,
43
+ syncOptions: ['ServerSideApply=true'],
44
+ })
45
+ })
46
+
47
+ it('preserves an empty core API group', () => {
48
+ const variables = buildArgoResourceSyncVars(
49
+ 'argocd',
50
+ 'guestbook',
51
+ {
52
+ group: '',
53
+ kind: 'Service',
54
+ namespace: 'guestbook',
55
+ name: 'guestbook-ui',
56
+ },
57
+ options,
58
+ )
59
+
60
+ expect(variables.resources).toEqual([
61
+ {
62
+ group: '',
63
+ kind: 'Service',
64
+ namespace: 'guestbook',
65
+ name: 'guestbook-ui',
66
+ },
67
+ ])
68
+ })
69
+ })
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { skipToken } from '@tanstack/react-query'
3
+ import { getRightsizingScanCacheConfig } from './client'
4
+
5
+ describe('rightsizing scan cache config', () => {
6
+ it('is manual-only and retained briefly', () => {
7
+ const config = getRightsizingScanCacheConfig(['staging', 'default'], 'cluster-a')
8
+
9
+ expect(config.queryFn).toBe(skipToken)
10
+ expect(config.gcTime).toBe(5 * 60 * 1000)
11
+ })
12
+
13
+ it('normalizes namespace order and isolates cluster and namespace scopes', () => {
14
+ const config = getRightsizingScanCacheConfig(['staging', 'default'], 'cluster-a')
15
+
16
+ expect(config.namespaceKey).toBe('default,staging')
17
+ expect(config.queryKey).toEqual([
18
+ 'prometheus-rightsizing-scan',
19
+ 'cluster-a',
20
+ 'default,staging',
21
+ ])
22
+ expect(getRightsizingScanCacheConfig(['default', 'staging'], 'cluster-a').queryKey).toEqual(
23
+ config.queryKey,
24
+ )
25
+ expect(getRightsizingScanCacheConfig(['default'], 'cluster-a').queryKey).not.toEqual(
26
+ config.queryKey,
27
+ )
28
+ expect(getRightsizingScanCacheConfig(['staging', 'default'], 'cluster-b').queryKey).not.toEqual(
29
+ config.queryKey,
30
+ )
31
+ })
32
+ })