@skyhook-io/k8s-ui 1.8.11 → 1.8.13

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 (87) hide show
  1. package/package.json +4 -4
  2. package/src/components/applications/ApplicationDetail.test.tsx +136 -8
  3. package/src/components/applications/ApplicationDetail.tsx +1215 -487
  4. package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
  5. package/src/components/gitops/GitOpsTableView.tsx +10 -3
  6. package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
  7. package/src/components/gitops/SyncOptionsDialog.tsx +189 -45
  8. package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
  9. package/src/components/gitops/insights/GitOpsInsightViews.tsx +201 -21
  10. package/src/components/gitops/insights/index.ts +1 -0
  11. package/src/components/gitops/insights/insights-helpers.test.ts +16 -0
  12. package/src/components/gitops/insights/insights-helpers.ts +5 -0
  13. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -0
  14. package/src/components/issues/IssuesView.tsx +55 -29
  15. package/src/components/issues/ResourceIssuesSection.test.tsx +33 -1
  16. package/src/components/issues/ResourceIssuesSection.tsx +21 -1
  17. package/src/components/issues/severity.ts +1 -0
  18. package/src/components/resources/ResourcesView.tsx +109 -19
  19. package/src/components/resources/column-filter-serialization.test.ts +79 -1
  20. package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
  21. package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
  22. package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
  23. package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
  24. package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
  25. package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
  26. package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
  27. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  28. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
  29. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
  30. package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
  31. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
  32. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
  33. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
  34. package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
  35. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
  36. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
  37. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
  38. package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
  39. package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
  40. package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
  41. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
  42. package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
  43. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
  44. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
  45. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
  46. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
  47. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
  48. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
  49. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
  50. package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
  51. package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
  52. package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
  53. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
  54. package/src/components/resources/resource-utils-argo.test.ts +18 -1
  55. package/src/components/resources/resource-utils-argo.ts +8 -0
  56. package/src/components/resources/resource-utils.ts +94 -25
  57. package/src/components/shared/ResourceActionsBar.tsx +54 -22
  58. package/src/components/shared/UnifiedDiff.tsx +51 -0
  59. package/src/components/shared/index.ts +1 -0
  60. package/src/components/timeline/TimelineList.tsx +7 -3
  61. package/src/components/timeline/TimelineToolbarState.test.tsx +24 -0
  62. package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
  63. package/src/components/topology/TopologyControls.tsx +3 -1
  64. package/src/components/topology/TopologyGraph.tsx +76 -90
  65. package/src/components/topology/TopologyOverlayBar.tsx +30 -0
  66. package/src/components/topology/TopologySearch.tsx +23 -15
  67. package/src/components/topology/index.ts +2 -0
  68. package/src/components/ui/Badge.tsx +1 -1
  69. package/src/components/ui/SelectMenu.tsx +81 -0
  70. package/src/components/ui/Tooltip.test.tsx +21 -0
  71. package/src/components/ui/Tooltip.tsx +9 -12
  72. package/src/components/ui/drawer-components.tsx +9 -6
  73. package/src/components/ui/index.ts +2 -0
  74. package/src/components/workload/WorkloadView.tsx +24 -3
  75. package/src/theme/tailwind-theme.css +1 -0
  76. package/src/theme/variables.css +1 -0
  77. package/src/types/gitops-insights.ts +69 -1
  78. package/src/types/gitops.ts +3 -3
  79. package/src/utils/api-resources.test.ts +36 -1
  80. package/src/utils/api-resources.ts +16 -0
  81. package/src/utils/application-history.test.ts +538 -0
  82. package/src/utils/application-history.ts +257 -0
  83. package/src/utils/applications.test.ts +34 -0
  84. package/src/utils/applications.ts +25 -0
  85. package/src/utils/index.ts +1 -0
  86. package/src/utils/resource-hierarchy.test.ts +35 -1
  87. package/src/utils/resource-hierarchy.ts +20 -0
@@ -29,6 +29,7 @@ import {
29
29
  X,
30
30
  BarChart3,
31
31
  Network,
32
+ DollarSign,
32
33
  } from 'lucide-react'
33
34
  import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode, HPADiagnosis, WorkloadPodInfo } from '../../types'
34
35
  import type { GitOpsStatus } from '../../types/gitops'
@@ -64,7 +65,7 @@ import {
64
65
  } from '../resources/resource-utils'
65
66
  import { ServicePortCards, type ServicePortRenderProps } from '../resources/renderers/ServiceRenderer'
66
67
 
67
- export type WorkloadTabType = 'overview' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'yaml'
68
+ export type WorkloadTabType = 'overview' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'cost' | 'yaml'
68
69
  type TabType = WorkloadTabType
