@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.
- package/package.json +4 -4
- package/src/App.tsx +58 -50
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +1222 -234
- package/src/api/timelineSource.ts +4 -2
- package/src/components/applications/ApplicationsView.tsx +613 -219
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +103 -72
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +494 -229
- 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/diagnose/AISettings.tsx +7 -12
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +81 -14
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/HelmCompareRoute.tsx +1 -2
- package/src/components/helm/ManifestDiffViewer.tsx +1 -31
- package/src/components/helm/ValuesDiffPreview.tsx +2 -3
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +319 -123
- 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 +66 -116
- package/src/components/settings/SettingsDialog.tsx +1268 -318
- package/src/components/timeline/TimelineList.tsx +35 -8
- package/src/components/timeline/TimelineView.tsx +156 -26
- package/src/components/timeline/TimelineView.urlparams.test.ts +43 -2
- package/src/components/workload/WorkloadView.tsx +711 -328
- package/src/index.css +5 -1
- 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.
|
|
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.
|
|
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.
|
|
73
|
+
"lucide-react": "^1.24.0",
|
|
74
74
|
"postcss": "^8.5.16",
|
|
75
|
-
"prettier": "^3.9.
|
|
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 (
|
|
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
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
+
})
|