@skyhook-io/radar-app 1.8.7 → 1.8.9
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 +69 -61
- package/src/RadarApp.tsx +15 -1
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +1222 -244
- 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/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +3 -0
- package/src/components/diagnose/InvestigationView.tsx +16 -3
- package/src/components/diagnose/parts.tsx +140 -73
- 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/context/DiagnoseCustomization.tsx +36 -2
- package/src/index.css +5 -1
- package/src/index.ts +4 -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.9",
|
|
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,9 +9,9 @@ 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
|
-
import { useAPIResources,
|
|
14
|
+
import { useAPIResources, findAPIResourceForRoute } from './api/apiResources'
|
|
15
15
|
import { TimelineView } from './components/timeline/TimelineView'
|
|
16
16
|
import { ResourcesView } from './components/resources/ResourcesView'
|
|
17
17
|
import { serializeColumnFilters } from './components/resources/resource-utils'
|
|
@@ -61,8 +61,7 @@ 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 {
|
|
65
|
-
import type { TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
|
|
64
|
+
import type { APIResource, 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'
|
|
68
67
|
import { RadarOmnibar } from './components/ui/RadarOmnibar'
|
|
@@ -151,9 +150,13 @@ function getViewFromPath(pathname: string): ExtendedMainView {
|
|
|
151
150
|
function namespaceFilterDisabled(
|
|
152
151
|
view: ExtendedMainView,
|
|
153
152
|
pathname: string,
|
|
154
|
-
|
|
153
|
+
search = '',
|
|
154
|
+
apiResources?: APIResource[],
|
|
155
155
|
): { disabled: boolean; tooltip?: string } {
|
|
156
|
-
if (
|
|
156
|
+
if (
|
|
157
|
+
view === 'cost' &&
|
|
158
|
+
!pathname.startsWith('/cost/rightsizing')
|
|
159
|
+
) {
|
|
157
160
|
return {
|
|
158
161
|
disabled: true,
|
|
159
162
|
tooltip: 'Cost is reported per namespace across the whole cluster — the namespace filter doesn’t apply here.',
|
|
@@ -168,7 +171,8 @@ function namespaceFilterDisabled(
|
|
|
168
171
|
}
|
|
169
172
|
if (view === 'resources') {
|
|
170
173
|
const kindSlug = segments[1]
|
|
171
|
-
const
|
|
174
|
+
const group = new URLSearchParams(search).get('apiGroup') || ''
|
|
175
|
+
const match = kindSlug ? findAPIResourceForRoute(apiResources, kindSlug, group) : undefined
|
|
172
176
|
if (match && !match.namespaced) {
|
|
173
177
|
return {
|
|
174
178
|
disabled: true,
|
|
@@ -183,7 +187,7 @@ function namespaceFilterDisabled(
|
|
|
183
187
|
// correct regardless of which component renders it. A detail drawer that opens
|
|
184
188
|
// over a list (?resource=…) is deliberately NOT titled — it's the same page, so
|
|
185
189
|
// it keeps the list's title.
|
|
186
|
-
function radarPageTitle(pathname: string, search = '', apiResources?:
|
|
190
|
+
function radarPageTitle(pathname: string, search = '', apiResources?: APIResource[]): string | null {
|
|
187
191
|
const decode = (s: string) => {
|
|
188
192
|
try {
|
|
189
193
|
return decodeURIComponent(s)
|
|
@@ -194,7 +198,7 @@ function radarPageTitle(pathname: string, search = '', apiResources?: { name: st
|
|
|
194
198
|
const capitalize = (text: string) =>
|
|
195
199
|
text ? text.charAt(0).toUpperCase() + text.slice(1) : text
|
|
196
200
|
const pluralKindTitle = (kind: string, resourceName: string) =>
|
|
197
|
-
kind.toLowerCase() === resourceName.toLowerCase() ? kind : englishPlural(kind)
|
|
201
|
+
kind.toLowerCase() === resourceName.toLowerCase() || /Metrics$/.test(kind) ? kind : englishPlural(kind)
|
|
198
202
|
const pathSegments = pathname.replace(/^\//, '').split('/').filter(Boolean)
|
|
199
203
|
const view = getViewFromPath(pathname)
|
|
200
204
|
|
|
@@ -205,7 +209,8 @@ function radarPageTitle(pathname: string, search = '', apiResources?: { name: st
|
|
|
205
209
|
if (view === 'resources') {
|
|
206
210
|
const resourceName = decode(pathSegments[1] ?? '')
|
|
207
211
|
if (!resourceName) return 'Resources'
|
|
208
|
-
const
|
|
212
|
+
const group = new URLSearchParams(search).get('apiGroup') || ''
|
|
213
|
+
const match = findAPIResourceForRoute(apiResources, resourceName, group)
|
|
209
214
|
return pluralKindTitle(match?.kind ?? pluralToKind(resourceName), resourceName)
|
|
210
215
|
}
|
|
211
216
|
// GitOps detail is /gitops/detail/<kind>/<ns>/<name> → the resource name;
|
|
@@ -385,10 +390,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
385
390
|
|
|
386
391
|
// View-aware namespace scope: disabled on cluster-scoped surfaces so the
|
|
387
392
|
// chip isn't a dead control next to the cluster switcher.
|
|
388
|
-
|
|
389
|
-
// chip disables immediately on a cluster-scoped core kind (Nodes, PVs, …)
|
|
390
|
-
// instead of flickering enabled until /api-resources resolves.
|
|
391
|
-
const namespaceFilter = namespaceFilterDisabled(mainView, location.pathname, navApiResources ?? CORE_RESOURCES)
|
|
393
|
+
const namespaceFilter = namespaceFilterDisabled(mainView, location.pathname, location.search, navApiResources)
|
|
392
394
|
|
|
393
395
|
// One URL-derived tab title for every view (see radarPageTitle). Driving it
|
|
394
396
|
// from the URL — not the mounted component. Off unless the host opts in
|
|
@@ -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. */}
|
package/src/RadarApp.tsx
CHANGED
|
@@ -40,7 +40,10 @@ import type { ClusterLoadState } from "./types/clusterLoadState";
|
|
|
40
40
|
import { TimelineSourceProvider } from "./context/TimelineSource";
|
|
41
41
|
import type { TimelineSourceConfig } from "./api/timelineSource";
|
|
42
42
|
import { DiagnoseCustomizationProvider } from "./context/DiagnoseCustomization";
|
|
43
|
-
import type {
|
|
43
|
+
import type {
|
|
44
|
+
RenderDiagnoseAction,
|
|
45
|
+
DiagnoseConsentCopy,
|
|
46
|
+
} from "./context/DiagnoseCustomization";
|
|
44
47
|
import { defaultDiagnoseAction } from "./components/diagnose/LocalDiagnoseAction";
|
|
45
48
|
import { DiagnoseProvider } from "./components/diagnose/DiagnoseContext";
|
|
46
49
|
|
|
@@ -110,6 +113,15 @@ export interface RadarAppProps {
|
|
|
110
113
|
* agent-free. See ./context/DiagnoseCustomization for the render-prop shape.
|
|
111
114
|
*/
|
|
112
115
|
renderDiagnoseAction?: RenderDiagnoseAction;
|
|
116
|
+
/**
|
|
117
|
+
* Replaces the first-run consent card's trust copy. REQUIRED of any host whose
|
|
118
|
+
* backend runs the agent somewhere other than the user's own machine — the
|
|
119
|
+
* default copy states the agent runs locally, under the user's own model
|
|
120
|
+
* account, with transcripts kept on their disk, and none of that is true of a
|
|
121
|
+
* hosted runner. Radar keeps the card's chrome and the Approve/Cancel flow; a
|
|
122
|
+
* host only supplies the claims. See ./context/DiagnoseCustomization.
|
|
123
|
+
*/
|
|
124
|
+
diagnoseConsent?: DiagnoseConsentCopy;
|
|
113
125
|
/**
|
|
114
126
|
* Initial route for `router: 'memory'` (ignored for 'browser'). Lets a host
|
|
115
127
|
* deep-link a specific view (e.g. '/topology') without owning the URL bar —
|
|
@@ -184,6 +196,7 @@ export function RadarApp({
|
|
|
184
196
|
manageDocumentTitle = false,
|
|
185
197
|
documentTitleSuffix,
|
|
186
198
|
renderDiagnoseAction,
|
|
199
|
+
diagnoseConsent,
|
|
187
200
|
initialPath,
|
|
188
201
|
onClusterLoadStateChange,
|
|
189
202
|
timelineSource,
|
|
@@ -213,6 +226,7 @@ export function RadarApp({
|
|
|
213
226
|
<TimelineSourceProvider config={timelineSource}>
|
|
214
227
|
<DiagnoseCustomizationProvider
|
|
215
228
|
value={renderDiagnoseAction ?? defaultDiagnoseAction}
|
|
229
|
+
consentCopy={diagnoseConsent}
|
|
216
230
|
>
|
|
217
231
|
<DiagnoseProvider>
|
|
218
232
|
<App
|
package/src/api/apiResources.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { APIResource } from '../types'
|
|
|
3
3
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from './config'
|
|
4
4
|
|
|
5
5
|
// Re-export pure functions from package
|
|
6
|
-
export { categorizeResources, CORE_RESOURCES, formatGroupName, shortenGroupName, getKindLabel, getKindPlural } from '@skyhook-io/k8s-ui'
|
|
6
|
+
export { categorizeResources, CORE_RESOURCES, findAPIResourceForRoute, formatGroupName, shortenGroupName, getKindLabel, getKindPlural } from '@skyhook-io/k8s-ui'
|
|
7
7
|
export type { ResourceCategory } from '@skyhook-io/k8s-ui'
|
|
8
8
|
|
|
9
9
|
async function fetchJSON<T>(path: string): Promise<T> {
|
|
@@ -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
|
+
})
|