@skyhook-io/radar-app 0.2.2 → 0.3.1
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/README.md +7 -1
- package/package.json +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- 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/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- 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 +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -14,25 +14,35 @@ interface ResourceDetailDrawerProps {
|
|
|
14
14
|
expanded?: boolean
|
|
15
15
|
/** Called when user clicks collapse in expanded mode */
|
|
16
16
|
onCollapse?: () => void
|
|
17
|
-
/** Called when user clicks expand button */
|
|
18
|
-
onExpand?: (resource: SelectedResource) => void
|
|
17
|
+
/** Called when user clicks expand button (opts.yaml = expanding from YAML view) */
|
|
18
|
+
onExpand?: (resource: SelectedResource, opts?: { yaml?: boolean }) => void
|
|
19
|
+
/** Hide the collapse-to-drawer control (mobile: no drawer to collapse to). Default true. */
|
|
20
|
+
canCollapseToDrawer?: boolean
|
|
19
21
|
/** Navigate to another resource within expanded WorkloadView */
|
|
20
22
|
onNavigateToResource?: (resource: SelectedResource) => void
|
|
23
|
+
/** Top offset for the drawer (px). Defaults to Radar's 49px header height;
|
|
24
|
+
* pass 0 in chromeless embeds where there's no Radar header above it. */
|
|
25
|
+
headerHeight?: number
|
|
26
|
+
/** Right inset in px so the drawer sits beside a docked side panel (AI), not under it. */
|
|
27
|
+
rightInset?: number
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
export function ResourceDetailDrawer(props: ResourceDetailDrawerProps) {
|
|
24
31
|
return (
|
|
25
32
|
<BaseResourceDetailDrawer {...props}>
|
|
26
|
-
{({ resource, expanded, initialTab, onClose, onExpand, onBack, onNavigateToResource, onCollapseToDrawer }) => (
|
|
33
|
+
{({ resource, expanded, active, initialTab, onClose, onExpand, onExpandIntent, onCancelExpandIntent, onBack, onNavigateToResource, onCollapseToDrawer }) => (
|
|
27
34
|
<WorkloadView
|
|
28
35
|
kind={resource.kind}
|
|
29
36
|
namespace={resource.namespace}
|
|
30
37
|
name={resource.name}
|
|
31
38
|
group={resource.group}
|
|
32
39
|
expanded={expanded}
|
|
40
|
+
active={active}
|
|
33
41
|
initialTab={initialTab}
|
|
34
42
|
onClose={onClose}
|
|
35
43
|
onExpand={onExpand}
|
|
44
|
+
onExpandIntent={onExpandIntent}
|
|
45
|
+
onCancelExpandIntent={onCancelExpandIntent}
|
|
36
46
|
onBack={onBack ?? (() => {})}
|
|
37
47
|
onNavigateToResource={onNavigateToResource}
|
|
38
48
|
onCollapseToDrawer={onCollapseToDrawer}
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import { useState, useMemo, useCallback, useEffect } from 'react'
|
|
2
2
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { useQuery } from '@tanstack/react-query'
|
|
4
|
-
import { ApiError, fetchJSON, isForbiddenError, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics } from '../../api/client'
|
|
4
|
+
import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, useCapabilities, useNamespaceCapabilities, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
|
|
5
|
+
import { isBadgeWorthy } from '../../utils/auditBadges'
|
|
6
|
+
import type { AuditBadgeMessage } from '@skyhook-io/k8s-ui'
|
|
5
7
|
import { apiUrl, getAuthHeaders, getCredentialsMode, getBasename } from '../../api/config'
|
|
6
8
|
import { useAPIResources } from '../../api/apiResources'
|
|
9
|
+
import { useConnection } from '../../context/ConnectionContext'
|
|
7
10
|
import { initNavigationMap } from '@skyhook-io/k8s-ui'
|
|
8
11
|
import { usePinnedKinds } from '../../hooks/useFavorites'
|
|
9
12
|
import { useOpenLogs, useOpenWorkloadLogs } from '../dock'
|
|
10
13
|
import {
|
|
14
|
+
canBulkRestartKind,
|
|
15
|
+
canBulkScaleKind,
|
|
11
16
|
ResourcesView as BaseResourcesView,
|
|
12
17
|
CORE_RESOURCES,
|
|
18
|
+
intersectWorkloadWrites,
|
|
13
19
|
} from '@skyhook-io/k8s-ui'
|
|
14
|
-
import type { ResourceQueryResult } from '@skyhook-io/k8s-ui'
|
|
20
|
+
import type { Capabilities, ResourceQueryResult, WorkloadWritePermissions } from '@skyhook-io/k8s-ui'
|
|
15
21
|
import type { SelectedResource } from '../../types'
|
|
16
|
-
import type
|
|
22
|
+
import { kindToPlural, type NavigateToResource } from '../../utils/navigation'
|
|
17
23
|
import { CreateResourceDialog } from '../shared/CreateResourceDialog'
|
|
18
24
|
import { getSkeletonYaml } from '../../utils/skeleton-yaml'
|
|
19
25
|
|
|
20
26
|
interface ResourceCountsResponse {
|
|
21
27
|
counts: Record<string, number>
|
|
22
28
|
forbidden?: string[]
|
|
29
|
+
reasons?: Record<string, string>
|
|
30
|
+
unavailable?: string[]
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
interface ResourcesViewProps {
|
|
@@ -28,11 +36,65 @@ interface ResourcesViewProps {
|
|
|
28
36
|
onResourceClick?: (resource: SelectedResource | null) => void
|
|
29
37
|
onResourceClickYaml?: NavigateToResource
|
|
30
38
|
onKindChange?: () => void
|
|
39
|
+
onClearNamespaces?: () => void
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
type SelectedKindInfo = { name: string; kind: string; group: string } | null
|
|
43
|
+
|
|
44
|
+
const EMPTY_RESOURCE_COUNTS: Record<string, number> = {}
|
|
45
|
+
const LARGE_RESOURCE_LIST_LIMIT = 25000
|
|
46
|
+
const LARGE_RESOURCE_LIST_GUARD_KEYS = new Set([
|
|
47
|
+
'Pod',
|
|
48
|
+
'Event',
|
|
49
|
+
'apps/ReplicaSet',
|
|
50
|
+
'discovery.k8s.io/EndpointSlice',
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
const deniedWorkloadWrites: WorkloadWritePermissions = {
|
|
54
|
+
deployments: false,
|
|
55
|
+
daemonSets: false,
|
|
56
|
+
statefulSets: false,
|
|
57
|
+
rollouts: false,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resourceCountKey(kind: NonNullable<SelectedKindInfo>): string {
|
|
61
|
+
return kind.group ? `${kind.group}/${kind.kind}` : kind.kind
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasResourceCount(counts: Record<string, number> | undefined, key: string): boolean {
|
|
65
|
+
return Object.prototype.hasOwnProperty.call(counts ?? {}, key)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function ResourcesView({ namespaces, selectedResource, onResourceClick, onResourceClickYaml, onKindChange, onClearNamespaces }: ResourcesViewProps) {
|
|
34
69
|
const location = useLocation()
|
|
35
70
|
const navigate = useNavigate()
|
|
71
|
+
const { connection } = useConnection()
|
|
72
|
+
|
|
73
|
+
const { data: capabilities } = useCapabilities()
|
|
74
|
+
const namespaceForCapabilities = namespaces.length === 1 ? namespaces[0] : undefined
|
|
75
|
+
const { data: namespaceCapabilities } = useNamespaceCapabilities(namespaceForCapabilities, capabilities)
|
|
76
|
+
const namespaceCapabilityNames = useMemo(() => namespaces.length > 1 ? [...namespaces].sort() : [], [namespaces])
|
|
77
|
+
const { data: namespaceCapabilitiesList } = useQuery<Array<Pick<Capabilities, 'workloadWrites'>>>({
|
|
78
|
+
queryKey: ['capabilities', 'namespaces', namespaceCapabilityNames],
|
|
79
|
+
queryFn: async () => {
|
|
80
|
+
const results = await Promise.allSettled(
|
|
81
|
+
namespaceCapabilityNames.map(async ns => ({
|
|
82
|
+
namespace: ns,
|
|
83
|
+
capabilities: await fetchJSON<Capabilities>(`/capabilities?namespace=${encodeURIComponent(ns)}`),
|
|
84
|
+
}))
|
|
85
|
+
)
|
|
86
|
+
return results.map((result, index) => {
|
|
87
|
+
if (result.status === 'fulfilled') {
|
|
88
|
+
return { workloadWrites: result.value.capabilities.workloadWrites }
|
|
89
|
+
}
|
|
90
|
+
console.warn(`Failed to fetch namespace capabilities for ${namespaceCapabilityNames[index]}, withholding workload writes:`, result.reason)
|
|
91
|
+
return { workloadWrites: deniedWorkloadWrites }
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
enabled: namespaceCapabilityNames.length > 1 && capabilities != null,
|
|
95
|
+
staleTime: 60000,
|
|
96
|
+
})
|
|
97
|
+
const multiNamespaceWorkloadWrites = useMemo(() => intersectWorkloadWrites(namespaceCapabilitiesList), [namespaceCapabilitiesList])
|
|
36
98
|
|
|
37
99
|
// API resources discovery
|
|
38
100
|
const { data: apiResources } = useAPIResources()
|
|
@@ -43,16 +105,33 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
43
105
|
}, [apiResources])
|
|
44
106
|
|
|
45
107
|
// Track the selected kind from the k8s-ui component
|
|
46
|
-
const [selectedKind, setSelectedKind] = useState<
|
|
108
|
+
const [selectedKind, setSelectedKind] = useState<SelectedKindInfo>(null)
|
|
109
|
+
const workloadWrites = namespaces.length === 0
|
|
110
|
+
? capabilities?.workloadWrites
|
|
111
|
+
: namespaces.length === 1
|
|
112
|
+
? namespaceCapabilities?.workloadWrites
|
|
113
|
+
: multiNamespaceWorkloadWrites
|
|
114
|
+
const canBulkRestartSelectedKind = useMemo(() => canBulkRestartKind(selectedKind, workloadWrites), [selectedKind, workloadWrites])
|
|
115
|
+
const canBulkScaleSelectedKind = useMemo(() => canBulkScaleKind(selectedKind, workloadWrites), [selectedKind, workloadWrites])
|
|
47
116
|
|
|
48
117
|
// Lightweight resource counts for sidebar badges (~2KB instead of ~608MB)
|
|
49
118
|
const namespacesParam = namespaces.join(',')
|
|
50
|
-
const { data: countsData } = useQuery({
|
|
119
|
+
const { data: countsData, isError: countsIsError } = useQuery({
|
|
51
120
|
queryKey: ['resource-counts', namespacesParam],
|
|
52
121
|
queryFn: async () => {
|
|
53
122
|
const params = new URLSearchParams()
|
|
54
123
|
if (namespaces.length > 0) params.set('namespaces', namespacesParam)
|
|
55
|
-
|
|
124
|
+
const startedAt = performance.now()
|
|
125
|
+
debugNamespaceLog('resources:counts-fetch-start', { namespaces, params: params.toString() })
|
|
126
|
+
try {
|
|
127
|
+
return await fetchJSON<ResourceCountsResponse>(`/resource-counts?${params}`)
|
|
128
|
+
} finally {
|
|
129
|
+
debugNamespaceLog('resources:counts-fetch-end', {
|
|
130
|
+
namespaces,
|
|
131
|
+
params: params.toString(),
|
|
132
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
133
|
+
})
|
|
134
|
+
}
|
|
56
135
|
},
|
|
57
136
|
staleTime: 10000,
|
|
58
137
|
refetchInterval: 60000, // Safety net — SSE k8s_event drives near-real-time invalidation
|
|
@@ -67,6 +146,69 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
67
146
|
return match?.isCrd ?? (!!selectedKind.group) // default: has group = likely CRD
|
|
68
147
|
}, [selectedKind, apiResources])
|
|
69
148
|
|
|
149
|
+
// The canonical Kind for the selected resource. selectedKind.kind is the plural
|
|
150
|
+
// URL segment for CRDs/grouped kinds (e.g. "ingressroutes", "ingresses") — only
|
|
151
|
+
// core no-group kinds resolve to the real Kind there — so resolve it via
|
|
152
|
+
// discovery to match audit findings, which key by the real Kind ("IngressRoute").
|
|
153
|
+
const selectedKindCanonical = useMemo(() => {
|
|
154
|
+
if (!selectedKind) return undefined
|
|
155
|
+
const match = apiResources?.find(r => r.name === selectedKind.name && r.group === selectedKind.group)
|
|
156
|
+
?? CORE_RESOURCES.find(r => r.name === selectedKind.name && r.group === selectedKind.group)
|
|
157
|
+
return match?.kind ?? selectedKind.kind
|
|
158
|
+
}, [selectedKind, apiResources])
|
|
159
|
+
|
|
160
|
+
// Cluster Audit findings for the selected kind, keyed by "namespace/name" for
|
|
161
|
+
// the resource list. The list shows ONE kind at a time, so ns/name is enough;
|
|
162
|
+
// we still match the finding's group (built-ins → real group, CRDs → "") so a
|
|
163
|
+
// kind shared across groups doesn't bleed findings across the two lists. Only
|
|
164
|
+
// "badge-worthy" findings count (reference-integrity / lifecycle) — posture
|
|
165
|
+
// and best-practice nags fire near-universally and would just be noise.
|
|
166
|
+
const audit = useAudit(namespaces)
|
|
167
|
+
const auditBadges = useMemo(() => {
|
|
168
|
+
if (!selectedKind || !audit.data?.findings) return undefined
|
|
169
|
+
const wantGroup = isSelectedCrd ? '' : selectedKind.group
|
|
170
|
+
const map: Record<string, { danger: number; warning: number; messages: AuditBadgeMessage[] }> = {}
|
|
171
|
+
for (const f of audit.data.findings) {
|
|
172
|
+
if (f.kind !== selectedKindCanonical || (f.group ?? '') !== wantGroup) continue
|
|
173
|
+
if (!isBadgeWorthy(f, audit.data.checks)) continue
|
|
174
|
+
const k = `${f.namespace || ''}/${f.name}`
|
|
175
|
+
const cur = map[k] ?? { danger: 0, warning: 0, messages: [] }
|
|
176
|
+
if (f.severity === 'danger') cur.danger++
|
|
177
|
+
else if (f.severity === 'warning') cur.warning++
|
|
178
|
+
cur.messages.push({ severity: f.severity, message: f.message })
|
|
179
|
+
map[k] = cur
|
|
180
|
+
}
|
|
181
|
+
for (const cur of Object.values(map)) {
|
|
182
|
+
cur.messages.sort((a, b) => (a.severity === 'danger' ? 0 : 1) - (b.severity === 'danger' ? 0 : 1))
|
|
183
|
+
}
|
|
184
|
+
return map
|
|
185
|
+
}, [audit.data?.findings, audit.data?.checks, selectedKind, selectedKindCanonical, isSelectedCrd])
|
|
186
|
+
|
|
187
|
+
const selectedCountKey = selectedKind ? resourceCountKey(selectedKind) : ''
|
|
188
|
+
const selectedCount = selectedCountKey ? countsData?.counts[selectedCountKey] : undefined
|
|
189
|
+
const selectedCountKnown = selectedCountKey ? hasResourceCount(countsData?.counts, selectedCountKey) : false
|
|
190
|
+
const selectedCountUnavailable = selectedCountKey ? countsData?.unavailable?.includes(selectedCountKey) ?? false : false
|
|
191
|
+
const isSelectedKindGuarded = selectedCountKey !== '' && LARGE_RESOURCE_LIST_GUARD_KEYS.has(selectedCountKey)
|
|
192
|
+
const waitingForGuardCount = isSelectedKindGuarded && !countsData && !countsIsError
|
|
193
|
+
const largeListBlocked = isSelectedKindGuarded && countsData != null && (selectedCountUnavailable || (selectedCountKnown && (selectedCount ?? 0) > LARGE_RESOURCE_LIST_LIMIT))
|
|
194
|
+
const selectedKindQueryBlocked = waitingForGuardCount || largeListBlocked
|
|
195
|
+
const podCount = countsData?.counts.Pod
|
|
196
|
+
const podCountKnown = hasResourceCount(countsData?.counts, 'Pod')
|
|
197
|
+
const podCountUnavailable = countsData?.unavailable?.includes('Pod') ?? false
|
|
198
|
+
const podCountAllowsBulkMetrics = countsData != null && podCountKnown && !podCountUnavailable && (podCount ?? 0) <= LARGE_RESOURCE_LIST_LIMIT
|
|
199
|
+
const selectedKindName = selectedKind?.name.toLowerCase() ?? ''
|
|
200
|
+
const topPodMetricsEnabled = selectedKindName === 'pods' && podCountAllowsBulkMetrics
|
|
201
|
+
const topNodeMetricsEnabled = selectedKindName === 'nodes' && namespaces.length === 0 && podCountAllowsBulkMetrics
|
|
202
|
+
const largeListGuard = selectedKind && largeListBlocked
|
|
203
|
+
? {
|
|
204
|
+
kind: selectedKind.name,
|
|
205
|
+
count: selectedCountUnavailable ? undefined : selectedCount,
|
|
206
|
+
reason: selectedCountUnavailable ? 'count-unavailable' as const : 'too-many' as const,
|
|
207
|
+
limit: LARGE_RESOURCE_LIST_LIMIT,
|
|
208
|
+
namespaces,
|
|
209
|
+
}
|
|
210
|
+
: null
|
|
211
|
+
|
|
70
212
|
// Fetch full data only for the selected kind
|
|
71
213
|
const selectedKindQuery = useQuery({
|
|
72
214
|
queryKey: ['resources', selectedKind?.name, isSelectedCrd ? selectedKind?.group : '', namespaces],
|
|
@@ -75,17 +217,32 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
75
217
|
const params = new URLSearchParams()
|
|
76
218
|
if (namespaces.length > 0) params.set('namespaces', namespacesParam)
|
|
77
219
|
if (isSelectedCrd && selectedKind.group) params.set('group', selectedKind.group)
|
|
220
|
+
const startedAt = performance.now()
|
|
221
|
+
debugNamespaceLog('resources:selected-kind-fetch-start', {
|
|
222
|
+
kind: selectedKind.name,
|
|
223
|
+
group: isSelectedCrd ? selectedKind.group : '',
|
|
224
|
+
namespaces,
|
|
225
|
+
params: params.toString(),
|
|
226
|
+
})
|
|
78
227
|
const res = await fetch(apiUrl(`/resources/${selectedKind.name}?${params}`), {
|
|
79
228
|
credentials: getCredentialsMode(),
|
|
80
229
|
headers: getAuthHeaders(),
|
|
81
230
|
})
|
|
231
|
+
debugNamespaceLog('resources:selected-kind-fetch-response', {
|
|
232
|
+
kind: selectedKind.name,
|
|
233
|
+
group: isSelectedCrd ? selectedKind.group : '',
|
|
234
|
+
namespaces,
|
|
235
|
+
params: params.toString(),
|
|
236
|
+
status: res.status,
|
|
237
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
238
|
+
})
|
|
82
239
|
if (!res.ok) {
|
|
83
240
|
const errorData = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
84
241
|
throw new ApiError(errorData.error || `Failed to fetch ${selectedKind.name}`, res.status, errorData)
|
|
85
242
|
}
|
|
86
243
|
return res.json()
|
|
87
244
|
},
|
|
88
|
-
enabled: !!selectedKind,
|
|
245
|
+
enabled: !!selectedKind && !selectedKindQueryBlocked,
|
|
89
246
|
staleTime: 30000,
|
|
90
247
|
refetchInterval: 120000, // Safety net — SSE k8s_event drives near-real-time invalidation
|
|
91
248
|
retry: (failureCount: number, error: Error) => {
|
|
@@ -98,17 +255,19 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
98
255
|
const selectedKindQueryResult: ResourceQueryResult | undefined = useMemo(() => {
|
|
99
256
|
if (!selectedKind) return undefined
|
|
100
257
|
return {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
258
|
+
resourceName: selectedKind.name,
|
|
259
|
+
group: selectedKind.group,
|
|
260
|
+
data: selectedKindQueryBlocked ? [] : selectedKindQuery.data as any[] | undefined,
|
|
261
|
+
isLoading: waitingForGuardCount || selectedKindQuery.isLoading,
|
|
262
|
+
error: selectedKindQueryBlocked ? undefined : selectedKindQuery.error,
|
|
104
263
|
refetch: selectedKindQuery.refetch,
|
|
105
264
|
dataUpdatedAt: selectedKindQuery.dataUpdatedAt,
|
|
106
265
|
}
|
|
107
|
-
}, [selectedKind, selectedKindQuery.data, selectedKindQuery.isLoading, selectedKindQuery.error, selectedKindQuery.refetch, selectedKindQuery.dataUpdatedAt])
|
|
266
|
+
}, [selectedKind, selectedKindQueryBlocked, waitingForGuardCount, selectedKindQuery.data, selectedKindQuery.isLoading, selectedKindQuery.error, selectedKindQuery.refetch, selectedKindQuery.dataUpdatedAt])
|
|
108
267
|
|
|
109
268
|
// Metrics
|
|
110
|
-
const { data: topPodMetrics } = useTopPodMetrics()
|
|
111
|
-
const { data: topNodeMetrics } = useTopNodeMetrics()
|
|
269
|
+
const { data: topPodMetrics } = useTopPodMetrics({ enabled: topPodMetricsEnabled, namespaces })
|
|
270
|
+
const { data: topNodeMetrics } = useTopNodeMetrics({ enabled: topNodeMetricsEnabled })
|
|
112
271
|
|
|
113
272
|
// Certificate expiry
|
|
114
273
|
const { data: certExpiry, isError: certExpiryError } = useSecretCertExpiry()
|
|
@@ -120,6 +279,11 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
120
279
|
const openLogs = useOpenLogs()
|
|
121
280
|
const openWorkloadLogs = useOpenWorkloadLogs()
|
|
122
281
|
|
|
282
|
+
// Bulk delete
|
|
283
|
+
const bulkDeleteMutation = useBulkDeleteResources()
|
|
284
|
+
const bulkRestartMutation = useBulkRestartWorkloads()
|
|
285
|
+
const bulkScaleMutation = useBulkScaleWorkloads()
|
|
286
|
+
|
|
123
287
|
// Navigation adapter. k8s-ui constructs paths from `basePath` (which
|
|
124
288
|
// includes the router basename so they line up with window.location.pathname
|
|
125
289
|
// for path-equality checks) and from `window.location.pathname` directly.
|
|
@@ -160,23 +324,29 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
160
324
|
return (
|
|
161
325
|
<>
|
|
162
326
|
<BaseResourcesView
|
|
163
|
-
key={
|
|
327
|
+
key={connection.context || 'default'}
|
|
164
328
|
namespaces={namespaces}
|
|
165
329
|
selectedResource={selectedResource}
|
|
166
330
|
onResourceClick={onResourceClick}
|
|
167
331
|
onResourceClickYaml={onResourceClickYaml}
|
|
168
332
|
onKindChange={onKindChange}
|
|
333
|
+
onClearNamespaces={onClearNamespaces}
|
|
169
334
|
// Injected data
|
|
170
335
|
apiResources={apiResources}
|
|
171
336
|
// Lightweight counts for sidebar (replaces 233 parallel queries)
|
|
172
|
-
resourceCounts={countsData?.counts}
|
|
337
|
+
resourceCounts={countsData?.counts ?? EMPTY_RESOURCE_COUNTS}
|
|
173
338
|
resourceForbidden={countsData?.forbidden}
|
|
339
|
+
resourceReasons={countsData?.reasons}
|
|
340
|
+
resourceUnavailable={countsData?.unavailable}
|
|
174
341
|
selectedKindQuery={selectedKindQueryResult}
|
|
342
|
+
connectionState={connection.state}
|
|
343
|
+
largeListGuard={largeListGuard}
|
|
175
344
|
onSelectedKindChange={setSelectedKind}
|
|
176
345
|
topPodMetrics={topPodMetrics}
|
|
177
346
|
topNodeMetrics={topNodeMetrics}
|
|
178
347
|
certExpiry={certExpiry}
|
|
179
348
|
certExpiryError={certExpiryError}
|
|
349
|
+
auditBadges={auditBadges}
|
|
180
350
|
// Pinned kinds
|
|
181
351
|
pinned={pinned}
|
|
182
352
|
togglePin={togglePin}
|
|
@@ -197,6 +367,13 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
197
367
|
onOpenWorkloadLogs={openWorkloadLogs}
|
|
198
368
|
// Create resource
|
|
199
369
|
onCreateResource={handleCreateResource}
|
|
370
|
+
// Bulk operations
|
|
371
|
+
onBulkDelete={(items, options) => bulkDeleteMutation.mutate({ items, force: options?.force }, { onSuccess: options?.onSuccess })}
|
|
372
|
+
isBulkDeleting={bulkDeleteMutation.isPending}
|
|
373
|
+
onBulkRestart={canBulkRestartSelectedKind ? (items, options) => bulkRestartMutation.mutate({ items }, { onSuccess: options?.onSuccess }) : undefined}
|
|
374
|
+
isBulkRestarting={canBulkRestartSelectedKind && bulkRestartMutation.isPending}
|
|
375
|
+
onBulkScale={canBulkScaleSelectedKind ? (items, replicas, options) => bulkScaleMutation.mutate({ items, replicas }, { onSuccess: options?.onSuccess }) : undefined}
|
|
376
|
+
isBulkScaling={canBulkScaleSelectedKind && bulkScaleMutation.isPending}
|
|
200
377
|
/>
|
|
201
378
|
<CreateResourceDialog
|
|
202
379
|
open={createDialogOpen}
|
|
@@ -204,7 +381,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
204
381
|
initialYaml={createDialogYaml}
|
|
205
382
|
title={createDialogTitle}
|
|
206
383
|
onCreated={(result) => {
|
|
207
|
-
onResourceClick?.({ kind: result.kind, namespace: result.namespace, name: result.name, group: '' })
|
|
384
|
+
onResourceClick?.({ kind: kindToPlural(result.kind), namespace: result.namespace, name: result.name, group: '' })
|
|
208
385
|
}}
|
|
209
386
|
/>
|
|
210
387
|
</>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@skyhook-io/k8s-ui/components/resources/renderers/CronWorkflowRenderer'
|
|
@@ -1 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
import { HPARenderer as BaseHPARenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/HPARenderer'
|
|
2
|
+
import { HPACharts } from '../../resource/HPACharts'
|
|
3
|
+
import type { HPADiagnosis } from '@skyhook-io/k8s-ui'
|
|
4
|
+
|
|
5
|
+
interface HPARendererProps {
|
|
6
|
+
data: any
|
|
7
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
8
|
+
hpaDiagnosis?: HPADiagnosis
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function HPARenderer({ data, onNavigate, hpaDiagnosis }: HPARendererProps) {
|
|
12
|
+
return (
|
|
13
|
+
<BaseHPARenderer
|
|
14
|
+
data={data}
|
|
15
|
+
onNavigate={onNavigate}
|
|
16
|
+
hpaDiagnosis={hpaDiagnosis}
|
|
17
|
+
extraSections={<HPACharts data={data} />}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NamespaceRenderer as BaseNamespaceRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/NamespaceRenderer'
|
|
2
|
+
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { useRBACNamespace } from '../../../api/rbac'
|
|
4
|
+
import { useNamespaceQuotas } from '../../../api/quotas'
|
|
5
|
+
import { isForbiddenError } from '../../../api/client'
|
|
6
|
+
|
|
7
|
+
interface NamespaceRendererProps {
|
|
8
|
+
data: any
|
|
9
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function NamespaceRenderer({ data, onNavigate }: NamespaceRendererProps) {
|
|
13
|
+
const name = data?.metadata?.name ?? ''
|
|
14
|
+
const { data: rbacData, isLoading, error } = useRBACNamespace(name, !!name)
|
|
15
|
+
const { data: quotaData, error: quotaError } = useNamespaceQuotas(name, !!name)
|
|
16
|
+
// 403 → the user can't see quotas; hide the section (same posture as the
|
|
17
|
+
// RBAC sections). Surface other errors (500/503) so a quota-constrained
|
|
18
|
+
// namespace doesn't silently render as quota-free.
|
|
19
|
+
const quotaErr = quotaError && !isForbiddenError(quotaError) ? (quotaError as Error) : null
|
|
20
|
+
return (
|
|
21
|
+
<BaseNamespaceRenderer
|
|
22
|
+
data={data}
|
|
23
|
+
rbacData={rbacData ?? null}
|
|
24
|
+
rbacLoading={isLoading}
|
|
25
|
+
rbacError={error as Error | null}
|
|
26
|
+
quotaData={quotaData}
|
|
27
|
+
quotaError={quotaErr}
|
|
28
|
+
onNavigate={onNavigate}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NodeRenderer as BaseNodeRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/NodeRenderer'
|
|
2
2
|
import { useNavigate } from 'react-router-dom'
|
|
3
|
-
import { useNodeMetrics, useNodeMetricsHistory, usePrometheusResourceMetrics, usePrometheusStatus } from '../../../api/client'
|
|
3
|
+
import { getVisibleLiveMetrics, isLiveMetricsUnavailable, shouldFetchLiveMetrics, useNodeMetrics, useNodeMetricsHistory, usePrometheusResourceMetrics, usePrometheusStatus } from '../../../api/client'
|
|
4
4
|
import { serializeColumnFilters } from '../resource-utils'
|
|
5
5
|
|
|
6
6
|
interface NodeRendererProps {
|
|
@@ -13,8 +13,13 @@ export function NodeRenderer({ data, relationships }: NodeRendererProps) {
|
|
|
13
13
|
const nodeName = data.metadata?.name
|
|
14
14
|
|
|
15
15
|
// Fetch node metrics
|
|
16
|
-
const
|
|
17
|
-
const { data: metricsHistory } =
|
|
16
|
+
const metricsHistoryQuery = useNodeMetricsHistory(nodeName)
|
|
17
|
+
const { data: metricsHistory } = metricsHistoryQuery
|
|
18
|
+
const historyMetricsUnavailable = metricsHistory?.metricsUnavailable === true
|
|
19
|
+
const liveMetricsEnabled = shouldFetchLiveMetrics(metricsHistoryQuery.isFetched || metricsHistoryQuery.isError, historyMetricsUnavailable)
|
|
20
|
+
const { data: metrics } = useNodeMetrics(nodeName, { enabled: liveMetricsEnabled })
|
|
21
|
+
const metricsUnavailable = historyMetricsUnavailable || isLiveMetricsUnavailable(liveMetricsEnabled, metrics)
|
|
22
|
+
const visibleMetrics = getVisibleLiveMetrics(liveMetricsEnabled, metricsUnavailable, metrics)
|
|
18
23
|
|
|
19
24
|
// Determine whether to hide metrics-server section (Prometheus has data)
|
|
20
25
|
const { data: prometheusStatus } = usePrometheusStatus()
|
|
@@ -36,8 +41,9 @@ export function NodeRenderer({ data, relationships }: NodeRendererProps) {
|
|
|
36
41
|
params.set('filters', serializeColumnFilters({ node: [nodeName] }))
|
|
37
42
|
navigate(`/resources/pods?${params.toString()}`)
|
|
38
43
|
} : undefined}
|
|
39
|
-
metrics={
|
|
44
|
+
metrics={visibleMetrics}
|
|
40
45
|
metricsHistory={metricsHistory}
|
|
46
|
+
metricsUnavailable={metricsUnavailable}
|
|
41
47
|
hideMetricsServer={hideMetricsServer}
|
|
42
48
|
/>
|
|
43
49
|
)
|
|
@@ -1 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
import { PVCRenderer as BasePVCRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/PVCRenderer'
|
|
2
|
+
import { PVCUsageBar } from '../../resource/PVCUsageBar'
|
|
3
|
+
|
|
4
|
+
interface PVCRendererProps {
|
|
5
|
+
data: any
|
|
6
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function PVCRenderer({ data, onNavigate }: PVCRendererProps) {
|
|
10
|
+
const namespace = data?.metadata?.namespace ?? ''
|
|
11
|
+
const name = data?.metadata?.name ?? ''
|
|
12
|
+
return (
|
|
13
|
+
<BasePVCRenderer
|
|
14
|
+
data={data}
|
|
15
|
+
onNavigate={onNavigate}
|
|
16
|
+
extraSections={namespace && name ? <PVCUsageBar namespace={namespace} name={name} /> : undefined}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -2,8 +2,9 @@ import { PodRenderer as BasePodRenderer } from '@skyhook-io/k8s-ui/components/re
|
|
|
2
2
|
import type { CopyHandler } from '@skyhook-io/k8s-ui/components/ui/drawer-components'
|
|
3
3
|
import type { ResolvedEnvFrom } from '@skyhook-io/k8s-ui'
|
|
4
4
|
import { useOpenTerminal, useOpenLogs } from '../../dock'
|
|
5
|
-
import { useNamespacedCapabilities } from '../../../contexts/CapabilitiesContext'
|
|
6
|
-
import { usePodMetrics, usePodMetricsHistory, usePrometheusResourceMetrics, usePrometheusStatus } from '../../../api/client'
|
|
5
|
+
import { useNamespacedCapabilities, useIsLocalDeployment } from '../../../contexts/CapabilitiesContext'
|
|
6
|
+
import { getVisibleLiveMetrics, isLiveMetricsUnavailable, shouldFetchLiveMetrics, usePodMetrics, usePodMetricsHistory, usePrometheusResourceMetrics, usePrometheusStatus } from '../../../api/client'
|
|
7
|
+
import { useRBACSubject } from '../../../api/rbac'
|
|
7
8
|
import { PortForwardInlineButton } from '../../portforward/PortForwardButton'
|
|
8
9
|
import { ImageFilesystemModal } from '../ImageFilesystemModal'
|
|
9
10
|
import { PodFilesystemModal } from '../PodFilesystemModal'
|
|
@@ -26,10 +27,20 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
|
|
|
26
27
|
|
|
27
28
|
// Capabilities (namespace-scoped: re-checks RBAC if globally denied)
|
|
28
29
|
const { canExec, canViewLogs, canPortForward } = useNamespacedCapabilities(namespace)
|
|
30
|
+
// Show the port-forward affordance for a live forward (local + RBAC) OR when
|
|
31
|
+
// not local — in-cluster/Cloud surfaces a copy-paste kubectl command instead.
|
|
32
|
+
// The button itself picks live vs. copy-command based on deployment mode.
|
|
33
|
+
const isLocal = useIsLocalDeployment()
|
|
34
|
+
const showPortForward = canPortForward || !isLocal
|
|
29
35
|
|
|
30
36
|
// Metrics
|
|
31
|
-
const
|
|
32
|
-
const { data: metricsHistory } =
|
|
37
|
+
const metricsHistoryQuery = usePodMetricsHistory(namespace, podName)
|
|
38
|
+
const { data: metricsHistory } = metricsHistoryQuery
|
|
39
|
+
const historyMetricsUnavailable = metricsHistory?.metricsUnavailable === true
|
|
40
|
+
const liveMetricsEnabled = shouldFetchLiveMetrics(metricsHistoryQuery.isFetched || metricsHistoryQuery.isError, historyMetricsUnavailable)
|
|
41
|
+
const { data: metrics } = usePodMetrics(namespace, podName, { enabled: liveMetricsEnabled })
|
|
42
|
+
const metricsUnavailable = historyMetricsUnavailable || isLiveMetricsUnavailable(liveMetricsEnabled, metrics)
|
|
43
|
+
const visibleMetrics = getVisibleLiveMetrics(liveMetricsEnabled, metricsUnavailable, metrics)
|
|
33
44
|
|
|
34
45
|
// Hide metrics-server section when Prometheus has CPU data
|
|
35
46
|
const { data: prometheusStatus } = usePrometheusStatus()
|
|
@@ -42,6 +53,15 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
|
|
|
42
53
|
) ?? false)
|
|
43
54
|
const hideMetricsServer = prometheusHasCPU || (prometheusConnected && prometheusCPULoading)
|
|
44
55
|
|
|
56
|
+
// RBAC reverse-lookup for the Pod's ServiceAccount. Defaults to "default" —
|
|
57
|
+
// that's the SA every Pod uses when spec.serviceAccountName is unset, which
|
|
58
|
+
// is itself a useful signal (operators often don't realize "default" still
|
|
59
|
+
// has whatever permissions the namespace's defaults grant).
|
|
60
|
+
const saName = data.spec?.serviceAccountName || 'default'
|
|
61
|
+
const { data: rbacData, isLoading: rbacLoading, error: rbacError } = useRBACSubject(
|
|
62
|
+
'ServiceAccount', namespace ?? '', saName, !!namespace,
|
|
63
|
+
)
|
|
64
|
+
|
|
45
65
|
return (
|
|
46
66
|
<BasePodRenderer
|
|
47
67
|
data={data}
|
|
@@ -50,9 +70,12 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
|
|
|
50
70
|
onNavigate={onNavigate}
|
|
51
71
|
onOpenLogs={onOpenLogs}
|
|
52
72
|
resolvedEnvFrom={resolvedEnvFrom}
|
|
73
|
+
rbacData={rbacData ?? null}
|
|
74
|
+
rbacLoading={rbacLoading}
|
|
75
|
+
rbacError={rbacError as Error | null}
|
|
53
76
|
canExec={canExec}
|
|
54
77
|
canViewLogs={canViewLogs}
|
|
55
|
-
canPortForward={
|
|
78
|
+
canPortForward={showPortForward}
|
|
56
79
|
onOpenTerminal={(params) => openTerminal(params)}
|
|
57
80
|
onOpenLogsPanel={(params) => openLogsPanel(params)}
|
|
58
81
|
renderPortAction={({ namespace: ns, podName: pod, port, protocol, disabled }) => (
|
|
@@ -64,8 +87,9 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
|
|
|
64
87
|
disabled={disabled}
|
|
65
88
|
/>
|
|
66
89
|
)}
|
|
67
|
-
metrics={
|
|
90
|
+
metrics={visibleMetrics}
|
|
68
91
|
metricsHistory={metricsHistory}
|
|
92
|
+
metricsUnavailable={metricsUnavailable}
|
|
69
93
|
hideMetricsServer={hideMetricsServer}
|
|
70
94
|
renderImageBrowser={({ image, namespace: ns, podName: pod, pullSecrets, onClose, onSwitchToPodFiles }) => (
|
|
71
95
|
<ImageFilesystemModal
|
|
@@ -1 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
import { RoleBindingRenderer as BaseRoleBindingRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/RoleBindingRenderer'
|
|
2
|
+
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { useResource } from '../../../api/client'
|
|
4
|
+
|
|
5
|
+
interface RoleBindingRendererProps {
|
|
6
|
+
data: any
|
|
7
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Reuses the generic resource endpoint to fetch the referenced Role/ClusterRole
|
|
11
|
+
// so the base renderer can show an inline rules preview. A dedicated /api/rbac
|
|
12
|
+
// "rules-only" endpoint would be tighter, but the generic one is already
|
|
13
|
+
// cached and respects per-user RBAC the same way.
|
|
14
|
+
export function RoleBindingRenderer({ data, onNavigate }: RoleBindingRendererProps) {
|
|
15
|
+
const roleRef = data?.roleRef ?? {}
|
|
16
|
+
const isClusterRole = roleRef.kind === 'ClusterRole'
|
|
17
|
+
const kind = isClusterRole ? 'clusterroles' : 'roles'
|
|
18
|
+
// For ClusterRole the namespace is cluster-scoped (empty); for Role the
|
|
19
|
+
// binding's namespace is the role's namespace (Roles are namespaced and
|
|
20
|
+
// RoleBindings can only reference Roles in their own namespace).
|
|
21
|
+
const namespace = isClusterRole ? '' : (data?.metadata?.namespace ?? '')
|
|
22
|
+
const name = roleRef.name ?? ''
|
|
23
|
+
|
|
24
|
+
const { data: role, isLoading, error } = useResource<any>(kind, namespace, name)
|
|
25
|
+
// `role` is undefined while loading, then the resource, then potentially
|
|
26
|
+
// null on 404 / 403. Pass [] for "loaded but no rules" so the base
|
|
27
|
+
// renderer's `rules === null` branch fires only on outright fetch failure
|
|
28
|
+
// (orphan or forbidden); `roleRulesError` then disambiguates the two.
|
|
29
|
+
const rules =
|
|
30
|
+
isLoading
|
|
31
|
+
? undefined
|
|
32
|
+
: role
|
|
33
|
+
? (role.rules ?? [])
|
|
34
|
+
: null
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<BaseRoleBindingRenderer
|
|
38
|
+
data={data}
|
|
39
|
+
onNavigate={onNavigate}
|
|
40
|
+
roleRules={rules ?? null}
|
|
41
|
+
roleRulesLoading={isLoading}
|
|
42
|
+
roleRulesError={error}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -1 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import { RoleRenderer as BaseRoleRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/RoleRenderer'
|
|
2
|
+
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { useRBACRole } from '../../../api/rbac'
|
|
4
|
+
|
|
5
|
+
interface RoleRendererProps {
|
|
6
|
+
data: any
|
|
7
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function RoleRenderer({ data, onNavigate }: RoleRendererProps) {
|
|
11
|
+
// ClusterRole has no namespace; Role does. The kind in the manifest is
|
|
12
|
+
// authoritative — RoleRenderer is shared between both kinds via the
|
|
13
|
+
// dispatch, so checking it here is the only way to know which we are.
|
|
14
|
+
const kind: 'Role' | 'ClusterRole' = data?.kind === 'ClusterRole' ? 'ClusterRole' : 'Role'
|
|
15
|
+
const namespace = data?.metadata?.namespace ?? ''
|
|
16
|
+
const name = data?.metadata?.name ?? ''
|
|
17
|
+
const { data: rbacRoleData, isLoading, error } = useRBACRole(kind, namespace, name, !!name)
|
|
18
|
+
return (
|
|
19
|
+
<BaseRoleRenderer
|
|
20
|
+
data={data}
|
|
21
|
+
rbacRoleData={rbacRoleData ?? null}
|
|
22
|
+
rbacRoleLoading={isLoading}
|
|
23
|
+
rbacRoleError={error as Error | null}
|
|
24
|
+
onNavigate={onNavigate}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|