@skyhook-io/k8s-ui 1.8.10 → 1.8.12
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 +7 -6
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +697 -0
- package/src/components/applications/ApplicationDetail.tsx +3269 -474
- package/src/components/applications/ApplicationsView.tsx +62 -10
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +2 -3
- package/src/components/checks/severity.ts +14 -10
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +13 -6
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +190 -46
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +474 -121
- package/src/components/gitops/insights/index.ts +1 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +102 -1
- package/src/components/gitops/insights/insights-helpers.ts +74 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +10 -2
- package/src/components/issues/IssuesView.tsx +91 -52
- package/src/components/issues/ResourceIssuesSection.test.tsx +39 -6
- package/src/components/issues/ResourceIssuesSection.tsx +46 -144
- package/src/components/issues/index.ts +3 -0
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +102 -1
- package/src/components/issues/severity.ts +3 -0
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
- package/src/components/resources/ResourcesSidebar.tsx +46 -46
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +233 -51
- package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-argo.test.ts +18 -1
- package/src/components/resources/resource-utils-argo.ts +8 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +142 -111
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/shared/UnifiedDiff.tsx +51 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +375 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +88 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
- package/src/components/topology/TopologyControls.tsx +3 -1
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +149 -90
- package/src/components/topology/TopologyOverlayBar.tsx +30 -0
- package/src/components/topology/TopologySearch.tsx +25 -17
- package/src/components/topology/index.ts +2 -0
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +6 -1
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/SelectMenu.tsx +81 -0
- package/src/components/ui/drawer-components.tsx +48 -32
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +11 -4
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2706 -475
- package/src/components/workload/index.ts +1 -1
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +2 -1
- package/src/types/core.ts +57 -4
- package/src/types/gitops-insights.ts +69 -1
- package/src/types/gitops.ts +3 -3
- package/src/utils/application-history.test.ts +538 -0
- package/src/utils/application-history.ts +257 -0
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +201 -1
- package/src/utils/applications.ts +1182 -393
- package/src/utils/index.ts +3 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +1024 -1
- package/src/utils/resource-hierarchy.ts +925 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
-
import { ChevronDown, Globe, Search, AlertTriangle
|
|
3
|
+
import { ChevronDown, Globe, Search, AlertTriangle } from 'lucide-react'
|
|
4
4
|
import { Tooltip } from '../ui/Tooltip'
|
|
5
|
+
import { MultiSelectPicker } from '../ui/MultiSelectPicker'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Backend-reported namespace scope. Mirrors Radar's `/cluster/namespace-scope`
|
|
@@ -192,18 +193,6 @@ export const NamespacePicker = forwardRef<NamespacePickerHandle, NamespacePicker
|
|
|
192
193
|
applySelection(new Set())
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
const selectAllVisible = () => {
|
|
196
|
-
const next = new Set(draft)
|
|
197
|
-
for (const ns of filteredItems) next.add(ns)
|
|
198
|
-
setDraft(next)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const clearVisible = () => {
|
|
202
|
-
const next = new Set(draft)
|
|
203
|
-
for (const ns of filteredItems) next.delete(ns)
|
|
204
|
-
setDraft(next)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
196
|
const activeCount = scopeActives.length
|
|
208
197
|
const triggerLabel =
|
|
209
198
|
activeCount === 0 ? 'All namespaces' : activeCount === 1 ? scopeActives[0] : `${activeCount} namespaces`
|
|
@@ -226,11 +215,6 @@ export const NamespacePicker = forwardRef<NamespacePickerHandle, NamespacePicker
|
|
|
226
215
|
? `View is filtered to namespace ${scopeActives[0]}. Click to switch or reset.`
|
|
227
216
|
: `View is filtered to ${activeCount} namespaces. Click to adjust or reset.`
|
|
228
217
|
|
|
229
|
-
// Counts used to label the bulk-action buttons; computed against the visible
|
|
230
|
-
// (filtered) set so the labels match what the action will affect.
|
|
231
|
-
const visibleSelectedCount = filteredItems.reduce((n, ns) => n + (draft.has(ns) ? 1 : 0), 0)
|
|
232
|
-
const allVisibleSelected = filteredItems.length > 0 && visibleSelectedCount === filteredItems.length
|
|
233
|
-
|
|
234
218
|
return (
|
|
235
219
|
<>
|
|
236
220
|
<Tooltip
|
|
@@ -271,100 +255,95 @@ export const NamespacePicker = forwardRef<NamespacePickerHandle, NamespacePicker
|
|
|
271
255
|
style={{ position: 'fixed', top: pos.top, left: pos.left, minWidth: pos.width, zIndex: 100 }}
|
|
272
256
|
className="bg-theme-surface border border-theme-border rounded-md shadow-theme-lg overflow-hidden"
|
|
273
257
|
>
|
|
274
|
-
{items.length > 6 && (
|
|
275
|
-
<div className="flex items-center gap-2 px-2 py-1.5 border-b border-theme-border">
|
|
276
|
-
<Search className="w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
277
|
-
<input
|
|
278
|
-
autoFocus
|
|
279
|
-
value={search}
|
|
280
|
-
onChange={e => setSearch(e.target.value)}
|
|
281
|
-
placeholder="Filter namespaces"
|
|
282
|
-
className="flex-1 bg-transparent text-sm outline-none text-theme-text-primary placeholder:text-theme-text-tertiary"
|
|
283
|
-
/>
|
|
284
|
-
</div>
|
|
285
|
-
)}
|
|
286
|
-
|
|
287
258
|
{scope.cacheScoped ? (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
className="px-1.5 py-0.5 rounded hover:bg-theme-hover disabled:opacity-50 disabled:hover:bg-transparent"
|
|
309
|
-
>
|
|
310
|
-
{allVisibleSelected
|
|
311
|
-
? `Clear ${filteredItems.length} visible`
|
|
312
|
-
: search.trim()
|
|
313
|
-
? `Select ${filteredItems.length} visible`
|
|
314
|
-
: 'Select all'}
|
|
315
|
-
</button>
|
|
316
|
-
</div>
|
|
317
|
-
)}
|
|
259
|
+
<>
|
|
260
|
+
{items.length > 6 && (
|
|
261
|
+
<div className="flex items-center gap-2 px-2 py-1.5 border-b border-theme-border">
|
|
262
|
+
<Search className="w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
263
|
+
<input
|
|
264
|
+
autoFocus
|
|
265
|
+
value={search}
|
|
266
|
+
onChange={e => setSearch(e.target.value)}
|
|
267
|
+
placeholder="Filter namespaces"
|
|
268
|
+
className="flex-1 bg-transparent text-sm outline-none text-theme-text-primary placeholder:text-theme-text-tertiary"
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
<div className="px-3 py-1.5 border-b border-theme-border text-[11px] leading-snug text-theme-text-secondary">
|
|
274
|
+
Radar is watching one namespace to stay fast on large clusters.
|
|
275
|
+
{scope.namespaceRescope
|
|
276
|
+
? ' Pick another to re-point it — takes a moment and closes open terminals.'
|
|
277
|
+
: ' This instance is locked to its startup namespace.'}
|
|
278
|
+
</div>
|
|
318
279
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
280
|
+
<ul className="max-h-80 overflow-y-auto py-1">
|
|
281
|
+
{filteredItems.length === 0 && (
|
|
282
|
+
<li className="px-3 py-2 text-xs text-theme-text-tertiary">
|
|
283
|
+
{search ? 'No matches.' : 'No namespaces available.'}
|
|
284
|
+
</li>
|
|
285
|
+
)}
|
|
325
286
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
287
|
+
{filteredItems.map(ns => {
|
|
288
|
+
const isChecked = draft.has(ns)
|
|
289
|
+
const isContextDefault = ns === scope.kubeconfigNamespace && ns !== ''
|
|
290
|
+
return (
|
|
291
|
+
<li key={ns}>
|
|
292
|
+
<label className="w-full flex items-center justify-between px-3 py-1.5 text-sm hover:bg-theme-hover text-left text-theme-text-primary cursor-pointer">
|
|
293
|
+
<span className="flex items-center gap-2 min-w-0">
|
|
294
|
+
<input
|
|
295
|
+
type="radio"
|
|
296
|
+
name="namespace-cache-scope"
|
|
297
|
+
checked={isChecked}
|
|
298
|
+
onChange={() => toggle(ns)}
|
|
299
|
+
className="shrink-0 accent-current"
|
|
300
|
+
/>
|
|
301
|
+
<span className="truncate">{ns}</span>
|
|
302
|
+
{isContextDefault && (
|
|
303
|
+
<span className="text-[10px] uppercase tracking-wide text-theme-text-tertiary shrink-0">
|
|
304
|
+
kubeconfig
|
|
305
|
+
</span>
|
|
306
|
+
)}
|
|
346
307
|
</span>
|
|
347
|
-
|
|
348
|
-
</
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
})}
|
|
353
|
-
</ul>
|
|
308
|
+
</label>
|
|
309
|
+
</li>
|
|
310
|
+
)
|
|
311
|
+
})}
|
|
312
|
+
</ul>
|
|
354
313
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
314
|
+
<div className="flex items-center justify-between px-3 py-1.5 border-t border-theme-border text-[11px] text-theme-text-tertiary">
|
|
315
|
+
<span>{draft.size === 1 ? Array.from(draft)[0] : 'Select a namespace'}</span>
|
|
316
|
+
<button
|
|
317
|
+
onClick={closeAndApply}
|
|
318
|
+
className="px-2 py-0.5 rounded bg-theme-elevated hover:bg-theme-hover text-theme-text-primary"
|
|
319
|
+
>
|
|
320
|
+
Done
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
</>
|
|
324
|
+
) : (
|
|
325
|
+
<MultiSelectPicker
|
|
326
|
+
items={items}
|
|
327
|
+
selected={draft}
|
|
328
|
+
onSelectionChange={setDraft}
|
|
329
|
+
onClearAll={clearAll}
|
|
330
|
+
onDone={closeAndApply}
|
|
331
|
+
search={search}
|
|
332
|
+
onSearchChange={setSearch}
|
|
333
|
+
searchPlaceholder="Filter namespaces"
|
|
334
|
+
summaryEmptyLabel="All namespaces"
|
|
335
|
+
noItemsLabel="No namespaces available."
|
|
336
|
+
clearAllDisabled={!canClearAll || activeCount === 0}
|
|
337
|
+
clearAllAriaLabel="Clear namespace selection"
|
|
338
|
+
renderItemMeta={ns =>
|
|
339
|
+
ns === scope.kubeconfigNamespace && ns !== '' ? (
|
|
340
|
+
<span className="text-[10px] uppercase tracking-wide text-theme-text-tertiary shrink-0">
|
|
341
|
+
kubeconfig
|
|
342
|
+
</span>
|
|
343
|
+
) : null
|
|
344
|
+
}
|
|
345
|
+
/>
|
|
346
|
+
)}
|
|
368
347
|
|
|
369
348
|
{!scope.authoritative && (
|
|
370
349
|
<div className="px-3 py-2 border-t border-theme-border text-[11px] status-degraded">
|
|
@@ -13,6 +13,16 @@ const sqlInstance: APIResource = {
|
|
|
13
13
|
verbs: ['list'],
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const horizontalPodAutoscaler: APIResource = {
|
|
17
|
+
group: 'autoscaling',
|
|
18
|
+
version: 'v2',
|
|
19
|
+
kind: 'HorizontalPodAutoscaler',
|
|
20
|
+
name: 'horizontalpodautoscalers',
|
|
21
|
+
namespaced: true,
|
|
22
|
+
isCrd: false,
|
|
23
|
+
verbs: ['list'],
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
describe('ResourcesSidebar CRD group labels', () => {
|
|
17
27
|
it('keeps raw API groups available for filtering and hover recovery', () => {
|
|
18
28
|
expect(resourceMatchesSidebarFilter(sqlInstance, 'cnrm')).toBe(true)
|
|
@@ -34,3 +44,64 @@ describe('ResourcesSidebar CRD group labels', () => {
|
|
|
34
44
|
expect(html).toContain('title="API group: sql.cnrm.cloud.google.com"')
|
|
35
45
|
})
|
|
36
46
|
})
|
|
47
|
+
|
|
48
|
+
describe('ResourcesSidebar count visibility', () => {
|
|
49
|
+
it('keeps resources visible when their count is unknown', () => {
|
|
50
|
+
const html = renderToString(
|
|
51
|
+
<ResourcesSidebar
|
|
52
|
+
selectedKind={null}
|
|
53
|
+
onSelectedKindChange={() => {}}
|
|
54
|
+
apiResources={[horizontalPodAutoscaler]}
|
|
55
|
+
resourceCounts={{}}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
expect(html).toContain('HorizontalPodAutoscaler')
|
|
60
|
+
expect(html).toContain('–')
|
|
61
|
+
expect(html).toContain('aria-label="Count unavailable. Open to view resources."')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('hides confirmed-empty resources by default', () => {
|
|
65
|
+
const html = renderToString(
|
|
66
|
+
<ResourcesSidebar
|
|
67
|
+
selectedKind={null}
|
|
68
|
+
onSelectedKindChange={() => {}}
|
|
69
|
+
apiResources={[horizontalPodAutoscaler]}
|
|
70
|
+
resourceCounts={{ 'autoscaling/HorizontalPodAutoscaler': 0 }}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
expect(html).not.toContain('HorizontalPodAutoscaler')
|
|
75
|
+
expect(html).toContain('Show')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('keeps the selected confirmed-empty resource visible', () => {
|
|
79
|
+
const html = renderToString(
|
|
80
|
+
<ResourcesSidebar
|
|
81
|
+
selectedKind={{ name: 'horizontalpodautoscalers', kind: 'HorizontalPodAutoscaler', group: 'autoscaling' }}
|
|
82
|
+
onSelectedKindChange={() => {}}
|
|
83
|
+
apiResources={[horizontalPodAutoscaler]}
|
|
84
|
+
resourceCounts={{ 'autoscaling/HorizontalPodAutoscaler': 0 }}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
expect(html).toContain('HorizontalPodAutoscaler')
|
|
89
|
+
expect(html).toContain('>0</')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('keeps pinned confirmed-empty resources visible in Favorites', () => {
|
|
93
|
+
const html = renderToString(
|
|
94
|
+
<ResourcesSidebar
|
|
95
|
+
selectedKind={null}
|
|
96
|
+
onSelectedKindChange={() => {}}
|
|
97
|
+
apiResources={[horizontalPodAutoscaler]}
|
|
98
|
+
resourceCounts={{ 'autoscaling/HorizontalPodAutoscaler': 0 }}
|
|
99
|
+
pinned={[{ name: 'horizontalpodautoscalers', kind: 'HorizontalPodAutoscaler', group: 'autoscaling' }]}
|
|
100
|
+
isPinned={(name, group) => name === 'horizontalpodautoscalers' && group === 'autoscaling'}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
expect(html).toContain('Favorites')
|
|
105
|
+
expect(html).toContain('HorizontalPodAutoscaler')
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -14,6 +14,7 @@ import type { APIResource } from '../../types'
|
|
|
14
14
|
import { categorizeResources, CORE_RESOURCES } from '../../utils/api-resources'
|
|
15
15
|
import { getResourceIcon } from '../../utils/resource-icons'
|
|
16
16
|
import { Tooltip } from '../ui/Tooltip'
|
|
17
|
+
import { Input } from '../ui/Input'
|
|
17
18
|
|
|
18
19
|
// Selected resource type info (need both name for API and kind for display)
|
|
19
20
|
export interface SelectedKindInfo {
|
|
@@ -52,29 +53,7 @@ export interface ResourcesSidebarProps {
|
|
|
52
53
|
|
|
53
54
|
// Persisted across remounts so collapsed categories survive tab switches
|
|
54
55
|
let persistedExpandedCategories: Set<string> | null = null
|
|
55
|
-
|
|
56
|
-
// Core kinds that are always shown even with 0 instances
|
|
57
|
-
// These are the most commonly used Kubernetes resources (using Kind names, not plural names)
|
|
58
|
-
const ALWAYS_SHOWN_KINDS = new Set([
|
|
59
|
-
'Pod',
|
|
60
|
-
'Deployment',
|
|
61
|
-
'DaemonSet',
|
|
62
|
-
'StatefulSet',
|
|
63
|
-
'ReplicaSet',
|
|
64
|
-
'Service',
|
|
65
|
-
'Ingress',
|
|
66
|
-
'ConfigMap',
|
|
67
|
-
'Secret',
|
|
68
|
-
'Job',
|
|
69
|
-
'CronJob',
|
|
70
|
-
'HorizontalPodAutoscaler',
|
|
71
|
-
'PersistentVolumeClaim',
|
|
72
|
-
'Node',
|
|
73
|
-
'Namespace',
|
|
74
|
-
'ServiceAccount',
|
|
75
|
-
'NetworkPolicy',
|
|
76
|
-
'Event',
|
|
77
|
-
])
|
|
56
|
+
const COUNT_UNAVAILABLE_MESSAGE = 'Count unavailable. Open to view resources.'
|
|
78
57
|
|
|
79
58
|
// Fallback resource types when API resources aren't loaded yet
|
|
80
59
|
const CORE_RESOURCE_TYPES = [
|
|
@@ -173,15 +152,25 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
|
|
|
173
152
|
<Tooltip content="Insufficient permissions" position="left">
|
|
174
153
|
<Shield className="w-3.5 h-3.5 text-amber-400/60" />
|
|
175
154
|
</Tooltip>
|
|
155
|
+
) : count === null ? (
|
|
156
|
+
<Tooltip content={COUNT_UNAVAILABLE_MESSAGE} position="left">
|
|
157
|
+
<span
|
|
158
|
+
className={clsx(
|
|
159
|
+
'text-xs py-0.5 rounded text-center font-mono w-8 text-theme-text-disabled',
|
|
160
|
+
isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated',
|
|
161
|
+
)}
|
|
162
|
+
aria-label={COUNT_UNAVAILABLE_MESSAGE}
|
|
163
|
+
>
|
|
164
|
+
–
|
|
165
|
+
</span>
|
|
166
|
+
</Tooltip>
|
|
176
167
|
) : (
|
|
177
168
|
<span className={clsx(
|
|
178
169
|
'text-xs py-0.5 rounded text-center font-mono',
|
|
179
170
|
isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated',
|
|
180
|
-
count
|
|
181
|
-
? 'w-8 text-theme-text-disabled'
|
|
182
|
-
: count < 1000 ? 'w-8' : 'w-9',
|
|
171
|
+
count < 1000 ? 'w-8' : 'w-9',
|
|
183
172
|
)}>
|
|
184
|
-
{count
|
|
173
|
+
{count}
|
|
185
174
|
</span>
|
|
186
175
|
)}
|
|
187
176
|
</div>
|
|
@@ -299,7 +288,7 @@ export function ResourcesSidebar({
|
|
|
299
288
|
} else if (key in resourceCounts) {
|
|
300
289
|
results[key] = resourceCounts[key] ?? null
|
|
301
290
|
} else {
|
|
302
|
-
results[key] =
|
|
291
|
+
results[key] = null
|
|
303
292
|
}
|
|
304
293
|
}
|
|
305
294
|
return results
|
|
@@ -326,14 +315,14 @@ export function ResourcesSidebar({
|
|
|
326
315
|
0
|
|
327
316
|
)
|
|
328
317
|
|
|
329
|
-
// Filter resources:
|
|
330
|
-
//
|
|
331
|
-
// showEmptyKinds is true.
|
|
318
|
+
// Filter resources: hide only confirmed-empty kinds. Unknown counts stay
|
|
319
|
+
// visible as a dash so count coverage gaps do not masquerade as emptiness.
|
|
332
320
|
const visibleResources = category.resources.filter(resource => {
|
|
333
321
|
const count = counts[resource.group ? `${resource.group}/${resource.kind}` : resource.kind]
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
322
|
+
const isSelectedResource =
|
|
323
|
+
(effectiveSelectedKind.name === resource.name && effectiveSelectedKind.group === resource.group) ||
|
|
324
|
+
(effectiveSelectedKind.kind.toLowerCase() === resource.kind.toLowerCase() && effectiveSelectedKind.group === resource.group)
|
|
325
|
+
const shouldShow = isSelectedResource || count === null || (count ?? 0) > 0 || showEmptyKinds
|
|
337
326
|
if (!shouldShow) totalHiddenKinds++
|
|
338
327
|
return shouldShow
|
|
339
328
|
})
|
|
@@ -348,16 +337,15 @@ export function ResourcesSidebar({
|
|
|
348
337
|
return 0
|
|
349
338
|
})
|
|
350
339
|
|
|
351
|
-
// Filter out empty groups unless they have visible resources
|
|
340
|
+
// Filter out empty groups unless they have visible resources or showEmptyKinds is true.
|
|
352
341
|
const visibleCategories = sorted.filter(category => {
|
|
353
|
-
// Show if: has resources with instances, OR has visible resources (core kinds), OR showEmptyKinds
|
|
354
342
|
const shouldShow = category.total > 0 || category.visibleResources.length > 0 || showEmptyKinds
|
|
355
343
|
if (!shouldShow) totalHiddenGroups++
|
|
356
344
|
return shouldShow
|
|
357
345
|
})
|
|
358
346
|
|
|
359
347
|
return { sortedCategories: visibleCategories, hiddenKindsCount: totalHiddenKinds, hiddenGroupsCount: totalHiddenGroups }
|
|
360
|
-
}, [categories, counts, showEmptyKinds])
|
|
348
|
+
}, [categories, counts, showEmptyKinds, effectiveSelectedKind.name, effectiveSelectedKind.kind, effectiveSelectedKind.group])
|
|
361
349
|
|
|
362
350
|
// Filter sidebar categories/kinds by the kind search term
|
|
363
351
|
const filteredCategories = useMemo(() => {
|
|
@@ -489,9 +477,8 @@ export function ResourcesSidebar({
|
|
|
489
477
|
<div className="px-2 py-3 border-b border-theme-border">
|
|
490
478
|
<div className="relative">
|
|
491
479
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
492
|
-
<
|
|
480
|
+
<Input
|
|
493
481
|
ref={kindSearchRef}
|
|
494
|
-
type="text"
|
|
495
482
|
placeholder="Filter resources..."
|
|
496
483
|
value={kindFilter}
|
|
497
484
|
onChange={(e) => setKindFilter(e.target.value)}
|
|
@@ -647,13 +634,26 @@ export function ResourcesSidebar({
|
|
|
647
634
|
>
|
|
648
635
|
<Icon className="w-4 h-4 shrink-0" />
|
|
649
636
|
<span className="flex-1 text-left">{type.label}</span>
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
637
|
+
{count === null ? (
|
|
638
|
+
<Tooltip content={COUNT_UNAVAILABLE_MESSAGE} position="left">
|
|
639
|
+
<span
|
|
640
|
+
className={clsx(
|
|
641
|
+
'badge font-mono text-theme-text-disabled',
|
|
642
|
+
isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated',
|
|
643
|
+
)}
|
|
644
|
+
aria-label={COUNT_UNAVAILABLE_MESSAGE}
|
|
645
|
+
>
|
|
646
|
+
–
|
|
647
|
+
</span>
|
|
648
|
+
</Tooltip>
|
|
649
|
+
) : (
|
|
650
|
+
<span className={clsx(
|
|
651
|
+
'badge font-mono',
|
|
652
|
+
isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated',
|
|
653
|
+
)}>
|
|
654
|
+
{count}
|
|
655
|
+
</span>
|
|
656
|
+
)}
|
|
657
657
|
</button>
|
|
658
658
|
)
|
|
659
659
|
})
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { APIResource } from '../../types'
|
|
3
|
+
import {
|
|
4
|
+
canonicalizeSelectedKind,
|
|
5
|
+
deriveSidebarResourceCounts,
|
|
6
|
+
LOADED_RESOURCE_COUNT_TTL_MS,
|
|
7
|
+
resourceQueryMatchesSelectedKind,
|
|
8
|
+
} from './ResourcesView'
|
|
9
|
+
|
|
10
|
+
const endpoints: APIResource = {
|
|
11
|
+
group: '',
|
|
12
|
+
version: 'v1',
|
|
13
|
+
kind: 'Endpoints',
|
|
14
|
+
name: 'endpoints',
|
|
15
|
+
namespaced: true,
|
|
16
|
+
isCrd: false,
|
|
17
|
+
verbs: ['list', 'get', 'watch'],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const httpRoute: APIResource = {
|
|
21
|
+
group: 'gateway.networking.k8s.io',
|
|
22
|
+
version: 'v1',
|
|
23
|
+
kind: 'HTTPRoute',
|
|
24
|
+
name: 'httproutes',
|
|
25
|
+
namespaced: true,
|
|
26
|
+
isCrd: true,
|
|
27
|
+
verbs: ['list', 'get', 'watch'],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('canonicalizeSelectedKind', () => {
|
|
31
|
+
it('restores the canonical Kind when URL hydration only has the plural resource name', () => {
|
|
32
|
+
expect(
|
|
33
|
+
canonicalizeSelectedKind(
|
|
34
|
+
{ name: 'endpoints', kind: 'endpoints', group: '' },
|
|
35
|
+
[endpoints],
|
|
36
|
+
[endpoints]
|
|
37
|
+
)
|
|
38
|
+
).toEqual({ name: 'endpoints', kind: 'Endpoints', group: '' })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('leaves an already canonical selection alone', () => {
|
|
42
|
+
expect(
|
|
43
|
+
canonicalizeSelectedKind(
|
|
44
|
+
{ name: 'endpoints', kind: 'Endpoints', group: '' },
|
|
45
|
+
[endpoints],
|
|
46
|
+
[endpoints]
|
|
47
|
+
)
|
|
48
|
+
).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('resolves CRD deep links from Kind to plural resource name', () => {
|
|
52
|
+
expect(
|
|
53
|
+
canonicalizeSelectedKind(
|
|
54
|
+
{ name: 'HTTPRoute', kind: 'HTTPRoute', group: 'gateway.networking.k8s.io' },
|
|
55
|
+
[],
|
|
56
|
+
[httpRoute]
|
|
57
|
+
)
|
|
58
|
+
).toEqual({ name: 'httproutes', kind: 'HTTPRoute', group: 'gateway.networking.k8s.io' })
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('deriveSidebarResourceCounts', () => {
|
|
63
|
+
it('keeps recently loaded counts for resources omitted from resource-counts', () => {
|
|
64
|
+
const now = 10_000
|
|
65
|
+
|
|
66
|
+
expect(
|
|
67
|
+
deriveSidebarResourceCounts(
|
|
68
|
+
[endpoints],
|
|
69
|
+
{},
|
|
70
|
+
undefined,
|
|
71
|
+
{ Endpoints: { count: 132, expiresAt: now + LOADED_RESOURCE_COUNT_TTL_MS } },
|
|
72
|
+
now
|
|
73
|
+
)
|
|
74
|
+
).toEqual({ Endpoints: 132 })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('expires loaded counts after the TTL', () => {
|
|
78
|
+
const now = 10_000
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
deriveSidebarResourceCounts(
|
|
82
|
+
[endpoints],
|
|
83
|
+
{},
|
|
84
|
+
undefined,
|
|
85
|
+
{ Endpoints: { count: 132, expiresAt: now } },
|
|
86
|
+
now
|
|
87
|
+
)
|
|
88
|
+
).toEqual({ Endpoints: null })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('prefers authoritative resource-counts over cached loaded counts', () => {
|
|
92
|
+
const now = 10_000
|
|
93
|
+
|
|
94
|
+
expect(
|
|
95
|
+
deriveSidebarResourceCounts(
|
|
96
|
+
[endpoints],
|
|
97
|
+
{ Endpoints: 7 },
|
|
98
|
+
undefined,
|
|
99
|
+
{ Endpoints: { count: 132, expiresAt: now + LOADED_RESOURCE_COUNT_TTL_MS } },
|
|
100
|
+
now
|
|
101
|
+
)
|
|
102
|
+
).toEqual({ Endpoints: 7 })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('lets a fresh loaded count override an unavailable count marker', () => {
|
|
106
|
+
const now = 10_000
|
|
107
|
+
|
|
108
|
+
expect(
|
|
109
|
+
deriveSidebarResourceCounts(
|
|
110
|
+
[endpoints],
|
|
111
|
+
{},
|
|
112
|
+
['Endpoints'],
|
|
113
|
+
{ Endpoints: { count: 132, expiresAt: now + LOADED_RESOURCE_COUNT_TTL_MS } },
|
|
114
|
+
now
|
|
115
|
+
)
|
|
116
|
+
).toEqual({ Endpoints: 132 })
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('resourceQueryMatchesSelectedKind', () => {
|
|
121
|
+
it('accepts untagged legacy query results', () => {
|
|
122
|
+
expect(resourceQueryMatchesSelectedKind(undefined, { name: 'endpoints', kind: 'Endpoints', group: '' })).toBe(true)
|
|
123
|
+
expect(resourceQueryMatchesSelectedKind({}, { name: 'endpoints', kind: 'Endpoints', group: '' })).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('rejects stale selected-kind query results from the previous kind', () => {
|
|
127
|
+
expect(
|
|
128
|
+
resourceQueryMatchesSelectedKind(
|
|
129
|
+
{ resourceName: 'pods', group: '' },
|
|
130
|
+
{ name: 'endpoints', kind: 'Endpoints', group: '' }
|
|
131
|
+
)
|
|
132
|
+
).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('matches grouped built-in resources by plural name and group', () => {
|
|
136
|
+
expect(
|
|
137
|
+
resourceQueryMatchesSelectedKind(
|
|
138
|
+
{ resourceName: 'endpointslices', group: 'discovery.k8s.io' },
|
|
139
|
+
{ name: 'endpointslices', kind: 'EndpointSlice', group: 'discovery.k8s.io' }
|
|
140
|
+
)
|
|
141
|
+
).toBe(true)
|
|
142
|
+
})
|
|
143
|
+
})
|