69
70
 
70
71
  export interface ResourceOwnershipContext {
@@ -257,6 +258,8 @@ interface WorkloadViewProps {
257
258
  }) => ReactNode
258
259
  /** Render the metrics tab content */
259
260
  renderMetricsTab?: (props: { kind: string; namespace: string; name: string }) => ReactNode
261
+ /** Render the cost tab content */
262
+ renderCostTab?: (props: { kind: string; namespace: string; name: string }) => ReactNode
260
263
  /** Render a read-only YAML view for a related object from the workload's
261
264
  * neighborhood. Providing this turns the YAML tab into an object explorer
262
265
  * (rail of the workload + its Services/config/policies/pods); omitting it
@@ -265,6 +268,8 @@ interface WorkloadViewProps {
265
268
  renderRelatedYaml?: (ref: { kind: string; namespace: string; name: string; group?: string }) => ReactNode
266
269
  /** Whether metrics are available for this resource kind */
267
270
  isMetricsAvailable?: (kind: string, resource: any) => boolean
271
+ /** Whether cost is available for this resource kind */
272
+ isCostAvailable?: (kind: string, resource: any) => boolean
268
273
  /** Render extra content at the bottom of the overview tab (e.g. audit findings) */
269
274
  renderOverviewExtra?: (props: { kind: string; namespace: string; name: string }) => ReactNode
270
275
  /** Render lightweight overview intro content before the default renderer. */
@@ -280,6 +285,10 @@ interface WorkloadViewProps {
280
285
  * live issues for this resource). Avoids showing the same failure twice.
281
286
  * Also gates the `renderOverviewLead` wrapper (see above). */
282
287
  hasOperationalIssues?: boolean
288
+ /** True while the live-issues fetch is still pending. Renderers suppress their
289
+ * banners during this window too — a banner whose failure the pipeline covers
290
+ * would otherwise flash on first paint before the issues arrive and hide it. */
291
+ operationalIssuesPending?: boolean
283
292
 
284
293
  // ── Duplicate ────────────────────────────────────────────────────────────
285
294
  /** Duplicate handler — opens create dialog with this resource's YAML */
