@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.
- package/package.json +4 -4
- package/src/components/applications/ApplicationDetail.test.tsx +136 -8
- package/src/components/applications/ApplicationDetail.tsx +1215 -487
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
- package/src/components/gitops/GitOpsTableView.tsx +10 -3
- package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +189 -45
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +201 -21
- package/src/components/gitops/insights/index.ts +1 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +16 -0
- package/src/components/gitops/insights/insights-helpers.ts +5 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -0
- package/src/components/issues/IssuesView.tsx +55 -29
- package/src/components/issues/ResourceIssuesSection.test.tsx +33 -1
- package/src/components/issues/ResourceIssuesSection.tsx +21 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.tsx +109 -19
- package/src/components/resources/column-filter-serialization.test.ts +79 -1
- 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/CompositeRenderer.tsx +3 -2
- 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/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/SealedSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
- 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.ts +94 -25
- package/src/components/shared/ResourceActionsBar.tsx +54 -22
- package/src/components/shared/UnifiedDiff.tsx +51 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +7 -3
- package/src/components/timeline/TimelineToolbarState.test.tsx +24 -0
- package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
- package/src/components/topology/TopologyControls.tsx +3 -1
- package/src/components/topology/TopologyGraph.tsx +76 -90
- package/src/components/topology/TopologyOverlayBar.tsx +30 -0
- package/src/components/topology/TopologySearch.tsx +23 -15
- package/src/components/topology/index.ts +2 -0
- package/src/components/ui/Badge.tsx +1 -1
- package/src/components/ui/SelectMenu.tsx +81 -0
- package/src/components/ui/Tooltip.test.tsx +21 -0
- package/src/components/ui/Tooltip.tsx +9 -12
- package/src/components/ui/drawer-components.tsx +9 -6
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +24 -3
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +1 -0
- package/src/types/gitops-insights.ts +69 -1
- package/src/types/gitops.ts +3 -3
- package/src/utils/api-resources.test.ts +36 -1
- package/src/utils/api-resources.ts +16 -0
- package/src/utils/application-history.test.ts +538 -0
- package/src/utils/application-history.ts +257 -0
- package/src/utils/applications.test.ts +34 -0
- package/src/utils/applications.ts +25 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/resource-hierarchy.test.ts +35 -1
- 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);
|
package/src/theme/variables.css
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/types/gitops.ts
CHANGED
|
@@ -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
|
|