@skyhook-io/radar-app 1.13.5 → 1.14.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/package.json +9 -9
- package/src/App.tsx +66 -28
- package/src/api/client.authRedirect.test.ts +12 -1
- package/src/api/client.prometheusStatus.test.ts +24 -0
- package/src/api/client.ts +324 -16
- package/src/api/diagnose.ts +32 -1
- package/src/api/drain-plan.test.ts +86 -0
- package/src/api/quotas.test.ts +26 -0
- package/src/api/quotas.ts +20 -0
- package/src/api/workloadMetrics.test.ts +40 -0
- package/src/api/workloadMetrics.ts +68 -0
- package/src/components/CloudConnectFlow.tsx +343 -46
- package/src/components/CloudFunnelButton.tsx +110 -37
- package/src/components/ConnectionErrorView.tsx +9 -10
- package/src/components/DebugOverlay.tsx +10 -6
- package/src/components/SyncProgressPanel.tsx +73 -0
- package/src/components/audit/AuditView.tsx +2 -0
- package/src/components/cloudConnectHandoff.test.ts +245 -0
- package/src/components/cloudConnectHandoff.ts +276 -0
- package/src/components/curl/ServiceCurlButton.tsx +19 -26
- package/src/components/diagnose/AgentCase.tsx +18 -5
- package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
- package/src/components/diagnose/AnalysisStory.tsx +564 -0
- package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
- package/src/components/diagnose/DiagnoseContext.tsx +26 -8
- package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
- package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
- package/src/components/diagnose/InvestigationView.tsx +493 -529
- package/src/components/diagnose/investigationCase.test.tsx +267 -129
- package/src/components/diagnose/investigationCase.ts +122 -77
- package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
- package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +81 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +285 -0
- package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +427 -0
- package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +632 -0
- package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
- package/src/components/diagnose/investigationEvidence/index.ts +55 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +978 -0
- package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +2736 -0
- package/src/components/diagnose/investigationEvidence/types.ts +514 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
- package/src/components/diagnose/investigationState.test.ts +316 -58
- package/src/components/diagnose/investigationState.ts +257 -44
- package/src/components/diagnose/investigationStory.test.ts +161 -0
- package/src/components/diagnose/investigationStory.ts +207 -0
- package/src/components/diagnose/parts.test.tsx +9 -6
- package/src/components/diagnose/parts.tsx +977 -193
- package/src/components/diagnose/storyParts.test.tsx +426 -0
- package/src/components/diagnose/target.test.ts +19 -0
- package/src/components/diagnose/target.ts +3 -2
- package/src/components/diagnose/toolCallLabel.test.ts +51 -0
- package/src/components/diagnose/toolCallLabel.ts +135 -0
- package/src/components/diagnose/useDisclosureReveal.ts +10 -11
- package/src/components/gitops/GitOpsView.tsx +7 -0
- package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
- package/src/components/helm/HelmCompareRoute.tsx +18 -4
- package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
- package/src/components/helm/InstallWizard.tsx +8 -3
- package/src/components/home/MCPSetupDialog.tsx +15 -9
- package/src/components/portforward/PortForwardManager.tsx +6 -13
- package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
- package/src/components/resource/PrometheusCharts.tsx +4 -3
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +75 -35
- package/src/components/resource/RightsizingStrip.test.ts +38 -0
- package/src/components/resource/RightsizingStrip.tsx +15 -3
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
- package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
- package/src/components/resource/WorkloadMetricsSection.tsx +480 -0
- package/src/components/resource/workloadMetricValues.test.ts +65 -0
- package/src/components/resource/workloadMetricValues.ts +30 -0
- package/src/components/resources/PodFilePreview.tsx +3 -3
- package/src/components/resources/ResourcesView.tsx +44 -6
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
- package/src/components/resources/renderers/PodRenderer.tsx +4 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
- package/src/components/settings/SettingsDialog.tsx +98 -16
- package/src/components/traffic/TrafficFlowList.tsx +229 -70
- package/src/components/traffic/TrafficGraph.tsx +6 -6
- package/src/components/traffic/TrafficView.tsx +10 -9
- package/src/components/traffic/trafficFilters.test.ts +28 -1
- package/src/components/traffic/trafficFilters.ts +31 -0
- package/src/components/ui/CommandPalette.tsx +10 -6
- package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
- package/src/components/ui/Disclosure.tsx +47 -0
- package/src/components/ui/Markdown.tsx +23 -11
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
- package/src/components/ui/UpdateNotification.tsx +18 -2
- package/src/components/workload/WorkloadView.tsx +19 -8
- package/src/context/ConnectionContext.tsx +48 -3
- package/src/context/DiagnoseCustomization.tsx +1 -0
- package/src/index.css +36 -6
- package/src/utils/navigation.ts +1 -1
- package/src/utils/topology-namespace.test.ts +64 -0
- package/src/utils/topology-namespace.ts +18 -0
- package/src/components/diagnose/investigationEvidence.test.ts +0 -6617
- package/src/components/diagnose/investigationEvidence.ts +0 -5572
- package/src/components/gitops/GitOpsActions.tsx +0 -1
- package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
- package/src/components/gitops/ManagedResourcesList.tsx +0 -1
- package/src/components/gitops/SyncCountdown.tsx +0 -1
- package/src/components/gitops/index.ts +0 -4
- package/src/components/helm/ManifestDiffViewer.tsx +0 -69
- package/src/components/logs/JsonLogLine.tsx +0 -1
- package/src/components/logs/LogCore.tsx +0 -2
- package/src/components/logs/useLogBuffer.ts +0 -2
- package/src/components/logs/useLogSearch.ts +0 -1
- package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
- package/src/components/resources/drawer-components.tsx +0 -1
- package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
- package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/EventRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/JobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/VPARenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
- package/src/components/resources/renderers/argo-cells.tsx +0 -1
- package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
- package/src/components/resources/renderers/eso-cells.tsx +0 -1
- package/src/components/resources/renderers/flux-cells.tsx +0 -1
- package/src/components/resources/renderers/index.ts +0 -94
- package/src/components/resources/renderers/istio-cells.tsx +0 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
- package/src/components/resources/renderers/keda-cells.tsx +0 -1
- package/src/components/resources/renderers/knative-cells.tsx +0 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
- package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
- package/src/components/resources/renderers/traefik-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-shared.tsx +0 -1
- package/src/components/resources/renderers/velero-cells.tsx +0 -1
- package/src/components/resources/resource-utils-argo.ts +0 -2
- package/src/components/resources/resource-utils-certmanager.ts +0 -2
- package/src/components/resources/resource-utils-eso.ts +0 -2
- package/src/components/resources/resource-utils-flux.ts +0 -2
- package/src/components/resources/resource-utils-istio.ts +0 -2
- package/src/components/resources/resource-utils-karpenter.ts +0 -2
- package/src/components/resources/resource-utils-knative.ts +0 -2
- package/src/components/resources/resource-utils-kyverno.ts +0 -2
- package/src/components/resources/resource-utils-prometheus.ts +0 -2
- package/src/components/resources/resource-utils-traefik.ts +0 -1
- package/src/components/resources/resource-utils-trivy.ts +0 -2
- package/src/components/resources/resource-utils-velero.ts +0 -2
- package/src/components/shared/EditableYamlView.tsx +0 -24
- package/src/components/timeline/DiffViewer.tsx +0 -1
- package/src/components/traffic/index.ts +0 -3
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
- package/src/components/ui/MetricsChart.tsx +0 -1
- package/src/components/ui/ResourceBar.tsx +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Collapse, CollapseChevron, useDisclosure } from '@skyhook-io/k8s-ui/components/ui/Collapse'
|
|
4
|
+
|
|
5
|
+
// In-flow disclosure with the app-standard motion — the replacement for native
|
|
6
|
+
// <details>/<summary> on primary paths. Native details snaps open with no
|
|
7
|
+
// transition and its marker ignores the caret language; this pairs a button
|
|
8
|
+
// header (aria-expanded / aria-controls via useDisclosure) with CollapseChevron
|
|
9
|
+
// and an animated Collapse panel. Content stays mounted while closed, exactly
|
|
10
|
+
// like native details, so server-rendered markup and in-page search still see
|
|
11
|
+
// the collapsed copy.
|
|
12
|
+
export function Disclosure({
|
|
13
|
+
summary,
|
|
14
|
+
children,
|
|
15
|
+
className,
|
|
16
|
+
summaryClassName,
|
|
17
|
+
chevronClassName = 'h-3.5 w-3.5',
|
|
18
|
+
defaultOpen = false,
|
|
19
|
+
}: {
|
|
20
|
+
summary: ReactNode
|
|
21
|
+
children: ReactNode
|
|
22
|
+
/** Wrapper (what the old <details> carried). */
|
|
23
|
+
className?: string
|
|
24
|
+
/** Header button (what the old <summary> carried). */
|
|
25
|
+
summaryClassName?: string
|
|
26
|
+
chevronClassName?: string
|
|
27
|
+
defaultOpen?: boolean
|
|
28
|
+
}) {
|
|
29
|
+
const [open, setOpen] = useState(defaultOpen)
|
|
30
|
+
const disclosure = useDisclosure(open)
|
|
31
|
+
return (
|
|
32
|
+
<div className={className}>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
{...disclosure.buttonProps}
|
|
36
|
+
onClick={() => setOpen((value) => !value)}
|
|
37
|
+
className={clsx('flex w-full cursor-pointer items-center gap-1.5 text-left', summaryClassName)}
|
|
38
|
+
>
|
|
39
|
+
<CollapseChevron open={open} className={chevronClassName} />
|
|
40
|
+
{summary}
|
|
41
|
+
</button>
|
|
42
|
+
<Collapse open={open} id={disclosure.panelId}>
|
|
43
|
+
{children}
|
|
44
|
+
</Collapse>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -2,12 +2,20 @@ import ReactMarkdown from 'react-markdown'
|
|
|
2
2
|
import remarkGfm from 'remark-gfm'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
|
|
5
7
|
interface MarkdownProps {
|
|
6
8
|
children: string
|
|
7
9
|
className?: string
|
|
10
|
+
/**
|
|
11
|
+
* Intercepts links before the default anchor renders. Return null to fall
|
|
12
|
+
* through. Lets a caller turn a reserved href (an in-page evidence
|
|
13
|
+
* reference, say) into a control instead of an external link.
|
|
14
|
+
*/
|
|
15
|
+
linkRenderer?: (href: string | undefined, children: ReactNode) => ReactNode | null
|
|
8
16
|
}
|
|
9
17
|
|
|
10
|
-
export function Markdown({ children, className }: MarkdownProps) {
|
|
18
|
+
export function Markdown({ children, className, linkRenderer }: MarkdownProps) {
|
|
11
19
|
return (
|
|
12
20
|
<div className={clsx('markdown-content', className)}>
|
|
13
21
|
<ReactMarkdown
|
|
@@ -28,16 +36,20 @@ export function Markdown({ children, className }: MarkdownProps) {
|
|
|
28
36
|
p: ({ children }) => (
|
|
29
37
|
<p className="text-theme-text-secondary my-2 leading-relaxed">{children}</p>
|
|
30
38
|
),
|
|
31
|
-
a: ({ href, children }) =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
a: ({ href, children }) => {
|
|
40
|
+
const custom = linkRenderer?.(href, children)
|
|
41
|
+
if (custom !== null && custom !== undefined) return <>{custom}</>
|
|
42
|
+
return (
|
|
43
|
+
<a
|
|
44
|
+
href={href}
|
|
45
|
+
target="_blank"
|
|
46
|
+
rel="noopener noreferrer"
|
|
47
|
+
className="text-blue-400 hover:text-blue-300 underline underline-offset-2"
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</a>
|
|
51
|
+
)
|
|
52
|
+
},
|
|
41
53
|
ul: ({ children }) => (
|
|
42
54
|
<ul className="list-disc list-outside pl-4 my-2 space-y-1 text-theme-text-secondary">{children}</ul>
|
|
43
55
|
),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
2
|
import { X } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
-
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
4
|
+
import { TRANSITION_BACKDROP, TRANSITION_PANEL, overlayTransitionStyle } from '../../utils/animation'
|
|
5
5
|
import { useActiveShortcuts, type ShortcutCategory } from '../../hooks/useKeyboardShortcuts'
|
|
6
6
|
|
|
7
7
|
interface ShortcutHelpOverlayProps {
|
|
@@ -232,6 +232,7 @@ export function ShortcutHelpOverlay({ onClose, currentView, isOpen = true }: Sho
|
|
|
232
232
|
TRANSITION_BACKDROP,
|
|
233
233
|
isOpen ? 'opacity-100' : 'opacity-0'
|
|
234
234
|
)}
|
|
235
|
+
style={overlayTransitionStyle(isOpen, 'dialog')}
|
|
235
236
|
onClick={onClose}
|
|
236
237
|
/>
|
|
237
238
|
|
|
@@ -243,6 +244,7 @@ export function ShortcutHelpOverlay({ onClose, currentView, isOpen = true }: Sho
|
|
|
243
244
|
TRANSITION_PANEL,
|
|
244
245
|
isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-[0.97]'
|
|
245
246
|
)}
|
|
247
|
+
style={overlayTransitionStyle(isOpen, 'dialog')}
|
|
246
248
|
>
|
|
247
249
|
{/* Header */}
|
|
248
250
|
<div className="flex items-center justify-between px-5 py-3.5 border-b border-theme-border">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
2
3
|
import { Download, X, Copy, Check, RotateCw, ArrowDownToLine, Loader2 } from 'lucide-react'
|
|
3
4
|
import { useQueryClient } from '@tanstack/react-query'
|
|
4
5
|
import {
|
|
@@ -10,6 +11,8 @@ import {
|
|
|
10
11
|
} from '../../api/client'
|
|
11
12
|
import type { DesktopUpdateState } from '../../api/client'
|
|
12
13
|
import { WithTooltip } from './Tooltip'
|
|
14
|
+
import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
|
|
15
|
+
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
13
16
|
|
|
14
17
|
const DISMISSED_KEY = 'radar-update-dismissed'
|
|
15
18
|
|
|
@@ -103,7 +106,11 @@ export function UpdateNotification() {
|
|
|
103
106
|
|
|
104
107
|
// Shared in-cluster viewers get a persistent Home notice instead of a
|
|
105
108
|
// floating action prompt they may not be able to act on.
|
|
106
|
-
|
|
109
|
+
const show = !!versionInfo?.updateAvailable && !dismissed && deploymentMode !== undefined && deploymentMode !== 'in-cluster' && deploymentMode !== 'cloud'
|
|
110
|
+
// Presence outlives `show` by the menu exit so a dismiss fades the chip out
|
|
111
|
+
// instead of snapping it away; the enter runs the same transition in reverse.
|
|
112
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(show, overlayExitMs('menu'))
|
|
113
|
+
if (!shouldRender || !versionInfo) {
|
|
107
114
|
return null
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -111,7 +118,16 @@ export function UpdateNotification() {
|
|
|
111
118
|
const effectiveState: DesktopUpdateState = updateStatus?.state ?? 'idle'
|
|
112
119
|
|
|
113
120
|
return (
|
|
114
|
-
<div
|
|
121
|
+
<div
|
|
122
|
+
inert={!show || undefined}
|
|
123
|
+
className={clsx(
|
|
124
|
+
'fixed bottom-4 right-4 z-50 max-w-sm bg-theme-surface border border-accent/50 rounded-lg shadow-xl p-4 origin-bottom-right',
|
|
125
|
+
TRANSITION_MENU,
|
|
126
|
+
isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 translate-y-1 scale-[0.97]',
|
|
127
|
+
!show && 'pointer-events-none',
|
|
128
|
+
)}
|
|
129
|
+
style={overlayTransitionStyle(isOpen, 'menu')}
|
|
130
|
+
>
|
|
115
131
|
<div className="flex items-start gap-3">
|
|
116
132
|
<div className="flex items-center justify-center w-8 h-8 bg-accent-muted rounded-full shrink-0">
|
|
117
133
|
<UpdateIcon state={effectiveState} />
|
|
@@ -70,6 +70,8 @@ import {
|
|
|
70
70
|
useCordonNode,
|
|
71
71
|
useUncordonNode,
|
|
72
72
|
useDrainNode,
|
|
73
|
+
useDrainPlan,
|
|
74
|
+
DrainPlanUnsupportedError,
|
|
73
75
|
useCascadeDeletePreview,
|
|
74
76
|
useResourceEvents,
|
|
75
77
|
useResource,
|
|
@@ -81,7 +83,7 @@ import {
|
|
|
81
83
|
import { PrometheusCharts, isPrometheusSupported } from '../resource/PrometheusCharts'
|
|
82
84
|
import { PrometheusChartsGrid } from '../resource/PrometheusChartsGrid'
|
|
83
85
|
import { RestartEventLane } from '../resource/RestartChart'
|
|
84
|
-
import { RightsizingPanel
|
|
86
|
+
import { RightsizingPanel } from '../resource/RightsizingStrip'
|
|
85
87
|
import { WorkloadCostTab } from '../cost/WorkloadCostTab'
|
|
86
88
|
import { isOpenCostWorkloadKind } from '../cost/kinds'
|
|
87
89
|
import { useResourceAudit, useResourceIssues, useResources, useTrace, fetchTraceWithProbes, fetchInClusterCapability, runInClusterMerged } from '../../api/client'
|
|
@@ -381,6 +383,7 @@ function useActionsBarProps(
|
|
|
381
383
|
const cordonMutation = useCordonNode()
|
|
382
384
|
const uncordonMutation = useUncordonNode()
|
|
383
385
|
const drainMutation = useDrainNode()
|
|
386
|
+
const drainPlanMutation = useDrainPlan()
|
|
384
387
|
|
|
385
388
|
const { renderAction: renderDiagnose } = useDiagnoseCustomization()
|
|
386
389
|
|
|
@@ -485,6 +488,13 @@ function useActionsBarProps(
|
|
|
485
488
|
onDrainNode: (params: Parameters<typeof drainMutation.mutate>[0]) =>
|
|
486
489
|
drainMutation.mutate(params),
|
|
487
490
|
isDrainingNode: drainMutation.isPending,
|
|
491
|
+
onPlanDrain: (params: Parameters<typeof drainPlanMutation.mutate>[0]) =>
|
|
492
|
+
drainPlanMutation.mutate(params),
|
|
493
|
+
onPlanDrainReset: () => drainPlanMutation.reset(),
|
|
494
|
+
drainPlan: drainPlanMutation.data ?? null,
|
|
495
|
+
isPlanningDrain: drainPlanMutation.isPending,
|
|
496
|
+
drainPlanError: drainPlanMutation.error?.message ?? null,
|
|
497
|
+
drainPlanUnsupported: drainPlanMutation.error instanceof DrainPlanUnsupportedError,
|
|
488
498
|
}
|
|
489
499
|
}
|
|
490
500
|
|
|
@@ -2411,16 +2421,9 @@ function MetricsTabContent({
|
|
|
2411
2421
|
resource: any
|
|
2412
2422
|
expanded: boolean
|
|
2413
2423
|
}) {
|
|
2414
|
-
const showRightsizing = expanded && ['Deployment', 'StatefulSet', 'DaemonSet'].includes(kind)
|
|
2415
|
-
|
|
2416
2424
|
if (expanded) {
|
|
2417
2425
|
return (
|
|
2418
2426
|
<div className="flex flex-col h-full">
|
|
2419
|
-
{showRightsizing && (
|
|
2420
|
-
<div className="px-4 pt-4">
|
|
2421
|
-
<RightsizingStrip kind={kind} namespace={namespace} name={name} />
|
|
2422
|
-
</div>
|
|
2423
|
-
)}
|
|
2424
2427
|
<div className="flex-1 min-h-0">
|
|
2425
2428
|
<PrometheusChartsGrid kind={kind} namespace={namespace} name={name} resource={resource} />
|
|
2426
2429
|
</div>
|
|
@@ -2446,9 +2449,17 @@ function DrawerMetricsContent({
|
|
|
2446
2449
|
}) {
|
|
2447
2450
|
const [chartRange, setChartRange] = useState<import('../../api/client').PrometheusTimeRange>('1h')
|
|
2448
2451
|
const showRestartLane = isPrometheusSupported(kind) && kind !== 'Node'
|
|
2452
|
+
const navigate = useNavigate()
|
|
2449
2453
|
|
|
2450
2454
|
return (
|
|
2451
2455
|
<div className="flex flex-col h-full">
|
|
2456
|
+
{['Deployment', 'StatefulSet', 'DaemonSet'].includes(kind) && (
|
|
2457
|
+
<div className="px-4 pt-3">
|
|
2458
|
+
<button className="text-xs text-accent hover:underline" onClick={() => navigate(`${buildWorkloadPath({ kind, namespace, name })}?tab=metrics&metricsRange=${chartRange}`)}>
|
|
2459
|
+
Open request and resource dashboard →
|
|
2460
|
+
</button>
|
|
2461
|
+
</div>
|
|
2462
|
+
)}
|
|
2452
2463
|
<div className="flex-1 min-h-0">
|
|
2453
2464
|
<PrometheusCharts
|
|
2454
2465
|
kind={kind}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createContext, useContext, useState, useCallback, useEffect, useRef, ReactNode } from 'react'
|
|
2
2
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
3
|
import { getApiBase } from '../api/config'
|
|
4
|
-
import { apiFetch } from '../api/client'
|
|
4
|
+
import { apiFetch, type SyncStatusSnapshot } from '../api/client'
|
|
5
5
|
|
|
6
6
|
export type ConnectionStateType = 'connected' | 'disconnected' | 'connecting'
|
|
7
7
|
|
|
@@ -12,6 +12,10 @@ export interface ConnectionState {
|
|
|
12
12
|
error?: string
|
|
13
13
|
errorType?: string // config, auth, auth-rejected, auth-plugin-stuck, rbac, network, timeout, tls, unknown
|
|
14
14
|
progressMessage?: string
|
|
15
|
+
// Per-kind readiness while the initial informer sync runs ('connecting'
|
|
16
|
+
// with informers started). Drives the progressive app shell — absent once
|
|
17
|
+
// connected or before informers exist.
|
|
18
|
+
syncStatus?: SyncStatusSnapshot
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
interface ConnectionStatusResponse extends ConnectionState {
|
|
@@ -157,7 +161,15 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
157
161
|
if (!firstConnect || cacheWarmAtMountRef.current) {
|
|
158
162
|
queryClient.invalidateQueries()
|
|
159
163
|
} else {
|
|
160
|
-
|
|
164
|
+
// Detail responses served during the progressive shell are partial —
|
|
165
|
+
// the server skips relationship computation while the topology cache
|
|
166
|
+
// is unavailable — and they resolve as successes, so the error-only
|
|
167
|
+
// sweep would leave an open drawer without its Related Resources
|
|
168
|
+
// forever (focus refetch is globally off). Refetching the resource
|
|
169
|
+
// detail keys is at most the open drawer, so it stays cheap.
|
|
170
|
+
queryClient.invalidateQueries({
|
|
171
|
+
predicate: (q) => q.state.status === 'error' || q.queryKey[0] === 'resource',
|
|
172
|
+
})
|
|
161
173
|
}
|
|
162
174
|
}, [queryClient])
|
|
163
175
|
|
|
@@ -195,12 +207,35 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
195
207
|
// while the retry is still running; the retry's own result supersedes it.
|
|
196
208
|
if (manualRetryPendingRef.current || autoRetryInFlightRef.current) return
|
|
197
209
|
const current = connectionRef.current
|
|
210
|
+
// The progressive shell renders real resource lists while still
|
|
211
|
+
// 'connecting'. A query client shared across cluster mounts may hold
|
|
212
|
+
// another cluster's data under identical keys — drop it before the
|
|
213
|
+
// shell can show it (the 'connected' path repeats this for the
|
|
214
|
+
// full-refresh case). Deliberately ahead of the SSE-generation guard:
|
|
215
|
+
// live connection_state frames bump the generation and drop the poll,
|
|
216
|
+
// and the shell must not spend the whole sync on another cluster's rows.
|
|
217
|
+
if (current.state === 'connecting' && data.state === 'connecting' &&
|
|
218
|
+
data.syncStatus && cacheWarmAtMountRef.current) {
|
|
219
|
+
cacheWarmAtMountRef.current = false
|
|
220
|
+
// Drop only cluster-scoped data. App chrome (auth identity, kubeconfig
|
|
221
|
+
// contexts) is not another cluster's data, and purging it flips
|
|
222
|
+
// authMePending back on — which holds the whole shell on the splash
|
|
223
|
+
// until /auth/me refetches, delaying the first progressive paint.
|
|
224
|
+
const keep = new Set(['connection-status', 'auth-me', 'contexts'])
|
|
225
|
+
queryClient.removeQueries({ predicate: (q) => !keep.has(String(q.queryKey[0])) })
|
|
226
|
+
}
|
|
198
227
|
if (!shouldApplyPolledConnection(
|
|
199
228
|
current.state,
|
|
200
229
|
data.state,
|
|
201
230
|
data.sseGenerationAtStart,
|
|
202
231
|
sseGenerationRef.current,
|
|
203
232
|
)) {
|
|
233
|
+
// SSE owns the state fields, but connection_state frames don't carry
|
|
234
|
+
// per-kind sync progress — merge it from the poll so the progressive
|
|
235
|
+
// shell keeps ticking while still connecting.
|
|
236
|
+
if (data.state === 'connecting' && data.syncStatus) {
|
|
237
|
+
setConnection(prev => prev.state === 'connecting' ? { ...prev, syncStatus: data.syncStatus } : prev)
|
|
238
|
+
}
|
|
204
239
|
return
|
|
205
240
|
}
|
|
206
241
|
const becameConnected = current.state !== 'connected' && data.state === 'connected'
|
|
@@ -211,11 +246,12 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
211
246
|
error: data.error,
|
|
212
247
|
errorType: data.errorType,
|
|
213
248
|
progressMessage: data.progressMessage,
|
|
249
|
+
syncStatus: data.syncStatus,
|
|
214
250
|
})
|
|
215
251
|
if (becameConnected) {
|
|
216
252
|
refreshCachesOnConnect()
|
|
217
253
|
}
|
|
218
|
-
}, [data, dataUpdatedAt, refreshCachesOnConnect])
|
|
254
|
+
}, [data, dataUpdatedAt, refreshCachesOnConnect, queryClient])
|
|
219
255
|
|
|
220
256
|
// Retry mutation
|
|
221
257
|
const retryMutation = useMutation({
|
|
@@ -231,6 +267,9 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
231
267
|
state: 'connecting',
|
|
232
268
|
error: undefined,
|
|
233
269
|
progressMessage: 'Connecting to cluster...',
|
|
270
|
+
// A retry is a fresh attempt — per-kind progress from the previous
|
|
271
|
+
// attempt would mislabel readiness until the next poll.
|
|
272
|
+
syncStatus: undefined,
|
|
234
273
|
}))
|
|
235
274
|
},
|
|
236
275
|
onSuccess: (result) => {
|
|
@@ -371,6 +410,12 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
371
410
|
if (prev.state === 'connected' && status.state === 'connecting') {
|
|
372
411
|
return prev
|
|
373
412
|
}
|
|
413
|
+
// connection_state frames don't carry per-kind sync progress — keep the
|
|
414
|
+
// last polled snapshot while still connecting, or each SSE progress
|
|
415
|
+
// frame would flicker the progressive shell back to the splash.
|
|
416
|
+
if (status.state === 'connecting' && !status.syncStatus && prev.state === 'connecting') {
|
|
417
|
+
return { ...status, syncStatus: prev.syncStatus }
|
|
418
|
+
}
|
|
374
419
|
return status
|
|
375
420
|
})
|
|
376
421
|
|
|
@@ -23,6 +23,7 @@ export type RenderInvestigationRunActions = (props: {
|
|
|
23
23
|
|
|
24
24
|
/** Render prop for the resource-level Investigate action. */
|
|
25
25
|
export type RenderDiagnoseAction = (ctx: {
|
|
26
|
+
/** Kubernetes Kind when loaded or discovered; otherwise its API resource name. */
|
|
26
27
|
kind: string;
|
|
27
28
|
/** Kubernetes API group; empty means core. */
|
|
28
29
|
group?: string;
|
package/src/index.css
CHANGED
|
@@ -21,6 +21,23 @@
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
@layer components {
|
|
24
|
+
.metrics-layout {
|
|
25
|
+
container-type: inline-size;
|
|
26
|
+
container-name: metrics;
|
|
27
|
+
}
|
|
28
|
+
.metrics-chart-grid {
|
|
29
|
+
display: grid;
|
|
30
|
+
grid-template-columns: minmax(0, 1fr);
|
|
31
|
+
gap: 0.75rem;
|
|
32
|
+
}
|
|
33
|
+
@container metrics (min-width: 760px) {
|
|
34
|
+
.metrics-chart-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
35
|
+
.metrics-chart-wide { grid-column: 1 / -1; }
|
|
36
|
+
}
|
|
37
|
+
@container metrics (min-width: 1200px) {
|
|
38
|
+
.metrics-chart-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
39
|
+
.metrics-chart-wide { grid-column: auto; }
|
|
40
|
+
}
|
|
24
41
|
.investigation-assessment {
|
|
25
42
|
background: var(--color-investigation-assessment);
|
|
26
43
|
border-color: var(--color-investigation-assessment-border);
|
|
@@ -327,10 +344,26 @@ body {
|
|
|
327
344
|
animation: status-enter 140ms ease-out both;
|
|
328
345
|
}
|
|
329
346
|
|
|
347
|
+
/* Reduced motion is an app-level policy, not a per-class list. Durations and
|
|
348
|
+
delays go to ~0 (a delay left in place would hold a fill-mode:both element
|
|
349
|
+
invisible), every loop runs once (an infinite animation at 0.01ms is still an
|
|
350
|
+
active animation — pulse, ping, Monaco's progress bit), and status spinners
|
|
351
|
+
keep turning because they convey state, not motion. Components that coordinate motion in
|
|
352
|
+
JS (presence hooks, drawer morph) check prefersReducedMotion() themselves and
|
|
353
|
+
dispose at logical close. */
|
|
330
354
|
@media (prefers-reduced-motion: reduce) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
animation:
|
|
355
|
+
*, *::before, *::after {
|
|
356
|
+
animation-duration: 0.01ms !important;
|
|
357
|
+
animation-delay: 0ms !important;
|
|
358
|
+
animation-iteration-count: 1 !important;
|
|
359
|
+
transition-duration: 0.01ms !important;
|
|
360
|
+
transition-delay: 0ms !important;
|
|
361
|
+
scroll-behavior: auto !important;
|
|
362
|
+
}
|
|
363
|
+
/* The one exception: a spinner that stops reads as "done", not "calm". */
|
|
364
|
+
.animate-spin {
|
|
365
|
+
animation-duration: 1s !important;
|
|
366
|
+
animation-iteration-count: infinite !important;
|
|
334
367
|
}
|
|
335
368
|
}
|
|
336
369
|
|
|
@@ -386,9 +419,6 @@ body {
|
|
|
386
419
|
-webkit-text-fill-color: currentColor;
|
|
387
420
|
color: var(--text-secondary);
|
|
388
421
|
}
|
|
389
|
-
.animate-result-in {
|
|
390
|
-
animation: none;
|
|
391
|
-
}
|
|
392
422
|
}
|
|
393
423
|
|
|
394
424
|
/* Gentle colorize when syntax-highlighted code swaps in over plain text — opacity
|
package/src/utils/navigation.ts
CHANGED
|
@@ -14,7 +14,7 @@ export function searchHitToSelectedResource(hit: SearchHit): SelectedResource {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Re-export shared navigation utilities from @skyhook-io/k8s-ui.
|
|
17
|
-
export { kindToPlural, kindToPluralWithGroup, pluralToKind, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
17
|
+
export { kindToPlural, kindToPluralWithGroup, pluralToKind, knownKindForPluralWithGroup, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
18
18
|
export type { NavigateToResource } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
19
19
|
|
|
20
20
|
const NETWORK_POLICY_TOPOLOGY_KINDS = new Set([
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { TopologyNode } from '@skyhook-io/k8s-ui/types/core'
|
|
3
|
+
import { isInNamespaceScope, scopeNodesToNamespaces } from './topology-namespace'
|
|
4
|
+
|
|
5
|
+
function node(id: string, kind: string, namespace?: string): TopologyNode {
|
|
6
|
+
return {
|
|
7
|
+
id,
|
|
8
|
+
kind,
|
|
9
|
+
name: id,
|
|
10
|
+
status: 'healthy',
|
|
11
|
+
data: namespace === undefined ? {} : { namespace },
|
|
12
|
+
} as TopologyNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const nodes = [
|
|
16
|
+
node('web', 'Deployment', 'prod'),
|
|
17
|
+
node('ing', 'Ingress', 'staging'),
|
|
18
|
+
node('db', 'StatefulSet', 'prod'),
|
|
19
|
+
node('node-1', 'Node'), // cluster-scoped
|
|
20
|
+
node('pv-1', 'PersistentVolume'), // cluster-scoped
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
describe('scopeNodesToNamespaces', () => {
|
|
24
|
+
it('returns every node when no namespace is selected', () => {
|
|
25
|
+
expect(scopeNodesToNamespaces(nodes, [])).toHaveLength(5)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('keeps only the selected namespace, plus cluster-scoped nodes', () => {
|
|
29
|
+
const scoped = scopeNodesToNamespaces(nodes, ['prod'])
|
|
30
|
+
expect(scoped.map(n => n.id)).toEqual(['web', 'db', 'node-1', 'pv-1'])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('drops a kind that exists only outside the scope', () => {
|
|
34
|
+
const scoped = scopeNodesToNamespaces(nodes, ['prod'])
|
|
35
|
+
expect(scoped.some(n => n.kind === 'Ingress')).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('accepts several namespaces at once', () => {
|
|
39
|
+
const scoped = scopeNodesToNamespaces(nodes, ['prod', 'staging'])
|
|
40
|
+
expect(scoped).toHaveLength(5)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns the same array identity when the scope is empty', () => {
|
|
44
|
+
expect(scopeNodesToNamespaces(nodes, [])).toBe(nodes)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('isInNamespaceScope', () => {
|
|
49
|
+
it('passes everything for a null scope', () => {
|
|
50
|
+
expect(isInNamespaceScope(node('x', 'Pod', 'any'), null)).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('always passes a node with no namespace', () => {
|
|
54
|
+
expect(isInNamespaceScope(node('node-1', 'Node'), new Set(['prod']))).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('treats an empty-string namespace as cluster-scoped', () => {
|
|
58
|
+
expect(isInNamespaceScope(node('n', 'Node', ''), new Set(['prod']))).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('rejects a namespaced node outside the scope', () => {
|
|
62
|
+
expect(isInNamespaceScope(node('ing', 'Ingress', 'staging'), new Set(['prod']))).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TopologyNode } from '@skyhook-io/k8s-ui/types/core'
|
|
2
|
+
|
|
3
|
+
// A node with no namespace is cluster-scoped (Node, PersistentVolume,
|
|
4
|
+
// Namespace, ...) and stays visible in every scope.
|
|
5
|
+
export function isInNamespaceScope(node: TopologyNode, nsSet: Set<string> | null): boolean {
|
|
6
|
+
if (!nsSet) return true
|
|
7
|
+
const ns = node.data?.namespace as string | undefined
|
|
8
|
+
return !ns || nsSet.has(ns)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Shared by the topology graph and the filter sidebar so the two cannot
|
|
12
|
+
// disagree about which nodes are in scope. Namespace carve-out only — kind
|
|
13
|
+
// filtering stays on the graph.
|
|
14
|
+
export function scopeNodesToNamespaces(nodes: TopologyNode[], namespaces: string[]): TopologyNode[] {
|
|
15
|
+
if (namespaces.length === 0) return nodes
|
|
16
|
+
const nsSet = new Set(namespaces)
|
|
17
|
+
return nodes.filter(node => isInNamespaceScope(node, nsSet))
|
|
18
|
+
}
|