@@ -359,7 +368,9 @@ export function WorkloadView({
359
368
  renderExpandedOverview,
360
369
  renderRelatedYaml,
361
370
  renderMetricsTab,
371
+ renderCostTab,
362
372
  isMetricsAvailable,
373
+ isCostAvailable,
363
374
  // Duplicate
364
375
  onDuplicate,
365
376
  onDownload,
@@ -367,6 +378,7 @@ export function WorkloadView({
367
378
  renderOverviewIntro,
368
379
  renderOverviewLead,
369
380
  hasOperationalIssues,
381
+ operationalIssuesPending,
370
382
  // Actions bar
371
383
  actionsBarProps,
372
384
  // Renderer overrides
@@ -644,8 +656,10 @@ export function WorkloadView({
644
656
  })
645
657
 
646
658
  const showMetricsTab = isMetricsAvailable ? isMetricsAvailable(kind, resource) : false
659
+ const showCostTab = isCostAvailable ? isCostAvailable(kind, resource) : false
647
660
  const logsTabVisible = Boolean(renderLogsTab) && (allPods.length > 0 || LOGS_TAB_WITHOUT_PODS_KINDS.has(kindToPlural(kind).toLowerCase()))
648
661
  const metricsTabVisible = Boolean(showMetricsTab && renderMetricsTab)
662
+ const costTabVisible = Boolean(showCostTab && renderCostTab)
649
663
  const podEvidenceLoading = resourceLoading || workloadPodsLoading || eventsLoading
650
664
  const logsFallbackReady = !renderLogsTab || (!logsTabVisible && !podEvidenceLoading)
651
665
  const requestedTab: TabType = activeTab
@@ -660,6 +674,7 @@ export function WorkloadView({
660
674
  },
661
675
  { id: 'logs', label: 'Logs', icon: <Terminal className="w-4 h-4" />, hidden: !logsTabVisible },
662
676
  { id: 'metrics', label: 'Metrics', icon: <BarChart3 className="w-4 h-4" />, hidden: !metricsTabVisible },
677
+ { id: 'cost', label: 'Cost', icon: <DollarSign className="w-4 h-4" />, hidden: !costTabVisible },
663
678
  { id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
664
679
  ]
665
680
  const requestedTabAvailable = tabs.some((tab) => tab.id === requestedTab && !tab.hidden)
@@ -670,6 +685,7 @@ export function WorkloadView({
670
685
  (
671
686
  (requestedTab === 'topology' && topologyTabHidden) ||
672
687
  (requestedTab === 'metrics' && (!renderMetricsTab || (!!resource && !resourceLoading && !showMetricsTab))) ||
688
+ (requestedTab === 'cost' && (!renderCostTab || (!!resource && !resourceLoading && !showCostTab))) ||
673
689
  (requestedTab === 'logs' && logsFallbackReady)
674
690
  )
675
691
  useEffect(() => {
@@ -794,7 +810,7 @@ export function WorkloadView({
794
810
  onDownload={onDownload}
795
811
  />
796
812
  ) : (
797
- <OperationalIssuesShownContext.Provider value={!!hasOperationalIssues}>
813
+ <OperationalIssuesShownContext.Provider value={!!hasOperationalIssues || !!operationalIssuesPending}>
798
814
  {renderOverviewLead && hasOperationalIssues && (
799
815
  <div className="px-4 pt-4">
800
816
  {renderOverviewLead({ kind, namespace, name })}
@@ -831,7 +847,7 @@ export function WorkloadView({
831
847
 
832
848
  // ── Expanded (full) mode ─────────────────────────────────────────────────
833
849
  return (
834
- <OperationalIssuesShownContext.Provider value={!!hasOperationalIssues}>
850
+ <OperationalIssuesShownContext.Provider value={!!hasOperationalIssues || !!operationalIssuesPending}>
835
851
  <DetailShell
836
852
  breadcrumb={breadcrumb}
837
853
  nav={
@@ -1038,6 +1054,11 @@ export function WorkloadView({
1038
1054
  {renderMetricsTab({ kind: resource?.kind || kind, namespace, name })}
1039
1055
  </div>
1040
1056
  )}
1057
+ {effectiveTab === 'cost' && renderCostTab && (
1058
+ <div className="h-full overflow-auto p-4">
1059
+ {renderCostTab({ kind: resource?.kind || kind, namespace, name })}
1060
+ </div>
1061
+ )}
1041
1062
  {effectiveTab === 'yaml' && (
1042
1063
  <div className="flex h-full min-h-0">
1043
1064
  {renderRelatedYaml && yamlObjects.length > 1 && (
@@ -20,6 +20,7 @@
20
20
  --color-theme-text-secondary: var(--text-secondary);
21
21
  --color-theme-text-tertiary: var(--text-tertiary);
22
22
  --color-theme-text-disabled: var(--text-disabled);
23
+ --color-warning-text: var(--warning-text);
23
24
 
24
25
  /* Borders */
25
26
  --color-theme-border: var(--border-default);
@@ -49,6 +49,7 @@
49
49
  --text-secondary: light-dark(#555860, #D8DAE0);
50
50
  --text-tertiary: light-dark(#6B7080, #B0B5C0);
51
51
  --text-disabled: light-dark(#A0A4AC, #6A7080);
52
+ --warning-text: light-dark(#92400E, #FBBF24);
52
53
 
53
54
  /* ── Borders (lightly blue-tinted in light mode) ── */
54
55
  --border-default: light-dark(#D8E0EE, #222840);
@@ -32,7 +32,13 @@ export type GitOpsCategory =
32
32
  | 'Suspended'
33
33
  | 'Unknown'
34
34
 
35
- export type GitOpsDriftSource = 'lastAppliedAnnotation'
35
+ // How a Drift's field entries were computed. `lastAppliedAnnotation` is the
36
+ // built-in path (kubectl.kubernetes.io/last-applied-configuration); `argocd-api`
37
+ // means the diff came from a connected Argo CD server's managed-resource diff.
38
+ // The `(string & {})` arm keeps the union open: a library consumer may lag the
39
+ // backend and receive a source it doesn't recognize — such values MUST render
40
+ // with the neutral fallback (no source label), never crash a switch.
41
+ export type GitOpsDriftSource = 'lastAppliedAnnotation' | 'argocd-api' | (string & {})
36
42
 
37
43
  export interface GitOpsInsightSummary {
38
44
  tool: GitOpsTool
@@ -66,6 +72,21 @@ export interface GitOpsInsightSummary {
66
72
  // Finalizers blocking deletion. When stuck, naming the finalizer points
67
73
  // the user at the controller they need to investigate.
68
74
  finalizers?: string[]
75
+ // Argo comparison-coverage disclosure: the field exclusions declared in
76
+ // spec.ignoreDifferences that suppress drift from comparison (both Argo's
77
+ // and Radar's). Undefined for Flux roots and Applications without any
78
+ // exclusions. unsupportedRuleCount counts entries Radar does NOT evaluate
79
+ // (jqPathExpressions / managedFieldsManagers rules) — the
80
+ // drift panel may surface fields Argo's own UI suppresses.
81
+ ignoredDifferences?: GitOpsIgnoredDifferences
82
+ }
83
+
84
+ export interface GitOpsIgnoredDifferences {
85
+ ruleCount: number
86
+ unsupportedRuleCount: number
87
+ // Sorted unique "Group/Kind" targets ("Kind" for core resources,
88
+ // "group/*" for a group-wide rule that omits kind).
89
+ kinds: string[]
69
90
  }
70
91
 
71
92
  export interface GitOpsInsightRef {
@@ -192,6 +213,53 @@ export interface GitOpsCapabilities {
192
213
  syncWithSource: boolean
193
214
  selectiveSync: boolean
194
215
  rollback: boolean
216
+ // True on an Argo CD Application detail when the Argo CD integration is
217
+ // connected, meaning the full Git-rendered desired-vs-live diff endpoint
218
+ // (/api/argo/applications/{ns}/{name}/resource-diff) can be called for this
219
+ // app's managed resources. Absent/false → offer only the last-applied field
220
+ // diff and (for disconnected Argo apps) the "connect Argo CD" hint.
221
+ argoDiffAvailable?: boolean
222
+ // True when the Argo CD integration has settings saved, even if the live
223
+ // connection is down / the token is rejected. Paired with argoDiffAvailable to
224
+ // tell "not set up" (offer Connect) from "set up but disconnected" (offer
225
+ // Reconnect) — instead of advertising a diff that would fail.
226
+ argoConfigured?: boolean
227
+ // True on an Argo CD Application detail when the integration is connected,
228
+ // meaning the revision-metadata endpoint can resolve Git commit details
229
+ // (author, message, signature) for this app's deployed revisions.
230
+ revisionMetadataAvailable?: boolean
195
231
  unsupportedReason?: string
196
232
  warnings?: string[]
197
233
  }
234
+
235
+ // Response of GET /api/argo/applications/{ns}/{name}/revision-metadata — the Git
236
+ // commit metadata for one deployed revision. Every field is best-effort (varies
237
+ // across Argo CD versions); signatureInfo non-empty means a signature was checked.
238
+ export interface ArgoRevisionMetadata {
239
+ author?: string
240
+ date?: string
241
+ tags?: string[]
242
+ message?: string
243
+ signatureInfo?: string
244
+ }
245
+
246
+ // Response of GET /api/argo/applications/{ns}/{name}/resource-diff. `desired`
247
+ // is the Git-rendered manifest, `live` the normalized cluster state — both YAML
248
+ // for the line-by-line view. `fieldEntries` is the same diff structured per
249
+ // path for a compact summary. `redacted` is set when Secret values were masked
250
+ // server-side; `hook` marks Argo sync-hook resources.
251
+ export interface GitOpsResourceDiff {
252
+ source: string
253
+ desired: string
254
+ live: string
255
+ fieldEntries: GitOpsResourceDiffFieldEntry[]
256
+ redacted: boolean
257
+ hook: boolean
258
+ }
259
+
260
+ export interface GitOpsResourceDiffFieldEntry {
261
+ path: string
262
+ op: 'added' | 'removed' | 'changed'
263
+ desired?: string
264
+ live?: string
265
+ }
@@ -254,7 +254,7 @@ export interface ArgoAppStatus {
254
254
  * ArgoCD status patterns:
255
255
  * - sync.status: Synced, OutOfSync, Unknown
256
256
  * - health.status: Healthy, Progressing, Degraded, Suspended, Missing, Unknown
257
- * - operationState.phase: Running, Succeeded, Failed, Error
257
+ * - operationState.phase: Running, Terminating, Succeeded, Failed, Error
258
258
  */
259
259
  export function argoStatusToGitOpsStatus(status: ArgoAppStatus): GitOpsStatus {
260
260
  const syncStatus = status.sync?.status
@@ -267,7 +267,7 @@ export function argoStatusToGitOpsStatus(status: ArgoAppStatus): GitOpsStatus {
267
267
  sync = 'Synced'
268
268
  } else if (syncStatus === 'OutOfSync') {
269
269
  sync = 'OutOfSync'
270
- } else if (opPhase === 'Running') {
270
+ } else if (opPhase === 'Running' || opPhase === 'Terminating') {
271
271
  sync = 'Reconciling'
272
272
  }
273
273
 
@@ -288,7 +288,7 @@ export function argoStatusToGitOpsStatus(status: ArgoAppStatus): GitOpsStatus {
288
288
  }
289
289
 
290
290
  // Override with operation state if active
291
- if (opPhase === 'Running') {
291
+ if (opPhase === 'Running' || opPhase === 'Terminating') {
292
292
  health = 'Progressing'
293
293
  } else if (opPhase === 'Failed' || opPhase === 'Error') {
294
294
  health = 'Degraded'
@@ -1,5 +1,40 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { categorizeResources, formatGroupName, shortenGroupName } from './api-resources'
2
+ import { categorizeResources, findAPIResourceForRoute, formatGroupName, shortenGroupName } from './api-resources'
3
+
4
+ describe('findAPIResourceForRoute', () => {
5
+ const resources = [
6
+ { group: 'metrics.k8s.io', version: 'v1beta1', kind: 'PodMetrics', name: 'pods', namespaced: true, isCrd: false, verbs: ['get'] },
7
+ ]
8
+
9
+ it('prefers the canonical core resource when a discovered API has the same plural name', () => {
10
+ expect(findAPIResourceForRoute(resources, 'pods')?.kind).toBe('Pod')
11
+ })
12
+
13
+ it('respects an explicit API group for a colliding resource', () => {
14
+ expect(findAPIResourceForRoute(resources, 'pods', 'metrics.k8s.io')?.kind).toBe('PodMetrics')
15
+ })
16
+
17
+ it('resolves a discovered resource without a core collision', () => {
18
+ const crd = { group: 'example.io', version: 'v1', kind: 'Widget', name: 'widgets', namespaced: true, isCrd: true, verbs: ['list'] }
19
+ expect(findAPIResourceForRoute([crd], 'widgets')).toBe(crd)
20
+ })
21
+
22
+ it('resolves core resources before discovery loads', () => {
23
+ expect(findAPIResourceForRoute(undefined, 'pods')?.kind).toBe('Pod')
24
+ })
25
+
26
+ it('falls back to an exact built-in API group before discovery loads', () => {
27
+ expect(findAPIResourceForRoute(undefined, 'storageclasses', 'storage.k8s.io')).toMatchObject({
28
+ kind: 'StorageClass',
29
+ namespaced: false,
30
+ })
31
+ })
32
+
33
+ it('supports the legacy Kind route form without weakening group matching', () => {
34
+ expect(findAPIResourceForRoute(undefined, 'Pod')?.name).toBe('pods')
35
+ expect(findAPIResourceForRoute(undefined, 'Pod', 'metrics.k8s.io')).toBeUndefined()
36
+ })
37
+ })
3
38
 
4
39
  describe('formatGroupName', () => {
5
40
  it('uses friendly names for common CRD groups seen in clusters', () => {
@@ -47,9 +47,25 @@ export const CORE_RESOURCES: APIResource[] = [
47
47
  { group: 'scheduling.k8s.io', version: 'v1', kind: 'PriorityClass', name: 'priorityclasses', namespaced: false, isCrd: false, verbs: ['list', 'get', 'watch'] },
48
48
  { group: 'node.k8s.io', version: 'v1', kind: 'RuntimeClass', name: 'runtimeclasses', namespaced: false, isCrd: false, verbs: ['list', 'get', 'watch'] },
49
49
  { group: 'coordination.k8s.io', version: 'v1', kind: 'Lease', name: 'leases', namespaced: true, isCrd: false, verbs: ['list', 'get', 'watch'] },
50
+ { group: 'storage.k8s.io', version: 'v1', kind: 'StorageClass', name: 'storageclasses', namespaced: false, isCrd: false, verbs: ['list', 'get', 'watch'] },
50
51
  { group: 'storage.k8s.io', version: 'v1', kind: 'VolumeAttachment', name: 'volumeattachments', namespaced: false, isCrd: false, verbs: ['list', 'get', 'watch'] },
51
52
  ]
52
53
 
54
+ export function findAPIResourceForRoute(
55
+ resources: APIResource[] | undefined,
56
+ routeSlug: string,
57
+ group = '',
58
+ ): APIResource | undefined {
59
+ const matchesRoute = (resource: APIResource) =>
60
+ resource.name === routeSlug || resource.kind === routeSlug
61
+ if (group) {
62
+ return resources?.find(r => matchesRoute(r) && r.group === group)
63
+ ?? CORE_RESOURCES.find(r => matchesRoute(r) && r.group === group)
64
+ }
65
+ return CORE_RESOURCES.find(matchesRoute)
66
+ ?? resources?.find(matchesRoute)
67
+ }
68
+
53
69
  // Resources that should be hidden from the sidebar
54
70
  const HIDDEN_KINDS = ['PodMetrics', 'NodeMetrics']
55
71